Merge branch 'master' of github.com:bigbluebutton/bigbluebutton into meteor-merge-with-m
Conflicts: bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/chat/ChatMessageListener.java bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonActor.scala bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-voice-app.xml
This commit is contained in:
commit
4172537f24
@ -34,16 +34,12 @@ import org.red5.server.api.IConnection;
|
||||
import org.red5.server.api.IContext;
|
||||
import org.red5.server.api.scope.IScope;
|
||||
import org.slf4j.Logger;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.support.AbstractApplicationContext;
|
||||
import com.google.gson.Gson;
|
||||
|
||||
public class BigBlueButtonApplication extends MultiThreadedApplicationAdapter {
|
||||
private static Logger log = Red5LoggerFactory.getLogger(BigBlueButtonApplication.class, "bigbluebutton");
|
||||
|
||||
private RecorderApplication recorderApplication;
|
||||
private AbstractApplicationContext appCtx;
|
||||
private ConnectionInvokerService connInvokerService;
|
||||
private IBigBlueButtonInGW bbbGW;
|
||||
|
||||
@ -51,72 +47,54 @@ public class BigBlueButtonApplication extends MultiThreadedApplicationAdapter {
|
||||
|
||||
@Override
|
||||
public boolean appConnect(IConnection conn, Object[] params) {
|
||||
log.debug("***** " + APP + " [ " + " appConnect *********");
|
||||
return super.appConnect(conn, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appDisconnect(IConnection conn) {
|
||||
log.debug("***** " + APP + " [ " + " appDisconnect *********");
|
||||
super.appDisconnect(conn);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean appJoin(IClient client, IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " appJoin [ " + scope.getName() + "] *********");
|
||||
return super.appJoin(client, scope);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appLeave(IClient client, IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " appLeave [ " + scope.getName() + "] *********");
|
||||
super.appLeave(client, scope);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean roomJoin(IClient client, IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " roomJoin [ " + scope.getName() + "] *********");
|
||||
return super.roomJoin(client, scope);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void roomLeave(IClient client, IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " roomLeave [ " + scope.getName() + "] *********");
|
||||
super.roomLeave(client, scope);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean appStart(IScope app) {
|
||||
log.debug("***** " + APP + " [ " + " appStart [ " + scope.getName() + "] *********");
|
||||
IContext context = app.getContext();
|
||||
appCtx = (AbstractApplicationContext) context.getApplicationContext();
|
||||
appCtx.addApplicationListener(new ShutdownHookListener());
|
||||
appCtx.registerShutdownHook();
|
||||
super.appStart(app);
|
||||
|
||||
public boolean appStart(IScope app) {
|
||||
super.appStart(app);
|
||||
connInvokerService.setAppScope(app);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appStop(IScope app) {
|
||||
log.debug("***** " + APP + " [ " + " appStop [ " + scope.getName() + "] *********");
|
||||
super.appStop(app);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean roomStart(IScope room) {
|
||||
log.debug("***** " + APP + " [ " + " roomStart [ " + scope.getName() + "] *********");
|
||||
|
||||
connInvokerService.addScope(room.getName(), room);
|
||||
return super.roomStart(room);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void roomStop(IScope room) {
|
||||
log.debug("***** " + APP + " [ " + " roomStop [ " + scope.getName() + "] *********");
|
||||
|
||||
recorderApplication.destroyRecordSession(room.getName());
|
||||
connInvokerService.removeScope(room.getName());
|
||||
|
||||
@ -125,8 +103,6 @@ public class BigBlueButtonApplication extends MultiThreadedApplicationAdapter {
|
||||
|
||||
@Override
|
||||
public boolean roomConnect(IConnection connection, Object[] params) {
|
||||
log.debug("***** " + APP + " [ " + " roomConnect [ " + connection.getScope().getName() + "] *********");
|
||||
|
||||
String username = ((String) params[0]).toString();
|
||||
String role = ((String) params[1]).toString();
|
||||
String room = ((String)params[2]).toString();
|
||||
@ -181,7 +157,7 @@ public class BigBlueButtonApplication extends MultiThreadedApplicationAdapter {
|
||||
String connId = Red5.getConnectionLocal().getSessionId();
|
||||
|
||||
log.info("User connected: sessionId=[" + connId + "], encoding=[" + connType +
|
||||
"(persistent=RTMP,polling=RTMPT)], meetingId= [" + meetingId
|
||||
"], meetingId= [" + meetingId
|
||||
+ "], userId=[" + userId + "] username=[" + userFullname +"]");
|
||||
|
||||
|
||||
@ -215,8 +191,7 @@ public class BigBlueButtonApplication extends MultiThreadedApplicationAdapter {
|
||||
|
||||
@Override
|
||||
public void roomDisconnect(IConnection conn) {
|
||||
log.debug("***** " + APP + " [ " + " roomDisconnect [ " + conn.getScope().getName() + "] *********");
|
||||
|
||||
|
||||
String remoteHost = Red5.getConnectionLocal().getRemoteAddress();
|
||||
int remotePort = Red5.getConnectionLocal().getRemotePort();
|
||||
String clientId = Red5.getConnectionLocal().getClient().getId();
|
||||
@ -233,7 +208,7 @@ public class BigBlueButtonApplication extends MultiThreadedApplicationAdapter {
|
||||
String connId = Red5.getConnectionLocal().getSessionId();
|
||||
|
||||
log.info("User disconnected: sessionId=[" + connId + "], encoding=[" + connType +
|
||||
"(persistent=RTMP,polling=RTMPT)], meetingId= [" + meetingId + "], userId=[" + userId + "] username=[" + userFullname +"]");
|
||||
"], meetingId= [" + meetingId + "], userId=[" + userId + "] username=[" + userFullname +"]");
|
||||
|
||||
Map<String, Object> logData = new HashMap<String, Object>();
|
||||
logData.put("meetingId", meetingId);
|
||||
@ -293,15 +268,4 @@ public class BigBlueButtonApplication extends MultiThreadedApplicationAdapter {
|
||||
public void setBigBlueButtonInGW(IBigBlueButtonInGW bbbGW) {
|
||||
this.bbbGW = bbbGW;
|
||||
}
|
||||
|
||||
private class ShutdownHookListener implements ApplicationListener<ApplicationEvent> {
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ApplicationEvent event) {
|
||||
if (event instanceof org.springframework.context.event.ContextStoppedEvent) {
|
||||
log.info("Received shutdown event. Red5 is shutting down. Destroying all rooms.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ public class MeetingMessageHandler implements MessageHandler {
|
||||
bbbGW.endMeeting(emm.meetingId);
|
||||
} else if (msg instanceof CreateMeetingMessage) {
|
||||
CreateMeetingMessage emm = (CreateMeetingMessage) msg;
|
||||
bbbGW.createMeeting2(emm.id, emm.name, emm.record, emm.voiceBridge,
|
||||
bbbGW.createMeeting2(emm.id, emm.externalId, emm.name, emm.record, emm.voiceBridge,
|
||||
emm.duration, emm.autoStartRecording, emm.allowStartStopRecording);
|
||||
} else if (msg instanceof RegisterUserMessage) {
|
||||
RegisterUserMessage emm = (RegisterUserMessage) msg;
|
||||
|
@ -1,114 +0,0 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.bigbluebutton.conference.service.chat;
|
||||
|
||||
import org.red5.server.adapter.IApplication;
|
||||
import org.red5.server.api.IClient;
|
||||
import org.red5.server.api.IConnection;
|
||||
import org.slf4j.Logger;
|
||||
import org.red5.logging.Red5LoggerFactory;
|
||||
import org.red5.server.api.scope.IScope;
|
||||
import org.bigbluebutton.conference.service.recorder.RecorderApplication;
|
||||
|
||||
public class ChatHandler implements IApplication{
|
||||
private static Logger log = Red5LoggerFactory.getLogger( ChatHandler.class, "bigbluebutton" );
|
||||
|
||||
private RecorderApplication recorderApplication;
|
||||
private ChatApplication chatApplication;
|
||||
|
||||
private static final String APP = "CHAT";
|
||||
|
||||
@Override
|
||||
public boolean appConnect(IConnection conn, Object[] params) {
|
||||
log.debug("***** " + APP + " [ " + " appConnect *********");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appDisconnect(IConnection conn) {
|
||||
log.debug("***** " + APP + " [ " + " appDisconnect *********");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean appJoin(IClient client, IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " appJoin [ " + scope.getName() + "] *********");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appLeave(IClient client, IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " appLeave [ " + scope.getName() + "] *********");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean appStart(IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " appStart [ " + scope.getName() + "] *********");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appStop(IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " appStop [ " + scope.getName() + "] *********");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void roomDisconnect(IConnection connection) {
|
||||
log.debug("***** " + APP + " [ " + " roomDisconnect [ " + connection.getScope().getName() + "] *********");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean roomJoin(IClient client, IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " roomJoin [ " + scope.getName() + "] *********");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void roomLeave(IClient client, IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " roomLeave [ " + scope.getName() + "] *********");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean roomConnect(IConnection connection, Object[] params) {
|
||||
log.debug("***** " + APP + " [ " + " roomConnect [ " + connection.getScope().getName() + "] *********");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean roomStart(IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " roomStart [ " + scope.getName() + "] *********");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void roomStop(IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " roomStop [ " + scope.getName() + "] *********");
|
||||
}
|
||||
|
||||
public void setChatApplication(ChatApplication a) {
|
||||
log.debug("Setting chat application");
|
||||
chatApplication = a;
|
||||
}
|
||||
|
||||
public void setRecorderApplication(RecorderApplication a) {
|
||||
log.debug("Setting archive application");
|
||||
recorderApplication = a;
|
||||
}
|
||||
}
|
@ -7,7 +7,6 @@ public class ChatKeyUtil {
|
||||
public static final String FROM_COLOR = "fromColor";
|
||||
public static final String FROM_TIME = "fromTime";
|
||||
public static final String FROM_TZ_OFFSET = "fromTimezoneOffset";
|
||||
public static final String FROM_LANG = "fromLang";
|
||||
public static final String TO_USERID = "toUserID";
|
||||
public static final String TO_USERNAME = "toUsername";
|
||||
public static final String MESSAGE = "message";
|
||||
|
56
bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/chat/ChatMessageListener.java
Normal file → Executable file
56
bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/chat/ChatMessageListener.java
Normal file → Executable file
@ -37,33 +37,39 @@ public class ChatMessageListener implements MessageHandler{
|
||||
String meetingID = payloadObject.get("meeting_id").toString().replace("\"", "");
|
||||
String requesterID = payloadObject.get("requester_id").toString().replace("\"", "");
|
||||
|
||||
String chatType = messageObject.get("chat_type").toString().replace("\"", "");
|
||||
String fromUserID = messageObject.get("from_userid").toString().replace("\"", "");
|
||||
String fromUsername = messageObject.get("from_username").toString().replace("\"", "");
|
||||
String fromColor = messageObject.get("from_color").toString().replace("\"", "");
|
||||
String fromTime = messageObject.get("from_time").toString().replace("\"", "");
|
||||
String fromTimezoneOffset = messageObject.get("from_tz_offset").toString().replace("\"", "");
|
||||
String fromLang = messageObject.get("from_lang").toString().replace("\"", "");
|
||||
String toUserID = messageObject.get("to_userid").toString().replace("\"", "");
|
||||
String toUsername = messageObject.get("to_username").toString().replace("\"", "");
|
||||
String chatText = messageObject.get("message").toString().replace("\"", "");
|
||||
//case getChatHistory
|
||||
if(eventName.equalsIgnoreCase("get_chat_history")) {
|
||||
String replyTo = meetingID + "/" + requesterID;
|
||||
bbbGW.getChatHistory(meetingID, requesterID, replyTo);
|
||||
}
|
||||
else {
|
||||
String chatType = messageObject.get("chat_type").toString().replace("\"", "");
|
||||
String fromUserID = messageObject.get("from_userid").toString().replace("\"", "");
|
||||
String fromUsername = messageObject.get("from_username").toString().replace("\"", "");
|
||||
String fromColor = messageObject.get("from_color").toString().replace("\"", "");
|
||||
String fromTime = messageObject.get("from_time").toString().replace("\"", "");
|
||||
String fromTimezoneOffset = messageObject.get("from_tz_offset").toString().replace("\"", "");
|
||||
String toUserID = messageObject.get("to_userid").toString().replace("\"", "");
|
||||
String toUsername = messageObject.get("to_username").toString().replace("\"", "");
|
||||
String chatText = messageObject.get("message").toString().replace("\"", "");
|
||||
|
||||
Map<String, String> map = new HashMap<String, String>();
|
||||
map.put(ChatKeyUtil.CHAT_TYPE, chatType);
|
||||
map.put(ChatKeyUtil.FROM_USERID, fromUserID);
|
||||
map.put(ChatKeyUtil.FROM_USERNAME, fromUsername);
|
||||
map.put(ChatKeyUtil.FROM_COLOR, fromColor);
|
||||
map.put(ChatKeyUtil.FROM_TIME, fromTime);
|
||||
map.put(ChatKeyUtil.FROM_TZ_OFFSET, fromTimezoneOffset);
|
||||
map.put(ChatKeyUtil.FROM_LANG, fromLang);
|
||||
map.put(ChatKeyUtil.TO_USERID, toUserID);
|
||||
map.put(ChatKeyUtil.TO_USERNAME, toUsername);
|
||||
map.put(ChatKeyUtil.MESSAGE, chatText);
|
||||
Map<String, String> map = new HashMap<String, String>();
|
||||
map.put(ChatKeyUtil.CHAT_TYPE, chatType);
|
||||
map.put(ChatKeyUtil.FROM_USERID, fromUserID);
|
||||
map.put(ChatKeyUtil.FROM_USERNAME, fromUsername);
|
||||
map.put(ChatKeyUtil.FROM_COLOR, fromColor);
|
||||
map.put(ChatKeyUtil.FROM_TIME, fromTime);
|
||||
map.put(ChatKeyUtil.FROM_TZ_OFFSET, fromTimezoneOffset);
|
||||
map.put(ChatKeyUtil.TO_USERID, toUserID);
|
||||
map.put(ChatKeyUtil.TO_USERNAME, toUsername);
|
||||
map.put(ChatKeyUtil.MESSAGE, chatText);
|
||||
|
||||
if(eventName.equalsIgnoreCase(MessagingConstants.SEND_PUBLIC_CHAT_MESSAGE_REQUEST)) {
|
||||
bbbGW.sendPublicMessage(meetingID, requesterID, map);
|
||||
} else if(eventName.equalsIgnoreCase(MessagingConstants.SEND_PRIVATE_CHAT_MESSAGE_REQUEST)) {
|
||||
bbbGW.sendPrivateMessage(meetingID, requesterID, map);
|
||||
if(eventName.equalsIgnoreCase(MessagingConstants.SEND_PUBLIC_CHAT_MESSAGE_REQUEST)) {
|
||||
bbbGW.sendPublicMessage(meetingID, requesterID, map);
|
||||
}
|
||||
else if(eventName.equalsIgnoreCase(MessagingConstants.SEND_PRIVATE_CHAT_MESSAGE_REQUEST)) {
|
||||
bbbGW.sendPrivateMessage(meetingID, requesterID, map);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,7 +49,6 @@ public class ChatService {
|
||||
String fromColor = msg.get(ChatKeyUtil.FROM_COLOR).toString();
|
||||
String fromTime = msg.get(ChatKeyUtil.FROM_TIME).toString();
|
||||
String fromTimezoneOffset = msg.get(ChatKeyUtil.FROM_TZ_OFFSET).toString();
|
||||
String fromLang = msg.get(ChatKeyUtil.FROM_LANG).toString();
|
||||
String toUserID = msg.get(ChatKeyUtil.TO_USERID).toString();
|
||||
String toUsername = msg.get(ChatKeyUtil.TO_USERNAME).toString();
|
||||
String chatText = msg.get(ChatKeyUtil.MESSAGE).toString();
|
||||
@ -61,7 +60,6 @@ public class ChatService {
|
||||
message.put(ChatKeyUtil.FROM_COLOR, fromColor);
|
||||
message.put(ChatKeyUtil.FROM_TIME, fromTime);
|
||||
message.put(ChatKeyUtil.FROM_TZ_OFFSET, fromTimezoneOffset);
|
||||
message.put(ChatKeyUtil.FROM_LANG, fromLang);
|
||||
message.put(ChatKeyUtil.TO_USERID, toUserID);
|
||||
message.put(ChatKeyUtil.TO_USERNAME, toUsername);
|
||||
message.put(ChatKeyUtil.MESSAGE, chatText);
|
||||
@ -83,7 +81,6 @@ public class ChatService {
|
||||
String fromColor = msg.get(ChatKeyUtil.FROM_COLOR).toString();
|
||||
String fromTime = msg.get(ChatKeyUtil.FROM_TIME).toString();
|
||||
String fromTimezoneOffset = msg.get(ChatKeyUtil.FROM_TZ_OFFSET).toString();
|
||||
String fromLang = msg.get(ChatKeyUtil.FROM_LANG).toString();
|
||||
String toUserID = msg.get(ChatKeyUtil.TO_USERID).toString();
|
||||
String toUsername = msg.get(ChatKeyUtil.TO_USERNAME).toString();
|
||||
String chatText = msg.get(ChatKeyUtil.MESSAGE).toString();
|
||||
@ -95,7 +92,6 @@ public class ChatService {
|
||||
message.put(ChatKeyUtil.FROM_COLOR, fromColor);
|
||||
message.put(ChatKeyUtil.FROM_TIME, fromTime);
|
||||
message.put(ChatKeyUtil.FROM_TZ_OFFSET, fromTimezoneOffset);
|
||||
message.put(ChatKeyUtil.FROM_LANG, fromLang);
|
||||
message.put(ChatKeyUtil.TO_USERID, toUserID);
|
||||
message.put(ChatKeyUtil.TO_USERNAME, toUsername);
|
||||
message.put(ChatKeyUtil.MESSAGE, chatText);
|
||||
|
@ -1,115 +0,0 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package org.bigbluebutton.conference.service.layout;
|
||||
|
||||
import org.bigbluebutton.conference.BigBlueButtonSession;
|
||||
import org.bigbluebutton.conference.Constants;
|
||||
import org.red5.logging.Red5LoggerFactory;
|
||||
import org.red5.server.adapter.ApplicationAdapter;
|
||||
import org.red5.server.adapter.IApplication;
|
||||
import org.red5.server.api.IClient;
|
||||
import org.red5.server.api.IConnection;
|
||||
import org.red5.server.api.Red5;
|
||||
import org.red5.server.api.scope.IScope;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
public class LayoutHandler extends ApplicationAdapter implements IApplication {
|
||||
private static Logger log = Red5LoggerFactory.getLogger( LayoutHandler.class, "bigbluebutton" );
|
||||
|
||||
private static final String APP = "LAYOUT";
|
||||
private LayoutApplication layoutApplication;
|
||||
|
||||
@Override
|
||||
public boolean appConnect(IConnection conn, Object[] params) {
|
||||
log.debug("***** " + APP + " [ " + " appConnect *********");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appDisconnect(IConnection conn) {
|
||||
log.debug("***** " + APP + " [ " + " appDisconnect *********");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean appJoin(IClient client, IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " appJoin [ " + scope.getName() + "] *********");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appLeave(IClient client, IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " appLeave [ " + scope.getName() + "] *********");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean appStart(IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " appStart [ " + scope.getName() + "] *********");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appStop(IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " appStop [ " + scope.getName() + "] *********");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void roomDisconnect(IConnection connection) {
|
||||
log.debug("***** " + APP + " [ " + " roomDisconnect [ " + connection.getScope().getName() + "] *********");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean roomJoin(IClient client, IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " roomJoin [ " + scope.getName() + "] *********");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void roomLeave(IClient client, IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " roomLeave [ " + scope.getName() + "] *********");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean roomConnect(IConnection connection, Object[] params) {
|
||||
log.debug("***** " + APP + " [ " + " roomConnect [ " + connection.getScope().getName() + "] *********");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean roomStart(IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " roomStart [ " + scope.getName() + "] *********");
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void roomStop(IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " roomStop [ " + scope.getName() + "] *********");
|
||||
}
|
||||
|
||||
public void setLayoutApplication(LayoutApplication a) {
|
||||
log.debug("Setting layout application");
|
||||
layoutApplication = a;
|
||||
}
|
||||
|
||||
private BigBlueButtonSession getBbbSession() {
|
||||
return (BigBlueButtonSession) Red5.getConnectionLocal().getAttribute(Constants.SESSION);
|
||||
}
|
||||
|
||||
}
|
@ -5,6 +5,7 @@ public class Constants {
|
||||
public static final String HEADER = "header";
|
||||
public static final String PAYLOAD = "payload";
|
||||
public static final String MEETING_ID = "meeting_id";
|
||||
public static final String EXTERNAL_MEETING_ID = "external_meeting_id";
|
||||
public static final String TIMESTAMP = "timestamp";
|
||||
public static final String USER_ID = "userid";
|
||||
public static final String RECORDED = "recorded";
|
||||
|
@ -5,6 +5,7 @@ public class CreateMeetingMessage implements IMessage {
|
||||
public static final String VERSION = "0.0.1";
|
||||
|
||||
public final String id;
|
||||
public final String externalId;
|
||||
public final String name;
|
||||
public final Boolean record;
|
||||
public final String voiceBridge;
|
||||
@ -12,10 +13,11 @@ public class CreateMeetingMessage implements IMessage {
|
||||
public final Boolean autoStartRecording;
|
||||
public final Boolean allowStartStopRecording;
|
||||
|
||||
public CreateMeetingMessage(String id, String name, Boolean record, String voiceBridge,
|
||||
public CreateMeetingMessage(String id, String externalId, String name, Boolean record, String voiceBridge,
|
||||
Long duration, Boolean autoStartRecording,
|
||||
Boolean allowStartStopRecording) {
|
||||
this.id = id;
|
||||
this.externalId = externalId;
|
||||
this.name = name;
|
||||
this.record = record;
|
||||
this.voiceBridge = voiceBridge;
|
||||
|
@ -52,6 +52,7 @@ public class MessageFromJsonConverter {
|
||||
|
||||
private static IMessage processCreateMeeting(JsonObject payload) {
|
||||
String id = payload.get(Constants.MEETING_ID).getAsString();
|
||||
String externalId = payload.get(Constants.EXTERNAL_MEETING_ID).getAsString();
|
||||
String name = payload.get(Constants.NAME).getAsString();
|
||||
Boolean record = payload.get(Constants.RECORDED).getAsBoolean();
|
||||
String voiceBridge = payload.get(Constants.VOICE_CONF).getAsString();
|
||||
@ -59,7 +60,7 @@ public class MessageFromJsonConverter {
|
||||
Boolean autoStartRecording = payload.get(Constants.AUTO_START_RECORDING).getAsBoolean();
|
||||
Boolean allowStartStopRecording = payload.get(Constants.ALLOW_START_STOP_RECORDING).getAsBoolean();
|
||||
|
||||
return new CreateMeetingMessage(id, name, record, voiceBridge,
|
||||
return new CreateMeetingMessage(id, externalId, name, record, voiceBridge,
|
||||
duration, autoStartRecording, allowStartStopRecording);
|
||||
}
|
||||
|
||||
|
@ -25,14 +25,13 @@ import org.red5.server.api.IConnection;
|
||||
import org.slf4j.Logger;
|
||||
import org.red5.logging.Red5LoggerFactory;
|
||||
import org.red5.server.api.scope.IScope;
|
||||
import org.red5.server.adapter.ApplicationAdapter;
|
||||
import org.red5.server.api.Red5;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.bigbluebutton.conference.BigBlueButtonSession;
|
||||
import org.bigbluebutton.conference.Constants;
|
||||
|
||||
public class ParticipantsHandler extends ApplicationAdapter implements IApplication{
|
||||
public class ParticipantsHandler implements IApplication{
|
||||
private static Logger log = Red5LoggerFactory.getLogger( ParticipantsHandler.class, "bigbluebutton" );
|
||||
|
||||
private static final String APP = "USERS";
|
||||
@ -41,73 +40,57 @@ public class ParticipantsHandler extends ApplicationAdapter implements IApplicat
|
||||
|
||||
@Override
|
||||
public boolean appConnect(IConnection conn, Object[] params) {
|
||||
log.debug("***** " + APP + " [ " + " appConnect *********");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appDisconnect(IConnection conn) {
|
||||
log.debug("***** " + APP + " [ " + " appDisconnect *********");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean appJoin(IClient client, IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " appJoin [ " + scope.getName() + "] *********");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appLeave(IClient client, IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " appLeave [ " + scope.getName() + "] *********");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean appStart(IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " appStart [ " + scope.getName() + "] *********");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appStop(IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " appStop [ " + scope.getName() + "] *********");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void roomDisconnect(IConnection connection) {
|
||||
log.debug("***** " + APP + " [ " + " roomDisconnect [ " + connection.getScope().getName() + "] *********");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean roomStart(IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " roomStart [ " + scope.getName() + "] *********");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean roomConnect(IConnection connection, Object[] params) {
|
||||
log.debug("***** " + APP + " [ " + " roomConnect [ " + connection.getScope().getName() + "] *********");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean roomJoin(IClient client, IScope scope) {
|
||||
log.debug(APP + ":roomJoin " + scope.getName() + " - " + scope.getParent().getName());
|
||||
registerUser();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void roomLeave(IClient client, IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " roomLeave [ " + scope.getName() + "] *********");
|
||||
BigBlueButtonSession bbbSession = getBbbSession();
|
||||
if (bbbSession == null) {
|
||||
log.debug("roomLeave - session is null");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void roomStop(IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " roomStop [ " + scope.getName() + "] *********");
|
||||
}
|
||||
|
||||
public void registerUser() {
|
||||
|
@ -1,101 +0,0 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package org.bigbluebutton.conference.service.presentation;
|
||||
|
||||
import org.red5.server.adapter.IApplication;
|
||||
import org.red5.server.api.IClient;
|
||||
import org.red5.server.api.IConnection;
|
||||
import org.slf4j.Logger;
|
||||
import org.red5.logging.Red5LoggerFactory;
|
||||
import org.red5.server.api.scope.IScope;
|
||||
import org.red5.server.adapter.ApplicationAdapter;
|
||||
|
||||
public class PresentationHandler extends ApplicationAdapter implements IApplication{
|
||||
private static Logger log = Red5LoggerFactory.getLogger( PresentationHandler.class, "bigbluebutton" );
|
||||
private static String APP = "presentation";
|
||||
|
||||
@Override
|
||||
public boolean appConnect(IConnection conn, Object[] params) {
|
||||
log.debug("***** " + APP + " [ " + " appConnect *********");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appDisconnect(IConnection conn) {
|
||||
log.debug("***** " + APP + " [ " + " appDisconnect *********");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean appJoin(IClient client, IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " appJoin [ " + scope.getName() + "] *********");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appLeave(IClient client, IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " appLeave [ " + scope.getName() + "] *********");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean appStart(IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " appStart [ " + scope.getName() + "] *********");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appStop(IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " appStop [ " + scope.getName() + "] *********");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean roomConnect(IConnection connection, Object[] params) {
|
||||
log.debug("***** " + APP + " [ " + " roomConnect [ " + connection.getScope().getName() + "] *********");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void roomDisconnect(IConnection connection) {
|
||||
log.debug("***** " + APP + " [ " + " roomDisconnect [ " + connection.getScope().getName() + "] *********");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean roomJoin(IClient client, IScope scope) {
|
||||
log.debug(APP + ":roomJoin " + scope.getName() + " - " + scope.getParent().getName());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void roomLeave(IClient client, IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " roomLeave [ " + scope.getName() + "] *********");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean roomStart(IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " roomStart [ " + scope.getName() + "] *********");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void roomStop(IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " roomStop [ " + scope.getName() + "] *********");
|
||||
}
|
||||
|
||||
}
|
@ -22,7 +22,6 @@ package org.bigbluebutton.conference.service.recorder.chat;
|
||||
public class PublicChatRecordEvent extends AbstractChatRecordEvent {
|
||||
private static final String SENDER = "sender";
|
||||
private static final String MESSAGE = "message";
|
||||
private static final String LOCALE = "locale";
|
||||
private static final String COLOR = "color";
|
||||
|
||||
public PublicChatRecordEvent() {
|
||||
@ -38,10 +37,6 @@ public class PublicChatRecordEvent extends AbstractChatRecordEvent {
|
||||
eventMap.put(MESSAGE, message);
|
||||
}
|
||||
|
||||
public void setLocale(String locale) {
|
||||
eventMap.put(LOCALE, locale);
|
||||
}
|
||||
|
||||
public void setColor(String color) {
|
||||
eventMap.put(COLOR, color);
|
||||
}
|
||||
|
@ -1,133 +0,0 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package org.bigbluebutton.conference.service.voice;
|
||||
|
||||
import org.red5.server.adapter.IApplication;
|
||||
import org.red5.server.api.IClient;
|
||||
import org.red5.server.api.IConnection;
|
||||
import org.slf4j.Logger;
|
||||
import org.red5.server.api.scope.IScope;
|
||||
import org.red5.server.api.so.ISharedObject;
|
||||
import org.red5.server.adapter.ApplicationAdapter;
|
||||
import org.red5.server.api.Red5;
import org.bigbluebutton.conference.BigBlueButtonSession;
import org.bigbluebutton.conference.Constants;
import org.red5.logging.Red5LoggerFactory;
|
||||
|
||||
public class VoiceHandler extends ApplicationAdapter implements IApplication{
|
||||
private static Logger log = Red5LoggerFactory.getLogger(VoiceHandler.class, "bigbluebutton");
|
||||
|
||||
private static final String VOICE_SO = "meetMeUsersSO";
|
||||
private static final String APP = "VOICE";
|
||||
|
||||
@Override
|
||||
public boolean appConnect(IConnection conn, Object[] params) {
|
||||
log.debug("***** " + APP + " [ " + " appConnect *********");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appDisconnect(IConnection conn) {
|
||||
log.debug("***** " + APP + " [ " + " appDisconnect *********");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean appJoin(IClient client, IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " appJoin [ " + scope.getName() + "] *********");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appLeave(IClient client, IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " appLeave [ " + scope.getName() + "] *********");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean appStart(IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " appStart [ " + scope.getName() + "] *********");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appStop(IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " appStop [ " + scope.getName() + "] *********");
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void roomDisconnect(IConnection connection) {
|
||||
log.debug("***** " + APP + " [ " + " roomDisconnect [ " + connection.getScope().getName() + "] *********");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean roomJoin(IClient client, IScope scope) {
|
||||
log.debug(APP + ":roomJoin " + scope.getName() + " - " + scope.getParent().getName());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void roomLeave(IClient client, IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " roomLeave [ " + scope.getName() + "] *********");
|
||||
|
||||
System.out.println("********************* DISCONNECTING FROM VOICE ROOM **************************** ");
|
||||
if (scope.hasAttribute(VOICE_BRIDGE)) {
|
||||
String voiceBridge = (String) scope.getAttribute(VOICE_BRIDGE);
|
||||
String userID = getBbbSession().getExternUserID();
|
||||
log.info("User has left the meeting. Try to hangup user=[" + userID + "] from [" + voiceBridge + "] ");
|
||||
// conferenceService.hangupUser(userID, voiceBridge);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean roomStart(IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " roomStart [ " + scope.getName() + "] *********");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static final String VOICE_BRIDGE = "VOICE_BRIDGE";
|
||||
|
||||
@Override
|
||||
public boolean roomConnect(IConnection connection, Object[] params) {
|
||||
log.debug("***** " + APP + " [ " + " roomConnect [ " + connection.getScope().getName() + "] *********");
|
||||
|
||||
ISharedObject so = getSharedObject(connection.getScope(), VOICE_SO, false);
|
||||
|
||||
String voiceBridge = getBbbSession().getVoiceBridge();
|
||||
String meetingid = getBbbSession().getRoom();
|
||||
Boolean record = getBbbSession().getRecord();
|
||||
Boolean muted = getBbbSession().getStartAsMuted();
|
||||
|
||||
if (!connection.getScope().hasAttribute(VOICE_BRIDGE)) {
|
||||
connection.getScope().setAttribute(VOICE_BRIDGE, getBbbSession().getVoiceBridge());
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void roomStop(IScope scope) {
|
||||
log.debug("***** " + APP + " [ " + " roomStop [ " + scope.getName() + "] *********");
|
||||
|
||||
}
|
||||
|
||||
private BigBlueButtonSession getBbbSession() {
|
||||
return (BigBlueButtonSession) Red5.getConnectionLocal().getAttribute(Constants.SESSION);
|
||||
}
|
||||
}
|
@ -19,7 +19,6 @@
|
||||
package org.bigbluebutton.conference.service.whiteboard;
|
||||
|
||||
import org.red5.logging.Red5LoggerFactory;
|
||||
import org.red5.server.adapter.ApplicationAdapter;
|
||||
import org.red5.server.adapter.IApplication;
|
||||
import org.red5.server.api.IClient;
|
||||
import org.red5.server.api.IConnection;
|
||||
@ -27,7 +26,7 @@ import org.red5.server.api.scope.IScope;
|
||||
import org.slf4j.Logger;
|
||||
import org.bigbluebutton.core.api.IBigBlueButtonInGW;
|
||||
|
||||
public class WhiteboardApplication extends ApplicationAdapter implements IApplication {
|
||||
public class WhiteboardApplication implements IApplication {
|
||||
private static Logger log = Red5LoggerFactory.getLogger(WhiteboardApplication.class, "bigbluebutton");
|
||||
|
||||
private IBigBlueButtonInGW bbbInGW;
|
||||
@ -65,7 +64,6 @@ public class WhiteboardApplication extends ApplicationAdapter implements IApplic
|
||||
|
||||
@Override
|
||||
public boolean roomConnect(IConnection connection, Object[] params) {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@ public interface IBigBlueButtonInGW {
|
||||
void statusMeetingAudit(String meetingID);
|
||||
void endMeeting(String meetingID);
|
||||
void endAllMeetings();
|
||||
void createMeeting2(String meetingID, String meetingName, boolean recorded,
|
||||
void createMeeting2(String meetingID, String externalMeetingID, String meetingName, boolean recorded,
|
||||
String voiceBridge, long duration, boolean autoStartRecording,
|
||||
boolean allowStartStopRecording);
|
||||
void destroyMeeting(String meetingID);
|
||||
|
@ -83,13 +83,13 @@ class BigBlueButtonActor(outGW: MessageOutGateway) extends Actor {
|
||||
meetings.get(msg.meetingID) match {
|
||||
case None => {
|
||||
println("New meeting create request [" + msg.meetingName + "]")
|
||||
var m = new MeetingActor(msg.meetingID, msg.meetingName, msg.recorded,
|
||||
var m = new MeetingActor(msg.meetingID, msg.externalMeetingID, msg.meetingName, msg.recorded,
|
||||
msg.voiceBridge, msg.duration,
|
||||
msg.autoStartRecording, msg.allowStartStopRecording,
|
||||
outGW)
|
||||
m.start
|
||||
meetings += m.meetingID -> m
|
||||
outGW.send(new MeetingCreated(m.meetingID, m.recorded, m.meetingName, m.voiceBridge, msg.duration))
|
||||
outGW.send(new MeetingCreated(m.meetingID, m.externalMeetingID, m.recorded, m.meetingName, m.voiceBridge, msg.duration))
|
||||
|
||||
m ! new InitializeMeeting(m.meetingID, m.recorded)
|
||||
m ! "StartTimer"
|
||||
@ -140,4 +140,5 @@ class BigBlueButtonActor(outGW: MessageOutGateway) extends Actor {
|
||||
|
||||
outGW.send(new GetAllMeetingsReply(resultArray))
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -17,11 +17,11 @@ import org.bigbluebutton.core.apps.presentation.Presentation
|
||||
class BigBlueButtonInGW(bbbGW: BigBlueButtonGateway, presUtil: PreuploadedPresentationsUtil) extends IBigBlueButtonInGW {
|
||||
|
||||
// Meeting
|
||||
def createMeeting2(meetingID: String, meetingName: String, record: Boolean,
|
||||
def createMeeting2(meetingID: String, externalMeetingID:String, meetingName: String, record: Boolean,
|
||||
voiceBridge: String, duration: Long, autoStartRecording: Boolean,
|
||||
allowStartStopRecording: Boolean) {
|
||||
// println("******************** CREATING MEETING [" + meetingID + "] ***************************** ")
|
||||
bbbGW.accept(new CreateMeeting(meetingID, meetingName, record,
|
||||
bbbGW.accept(new CreateMeeting(meetingID, externalMeetingID, meetingName, record,
|
||||
voiceBridge, duration, autoStartRecording,
|
||||
allowStartStopRecording))
|
||||
}
|
||||
|
@ -228,6 +228,7 @@ class CollectorActor(dispatcher: IDispatcher) extends Actor {
|
||||
private def handleCreateMeeting(msg: CreateMeeting) {
|
||||
val payload = new java.util.HashMap[String, Any]()
|
||||
payload.put(Constants.MEETING_ID, msg.meetingID)
|
||||
payload.put(Constants.EXTERNAL_MEETING_ID, msg.externalMeetingID)
|
||||
payload.put(Constants.MEETING_NAME, msg.meetingName)
|
||||
payload.put(Constants.RECORDED, msg.recorded)
|
||||
payload.put(Constants.VOICE_CONF, msg.voiceBridge)
|
||||
|
@ -17,7 +17,7 @@ import org.bigbluebutton.core.util._
|
||||
|
||||
case object StopMeetingActor
|
||||
|
||||
class MeetingActor(val meetingID: String, val meetingName: String, val recorded: Boolean,
|
||||
class MeetingActor(val meetingID: String, val externalMeetingID: String, val meetingName: String, val recorded: Boolean,
|
||||
val voiceBridge: String, duration: Long,
|
||||
val autoStartRecording: Boolean, val allowStartStopRecording: Boolean,
|
||||
val outGW: MessageOutGateway)
|
||||
|
@ -5,6 +5,7 @@ object Constants {
|
||||
val HEADER = "header"
|
||||
val PAYLOAD = "payload"
|
||||
val MEETING_ID = "meeting_id"
|
||||
val EXTERNAL_MEETING_ID = "external_meeting_id"
|
||||
val TIMESTAMP = "timestamp"
|
||||
val CURRENT_TIME = "current_time"
|
||||
val USER_ID = "userid"
|
||||
|
@ -15,6 +15,7 @@ case class KeepAliveMessage
|
||||
case class CreateMeeting
|
||||
(
|
||||
meetingID: String,
|
||||
externalMeetingID: String,
|
||||
meetingName: String,
|
||||
recorded: Boolean,
|
||||
voiceBridge: String,
|
||||
|
@ -44,6 +44,7 @@ case class GetRecordingStatusReply(
|
||||
|
||||
case class MeetingCreated(
|
||||
meetingID: String,
|
||||
externalMeetingID: String,
|
||||
recorded: Boolean,
|
||||
name: String,
|
||||
voiceBridge: String,
|
||||
|
@ -22,7 +22,6 @@ class ChatEventRedisRecorder(recorder: RecorderApplication) extends OutMessageLi
|
||||
ev.setMeetingId(msg.meetingID);
|
||||
ev.setSender(message.get("fromUsername"));
|
||||
ev.setMessage(message.get("message"));
|
||||
ev.setLocale(message.get("fromLang"));
|
||||
ev.setColor(message.get("fromColor"));
|
||||
recorder.record(msg.meetingID, ev);
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ object ChatMessageToJsonConverter {
|
||||
res += "from_color" -> msg.get(ChatKeyUtil.FROM_COLOR).getOrElse(UNKNOWN)
|
||||
res += "from_time" -> msg.get(ChatKeyUtil.FROM_TIME).getOrElse(UNKNOWN)
|
||||
res += "from_tz_offset" -> msg.get(ChatKeyUtil.FROM_TZ_OFFSET).getOrElse(UNKNOWN)
|
||||
res += "from_lang" -> msg.get(ChatKeyUtil.FROM_LANG).getOrElse(UNKNOWN)
|
||||
res += "to_userid" -> msg.get(ChatKeyUtil.TO_USERID).getOrElse(UNKNOWN)
|
||||
res += "to_username" -> msg.get(ChatKeyUtil.TO_USERNAME).getOrElse(UNKNOWN)
|
||||
res += "message" -> msg.get(ChatKeyUtil.MESSAGE).getOrElse(UNKNOWN)
|
||||
|
32
bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp.scala
Normal file → Executable file
32
bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp.scala
Normal file → Executable file
@ -296,16 +296,10 @@ trait UsersApp {
|
||||
}
|
||||
}
|
||||
|
||||
def handleVoiceUserJoined(msg: VoiceUserJoined) = {
|
||||
val user = users.getUser(msg.voiceUser.webUserId) match {
|
||||
def handleUserJoinedVoiceFromPhone(msg: VoiceUserJoined) = {
|
||||
val user = users.getUserWithVoiceUserId(msg.voiceUser.userId) match {
|
||||
case Some(user) => {
|
||||
val nu = user.copy(voiceUser=msg.voiceUser)
|
||||
users.addUser(nu)
|
||||
logger.info("Received user joined voice for user [" + nu.name + "] userid=[" + msg.voiceUser.webUserId + "]" )
|
||||
outGW.send(new UserJoinedVoice(meetingID, recorded, voiceBridge, nu))
|
||||
|
||||
if (meetingMuted)
|
||||
outGW.send(new MuteVoiceUser(meetingID, recorded, nu.userID, nu.userID, meetingMuted))
|
||||
logger.info("Voice user=[" + msg.voiceUser.userId + "] is already in conf=[" + voiceBridge + "]. Must be duplicate message.")
|
||||
}
|
||||
case None => {
|
||||
// No current web user. This means that the user called in through
|
||||
@ -326,7 +320,25 @@ trait UsersApp {
|
||||
|
||||
outGW.send(new UserJoinedVoice(meetingID, recorded, voiceBridge, uvo))
|
||||
if (meetingMuted)
|
||||
outGW.send(new MuteVoiceUser(meetingID, recorded, uvo.userID, uvo.userID, meetingMuted))
|
||||
outGW.send(new MuteVoiceUser(meetingID, recorded, uvo.userID, uvo.userID, meetingMuted))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def handleVoiceUserJoined(msg: VoiceUserJoined) = {
|
||||
val user = users.getUser(msg.voiceUser.webUserId) match {
|
||||
case Some(user) => {
|
||||
val nu = user.copy(voiceUser=msg.voiceUser)
|
||||
users.addUser(nu)
|
||||
logger.info("Received user joined voice for user [" + nu.name + "] userid=[" + msg.voiceUser.webUserId + "]" )
|
||||
outGW.send(new UserJoinedVoice(meetingID, recorded, voiceBridge, nu))
|
||||
|
||||
if (meetingMuted)
|
||||
outGW.send(new MuteVoiceUser(meetingID, recorded, nu.userID, nu.userID, meetingMuted))
|
||||
}
|
||||
case None => {
|
||||
handleUserJoinedVoiceFromPhone(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
4
bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersModel.scala
Normal file → Executable file
4
bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersModel.scala
Normal file → Executable file
@ -43,6 +43,10 @@ class UsersModel {
|
||||
uservos.values find (u => u.externUserID == userID)
|
||||
}
|
||||
|
||||
def getUserWithVoiceUserId(voiceUserId: String):Option[UserVO] = {
|
||||
uservos.values find (u => u.voiceUser.userId == voiceUserId)
|
||||
}
|
||||
|
||||
def getUser(userID:String):Option[UserVO] = {
|
||||
uservos.values find (u => u.userID == userID)
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ object MeetingMessageToJsonConverter {
|
||||
def meetingCreatedToJson(msg:MeetingCreated):String = {
|
||||
val payload = new java.util.HashMap[String, Any]()
|
||||
payload.put(Constants.MEETING_ID, msg.meetingID)
|
||||
payload.put(Constants.EXTERNAL_MEETING_ID, msg.externalMeetingID)
|
||||
payload.put(Constants.NAME, msg.name)
|
||||
payload.put(Constants.RECORDED, msg.recorded)
|
||||
payload.put(Constants.VOICE_CONF, msg.voiceBridge)
|
||||
|
@ -26,12 +26,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
http://www.springframework.org/schema/util
|
||||
http://www.springframework.org/schema/util/spring-util-2.0.xsd
|
||||
">
|
||||
|
||||
<bean id="chatHandler" class="org.bigbluebutton.conference.service.chat.ChatHandler">
|
||||
<property name="chatApplication"> <ref local="chatApplication"/></property>
|
||||
<property name="recorderApplication"> <ref bean="recorderApplication"/></property>
|
||||
</bean>
|
||||
|
||||
|
||||
<bean id="chatApplication" class="org.bigbluebutton.conference.service.chat.ChatApplication">
|
||||
<property name="bigBlueButtonInGW"> <ref bean="bbbInGW"/></property>
|
||||
</bean>
|
||||
|
@ -27,11 +27,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
http://www.springframework.org/schema/util/spring-util-2.0.xsd
|
||||
">
|
||||
|
||||
|
||||
<bean id="layoutHandler" class="org.bigbluebutton.conference.service.layout.LayoutHandler">
|
||||
<property name="layoutApplication"><ref local="layoutApplication"/></property>
|
||||
</bean>
|
||||
|
||||
<bean id="layoutApplication" class="org.bigbluebutton.conference.service.layout.LayoutApplication">
|
||||
<property name="bigBlueButtonInGW"> <ref bean="bbbInGW"/></property>
|
||||
</bean>
|
||||
|
@ -27,9 +27,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
http://www.springframework.org/schema/util/spring-util-2.0.xsd
|
||||
">
|
||||
|
||||
<bean id="presentationHandler" class="org.bigbluebutton.conference.service.presentation.PresentationHandler">
|
||||
</bean>
|
||||
|
||||
<bean id="presentationApplication" class="org.bigbluebutton.conference.service.presentation.PresentationApplication">
|
||||
<property name="bigBlueButtonInGW"> <ref bean="bbbInGW"/></property>
|
||||
</bean>
|
||||
|
@ -23,9 +23,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
xmlns:beans="http://www.springframework.org/schema/beans"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans
|
||||
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
|
||||
|
||||
<beans:bean id="voiceHandler" class="org.bigbluebutton.conference.service.voice.VoiceHandler">
|
||||
</beans:bean>
|
||||
|
||||
<beans:bean id="voiceEventRecorder" class="org.bigbluebutton.webconference.voice.VoiceEventRecorder">
|
||||
<beans:property name="recorderApplication" ref="recorderApplication"/>
|
||||
|
16
bigbluebutton-apps/src/main/webapp/WEB-INF/red5-web.xml
Normal file → Executable file
16
bigbluebutton-apps/src/main/webapp/WEB-INF/red5-web.xml
Normal file → Executable file
@ -52,16 +52,18 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
<property name="applicationListeners">
|
||||
<set>
|
||||
<ref bean="participantsHandler" />
|
||||
<ref bean="chatHandler" />
|
||||
<ref bean="layoutHandler" />
|
||||
<ref bean="presentationHandler" />
|
||||
<ref bean="voiceHandler" />
|
||||
<ref bean="whiteboardApplication" />
|
||||
</set>
|
||||
</property>
|
||||
<property name="recorderApplication"> <ref bean="recorderApplication"/></property>
|
||||
<property name="connInvokerService"> <ref bean="connInvokerService"/></property>
|
||||
<property name="bigBlueButtonInGW"> <ref bean="bbbInGW"/></property>
|
||||
<property name="recorderApplication">
|
||||
<ref bean="recorderApplication"/>
|
||||
</property>
|
||||
<property name="connInvokerService">
|
||||
<ref bean="connInvokerService"/>
|
||||
</property>
|
||||
<property name="bigBlueButtonInGW">
|
||||
<ref bean="bbbInGW"/>
|
||||
</property>
|
||||
</bean>
|
||||
|
||||
<bean id="connInvokerService" class="org.bigbluebutton.conference.meeting.messaging.red5.ConnectionInvokerService"
|
||||
|
5
bigbluebutton-client/resources/config.xml.template
Normal file → Executable file
5
bigbluebutton-client/resources/config.xml.template
Normal file → Executable file
@ -12,7 +12,8 @@
|
||||
<shortcutKeys showButton="true" />
|
||||
<layout showLogButton="false" showVideoLayout="false" showResetLayout="true" defaultLayout="bbb.layout.name.defaultlayout"
|
||||
showToolbar="true" showFooter="true" showMeetingName="true" showHelpButton="true"
|
||||
showLogoutWindow="true" showLayoutTools="true" showNetworkMonitor="false" confirmLogout="true"/>
|
||||
showLogoutWindow="true" showLayoutTools="true" showNetworkMonitor="false" confirmLogout="true"
|
||||
showRecordingNotification="true"/>
|
||||
<lock allowModeratorLocking="false" disableCamForLockedUsers="false" disableMicForLockedUsers="false" disablePrivateChatForLockedUsers="false"
|
||||
disablePublicChatForLockedUsers="false" lockLayoutForLockedUsers="false"/>
|
||||
|
||||
@ -21,8 +22,6 @@
|
||||
<module name="ChatModule" url="http://HOST/client/ChatModule.swf?v=VERSION"
|
||||
uri="rtmp://HOST/bigbluebutton"
|
||||
dependsOn="UsersModule"
|
||||
translationOn="false"
|
||||
translationEnabled="false"
|
||||
privateEnabled="true"
|
||||
position="top-right"
|
||||
baseTabIndex="701"
|
||||
|
12
bigbluebutton-client/src/ChatModule.mxml
Normal file → Executable file
12
bigbluebutton-client/src/ChatModule.mxml
Normal file → Executable file
@ -87,16 +87,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
return _attributes.userrole as String;
|
||||
}
|
||||
|
||||
public function get traslationEnabled():Boolean{
|
||||
if (_attributes.translationEnabled == "false") return false;
|
||||
else return true;
|
||||
}
|
||||
|
||||
public function get translationOn():Boolean{
|
||||
if (_attributes.translationOn == "false") return false;
|
||||
else return true;
|
||||
}
|
||||
|
||||
public function start(attributes:Object):void {
|
||||
LogUtil.debug("chat attr: " + attributes.username);
|
||||
_attributes = attributes;
|
||||
@ -105,8 +95,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
var event:StartChatModuleEvent = new StartChatModuleEvent(StartChatModuleEvent.START_CHAT_MODULE_EVENT)
|
||||
event.start = true;
|
||||
event.attributes = _attributes;
|
||||
event.translationOn = translationOn;
|
||||
event.translationEnabled = traslationEnabled;
|
||||
dispatcher.dispatchEvent(event);
|
||||
}
|
||||
|
||||
|
@ -34,6 +34,7 @@ package org.bigbluebutton.main.model
|
||||
[Bindable] public var showLayoutTools:Boolean = true;
|
||||
[Bindable] public var showNetworkMonitor:Boolean = true;
|
||||
[Bindable] public var confirmLogout:Boolean = true;
|
||||
[Bindable] public var showRecordingNotification:Boolean = true;
|
||||
|
||||
|
||||
public var defaultLayout:String = "Default";
|
||||
@ -93,6 +94,9 @@ package org.bigbluebutton.main.model
|
||||
showNetworkMonitor = (vxml.@showNetworkMonitor.toString().toUpperCase() == "TRUE") ? true : false;
|
||||
}
|
||||
|
||||
if(vxml.@showRecordingNotification != undefined){
|
||||
showRecordingNotification = (vxml.@showRecordingNotification.toString().toUpperCase() == "TRUE") ? true : false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,6 +30,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
? ResourceUtil.getInstance().getString('bbb.mainToolbar.recordBtn.toolTip.start')
|
||||
: ResourceUtil.getInstance().getString('bbb.mainToolbar.recordBtn.toolTip.notRecording')}"
|
||||
enabled="false"
|
||||
creationComplete="onCreationComplete()"
|
||||
visible="{UserManager.getInstance().getConference().record}"
|
||||
includeInLayout="{UserManager.getInstance().getConference().record}"
|
||||
mouseOver="onRecordButtonMouseOver(event)"
|
||||
@ -48,16 +49,22 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
import org.bigbluebutton.core.managers.UserManager;
|
||||
import org.bigbluebutton.core.model.MeetingModel;
|
||||
import org.bigbluebutton.main.events.BBBEvent;
|
||||
import org.bigbluebutton.main.model.LayoutOptions;
|
||||
import org.bigbluebutton.modules.phone.events.FlashJoinedVoiceConferenceEvent;
|
||||
import org.bigbluebutton.modules.phone.events.WebRTCCallEvent;
|
||||
import org.bigbluebutton.util.i18n.ResourceUtil;
|
||||
|
||||
private var recordingFlag:Boolean;
|
||||
private var firstAudioJoin:Boolean = true;
|
||||
private var layoutOptions:LayoutOptions = null;
|
||||
|
||||
[Embed(source="/org/bigbluebutton/common/assets/images/record.png")]
|
||||
private var recordReminderIcon:Class;
|
||||
|
||||
private function onCreationComplete():void {
|
||||
ResourceUtil.getInstance().addEventListener(Event.CHANGE, localeChanged); // Listen for locale changing
|
||||
}
|
||||
|
||||
private function confirmChangeRecordingStatus():void {
|
||||
trace("Confirming recording status change!!!!");
|
||||
|
||||
@ -106,21 +113,11 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
private function onRecordingStatusChanged(event:BBBEvent):void {
|
||||
if (event.payload.remote) {
|
||||
this.selected = event.payload.recording;
|
||||
this.styleName = this.selected? "recordButtonStyleStart": "recordButtonStyleNormal";
|
||||
|
||||
resourcesChanged();
|
||||
|
||||
if (UserManager.getInstance().getConference().amIModerator() && MeetingModel.getInstance().meeting.allowStartStopRecording) {
|
||||
this.enabled = true;
|
||||
if (event.payload.recording) {
|
||||
this.toolTip = ResourceUtil.getInstance().getString('bbb.mainToolbar.recordBtn.toolTip.stop');
|
||||
} else {
|
||||
this.toolTip = ResourceUtil.getInstance().getString('bbb.mainToolbar.recordBtn.toolTip.start');
|
||||
}
|
||||
} else {
|
||||
if (event.payload.recording) {
|
||||
this.toolTip = ResourceUtil.getInstance().getString('bbb.mainToolbar.recordBtn.toolTip.recording');
|
||||
} else {
|
||||
this.toolTip = ResourceUtil.getInstance().getString('bbb.mainToolbar.recordBtn.toolTip.notRecording');
|
||||
}
|
||||
}
|
||||
|
||||
trace("RecordButton:onRecordingStatusChanged changing record status to " + event.payload.recording);
|
||||
@ -136,9 +133,15 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
}
|
||||
|
||||
private function showRecordingNotification():void {
|
||||
if (layoutOptions == null) {
|
||||
layoutOptions = new LayoutOptions();
|
||||
layoutOptions.parseOptions();
|
||||
}
|
||||
|
||||
if (firstAudioJoin && this.visible && !this.selected
|
||||
&& UserManager.getInstance().getConference().amIModerator()
|
||||
&& MeetingModel.getInstance().meeting.allowStartStopRecording) {
|
||||
&& layoutOptions.showRecordingNotification
|
||||
&& UserManager.getInstance().getConference().amIModerator()
|
||||
&& MeetingModel.getInstance().meeting.allowStartStopRecording) {
|
||||
var alert:Alert = Alert.show(ResourceUtil.getInstance().getString("bbb.mainToolbar.recordBtn..notification.message1") + "\n\n" + ResourceUtil.getInstance().getString("bbb.mainToolbar.recordBtn..notification.message2"), ResourceUtil.getInstance().getString("bbb.mainToolbar.recordBtn..notification.title"), Alert.OK, this);
|
||||
alert.titleIcon = recordReminderIcon;
|
||||
|
||||
@ -163,6 +166,30 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
this.styleName = this.selected? "recordButtonStyleStart": "recordButtonStyleNormal";
|
||||
}
|
||||
}
|
||||
|
||||
override protected function resourcesChanged():void{
|
||||
super.resourcesChanged();
|
||||
|
||||
this.styleName = this.selected? "recordButtonStyleStart": "recordButtonStyleNormal";
|
||||
|
||||
if (UserManager.getInstance().getConference().amIModerator() && MeetingModel.getInstance().meeting.allowStartStopRecording) {
|
||||
if (this.selected) {
|
||||
this.toolTip = ResourceUtil.getInstance().getString('bbb.mainToolbar.recordBtn.toolTip.stop');
|
||||
} else {
|
||||
this.toolTip = ResourceUtil.getInstance().getString('bbb.mainToolbar.recordBtn.toolTip.start');
|
||||
}
|
||||
} else {
|
||||
if (this.selected) {
|
||||
this.toolTip = ResourceUtil.getInstance().getString('bbb.mainToolbar.recordBtn.toolTip.recording');
|
||||
} else {
|
||||
this.toolTip = ResourceUtil.getInstance().getString('bbb.mainToolbar.recordBtn.toolTip.notRecording');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function localeChanged(e:Event):void{
|
||||
resourcesChanged();
|
||||
}
|
||||
]]>
|
||||
</mx:Script>
|
||||
</mx:Button>
|
||||
|
@ -23,14 +23,8 @@ package org.bigbluebutton.modules.chat.events
|
||||
public class ChatOptionsEvent extends Event
|
||||
{
|
||||
public static const CHANGE_FONT_SIZE:String = "Change Font Size";
|
||||
public static const CHANGE_LANGUAGE:String = "Change Language";
|
||||
public static const TOGGLE_TRANSLATE:String = "Toggle Translate";
|
||||
public static const TRANSLATION_OPTION_ENABLED:String = "Translation_Enable";
|
||||
|
||||
public var fontSize:int;
|
||||
public var language:String="";
|
||||
public var translationEnabled:Boolean;
|
||||
public var translateOn:Boolean;
|
||||
|
||||
public function ChatOptionsEvent(type:String)
|
||||
{
|
||||
|
@ -1,35 +0,0 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package org.bigbluebutton.modules.chat.events
|
||||
{
|
||||
import flash.events.Event;
|
||||
|
||||
public class ConnectionEvent extends Event
|
||||
{
|
||||
public static const CONNECT_EVENT:String = 'CONNECT_SUCCESS_EVENT';
|
||||
public var success:Boolean = false;
|
||||
public var errors:Array;
|
||||
|
||||
public function ConnectionEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false)
|
||||
{
|
||||
super(type, bubbles, cancelable);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -27,9 +27,6 @@ package org.bigbluebutton.modules.chat.events
|
||||
public var start:Boolean = true;
|
||||
public var attributes:Object;
|
||||
|
||||
public var translationEnabled:Boolean;
|
||||
public var translationOn:Boolean;
|
||||
|
||||
public function StartChatModuleEvent(type:String, bubbles:Boolean=true, cancelable:Boolean=false)
|
||||
{
|
||||
super(type, bubbles, cancelable);
|
||||
|
9
bigbluebutton-client/src/org/bigbluebutton/modules/chat/maps/ChatEventMap.mxml
Normal file → Executable file
9
bigbluebutton-client/src/org/bigbluebutton/modules/chat/maps/ChatEventMap.mxml
Normal file → Executable file
@ -30,7 +30,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
import org.bigbluebutton.core.EventConstants;
|
||||
import org.bigbluebutton.main.events.ModuleStartedEvent;
|
||||
import org.bigbluebutton.modules.chat.events.ChatEvent;
|
||||
import org.bigbluebutton.modules.chat.events.ConnectionEvent;
|
||||
import org.bigbluebutton.modules.chat.events.SendPrivateChatMessageEvent;
|
||||
import org.bigbluebutton.modules.chat.events.SendPublicChatMessageEvent;
|
||||
import org.bigbluebutton.modules.chat.events.StartChatModuleEvent;
|
||||
@ -48,18 +47,14 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
<ObjectBuilder generator="{ChatEventMapDelegate}" constructorArguments="{scope.dispatcher}"/>
|
||||
</EventHandlers>
|
||||
|
||||
<EventHandlers type="{StartChatModuleEvent.START_CHAT_MODULE_EVENT}">
|
||||
<MethodInvoker generator="{ChatEventMapDelegate}" method="setTranslationOptions" arguments="{event}" />
|
||||
<EventHandlers type="{StartChatModuleEvent.START_CHAT_MODULE_EVENT}">
|
||||
<MethodInvoker generator="{ChatEventMapDelegate}" method="openChatWindow" />
|
||||
<ObjectBuilder generator="{ChatMessageService}"/>
|
||||
</EventHandlers>
|
||||
|
||||
<EventHandlers type="{StopChatModuleEvent.STOP_CHAT_MODULE_EVENT}">
|
||||
<MethodInvoker generator="{ChatEventMapDelegate}" method="closeChatWindow" />
|
||||
</EventHandlers>
|
||||
|
||||
<EventHandlers type="{ConnectionEvent.CONNECT_EVENT}">
|
||||
<MethodInvoker generator="{ChatEventMapDelegate}" method="openChatWindow"/>
|
||||
</EventHandlers>
|
||||
|
||||
<EventHandlers type="{EventConstants.SEND_PUBLIC_CHAT_REQ}">
|
||||
<MethodInvoker generator="{ChatMessageService}" method="sendPublicMessageFromApi" arguments="{event.message}"/>
|
||||
|
@ -22,29 +22,25 @@ package org.bigbluebutton.modules.chat.maps {
|
||||
import org.bigbluebutton.common.LogUtil;
|
||||
import org.bigbluebutton.common.events.CloseWindowEvent;
|
||||
import org.bigbluebutton.common.events.OpenWindowEvent;
|
||||
import org.bigbluebutton.core.BBB;
|
||||
import org.bigbluebutton.modules.chat.events.ChatOptionsEvent;
|
||||
import org.bigbluebutton.modules.chat.events.StartChatModuleEvent;
|
||||
import org.bigbluebutton.modules.chat.model.ChatOptions;
|
||||
import org.bigbluebutton.modules.chat.views.ChatWindow;
|
||||
import org.bigbluebutton.util.i18n.ResourceUtil;
|
||||
|
||||
public class ChatEventMapDelegate {
|
||||
private static var LOG:String = "ChatEventMapDelegate - ";
|
||||
|
||||
private var dispatcher:IEventDispatcher;
|
||||
|
||||
private var _chatWindow:ChatWindow;
|
||||
private var _chatWindowOpen:Boolean = false;
|
||||
private var globalDispatcher:Dispatcher;
|
||||
|
||||
private var translationEnabled:Boolean;
|
||||
private var translationOn:Boolean;
|
||||
private var chatOptions:ChatOptions;
|
||||
|
||||
public function ChatEventMapDelegate() {
|
||||
this.dispatcher = dispatcher;
|
||||
_chatWindow = new ChatWindow();
|
||||
globalDispatcher = new Dispatcher();
|
||||
openChatWindow();
|
||||
}
|
||||
|
||||
private function getChatOptions():void {
|
||||
@ -62,8 +58,7 @@ package org.bigbluebutton.modules.chat.maps {
|
||||
var event:OpenWindowEvent = new OpenWindowEvent(OpenWindowEvent.OPEN_WINDOW_EVENT);
|
||||
event.window = _chatWindow;
|
||||
globalDispatcher.dispatchEvent(event);
|
||||
_chatWindowOpen = true;
|
||||
dispatchTranslationOptions();
|
||||
_chatWindowOpen = true;
|
||||
}
|
||||
|
||||
public function closeChatWindow():void {
|
||||
@ -73,17 +68,5 @@ package org.bigbluebutton.modules.chat.maps {
|
||||
|
||||
_chatWindowOpen = false;
|
||||
}
|
||||
|
||||
public function setTranslationOptions(e:StartChatModuleEvent):void{
|
||||
translationEnabled = e.translationEnabled;
|
||||
translationOn = e.translationOn;
|
||||
}
|
||||
|
||||
private function dispatchTranslationOptions():void{
|
||||
var enableEvent:ChatOptionsEvent = new ChatOptionsEvent(ChatOptionsEvent.TRANSLATION_OPTION_ENABLED);
|
||||
enableEvent.translationEnabled = translationEnabled;
|
||||
enableEvent.translateOn = translationOn;
|
||||
globalDispatcher.dispatchEvent(enableEvent);
|
||||
}
|
||||
}
|
||||
}
|
12
bigbluebutton-client/src/org/bigbluebutton/modules/chat/model/ChatConversation.as
Normal file → Executable file
12
bigbluebutton-client/src/org/bigbluebutton/modules/chat/model/ChatConversation.as
Normal file → Executable file
@ -29,8 +29,6 @@ package org.bigbluebutton.modules.chat.model
|
||||
[Bindable]
|
||||
public var messages:ArrayCollection = new ArrayCollection();
|
||||
|
||||
public var autoTranslate:Boolean = false;
|
||||
|
||||
public function numMessages():int {
|
||||
return messages.length;
|
||||
}
|
||||
@ -47,16 +45,10 @@ package org.bigbluebutton.modules.chat.model
|
||||
}
|
||||
cm.senderId = msg.fromUserID;
|
||||
|
||||
cm.senderLanguage = msg.fromLang;
|
||||
cm.receiverLanguage = ChatUtil.getUserLang();
|
||||
cm.translate = autoTranslate;
|
||||
|
||||
cm.translatedText = msg.message;
|
||||
cm.senderText = msg.message;
|
||||
cm.text = msg.message;
|
||||
|
||||
cm.name = msg.fromUsername;
|
||||
cm.senderColor = uint(msg.fromColor);
|
||||
cm.translatedColor = uint(msg.fromColor);
|
||||
|
||||
cm.fromTime = msg.fromTime;
|
||||
cm.fromTimezoneOffset = msg.fromTimezoneOffset;
|
||||
@ -72,7 +64,7 @@ package org.bigbluebutton.modules.chat.model
|
||||
var allText:String = "";
|
||||
for (var i:int = 0; i < messages.length; i++){
|
||||
var item:ChatMessage = messages.getItemAt(i) as ChatMessage;
|
||||
allText += "\n" + item.name + " - " + item.time + " : " + item.translatedText;
|
||||
allText += "\n" + item.name + " - " + item.time + " : " + item.text;
|
||||
}
|
||||
return allText;
|
||||
}
|
||||
|
@ -17,30 +17,19 @@
|
||||
*
|
||||
*/
|
||||
package org.bigbluebutton.modules.chat.model {
|
||||
import be.boulevart.google.ajaxapi.translation.GoogleTranslation;
|
||||
import be.boulevart.google.ajaxapi.translation.data.GoogleTranslationResult;
|
||||
import be.boulevart.google.events.GoogleApiEvent;
|
||||
|
||||
import org.bigbluebutton.common.LogUtil;
|
||||
|
||||
public class ChatMessage {
|
||||
[Bindable] public var lastSenderId:String;
|
||||
[Bindable] public var senderId:String;
|
||||
[Bindable] public var senderLanguage:String;
|
||||
[Bindable] public var receiverLanguage:String;
|
||||
[Bindable] public var translate:Boolean;
|
||||
[Bindable] public var senderColor:uint;
|
||||
[Bindable] public var translateLocale:String = "";
|
||||
[Bindable] public var translatedLocaleTooltip:String = "";
|
||||
|
||||
[Bindable] public var name:String;
|
||||
|
||||
[Bindable] public var time:String;
|
||||
[Bindable] public var lastTime:String;
|
||||
[Bindable] public var senderText:String;
|
||||
[Bindable] public var translatedText:String;
|
||||
[Bindable] public var translated:Boolean = false;
|
||||
[Bindable] public var translatedColor:uint;
|
||||
[Bindable] public var text:String;
|
||||
|
||||
|
||||
// Stores the time (millis) when the sender sent the message.
|
||||
@ -52,43 +41,11 @@ package org.bigbluebutton.modules.chat.model {
|
||||
// Stores what we display to the user. The converted fromTime and fromTimezoneOffset to local time.
|
||||
[Bindable] public var senderTime:String;
|
||||
*/
|
||||
private var g:GoogleTranslation;
|
||||
|
||||
public function ChatMessage() {
|
||||
g = new GoogleTranslation();
|
||||
g.addEventListener(GoogleApiEvent.TRANSLATION_RESULT, onTranslationDone);
|
||||
}
|
||||
|
||||
public function translateMessage():void {
|
||||
if (!translate) return;
|
||||
|
||||
if ((senderLanguage != receiverLanguage) && !translated) {
|
||||
// LogUtil.debug("Translating " + senderText + " from " + senderLanguage + " to " + receiverLanguage + ".");
|
||||
g.translate(senderText, senderLanguage, receiverLanguage);
|
||||
} else {
|
||||
// LogUtil.debug("NOT Translating " + senderText + " from " + senderLanguage + " to " + receiverLanguage + ".");
|
||||
}
|
||||
}
|
||||
|
||||
private function onTranslationDone(e:GoogleApiEvent):void {
|
||||
var result:GoogleTranslationResult = e.data as GoogleTranslationResult;
|
||||
|
||||
if (result.result != senderText) {
|
||||
translated = true;
|
||||
// LogUtil.debug("Translated " + senderText + "to " + result.result + ".");
|
||||
|
||||
translatedText = result.result;
|
||||
|
||||
if (lastSenderId != senderId)
|
||||
translateLocale = "<i>" + senderLanguage + "->" + receiverLanguage + "</i>";
|
||||
translatedColor = 0xCF4C5C;
|
||||
}
|
||||
}
|
||||
|
||||
public function toString() : String {
|
||||
var result:String;
|
||||
// Remember to localize this later
|
||||
result = "Chat message " + name + " said " + stripTags(translatedText) + " at " + time;
|
||||
result = "Chat message " + name + " said " + stripTags(text) + " at " + time;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -23,12 +23,6 @@ package org.bigbluebutton.modules.chat.model
|
||||
|
||||
public class ChatOptions
|
||||
{
|
||||
[Bindable]
|
||||
public var translationOn:Boolean = true;
|
||||
|
||||
[Bindable]
|
||||
public var translationEnabled:Boolean = true;
|
||||
|
||||
[Bindable]
|
||||
public var privateEnabled:Boolean = true;
|
||||
|
||||
|
4
bigbluebutton-client/src/org/bigbluebutton/modules/chat/services/ChatMessageService.as
Normal file → Executable file
4
bigbluebutton-client/src/org/bigbluebutton/modules/chat/services/ChatMessageService.as
Normal file → Executable file
@ -43,7 +43,6 @@ package org.bigbluebutton.modules.chat.services
|
||||
msgVO.fromUserID = message.fromUserID;
|
||||
msgVO.fromUsername = message.fromUsername;
|
||||
msgVO.fromColor = message.fromColor;
|
||||
msgVO.fromLang = message.fromLang;
|
||||
msgVO.fromTime = message.fromTime;
|
||||
msgVO.fromTimezoneOffset = message.fromTimezoneOffset;
|
||||
|
||||
@ -60,7 +59,6 @@ package org.bigbluebutton.modules.chat.services
|
||||
msgVO.fromUserID = message.fromUserID;
|
||||
msgVO.fromUsername = message.fromUsername;
|
||||
msgVO.fromColor = message.fromColor;
|
||||
msgVO.fromLang = message.fromLang;
|
||||
msgVO.fromTime = message.fromTime;
|
||||
msgVO.fromTimezoneOffset = message.fromTimezoneOffset;
|
||||
|
||||
@ -96,7 +94,6 @@ package org.bigbluebutton.modules.chat.services
|
||||
msg.fromUserID = SPACE;
|
||||
msg.fromUsername = SPACE;
|
||||
msg.fromColor = "86187";
|
||||
msg.fromLang = "en";
|
||||
msg.fromTime = new Date().getTime();
|
||||
msg.fromTimezoneOffset = new Date().getTimezoneOffset();
|
||||
msg.toUserID = SPACE;
|
||||
@ -115,7 +112,6 @@ package org.bigbluebutton.modules.chat.services
|
||||
msg.fromUserID = SPACE;
|
||||
msg.fromUsername = SPACE;
|
||||
msg.fromColor = "86187";
|
||||
msg.fromLang = "en";
|
||||
msg.fromTime = new Date().getTime();
|
||||
msg.fromTimezoneOffset = new Date().getTimezoneOffset();
|
||||
msg.toUserID = SPACE;
|
||||
|
@ -80,7 +80,6 @@ package org.bigbluebutton.modules.chat.services
|
||||
msg.fromUserID = message.fromUserID;
|
||||
msg.fromUsername = message.fromUsername;
|
||||
msg.fromColor = message.fromColor;
|
||||
msg.fromLang = message.fromLang;
|
||||
msg.fromTime = message.fromTime;
|
||||
msg.fromTimezoneOffset = message.fromTimezoneOffset;
|
||||
msg.toUserID = message.toUserID;
|
||||
@ -104,7 +103,6 @@ package org.bigbluebutton.modules.chat.services
|
||||
msg.fromUserID = message.fromUserID;
|
||||
msg.fromUsername = message.fromUsername;
|
||||
msg.fromColor = message.fromColor;
|
||||
msg.fromLang = message.fromLang;
|
||||
msg.fromTime = message.fromTime;
|
||||
msg.fromTimezoneOffset = message.fromTimezoneOffset;
|
||||
msg.toUserID = message.toUserID;
|
||||
|
3
bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatBox.mxml
Normal file → Executable file
3
bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatBox.mxml
Normal file → Executable file
@ -246,7 +246,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
msg.fromUserID = SPACE;
|
||||
msg.fromUsername = SPACE;
|
||||
msg.fromColor = "0";
|
||||
msg.fromLang = "en";
|
||||
msg.fromTime = new Date().getTime();
|
||||
msg.fromTimezoneOffset = new Date().getTimezoneOffset();
|
||||
msg.toUserID = SPACE;
|
||||
@ -553,7 +552,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
cm.fromUsername = UsersUtil.getMyUsername();
|
||||
// get the color value from ColorPicker
|
||||
cm.fromColor = cmpColorPicker.selectedColor.toString();
|
||||
cm.fromLang = ChatUtil.getUserLang();
|
||||
// Get the current UTC time and the timezone for this sender.
|
||||
// The receiver will have to convert this to local time.
|
||||
var now:Date = new Date();
|
||||
@ -574,7 +572,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
cm.fromUsername = UsersUtil.getMyUsername();
|
||||
// get the color value from ColorPicker
|
||||
cm.fromColor = cmpColorPicker.selectedColor.toString();
|
||||
cm.fromLang = ChatUtil.getUserLang();
|
||||
|
||||
// Get the current UTC time and the timezone for this sender.
|
||||
// The receiver will have to convert this to local time.
|
||||
|
@ -33,10 +33,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import org.bigbluebutton.common.LogUtil;
|
||||
import org.bigbluebutton.modules.chat.model.ChatMessage;
|
||||
|
||||
[Bindable] private var rolledOver:Boolean = false;
|
||||
[Bindable] private var chatMsg:ChatMessage;
|
||||
[Bindable] private var chatTime:String;
|
||||
|
||||
private function onLinkClick(e:TextEvent):void{
|
||||
trace("Clicked on link[" + e.text + "] from chat");
|
||||
@ -74,8 +70,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
if (data == null) return;
|
||||
|
||||
data.translateMessage();
|
||||
|
||||
// The visibility check has to go here becasue ORs don't work with databinding
|
||||
lblTime.visible = (!(data.lastTime == data.time) || !(data.senderId == data.lastSenderId));
|
||||
// check the visibility of the name as well because events might fire in different order
|
||||
@ -90,14 +84,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
validateNow();
|
||||
}
|
||||
|
||||
private function onRollOver():void{
|
||||
rolledOver = true;
|
||||
}
|
||||
|
||||
private function onRollOut():void{
|
||||
rolledOver = false;
|
||||
}
|
||||
|
||||
private function onKeyDown(event:KeyboardEvent):void{
|
||||
if(event.ctrlKey == true && event.keyCode == Keyboard.C){
|
||||
System.setClipboard(UITextField(txtMessage.mx_internal::getTextField()).selectedText);
|
||||
@ -108,11 +94,11 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
</mx:Script>
|
||||
|
||||
<mx:Canvas width="100%" id="hbHeader">
|
||||
<mx:Label id="lblName" text="{data.name} " visible="true" color="gray" textAlign="left" left="0"/>
|
||||
<mx:Text id="lblTime" htmlText="{data.translateLocale} {data.time}" textAlign="right"
|
||||
<mx:Label id="lblName" text="{data.name}" visible="true" color="gray" textAlign="left" left="0"/>
|
||||
<mx:Text id="lblTime" htmlText="{data.time}" textAlign="right"
|
||||
visible="true"
|
||||
color="gray" right="0" />
|
||||
</mx:Canvas>
|
||||
<mx:Text id="txtMessage" htmlText="{rolledOver ? data.senderText : data.translatedText}" link="onLinkClick(event)" color="{data.senderColor}"
|
||||
rollOver="onRollOver()" rollOut="onRollOut()" keyDown="onKeyDown(event)" paddingLeft="5" width="100%" selectable="true"/>
|
||||
<mx:Text id="txtMessage" htmlText="{data.text}" link="onLinkClick(event)" color="{data.senderColor}"
|
||||
keyDown="onKeyDown(event)" paddingLeft="5" width="100%" selectable="true"/>
|
||||
</mx:VBox>
|
||||
|
@ -35,8 +35,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
<mx:Script>
|
||||
<![CDATA[
|
||||
import be.boulevart.google.ajaxapi.translation.GoogleTranslation;
|
||||
import be.boulevart.google.events.GoogleApiEvent;
|
||||
import com.asfusion.mate.events.Dispatcher;
|
||||
import flash.accessibility.Accessibility;
|
||||
import flash.events.Event;
|
||||
@ -80,8 +78,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
private var tabBox:AddChatTabBox;
|
||||
private var publicBox:ChatBox;
|
||||
private var focus:Boolean = true;
|
||||
private var globalDispatcher:Dispatcher = new Dispatcher();
|
||||
private var autoTranslation:Boolean=false;
|
||||
private var globalDispatcher:Dispatcher = new Dispatcher();
|
||||
|
||||
[Bindable] public var chatOptions:ChatOptions;
|
||||
|
||||
|
@ -32,8 +32,6 @@ package org.bigbluebutton.modules.chat.vo
|
||||
// Stores the timezone offset (in minutes) when the message was
|
||||
// sent. This is used by the receiver to convert to locale time.
|
||||
public var fromTimezoneOffset:Number;
|
||||
|
||||
public var fromLang:String;
|
||||
|
||||
// The receiver.
|
||||
public var toUserID:String = "public_chat_userid";
|
||||
@ -49,7 +47,6 @@ package org.bigbluebutton.modules.chat.vo
|
||||
m.fromColor = fromColor;
|
||||
m.fromTime = fromTime;
|
||||
m.fromTimezoneOffset = fromTimezoneOffset;
|
||||
m.fromLang = fromLang;
|
||||
m.message = message;
|
||||
m.toUserID = toUserID;
|
||||
m.toUsername = toUsername;
|
||||
|
@ -49,10 +49,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
import com.asfusion.mate.events.Dispatcher;
|
||||
|
||||
import flexlib.mdi.events.MDIWindowEvent;
|
||||
import flexlib.scheduling.scheduleClasses.BackgroundItem;
|
||||
|
||||
import mx.core.UIComponent;
|
||||
|
||||
import flexlib.scheduling.scheduleClasses.BackgroundItem;
|
||||
import mx.core.UIComponent;
|
||||
import org.bigbluebutton.common.Images;
|
||||
import org.bigbluebutton.common.LogUtil;
|
||||
import org.bigbluebutton.common.events.LocaleChangeEvent;
|
||||
@ -111,7 +109,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
cursor.graphics.lineStyle(6, 0xFF0000, 0.6);
|
||||
cursor.graphics.drawCircle(0,0,3);
|
||||
|
||||
if (isUsingChromeOnMac()) {
|
||||
if (isUsingLessThanChrome38OnMac()) {
|
||||
setCurrentState("chromeOnMacWarningState");
|
||||
}
|
||||
else {
|
||||
@ -366,8 +364,12 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
var CHECK_JAVA_URL:String = BBB.initConfigManager().config.javaTest.url;
|
||||
navigateToURL(new URLRequest(CHECK_JAVA_URL));
|
||||
}
|
||||
private function isUsingChromeOnMac():Boolean {
|
||||
return ((ExternalInterface.call("determineBrowser")[0] == "Chrome") && (Capabilities.os.indexOf("Mac") >= 0));
|
||||
private function isUsingLessThanChrome38OnMac():Boolean {
|
||||
var browser:Array = ExternalInterface.call("determineBrowser");
|
||||
|
||||
return ((browser[0] == "Chrome")
|
||||
&& (parseInt(browser[1]) <= 38)
|
||||
&& (Capabilities.os.indexOf("Mac") >= 0));
|
||||
}
|
||||
]]>
|
||||
</mx:Script>
|
||||
|
@ -49,6 +49,7 @@ package org.bigbluebutton.modules.phone.managers
|
||||
browserType = browserInfo[0];
|
||||
browserVersion = browserInfo[1];
|
||||
}
|
||||
options = new PhoneOptions();
|
||||
}
|
||||
|
||||
private function isWebRTCSupported():Boolean {
|
||||
@ -67,7 +68,6 @@ package org.bigbluebutton.modules.phone.managers
|
||||
|
||||
|
||||
private function checkIfToUseWebRTC():Boolean {
|
||||
options = new PhoneOptions();
|
||||
var webRTCSupported:Boolean = isWebRTCSupported();
|
||||
|
||||
trace(LOG + "- checkIfToUseWebRTC - useWebRTCIfAvailable=[" + options.useWebRTCIfAvailable
|
||||
|
@ -324,7 +324,7 @@ enable_webrtc(){
|
||||
sed -i "s@useWebrtcIfAvailable=\".*\"@useWebrtcIfAvailable=\"true\"@g" /var/www/bigbluebutton/client/conf/config.xml
|
||||
|
||||
# Enable port 5066 in FreeSWITCH
|
||||
sed -i "s@<!--<param name=\"ws-binding\" value=\":5066\"/>-->@<param name=\"ws-binding\" value=\":5066\"/>@g" /opt/freeswitch/conf/sip_profiles/external.xml
|
||||
sed -i 's/^.*<!--<param name="ws-binding" value=":5066"\/>-->.*$/\t<param name="ws-binding" value=":5066"\/>/g' /opt/freeswitch/conf/sip_profiles/external.xml
|
||||
|
||||
sed -i "s/proxy_pass .*/proxy_pass http:\/\/$IP:5066;/g" /etc/bigbluebutton/nginx/sip.nginx
|
||||
|
||||
@ -347,7 +347,7 @@ disable_webrtc(){
|
||||
sed -i "s@useWebrtcIfAvailable=\".*\"@useWebrtcIfAvailable=\"false\"@g" /var/www/bigbluebutton/client/conf/config.xml
|
||||
|
||||
# Disable port 5066 in FreeSWITCH
|
||||
sed -i "s@<param name=\"ws-binding\" value=\":5066\"/>@<!--<param name=\"ws-binding\" value=\":5066\"/>-->@g" /opt/freeswitch/conf/sip_profiles/external.xml
|
||||
sed -i 's/^.*<param name="ws-binding" value=":5066"\/>.*$/\t<!--<param name="ws-binding" value=":5066"\/>-->/g' /opt/freeswitch/conf/sip_profiles/external.xml
|
||||
|
||||
sed -i "s/proxy_pass .*/proxy_pass http:\/\/127.0.0.1:5066;/g" /etc/bigbluebutton/nginx/sip.nginx
|
||||
|
||||
|
@ -78,3 +78,9 @@ remove_raw_of_published_recordings(){
|
||||
|
||||
#remove_raw_of_published_recordings
|
||||
|
||||
#
|
||||
# Remove old *.afm and *.pfb files from /tmp directory (these were created by Ghostscript)
|
||||
#
|
||||
find /tmp -name "*.afm" -mtime +10 -exec rm '{}' \;
|
||||
find /tmp -name "*.pfb" -mtime +10 -exec rm '{}' \;
|
||||
|
||||
|
@ -282,7 +282,7 @@ public class MeetingService implements MessageListener {
|
||||
|
||||
log.info("Create meeting: data={}", logStr);
|
||||
|
||||
messagingService.createMeeting(m.getInternalId(), m.getName(), m.isRecord(),
|
||||
messagingService.createMeeting(m.getInternalId(), m.getExternalId(), m.getName(), m.isRecord(),
|
||||
m.getTelVoice(), m.getDuration(), m.getAutoStartRecording(), m.getAllowStartStopRecording());
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ public class Constants {
|
||||
public static final String HEADER = "header";
|
||||
public static final String PAYLOAD = "payload";
|
||||
public static final String MEETING_ID = "meeting_id";
|
||||
public static final String EXTERNAL_MEETING_ID = "external_meeting_id";
|
||||
public static final String TIMESTAMP = "timestamp";
|
||||
public static final String USER_ID = "userid";
|
||||
public static final String RECORDED = "recorded";
|
||||
|
@ -27,6 +27,7 @@ public class MessageToJson {
|
||||
public static String createMeetingMessageToJson(CreateMeetingMessage msg) {
|
||||
HashMap<String, Object> payload = new HashMap<String, Object>();
|
||||
payload.put(Constants.MEETING_ID, msg.id);
|
||||
payload.put(Constants.EXTERNAL_MEETING_ID, msg.externalId);
|
||||
payload.put(Constants.NAME, msg.name);
|
||||
payload.put(Constants.RECORDED, msg.record);
|
||||
payload.put(Constants.VOICE_CONF, msg.voiceBridge);
|
||||
|
@ -25,7 +25,7 @@ import java.util.Map;
|
||||
public interface MessagingService {
|
||||
void recordMeetingInfo(String meetingId, Map<String, String> info);
|
||||
void destroyMeeting(String meetingID);
|
||||
void createMeeting(String meetingID, String meetingName, Boolean recorded,
|
||||
void createMeeting(String meetingID, String externalMeetingID, String meetingName, Boolean recorded,
|
||||
String voiceBridge, Long duration, Boolean autoStartRecording,
|
||||
Boolean allowStartStopRecording);
|
||||
void endMeeting(String meetingId);
|
||||
|
@ -65,10 +65,10 @@ public class RedisMessagingService implements MessagingService {
|
||||
sender.send(MessagingConstants.TO_MEETING_CHANNEL, json);
|
||||
}
|
||||
|
||||
public void createMeeting(String meetingID, String meetingName, Boolean recorded,
|
||||
public void createMeeting(String meetingID, String externalMeetingID, String meetingName, Boolean recorded,
|
||||
String voiceBridge, Long duration,
|
||||
Boolean autoStartRecording, Boolean allowStartStopRecording) {
|
||||
CreateMeetingMessage msg = new CreateMeetingMessage(meetingID, meetingName,
|
||||
CreateMeetingMessage msg = new CreateMeetingMessage(meetingID, externalMeetingID, meetingName,
|
||||
recorded, voiceBridge, duration,
|
||||
autoStartRecording, allowStartStopRecording);
|
||||
String json = MessageToJson.createMeetingMessageToJson(msg);
|
||||
|
@ -6,6 +6,7 @@ public class CreateMeetingMessage {
|
||||
public static final String VERSION = "0.0.1";
|
||||
|
||||
public final String id;
|
||||
public final String externalId;
|
||||
public final String name;
|
||||
public final Boolean record;
|
||||
public final String voiceBridge;
|
||||
@ -13,10 +14,11 @@ public class CreateMeetingMessage {
|
||||
public boolean autoStartRecording;
|
||||
public boolean allowStartStopRecording;
|
||||
|
||||
public CreateMeetingMessage(String id, String name, Boolean record,
|
||||
public CreateMeetingMessage(String id, String externalId, String name, Boolean record,
|
||||
String voiceBridge, Long duration,
|
||||
Boolean autoStartRecording, Boolean allowStartStopRecording) {
|
||||
this.id = id;
|
||||
this.externalId = externalId;
|
||||
this.name = name;
|
||||
this.record = record;
|
||||
this.voiceBridge = voiceBridge;
|
||||
|
@ -55,7 +55,7 @@ public class NullMessagingService implements MessagingService {
|
||||
|
||||
}
|
||||
|
||||
public void createMeeting(String meetingID, String meetingName,
|
||||
public void createMeeting(String meetingID, String externalMeetingID, String meetingName,
|
||||
Boolean recorded, String voiceBridge, Long duration) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
|
@ -1,34 +0,0 @@
|
||||
bbb-callbacks
|
||||
-------------
|
||||
|
||||
|
||||
It's a webapp which allows to perform certain callbacks when an event happens in a bigbluebutton session.
|
||||
|
||||
The webapp uses node.js and redis.
|
||||
|
||||
For run: node app.js
|
||||
|
||||
To run in production, put bbb-callback.sh into /etc/init.d/
|
||||
See: https://www.exratione.com/2013/02/nodejs-and-forever-as-a-service-simple-upstart-and-init-scripts-for-ubuntu/
|
||||
|
||||
1. Install node by downloading source from http://nodejs.org/download/. Extract the downloaded file.
|
||||
./configure
|
||||
make
|
||||
sudo make install
|
||||
|
||||
2. Install forever
|
||||
sudo npm -g install forever
|
||||
|
||||
3. Copy the init.d script
|
||||
sudo cp bbb-callback.sh /etc/init.d/bbb-callback.sh
|
||||
sudo chmod a+x /etc/init.d/bbb-callback.sh
|
||||
sudo update-rc.d bbb-callback.sh defaults
|
||||
|
||||
4. Copy bbb-callback dir to /usr/local/bigbluebutton/bbb-callback
|
||||
|
||||
5. How to start/stop the service
|
||||
sudo service bbb-callback.sh start
|
||||
sudo service bbb-callback.sh status
|
||||
sudo service bbb-callback.sh restart
|
||||
sudo service bbb-callback.sh stop
|
||||
|
@ -1,51 +0,0 @@
|
||||
var request = require('request'),
|
||||
redis = require("redis"),
|
||||
subscriber = redis.createClient(),
|
||||
client = redis.createClient();
|
||||
|
||||
subscriber.on("subscribe", function (channel, count) {
|
||||
console.log("subscribed to " + channel);
|
||||
});
|
||||
|
||||
subscriber.on("message", function (channel, message) {
|
||||
var properties;
|
||||
|
||||
try {
|
||||
properties = JSON.parse(message);
|
||||
} catch (e) {
|
||||
// An error has occured, handle it, by e.g. logging it
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
if (properties != undefined){
|
||||
client.lrange("meeting:" + properties.meetingID + ":subscriptions", 0, -1, function(error,reply){
|
||||
reply.forEach(function (sid, index) {
|
||||
console.log("subscriber id = " + sid);
|
||||
client.hgetall("meeting:" + properties.meetingID + ":subscription:" + sid, function(err,rep){
|
||||
if (rep.active == "true") {
|
||||
properties.meetingID = rep.externalMeetingID;
|
||||
var post_options = {
|
||||
uri: rep.callbackURL,
|
||||
method: 'POST',
|
||||
json: properties
|
||||
};
|
||||
|
||||
request(post_options, function (error, response, body) {
|
||||
if (!error && response.statusCode == 200) {
|
||||
console.log("Error calling url: [" + post_options.uri + "]")
|
||||
console.log("Error: [" + JSON.stringify(error) + "]");
|
||||
console.log("Response: [" + JSON.stringify(response) + "]");
|
||||
} else {
|
||||
console.log("Passed calling url: [" + post_options.uri + "]")
|
||||
console.log("Response: [" + JSON.stringify(response) + "]");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
subscriber.subscribe("bigbluebutton:webhook_events");
|
@ -1,156 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# An example init script for running a Node.js process as a service
|
||||
# using Forever as the process monitor. For more configuration options
|
||||
# associated with Forever, see: https://github.com/nodejitsu/forever
|
||||
#
|
||||
# You will need to set the environment variables noted below to conform to
|
||||
# your use case, and change the init info comment block.
|
||||
#
|
||||
# This was written for Debian distributions such as Ubuntu, but should still
|
||||
# work on RedHat, Fedora, or other RPM-based distributions, since none
|
||||
# of the built-in service functions are used. If you do adapt it to a RPM-based
|
||||
# system, you'll need to replace the init info comment block with a chkconfig
|
||||
# comment block.
|
||||
#
|
||||
### BEGIN INIT INFO
|
||||
# Provides: bbb-callback
|
||||
# Required-Start: $syslog $remote_fs
|
||||
# Required-Stop: $syslog $remote_fs
|
||||
# Should-Start: $local_fs
|
||||
# Should-Stop: $local_fs
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: BigBlueButton Callback Application
|
||||
# Description: BigBlueButton application that callback URLs registered to receive events.
|
||||
### END INIT INFO
|
||||
#
|
||||
# Based on:
|
||||
# https://gist.github.com/3748766
|
||||
# https://github.com/hectorcorrea/hectorcorrea.com/blob/master/etc/forever-initd-hectorcorrea.sh
|
||||
# https://www.exratione.com/2011/07/running-a-nodejs-server-as-a-service-using-forever/
|
||||
|
||||
# Source function library. Note that this isn't used here, but remains to be
|
||||
# uncommented by those who want to edit this script to add more functionality.
|
||||
# Note that this is Ubuntu-specific. The scripts and script location are different on
|
||||
# RPM-based distributions.
|
||||
# . /lib/lsb/init-functions
|
||||
|
||||
# The example environment variables below assume that Node.js is
|
||||
# installed into /home/node/local/node by building from source as outlined
|
||||
# here:
|
||||
# https://www.exratione.com/2011/07/running-a-nodejs-server-as-a-service-using-forever/
|
||||
#
|
||||
# It should be easy enough to adapt to the paths to be appropriate to a
|
||||
# package installation, but note that the packages available for Ubuntu in
|
||||
# the default repositories are far behind the times. Most users will be
|
||||
# building from source to get a more recent Node.js version.
|
||||
#
|
||||
# An application name to display in echo text.
|
||||
# NAME="My Application"
|
||||
# The full path to the directory containing the node and forever binaries.
|
||||
# NODE_BIN_DIR=/home/node/local/node/bin
|
||||
# Set the NODE_PATH to the Node.js main node_modules directory.
|
||||
# NODE_PATH=/home/node/local/node/lib/node_modules
|
||||
# The directory containing the application start Javascript file.
|
||||
# APPLICATION_DIRECTORY=/home/node/my-application
|
||||
# The application start Javascript filename.
|
||||
# APPLICATION_START=start-my-application.js
|
||||
# Process ID file path.
|
||||
# PIDFILE=/var/run/my-application.pid
|
||||
# Log file path.
|
||||
# LOGFILE=/var/log/my-application.log
|
||||
#
|
||||
NAME="BBB Calback"
|
||||
NODE_BIN_DIR=/usr/local/bin
|
||||
NODE_PATH=/usr/local/lib/node_modules
|
||||
APPLICATION_DIRECTORY=/usr/local/bigbluebutton/bbb-callback
|
||||
APPLICATION_START=app.js
|
||||
PIDFILE=/var/run/bbb-callback.pid
|
||||
LOGFILE=/var/log/bbb-callback.log
|
||||
|
||||
# Add node to the path for situations in which the environment is passed.
|
||||
PATH=$NODE_BIN_DIR:$PATH
|
||||
# Export all environment variables that must be visible for the Node.js
|
||||
# application process forked by Forever. It will not see any of the other
|
||||
# variables defined in this script.
|
||||
export NODE_PATH=$NODE_PATH
|
||||
|
||||
start() {
|
||||
echo "Starting $NAME"
|
||||
# We're calling forever directly without using start-stop-daemon for the
|
||||
# sake of simplicity when it comes to environment, and because this way
|
||||
# the script will work whether it is executed directly or via the service
|
||||
# utility.
|
||||
#
|
||||
# The minUptime and spinSleepTime settings stop Forever from thrashing if
|
||||
# the application fails immediately on launch. This is generally necessary to
|
||||
# avoid loading development servers to the point of failure every time
|
||||
# someone makes an error in application initialization code, or bringing down
|
||||
# production servers the same way if a database or other critical service
|
||||
# suddenly becomes inaccessible.
|
||||
#
|
||||
# The pidfile contains the child process pid, not the forever process pid.
|
||||
# We're only using it as a marker for whether or not the process is
|
||||
# running.
|
||||
forever --pidFile $PIDFILE --sourceDir $APPLICATION_DIRECTORY \
|
||||
-a -l $LOGFILE --minUptime 5000 --spinSleepTime 2000 \
|
||||
start $APPLICATION_START &
|
||||
RETVAL=$?
|
||||
}
|
||||
|
||||
stop() {
|
||||
if [ -f $PIDFILE ]; then
|
||||
echo "Shutting down $NAME"
|
||||
# Tell Forever to stop the process. Note that doing it this way means
|
||||
# that each application that runs as a service must have a different
|
||||
# start file name, regardless of which directory it is in.
|
||||
forever stop $APPLICATION_START
|
||||
# Get rid of the pidfile, since Forever won't do that.
|
||||
rm -f $PIDFILE
|
||||
RETVAL=$?
|
||||
else
|
||||
echo "$NAME is not running."
|
||||
RETVAL=0
|
||||
fi
|
||||
}
|
||||
|
||||
restart() {
|
||||
echo "Restarting $NAME"
|
||||
stop
|
||||
start
|
||||
}
|
||||
|
||||
status() {
|
||||
echo "Status for $NAME:"
|
||||
# This is taking the lazy way out on status, as it will return a list of
|
||||
# all running Forever processes. You get to figure out what you want to
|
||||
# know from that information.
|
||||
#
|
||||
# On Ubuntu, this isn't even necessary. To find out whether the service is
|
||||
# running, use "service my-application status" which bypasses this script
|
||||
# entirely provided you used the service utility to start the process.
|
||||
forever list
|
||||
RETVAL=$?
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
start
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
status)
|
||||
status
|
||||
;;
|
||||
restart)
|
||||
restart
|
||||
;;
|
||||
*)
|
||||
echo "Usage: {start|stop|status|restart}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
exit $RETVAL
|
||||
|
@ -1,17 +0,0 @@
|
||||
{
|
||||
"name": "bbb-callbacks",
|
||||
"version": "0.8.1",
|
||||
"description": "a module that allows to do API callbacks",
|
||||
"keywords": [
|
||||
"bigbluebutton",
|
||||
"Web API",
|
||||
"callbacks"
|
||||
],
|
||||
"dependencies" : {
|
||||
"redis": "0.8.3",
|
||||
"request": "2.27.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "0.8.21"
|
||||
}
|
||||
}
|
6
labs/bbb-webhooks/.gitignore
vendored
Normal file
6
labs/bbb-webhooks/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
*~
|
||||
**/#*#
|
||||
*.log
|
||||
node_modules/
|
||||
config_local.coffee
|
||||
log/*
|
1
labs/bbb-webhooks/.nvmrc
Normal file
1
labs/bbb-webhooks/.nvmrc
Normal file
@ -0,0 +1 @@
|
||||
0.10.33
|
322
labs/bbb-webhooks/README.md
Normal file
322
labs/bbb-webhooks/README.md
Normal file
@ -0,0 +1,322 @@
|
||||
bbb-webhooks
|
||||
============
|
||||
|
||||
A node.js application that listens for all events on BigBlueButton and POSTs these events to
|
||||
hooks registered via an API. A hook is basically a URL that will receive HTTP POST calls with
|
||||
information about an event when this event happens on BigBlueButton.
|
||||
|
||||
An event can be: a meeting was created, a user joined, a new presentation was uploaded,
|
||||
a user left, a recording is being processed, and many others.
|
||||
|
||||
Registering hooks: API calls
|
||||
----------------------------
|
||||
|
||||
This application adds three new API calls to BigBlueButton's API.
|
||||
|
||||
### Hooks/Create
|
||||
|
||||
Creates a new hook. This call is idempotent: you can call it multiple times with the same parameters
|
||||
without side effects (just like the `/create` call for meetings).
|
||||
Can optionally receive a `meetingID` parameter: if informed, this hook
|
||||
will receive only events for this meeting; otherwise the hook will be global and will receive
|
||||
events for all meetings in the server.
|
||||
|
||||
**Resource URL:** `http://yourserver.com/bigbluebutton/api/hooks/create?[parameters]&checksum=[checksum]`
|
||||
|
||||
**Parameters**:
|
||||
|
||||
| Param Name | Required / Optional | Type | Description |
|
||||
| ------------ | -------------------- | ----- | ----------- |
|
||||
| calllbackURL | Required | String | The URL that will receive a POST call with the events. The same URL cannot be registered more than once. |
|
||||
| meetingID | Optional | String | A meeting ID to bind this hook to an specific meeting. If not informed, the hook will receive events for all meetings. |
|
||||
|
||||
**Response when a hook is successfully registered**:
|
||||
|
||||
```xml
|
||||
<response>
|
||||
<returncode>SUCCESS</returncode>
|
||||
<hookID>1</hookID>
|
||||
</response>
|
||||
```
|
||||
|
||||
**Response when a hook is already registered**:
|
||||
|
||||
```xml
|
||||
<response>
|
||||
<returncode>SUCCESS</returncode>
|
||||
<hookID>1</hookID>
|
||||
<messageKey>duplicateWarning</messageKey>
|
||||
<message>There is already a hook for this callback URL.</message>
|
||||
</response>
|
||||
```
|
||||
|
||||
**Response when there was an error registering the hook**:
|
||||
|
||||
```xml
|
||||
<response>
|
||||
<returncode>FAILED</returncode>
|
||||
<messageKey>createHookError</messageKey>
|
||||
<message>An error happened while creating your hook. Check the logs.</message>
|
||||
</response>
|
||||
```
|
||||
|
||||
|
||||
### Hooks/Destroy
|
||||
|
||||
Remove a previously created hook. A `hookID` must be passed in the parameters to identify
|
||||
the hook to be removed.
|
||||
|
||||
**Resource URL:** `http://yourserver.com/bigbluebutton/api/hooks/destroy?[parameters]&checksum=[checksum]`
|
||||
|
||||
**Parameters**:
|
||||
|
||||
| Param Name | Required / Optional | Type | Description |
|
||||
| ----------- | -------------------- | ----- | ----------- |
|
||||
| hookID | Required | Number | The ID of the hook that should be removed, as returned in the create hook call. |
|
||||
|
||||
**Response when a hook is successfully removed**:
|
||||
|
||||
```xml
|
||||
<response>
|
||||
<returncode>SUCCESS</returncode>
|
||||
<removed>true</removed>
|
||||
</response>
|
||||
```
|
||||
|
||||
**Response when a hook is not found**:
|
||||
|
||||
```xml
|
||||
<response>
|
||||
<returncode>FAILED</returncode>
|
||||
<messageKey>destroyMissingHook</messageKey>
|
||||
<message>The hook informed was not found.</message>
|
||||
</response>
|
||||
```
|
||||
|
||||
**Response when a hook is not passed in the parameters**:
|
||||
|
||||
```xml
|
||||
<response>
|
||||
<returncode>FAILED</returncode>
|
||||
<messageKey>missingParamHookID</messageKey>
|
||||
<message>You must specify a hookID in the parameters.</message>
|
||||
</response>
|
||||
```
|
||||
|
||||
**Response when there was an error removing the hook**:
|
||||
|
||||
```xml
|
||||
<response>
|
||||
<returncode>FAILED</returncode>
|
||||
<messageKey>destroyHookError</messageKey>
|
||||
<message>An error happened while removing your hook. Check the logs.</message>
|
||||
</response>
|
||||
```
|
||||
|
||||
|
||||
### Hooks/List
|
||||
|
||||
Returns the hooks registered. If a meeting ID is informed, will return the hooks created
|
||||
specifically for this meeting plus the global hooks that receive events for this meeting.
|
||||
If no meeting ID is informed, returns all the hooks available (not only the global hooks!).
|
||||
|
||||
**Resource URL:** `http://yourserver.com/bigbluebutton/api/hooks/list?[parameters]&checksum=[checksum]`
|
||||
|
||||
**Parameters**:
|
||||
|
||||
| Param Name | Required / Optional | Type | Description |
|
||||
| ----------- | -------------------- | ----- | ----------- |
|
||||
| meetingID | Optional | String | A meeting ID to restrict the hooks returned only to the hooks that receive events for this meeting. Will include hooks that receive events for this meeting only plus all global hooks. |
|
||||
|
||||
**Response when there are hooks registered**:
|
||||
|
||||
```xml
|
||||
<response>
|
||||
<returncode>SUCCESS</returncode>
|
||||
<hooks>
|
||||
<hook>
|
||||
<hookID>1</hookID>
|
||||
<callbackURL>http://postcatcher.in/catchers/abcdefghijk</callbackURL>
|
||||
<meetingID>my-meeting</meetingID> <!-- a hook created for this meeting only -->
|
||||
</hook>
|
||||
<hook>
|
||||
<hookID>2</hookID>
|
||||
<callbackURL>http://postcatcher.in/catchers/1234567890</callbackURL>
|
||||
<!-- no meetingID means this is a global hook -->
|
||||
</hook>
|
||||
</hooks>
|
||||
</response>
|
||||
```
|
||||
|
||||
**Response when there are no hooks registered**:
|
||||
|
||||
```xml
|
||||
<response>
|
||||
<returncode>FAILED</returncode>
|
||||
<hooks></hooks>
|
||||
</response>
|
||||
```
|
||||
|
||||
|
||||
Callback format
|
||||
---------------
|
||||
|
||||
All hooks registered are called via HTTP POST with all the information about the event in
|
||||
the body of this request. The request is sent with the `Content-type` HTTP header set to
|
||||
`application/x-www-form-urlencoded` and the content in the body is a json object in the
|
||||
following format:
|
||||
|
||||
```
|
||||
event={"header":{},"payload":{}}
|
||||
timestamp=1415900488797
|
||||
```
|
||||
|
||||
The attribute `timestamp` is the timestamp of when this callback was made. If the app tries
|
||||
to make a callback and it fails, it will try again several times more, always using the same
|
||||
timestamp. Timestamps will never be the same for different events and the value will always
|
||||
increase.
|
||||
|
||||
The attribute `event` is a stringified version of all the data from the event as received
|
||||
from redis. The data varies for different types of events, check the documentation for
|
||||
more information.
|
||||
|
||||
This is an example of the data sent for a meeting destroyed event:
|
||||
|
||||
```
|
||||
event={"payload":{"meeting_id":"82fe1e7040551a6044cf375d12d765b5f0f099a4-1415905067841"},"header":{"timestamp":17779896,"name":"meeting_destroyed_event","current_time":1415905177220,"version":"0.0.1"}}
|
||||
timestamp=1415900488797
|
||||
```
|
||||
|
||||
Moreover, the callback call is signed with a checksum, that is included in the URL of the
|
||||
request. If the registered URL is `http://my-server.com/callback`, it will receive the
|
||||
checksum as in `http://my-server.com/callback?checksum=yalsdk18129e019iklasd90i`.
|
||||
|
||||
The way the checksum is created is similar to how the checksums are calculated for
|
||||
BigBlueButton's API calls (take a look at the `setConfigXML` call).
|
||||
|
||||
```
|
||||
sha1(<callback URL>+<data body>+<shared secret>)
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
* `<callback URL>`: The original callback URL, that doesn't include the checksum.
|
||||
* `<data body as a string>`: All the data sent in the body of the request, concatenated and joined by `&`, as if they were parameters in a URL.
|
||||
* `<shared secret>`: The shared secret of the BigBlueButton server.
|
||||
|
||||
So, upon receiving a callback call, an application could validate the checksum as follows:
|
||||
|
||||
* Get the body of the request and convert the string as in the example below:
|
||||
```
|
||||
event={"header":{},"payload":{}}
|
||||
timestamp=1234567890
|
||||
```
|
||||
|
||||
Should become the string below:
|
||||
|
||||
```
|
||||
'event={"header":{},"payload":{}}×tamp=1234567890'
|
||||
```
|
||||
* Concatenate the original callback URL, the string from the previous step, and the BigBlueButton's salt.
|
||||
* Calculate a `sha1()` of this string.
|
||||
* The checksum calculated should equal the checksum received in the parameters of the request.
|
||||
|
||||
|
||||
More details
|
||||
------------
|
||||
* Callbacks are always triggered for one event at a time and in order. They are ordered the same way they appear on pubsub (which might not exactly be the order indicated by their timestamps). The timestamps will almost always be ordered as well, but it's not guaranteed.
|
||||
* The application assumes that events are never duplicated on pubsub. If they happen to be duplicated, the callback calls will also be duplicated.
|
||||
* Hooks are only removed if a call to `/destroy` is made or if the callbacks for the hook fail too many times (~12) for a long period of time (~5min). They are never removed otherwise. Valid for both global hooks and hooks for specific meetings.
|
||||
* Mappings are removed after 24 hours of inactivity. If there are no events at all for a given meeting, it will be assumed dead. This is done to prevent data from being stored forever on redis.
|
||||
* External URLs are expected to respond with the HTTP status 2xx (200 would be the default expected). Anything different from these values will be considered an error and the callback will be called again. This includes URLs that redirect to some other place.
|
||||
* If a meeting is created while the webhooks app is down, callbacks will never be triggered for this meeting. The app needs to detect the create event (`meeting_created_message`) to have a mapping of internal to external meeting IDs.
|
||||
|
||||
Development
|
||||
-----------
|
||||
|
||||
1. Install node. You can use [NVM](https://github.com/creationix/nvm) if you need multiple versions of node or install it from source. To install from source, first check the exact version you need on `package.json` and replace the all `vX.X.X` by the correct version when running the commands below.
|
||||
|
||||
```bash
|
||||
wget http://nodejs.org/dist/vX.X.X/node-vX.X.X.tar.gz
|
||||
tar -xvf node-vX.X.X.tar.gz
|
||||
cd node-vX.X.X/
|
||||
./configure
|
||||
make
|
||||
sudo make install
|
||||
```
|
||||
|
||||
2. Install the dependencies: `npm install`
|
||||
|
||||
3. Copy and edit the configuration file: `cp config_local.coffee.example config_local.coffee`
|
||||
|
||||
4. Run the application with:
|
||||
|
||||
```bash
|
||||
node app.js
|
||||
```
|
||||
|
||||
5. To test it you can use the test application `post_catcher.js`. It starts a node app that
|
||||
registers a hook on the webhooks app and prints all the events it receives. Open the file
|
||||
at `extra/post_catcher.js` and edit the salt and domain/IP of your server and then run it
|
||||
with `node extra/post_catcher.js`. Create meetings and do things on your BigBlueButton server
|
||||
and the events should be shown in the post_catcher.
|
||||
|
||||
Another option is to create hooks with the [API Mate](http://mconf.github.io/api-mate/) and
|
||||
catch the callbacks with [PostCatcher](http://postcatcher.in/).
|
||||
|
||||
Production
|
||||
----------
|
||||
|
||||
1. Install node. First check the exact version you need on `package.json` and replace the all `vX.X.X` by the correct version in the commands below.
|
||||
|
||||
```bash
|
||||
wget http://nodejs.org/dist/vX.X.X/node-vX.X.X.tar.gz
|
||||
tar -xvf node-vX.X.X.tar.gz
|
||||
cd node-vX.X.X/
|
||||
./configure
|
||||
make
|
||||
sudo make install
|
||||
```
|
||||
|
||||
2. Copy the entire webhooks directory to `/usr/local/bigbluebutton/bbb-webhooks` and cd into it.
|
||||
|
||||
3. Install the dependencies: `npm install`
|
||||
|
||||
4. Copy and edit the configuration file to adapt to your server: `cp config_local.coffee.example config_local.coffee`.
|
||||
|
||||
5. Drop the nginx configuration file in its place: `cp config/webhooks.nginx /etc/bigbluebutton/nginx/`.
|
||||
If you changed the port of the web server on your configuration file, you will have to edit it in `webhooks.nginx` as well.
|
||||
|
||||
6. Copy upstart's configuration file and make sure its permissions are ok:
|
||||
|
||||
```bash
|
||||
sudo cp config/upstart-bbb-webhooks.conf /etc/init/bbb-webhooks.conf
|
||||
sudo chown root:root /etc/init/bbb-webhooks.conf
|
||||
```
|
||||
|
||||
Open the file and edit it. You might need to change things like the user used to run the application.
|
||||
|
||||
7. Copy monit's configuration file and make sure its permissions are ok:
|
||||
|
||||
```bash
|
||||
sudo cp config/monit-bbb-webhooks /etc/monit/conf.d/bbb-webhooks
|
||||
sudo chown root:root /etc/monit/conf.d/bbb-webhooks
|
||||
```
|
||||
|
||||
Open the file and edit it. You might need to change things like the port used by the application.
|
||||
|
||||
8. Copy logrotate's configuration file and install it:
|
||||
|
||||
```bash
|
||||
sudo cp config/bbb-webhooks.logrotate /etc/logrotate.d/bbb-webhooks
|
||||
sudo chown root:root /etc/logrotate.d/bbb-webhooks
|
||||
sudo chmod 644 /etc/logrotate.d/bbb-webhooks
|
||||
sudo logrotate -s /var/lib/logrotate/status /etc/logrotate.d/bbb-webhooks
|
||||
```
|
||||
|
||||
9. Start the application:
|
||||
|
||||
```bash
|
||||
sudo service bbb-webhooks start
|
||||
sudo service bbb-webhooks stop
|
||||
```
|
7
labs/bbb-webhooks/app.js
Executable file
7
labs/bbb-webhooks/app.js
Executable file
@ -0,0 +1,7 @@
|
||||
// This is a simple wrapper to run the app with 'node app.js'
|
||||
|
||||
require("coffee-script/register");
|
||||
|
||||
Application = require('./application.coffee');
|
||||
application = new Application();
|
||||
application.start();
|
20
labs/bbb-webhooks/application.coffee
Normal file
20
labs/bbb-webhooks/application.coffee
Normal file
@ -0,0 +1,20 @@
|
||||
config = require("./config")
|
||||
Hook = require("./hook")
|
||||
IDMapping = require("./id_mapping")
|
||||
WebHooks = require("./web_hooks")
|
||||
WebServer = require("./web_server")
|
||||
|
||||
# Class that defines the application. Listens for events on redis and starts the
|
||||
# process to perform the callback calls.
|
||||
# TODO: add port (-p) and log level (-l) to the command line args
|
||||
module.exports = class Application
|
||||
|
||||
constructor: ->
|
||||
@webHooks = new WebHooks()
|
||||
@webServer = new WebServer()
|
||||
|
||||
start: ->
|
||||
Hook.initialize =>
|
||||
IDMapping.initialize =>
|
||||
@webServer.start(config.server.port)
|
||||
@webHooks.start()
|
87
labs/bbb-webhooks/callback_emitter.coffee
Normal file
87
labs/bbb-webhooks/callback_emitter.coffee
Normal file
@ -0,0 +1,87 @@
|
||||
_ = require('lodash')
|
||||
request = require("request")
|
||||
url = require('url')
|
||||
EventEmitter = require('events').EventEmitter
|
||||
|
||||
config = require("./config")
|
||||
Logger = require("./logger")
|
||||
Utils = require("./utils")
|
||||
|
||||
# Use to perform a callback. Will try several times until the callback is
|
||||
# properly emitted and stop when successful (or after a given number of tries).
|
||||
# Used to emit a single callback. Destroy it and create a new class for a new callback.
|
||||
# Emits "success" on success, "failure" on error and "stopped" when gave up trying
|
||||
# to perform the callback.
|
||||
module.exports = class CallbackEmitter extends EventEmitter
|
||||
|
||||
constructor: (@callbackURL, @message) ->
|
||||
@nextInterval = 0
|
||||
@timestap = 0
|
||||
|
||||
start: ->
|
||||
@timestamp = new Date().getTime()
|
||||
@nextInterval = 0
|
||||
@_scheduleNext 0
|
||||
|
||||
_scheduleNext: (timeout) ->
|
||||
setTimeout( =>
|
||||
@_emitMessage (error, result) =>
|
||||
if not error? and result
|
||||
@emit "success"
|
||||
else
|
||||
@emit "failure", error
|
||||
|
||||
# get the next interval we have to wait and schedule a new try
|
||||
interval = config.hooks.retryIntervals[@nextInterval]
|
||||
if interval?
|
||||
Logger.warn "xx> Trying the callback again in #{interval/1000.0} secs"
|
||||
@nextInterval++
|
||||
@_scheduleNext(interval)
|
||||
|
||||
# no intervals anymore, time to give up
|
||||
else
|
||||
@nextInterval = 0
|
||||
@emit "stopped"
|
||||
|
||||
, timeout)
|
||||
|
||||
_emitMessage: (callback) ->
|
||||
# data to be sent
|
||||
# note: keep keys in alphabetical order
|
||||
data =
|
||||
event: JSON.stringify(@message)
|
||||
timestamp: @timestamp
|
||||
|
||||
# calculate the checksum
|
||||
checksum = Utils.checksum("#{@callbackURL}#{JSON.stringify(data)}#{config.bbb.sharedSecret}")
|
||||
|
||||
# get the final callback URL, including the checksum
|
||||
urlObj = url.parse(@callbackURL, true)
|
||||
callbackURL = @callbackURL
|
||||
callbackURL += if _.isEmpty(urlObj.search) then "?" else "&"
|
||||
callbackURL += "checksum=#{checksum}"
|
||||
|
||||
requestOptions =
|
||||
followRedirect: true
|
||||
maxRedirects: 10
|
||||
uri: callbackURL
|
||||
method: "POST"
|
||||
form: data
|
||||
|
||||
request requestOptions, (error, response, body) ->
|
||||
if error? or not (response?.statusCode >= 200 and response?.statusCode < 300)
|
||||
Logger.warn "xx> Error in the callback call to: [#{requestOptions.uri}] for #{simplifiedEvent(data.event)}"
|
||||
Logger.warn "xx> Error:", error
|
||||
Logger.warn "xx> Status:", response?.statusCode
|
||||
callback error, false
|
||||
else
|
||||
Logger.info "==> Successful callback call to: [#{requestOptions.uri}] for #{simplifiedEvent(data.event)}"
|
||||
callback null, true
|
||||
|
||||
# A simple string that identifies the event
|
||||
simplifiedEvent = (event) ->
|
||||
try
|
||||
eventJs = JSON.parse(event)
|
||||
"event: { name: #{eventJs.header?.name}, timestamp: #{eventJs.header?.timestamp} }"
|
||||
catch e
|
||||
"event: #{event}"
|
102
labs/bbb-webhooks/config.coffee
Normal file
102
labs/bbb-webhooks/config.coffee
Normal file
@ -0,0 +1,102 @@
|
||||
# Global configuration file
|
||||
|
||||
# load the local configs
|
||||
config = require("./config_local")
|
||||
|
||||
# BigBlueButton configs
|
||||
config.bbb or= {}
|
||||
config.bbb.sharedSecret or= "33e06642a13942004fd83b3ba6e4104a"
|
||||
config.bbb.apiPath or= "/bigbluebutton/api"
|
||||
|
||||
# Web server configs
|
||||
config.server or= {}
|
||||
config.server.port or= 3005
|
||||
|
||||
# Web hooks configs
|
||||
config.hooks or= {}
|
||||
config.hooks.pchannel or= "bigbluebutton:*"
|
||||
config.hooks.meetingsChannel or= "bigbluebutton:from-bbb-apps:meeting"
|
||||
|
||||
# Filters to the events we want to generate callback calls for
|
||||
config.hooks.events or= [
|
||||
{ channel: "bigbluebutton:from-bbb-apps:meeting", name: "meeting_created_message" },
|
||||
{ channel: "bigbluebutton:from-bbb-apps:meeting", name: "meeting_destroyed_event" },
|
||||
{ channel: "bigbluebutton:from-bbb-apps:users", name: "user_joined_message" },
|
||||
{ channel: "bigbluebutton:from-bbb-apps:users", name: "user_left_message" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "sanity_started" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "sanity_ended" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "archive_started" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "archive_ended" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "post_archive_started" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "post_archive_ended" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "process_started" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "process_ended" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "post_process_started" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "post_process_ended" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "publish_started" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "publish_ended" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "post_publish_started" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "post_publish_ended" }
|
||||
]
|
||||
|
||||
# Retry intervals for failed attempts for perform callback calls.
|
||||
# In ms. Totals to around 5min.
|
||||
config.hooks.retryIntervals = [
|
||||
100, 500, 1000, 2000, 4000, 8000, 10000, 30000, 60000, 60000, 60000, 60000
|
||||
]
|
||||
|
||||
# Mappings of internal to external meeting IDs
|
||||
config.mappings = {}
|
||||
config.mappings.cleanupInterval = 10000 # 10 secs, in ms
|
||||
config.mappings.timeout = 1000*60*60*24 # 24 hours, in ms
|
||||
|
||||
# Redis
|
||||
config.redis = {}
|
||||
config.redis.keys = {}
|
||||
config.redis.keys.hook = (id) -> "bigbluebutton:webhooks:hook:#{id}"
|
||||
config.redis.keys.hooks = "bigbluebutton:webhooks:hooks"
|
||||
config.redis.keys.mappings = "bigbluebutton:webhooks:mappings"
|
||||
config.redis.keys.mapping = (id) -> "bigbluebutton:webhooks:mapping:#{id}"
|
||||
|
||||
config.api = {}
|
||||
config.api.responses = {}
|
||||
config.api.responses.failure = (key, msg) ->
|
||||
"<response> \
|
||||
<returncode>FAILED</returncode> \
|
||||
<messageKey>#{key}</messageKey> \
|
||||
<message>#{msg}</message> \
|
||||
</response>"
|
||||
config.api.responses.checksumError =
|
||||
config.api.responses.failure("checksumError", "You did not pass the checksum security check.")
|
||||
|
||||
config.api.responses.createSuccess = (id) ->
|
||||
"<response> \
|
||||
<returncode>SUCCESS</returncode> \
|
||||
<hookID>#{id}</hookID> \
|
||||
</response>"
|
||||
config.api.responses.createFailure =
|
||||
config.api.responses.failure("createHookError", "An error happened while creating your hook. Check the logs.")
|
||||
config.api.responses.createDuplicated = (id) ->
|
||||
"<response> \
|
||||
<returncode>SUCCESS</returncode> \
|
||||
<hookID>#{id}</hookID> \
|
||||
<messageKey>duplicateWarning</messageKey> \
|
||||
<message>There is already a hook for this callback URL.</message> \
|
||||
</response>"
|
||||
|
||||
config.api.responses.destroySuccess =
|
||||
"<response> \
|
||||
<returncode>SUCCESS</returncode> \
|
||||
<removed>true</removed> \
|
||||
</response>"
|
||||
config.api.responses.destroyFailure =
|
||||
config.api.responses.failure("destroyHookError", "An error happened while removing your hook. Check the logs.")
|
||||
config.api.responses.destroyNoHook =
|
||||
config.api.responses.failure("destroyMissingHook", "The hook informed was not found.")
|
||||
|
||||
config.api.responses.missingParamCallbackURL =
|
||||
config.api.responses.failure("missingParamCallbackURL", "You must specify a callbackURL in the parameters.")
|
||||
config.api.responses.missingParamHookID =
|
||||
config.api.responses.failure("missingParamHookID", "You must specify a hookID in the parameters.")
|
||||
|
||||
module.exports = config
|
8
labs/bbb-webhooks/config/bbb-webhooks.logrotate
Normal file
8
labs/bbb-webhooks/config/bbb-webhooks.logrotate
Normal file
@ -0,0 +1,8 @@
|
||||
/usr/local/bigbluebutton/bbb-webhooks/log/*.log {
|
||||
size 300M
|
||||
copytruncate
|
||||
rotate 30
|
||||
compress
|
||||
missingok
|
||||
notifempty
|
||||
}
|
12
labs/bbb-webhooks/config/monit-bbb-webhooks
Executable file
12
labs/bbb-webhooks/config/monit-bbb-webhooks
Executable file
@ -0,0 +1,12 @@
|
||||
#!monit
|
||||
set logfile /var/log/monit.log
|
||||
|
||||
check process bbb-webhooks with pidfile "/var/run/bbb-webhooks.pid"
|
||||
start program = "/sbin/start bbb-webhooks"
|
||||
stop program = "/sbin/stop bbb-webhooks"
|
||||
|
||||
if failed port 3005 protocol HTTP
|
||||
request /bigbluebutton/api/hooks/ping
|
||||
with timeout 30 seconds
|
||||
then restart
|
||||
# if 5 restarts within 5 cycles then timeout
|
34
labs/bbb-webhooks/config/upstart-bbb-webhooks.conf
Normal file
34
labs/bbb-webhooks/config/upstart-bbb-webhooks.conf
Normal file
@ -0,0 +1,34 @@
|
||||
# bbb-webhooks
|
||||
|
||||
description "bbb-webhooks"
|
||||
author "BigBlueButton"
|
||||
|
||||
start on (local-filesystems and net-device-up IFACE=eth3)
|
||||
stop on shutdown
|
||||
|
||||
# respawn # we're using monit for it
|
||||
|
||||
env USER=firstuser
|
||||
env APP=app.js
|
||||
env CMD_OPTS=""
|
||||
env SRC_DIR="/usr/local/bigbluebutton/bbb-webhooks"
|
||||
env LOGFILE="/var/log/bbb-webhooks.log"
|
||||
env NODE=/usr/local/bin/node
|
||||
env PIDFILE="/var/run/bbb-webhooks.pid"
|
||||
env NODE_ENV="production"
|
||||
|
||||
script
|
||||
cd $SRC_DIR
|
||||
echo $$ > $PIDFILE
|
||||
exec sudo -u $USER NODE_ENV=$NODE_ENV $NODE $APP $CMD_OPTS 1>> $LOGFILE 2>> $LOGFILE
|
||||
end script
|
||||
|
||||
pre-start script
|
||||
# Date format same as (new Date()).toISOString() for consistency
|
||||
echo "[`date -u +%Y-%m-%dT%T.%3NZ`] (sys) Starting" >> $LOGFILE
|
||||
end script
|
||||
|
||||
pre-stop script
|
||||
rm $PIDFILE
|
||||
echo "[`date -u +%Y-%m-%dT%T.%3NZ`] (sys) Stopping" >> $LOGFILE
|
||||
end script
|
9
labs/bbb-webhooks/config/webhooks.nginx
Normal file
9
labs/bbb-webhooks/config/webhooks.nginx
Normal file
@ -0,0 +1,9 @@
|
||||
# Pass to the webhooks app all requests made to the webhooks API.
|
||||
location /bigbluebutton/api/hooks {
|
||||
proxy_pass http://127.0.0.1:3005;
|
||||
proxy_redirect default;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
}
|
36
labs/bbb-webhooks/config_local.coffee.example
Normal file
36
labs/bbb-webhooks/config_local.coffee.example
Normal file
@ -0,0 +1,36 @@
|
||||
# Local configuration file
|
||||
|
||||
config = {}
|
||||
|
||||
# Shared secret of your BigBlueButton server.
|
||||
config.bbb = {}
|
||||
config.bbb.sharedSecret = "33e06642a13942004fd83b3ba6e4104a"
|
||||
|
||||
# The port in which the API server will run.
|
||||
config.server = {}
|
||||
config.server.port = 3005
|
||||
|
||||
# Callbacks will be triggered for all the events in this list and only for these events.
|
||||
config.hooks = {}
|
||||
config.hooks.events = [
|
||||
{ channel: "bigbluebutton:from-bbb-apps:meeting", name: "meeting_created_message" },
|
||||
{ channel: "bigbluebutton:from-bbb-apps:meeting", name: "meeting_destroyed_event" },
|
||||
{ channel: "bigbluebutton:from-bbb-apps:users", name: "user_joined_message" },
|
||||
{ channel: "bigbluebutton:from-bbb-apps:users", name: "user_left_message" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "sanity_started" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "sanity_ended" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "archive_started" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "archive_ended" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "post_archive_started" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "post_archive_ended" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "process_started" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "process_ended" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "post_process_started" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "post_process_ended" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "publish_started" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "publish_ended" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "post_publish_started" },
|
||||
{ channel: "bigbluebutton:from-rap", name: "post_publish_ended" }
|
||||
]
|
||||
|
||||
module.exports = config
|
53
labs/bbb-webhooks/extra/events.js
Normal file
53
labs/bbb-webhooks/extra/events.js
Normal file
@ -0,0 +1,53 @@
|
||||
// Lists all the events that happen in a meeting. Run with 'node events.js'.
|
||||
// Uses the first meeting started after the application runs and will list all
|
||||
// events, but only the first time they happen.
|
||||
|
||||
redis = require("redis");
|
||||
|
||||
var target_meeting = null;
|
||||
var events_printed = [];
|
||||
var subscriber = redis.createClient();
|
||||
|
||||
subscriber.on("psubscribe", function(channel, count) {
|
||||
console.log("subscribed to " + channel);
|
||||
});
|
||||
|
||||
subscriber.on("pmessage", function(pattern, channel, message) {
|
||||
try {
|
||||
message = JSON.parse(message);
|
||||
if (message !== null && message !== undefined && message.header !== undefined) {
|
||||
|
||||
var message_meeting_id = message.payload.meeting_id;
|
||||
var message_name = message.header.name;
|
||||
|
||||
if (message_name === "meeting_created_message") {
|
||||
if (target_meeting === null) {
|
||||
target_meeting = message_meeting_id;
|
||||
}
|
||||
}
|
||||
|
||||
if (target_meeting !== null && target_meeting === message_meeting_id) {
|
||||
if (!containsOrAdd(events_printed, message_name)) {
|
||||
console.log("\n###", message_name, "\n");
|
||||
console.log(message);
|
||||
console.log("\n");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} catch(e) {
|
||||
console.log("error processing the message", message, ":", e);
|
||||
}
|
||||
});
|
||||
|
||||
subscriber.psubscribe("bigbluebutton:*");
|
||||
|
||||
var containsOrAdd = function(list, value) {
|
||||
for (i = 0; i <= list.length-1; i++) {
|
||||
if (list[i] === value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
list.push(value);
|
||||
return false;
|
||||
}
|
55
labs/bbb-webhooks/extra/post_catcher.js
Normal file
55
labs/bbb-webhooks/extra/post_catcher.js
Normal file
@ -0,0 +1,55 @@
|
||||
// Lists all the events that happen in a meeting. Run with 'node events.js'.
|
||||
// Uses the first meeting started after the application runs and will list all
|
||||
// events, but only the first time they happen.
|
||||
|
||||
var redis = require("redis");
|
||||
var express = require("express");
|
||||
var request = require("request");
|
||||
var sha1 = require("sha1");
|
||||
var bodyParser = require('body-parser');
|
||||
|
||||
// server configs
|
||||
var port = 3006; // port in which to run this app
|
||||
var shared_secret = "33e06642a13942004fd83b3ba6e4104a"; // shared secret of your server
|
||||
var domain = "10.0.3.36"; // address of your server
|
||||
var target_domain = "10.0.3.36:3005"; // address of the webhooks app
|
||||
|
||||
var encodeForUrl = function(value) {
|
||||
return encodeURIComponent(value)
|
||||
.replace(/%20/g, '+')
|
||||
.replace(/[!'()]/g, escape)
|
||||
.replace(/\*/g, "%2A")
|
||||
}
|
||||
|
||||
// create a server to listen for callbacks
|
||||
var app = express();
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({
|
||||
extended: true
|
||||
}));
|
||||
app.listen(port);
|
||||
app.post("/callback", function(req, res, next) {
|
||||
console.log("-------------------------------------");
|
||||
console.log("* Received:", req.url);
|
||||
console.log("* Body:", req.body);
|
||||
console.log("-------------------------------------\n");
|
||||
res.statusCode = 200;
|
||||
res.send();
|
||||
});
|
||||
console.log("Server listening on port", port);
|
||||
|
||||
// registers a global hook on the webhooks app
|
||||
var myurl = "http://" + domain + ":" + port + "/callback";
|
||||
var params = "callbackURL=" + encodeForUrl(myurl);
|
||||
var checksum = sha1("hooks/create" + params + shared_secret);
|
||||
var fullurl = "http://" + target_domain + "/bigbluebutton/api/hooks/create?" +
|
||||
params + "&checksum=" + checksum
|
||||
|
||||
var requestOptions = {
|
||||
uri: fullurl,
|
||||
method: "GET"
|
||||
}
|
||||
console.log("Registering a hook with", fullurl);
|
||||
request(requestOptions, function(error, response, body) {
|
||||
console.log("Response from hook/create:", body);
|
||||
});
|
206
labs/bbb-webhooks/hook.coffee
Normal file
206
labs/bbb-webhooks/hook.coffee
Normal file
@ -0,0 +1,206 @@
|
||||
_ = require("lodash")
|
||||
async = require("async")
|
||||
redis = require("redis")
|
||||
|
||||
config = require("./config")
|
||||
CallbackEmitter = require("./callback_emitter")
|
||||
IDMapping = require("./id_mapping")
|
||||
Logger = require("./logger")
|
||||
|
||||
# The database of hooks.
|
||||
# Used always from memory, but saved to redis for persistence.
|
||||
#
|
||||
# Format:
|
||||
# { id: Hook }
|
||||
# Format on redis:
|
||||
# * a SET "...:hooks" with all ids
|
||||
# * a HASH "...:hook:<id>" for each hook with some of its attributes
|
||||
db = {}
|
||||
nextID = 1
|
||||
|
||||
# The representation of a hook and its properties. Stored in memory and persisted
|
||||
# to redis.
|
||||
# Hooks can be global, receiving callback calls for events from all meetings on the
|
||||
# server, or for a specific meeting. If an `externalMeetingID` is set in the hook,
|
||||
# it will only receive calls related to this meeting, otherwise it will be global.
|
||||
# Events are kept in a queue to be sent in the order they are received.
|
||||
# TODO: The queue should be cleared at some point. The hook is destroyed if too many
|
||||
# callback attempts fail, after ~5min. So the queue is already protected in this case.
|
||||
# But if the requests are going by but taking too long, the queue might be increasing
|
||||
# faster than the callbacks are made.
|
||||
module.exports = class Hook
|
||||
|
||||
constructor: ->
|
||||
@id = null
|
||||
@callbackURL = null
|
||||
@externalMeetingID = null
|
||||
@queue = []
|
||||
@emitter = null
|
||||
@redisClient = redis.createClient()
|
||||
|
||||
save: (callback) ->
|
||||
@redisClient.hmset config.redis.keys.hook(@id), @toRedis(), (error, reply) =>
|
||||
Logger.error "Hook: error saving hook to redis!", error, reply if error?
|
||||
@redisClient.sadd config.redis.keys.hooks, @id, (error, reply) =>
|
||||
Logger.error "Hook: error saving hookID to the list of hooks!", error, reply if error?
|
||||
|
||||
db[@id] = this
|
||||
callback?(error, db[@id])
|
||||
|
||||
destroy: (callback) ->
|
||||
@redisClient.srem config.redis.keys.hooks, @id, (error, reply) =>
|
||||
Logger.error "Hook: error removing hookID from the list of hooks!", error, reply if error?
|
||||
@redisClient.del config.redis.keys.hook(@id), (error) =>
|
||||
Logger.error "Hook: error removing hook from redis!", error if error?
|
||||
|
||||
if db[@id]
|
||||
delete db[@id]
|
||||
callback?(error, true)
|
||||
else
|
||||
callback?(error, false)
|
||||
|
||||
# Is this a global hook?
|
||||
isGlobal: ->
|
||||
not @externalMeetingID?
|
||||
|
||||
# The meeting from which this hook should receive events.
|
||||
targetMeetingID: ->
|
||||
@externalMeetingID
|
||||
|
||||
# Puts a new message in the queue. Will also trigger a processing in the queue so this
|
||||
# message might be processed instantly.
|
||||
enqueue: (message) ->
|
||||
Logger.info "Hook: enqueueing message", JSON.stringify(message)
|
||||
@queue.push message
|
||||
@_processQueue()
|
||||
|
||||
toRedis: ->
|
||||
r =
|
||||
"hookID": @id,
|
||||
"callbackURL": @callbackURL
|
||||
r.externalMeetingID = @externalMeetingID if @externalMeetingID?
|
||||
r
|
||||
|
||||
fromRedis: (redisData) ->
|
||||
@id = parseInt(redisData.hookID)
|
||||
@callbackURL = redisData.callbackURL
|
||||
if redisData.externalMeetingID?
|
||||
@externalMeetingID = redisData.externalMeetingID
|
||||
else
|
||||
@externalMeetingID = null
|
||||
|
||||
# Gets the first message in the queue and start an emitter to send it. Will only do it
|
||||
# if there is no emitter running already and if there is a message in the queue.
|
||||
_processQueue: ->
|
||||
message = @queue[0]
|
||||
return if not message? or @emitter?
|
||||
|
||||
@emitter = new CallbackEmitter(@callbackURL, message)
|
||||
@emitter.start()
|
||||
|
||||
@emitter.on "success", =>
|
||||
delete @emitter
|
||||
@queue.shift() # pop the first message just sent
|
||||
@_processQueue() # go to the next message
|
||||
|
||||
# gave up trying to perform the callback, remove the hook forever
|
||||
@emitter.on "stopped", (error) =>
|
||||
Logger.warn "Hook: too many failed attempts to perform a callback call, removing the hook", JSON.stringify(hook)
|
||||
@destroy()
|
||||
|
||||
@addSubscription = (callbackURL, meetingID=null, callback) ->
|
||||
hook = Hook.findByCallbackURLSync(callbackURL)
|
||||
if hook?
|
||||
callback?(new Error("There is already a subscription for this callback URL"), hook)
|
||||
else
|
||||
msg = "Hook: adding a hook with callback URL [#{callbackURL}]"
|
||||
msg += " for the meeting [#{meetingID}]" if meetingID?
|
||||
Logger.info msg
|
||||
|
||||
hook = new Hook()
|
||||
hook.id = nextID++
|
||||
hook.callbackURL = callbackURL
|
||||
hook.externalMeetingID = meetingID
|
||||
hook.save (error, hook) -> callback?(error, hook)
|
||||
|
||||
@removeSubscription = (hookID, callback) ->
|
||||
hook = Hook.getSync(hookID)
|
||||
if hook?
|
||||
msg = "Hook: removing the hook with callback URL [#{hook.callbackURL}]"
|
||||
msg += " for the meeting [#{hook.externalMeetingID}]" if hook.externalMeetingID?
|
||||
Logger.info msg
|
||||
|
||||
hook.destroy (error, removed) -> callback?(error, removed)
|
||||
else
|
||||
callback?(null, false)
|
||||
|
||||
@countSync = ->
|
||||
Object.keys(db).length
|
||||
|
||||
@getSync = (id) ->
|
||||
db[id]
|
||||
|
||||
@firstSync = ->
|
||||
keys = Object.keys(db)
|
||||
if keys.length > 0
|
||||
db[keys[0]]
|
||||
else
|
||||
null
|
||||
|
||||
@findByExternalMeetingIDSync = (externalMeetingID) ->
|
||||
hooks = Hook.allSync()
|
||||
_.filter(hooks, (hook) ->
|
||||
(externalMeetingID? and externalMeetingID is hook.externalMeetingID)
|
||||
)
|
||||
|
||||
@allGlobalSync = ->
|
||||
hooks = Hook.allSync()
|
||||
_.filter(hooks, (hook) -> hook.isGlobal())
|
||||
|
||||
@allSync = ->
|
||||
arr = Object.keys(db).reduce((arr, id) ->
|
||||
arr.push db[id]
|
||||
arr
|
||||
, [])
|
||||
arr
|
||||
|
||||
@clearSync = ->
|
||||
for id of db
|
||||
delete db[id]
|
||||
db = {}
|
||||
|
||||
@findByCallbackURLSync = (callbackURL) ->
|
||||
for id of db
|
||||
if db[id].callbackURL is callbackURL
|
||||
return db[id]
|
||||
|
||||
@initialize = (callback) ->
|
||||
Hook.resync(callback)
|
||||
|
||||
# Gets all hooks from redis to populate the local database.
|
||||
# Calls `callback()` when done.
|
||||
@resync = (callback) ->
|
||||
client = redis.createClient()
|
||||
tasks = []
|
||||
|
||||
client.smembers config.redis.keys.hooks, (error, hooks) =>
|
||||
Logger.error "Hook: error getting list of hooks from redis", error if error?
|
||||
|
||||
hooks.forEach (id) =>
|
||||
tasks.push (done) =>
|
||||
client.hgetall config.redis.keys.hook(id), (error, hookData) ->
|
||||
Logger.error "Hook: error getting information for a hook from redis", error if error?
|
||||
|
||||
if hookData?
|
||||
hook = new Hook()
|
||||
hook.fromRedis(hookData)
|
||||
hook.save (error, hook) ->
|
||||
nextID = hook.id + 1 if hook.id >= nextID
|
||||
done(null, hook)
|
||||
else
|
||||
done(null, null)
|
||||
|
||||
async.series tasks, (errors, result) ->
|
||||
hooks = _.map(Hook.allSync(), (hook) -> "[#{hook.id}] #{hook.callbackURL}")
|
||||
Logger.info "Hook: finished resync, hooks registered:", hooks
|
||||
callback?()
|
163
labs/bbb-webhooks/id_mapping.coffee
Normal file
163
labs/bbb-webhooks/id_mapping.coffee
Normal file
@ -0,0 +1,163 @@
|
||||
_ = require("lodash")
|
||||
async = require("async")
|
||||
redis = require("redis")
|
||||
|
||||
config = require("./config")
|
||||
Logger = require("./logger")
|
||||
|
||||
# The database of mappings. Uses the externalID as key because it changes less than
|
||||
# the internal ID (e.g. the internalID can change for different meetings in the same
|
||||
# room). Used always from memory, but saved to redis for persistence.
|
||||
#
|
||||
# Format:
|
||||
# {
|
||||
# externalMeetingID: {
|
||||
# id: @id
|
||||
# externalMeetingID: @exnternalMeetingID
|
||||
# internalMeetingID: @internalMeetingID
|
||||
# lastActivity: @lastActivity
|
||||
# }
|
||||
# }
|
||||
# Format on redis:
|
||||
# * a SET "...:mappings" with all ids (not meeting ids, the object id)
|
||||
# * a HASH "...:mapping:<id>" for each mapping with all its attributes
|
||||
db = {}
|
||||
nextID = 1
|
||||
|
||||
# A simple model to store mappings for meeting IDs.
|
||||
module.exports = class IDMapping
|
||||
|
||||
constructor: ->
|
||||
@id = null
|
||||
@externalMeetingID = null
|
||||
@internalMeetingID = null
|
||||
@lastActivity = null
|
||||
@redisClient = redis.createClient()
|
||||
|
||||
save: (callback) ->
|
||||
@redisClient.hmset config.redis.keys.mapping(@id), @toRedis(), (error, reply) =>
|
||||
Logger.error "Hook: error saving mapping to redis!", error, reply if error?
|
||||
@redisClient.sadd config.redis.keys.mappings, @id, (error, reply) =>
|
||||
Logger.error "Hook: error saving mapping ID to the list of mappings!", error, reply if error?
|
||||
|
||||
db[@externalMeetingID] = this
|
||||
callback?(error, db[@externalMeetingID])
|
||||
|
||||
destroy: (callback) ->
|
||||
@redisClient.srem config.redis.keys.mappings, @id, (error, reply) =>
|
||||
Logger.error "Hook: error removing mapping ID from the list of mappings!", error, reply if error?
|
||||
@redisClient.del config.redis.keys.mapping(@id), (error) =>
|
||||
Logger.error "Hook: error removing mapping from redis!", error if error?
|
||||
|
||||
if db[@externalMeetingID]
|
||||
delete db[@externalMeetingID]
|
||||
callback?(error, true)
|
||||
else
|
||||
callback?(error, false)
|
||||
|
||||
toRedis: ->
|
||||
r =
|
||||
"id": @id,
|
||||
"internalMeetingID": @internalMeetingID
|
||||
"externalMeetingID": @externalMeetingID
|
||||
"lastActivity": @lastActivity
|
||||
r
|
||||
|
||||
fromRedis: (redisData) ->
|
||||
@id = parseInt(redisData.id)
|
||||
@externalMeetingID = redisData.externalMeetingID
|
||||
@internalMeetingID = redisData.internalMeetingID
|
||||
@lastActivity = redisData.lastActivity
|
||||
|
||||
print: ->
|
||||
JSON.stringify(@toRedis())
|
||||
|
||||
@addOrUpdateMapping = (internalMeetingID, externalMeetingID, callback) ->
|
||||
mapping = new IDMapping()
|
||||
mapping.id = nextID++
|
||||
mapping.internalMeetingID = internalMeetingID
|
||||
mapping.externalMeetingID = externalMeetingID
|
||||
mapping.lastActivity = new Date().getTime()
|
||||
mapping.save (error, result) ->
|
||||
Logger.info "IDMapping: added or changed meeting mapping to the list #{externalMeetingID}:", mapping.print()
|
||||
callback?(error, result)
|
||||
|
||||
@removeMapping = (internalMeetingID, callback) ->
|
||||
for external, mapping of db
|
||||
if mapping.internalMeetingID is internalMeetingID
|
||||
mapping.destroy (error, result) ->
|
||||
Logger.info "IDMapping: removing meeting mapping from the list #{external}:", mapping.print()
|
||||
callback?(error, result)
|
||||
|
||||
@getInternalMeetingID = (externalMeetingID) ->
|
||||
db[externalMeetingID].internalMeetingID
|
||||
|
||||
@getExternalMeetingID = (internalMeetingID) ->
|
||||
mapping = IDMapping.findByInternalMeetingID(internalMeetingID)
|
||||
mapping?.externalMeetingID
|
||||
|
||||
@findByInternalMeetingID = (internalMeetingID) ->
|
||||
if internalMeetingID?
|
||||
for external, mapping of db
|
||||
if mapping.internalMeetingID is internalMeetingID
|
||||
return mapping
|
||||
null
|
||||
|
||||
@allSync = ->
|
||||
arr = Object.keys(db).reduce((arr, id) ->
|
||||
arr.push db[id]
|
||||
arr
|
||||
, [])
|
||||
arr
|
||||
|
||||
# Sets the last activity of the mapping for `internalMeetingID` to now.
|
||||
@reportActivity = (internalMeetingID) ->
|
||||
mapping = IDMapping.findByInternalMeetingID(internalMeetingID)
|
||||
if mapping?
|
||||
mapping.lastActivity = new Date().getTime()
|
||||
mapping.save()
|
||||
|
||||
# Checks all current mappings for their last activity and removes the ones that
|
||||
# are "expired", that had their last activity too long ago.
|
||||
@cleanup = ->
|
||||
now = new Date().getTime()
|
||||
all = IDMapping.allSync()
|
||||
toRemove = _.filter(all, (mapping) ->
|
||||
mapping.lastActivity < now - config.mappings.timeout
|
||||
)
|
||||
unless _.isEmpty(toRemove)
|
||||
Logger.info "IDMapping: expiring the mappings:", _.map(toRemove, (map) -> map.print())
|
||||
toRemove.forEach (mapping) -> mapping.destroy()
|
||||
|
||||
# Initializes global methods for this model.
|
||||
@initialize = (callback) ->
|
||||
IDMapping.resync(callback)
|
||||
IDMapping.cleanupInterval = setInterval(IDMapping.cleanup, config.mappings.cleanupInterval)
|
||||
|
||||
# Gets all mappings from redis to populate the local database.
|
||||
# Calls `callback()` when done.
|
||||
@resync = (callback) ->
|
||||
client = redis.createClient()
|
||||
tasks = []
|
||||
|
||||
client.smembers config.redis.keys.mappings, (error, mappings) =>
|
||||
Logger.error "Hook: error getting list of mappings from redis", error if error?
|
||||
|
||||
mappings.forEach (id) =>
|
||||
tasks.push (done) =>
|
||||
client.hgetall config.redis.keys.mapping(id), (error, mappingData) ->
|
||||
Logger.error "Hook: error getting information for a mapping from redis", error if error?
|
||||
|
||||
if mappingData?
|
||||
mapping = new IDMapping()
|
||||
mapping.fromRedis(mappingData)
|
||||
mapping.save (error, hook) ->
|
||||
nextID = mapping.id + 1 if mapping.id >= nextID
|
||||
done(null, mapping)
|
||||
else
|
||||
done(null, null)
|
||||
|
||||
async.series tasks, (errors, result) ->
|
||||
mappings = _.map(IDMapping.allSync(), (m) -> m.print())
|
||||
Logger.info "IDMapping: finished resync, mappings registered:", mappings
|
||||
callback?()
|
0
labs/bbb-webhooks/log/.gitkeep
Normal file
0
labs/bbb-webhooks/log/.gitkeep
Normal file
10
labs/bbb-webhooks/logger.coffee
Normal file
10
labs/bbb-webhooks/logger.coffee
Normal file
@ -0,0 +1,10 @@
|
||||
winston = require("winston")
|
||||
|
||||
logger = new (winston.Logger)(
|
||||
transports: [
|
||||
new (winston.transports.Console)({ timestamp: true, colorize: true }),
|
||||
new (winston.transports.File)({ filename: "log/application.log", timestamp: true })
|
||||
]
|
||||
)
|
||||
|
||||
module.exports = logger
|
24
labs/bbb-webhooks/package.json
Executable file
24
labs/bbb-webhooks/package.json
Executable file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "bbb-callbacks",
|
||||
"version": "0.9.0",
|
||||
"description": "A module for BigBlueButton for API callbacks",
|
||||
"keywords": [
|
||||
"bigbluebutton",
|
||||
"Web API",
|
||||
"callbacks"
|
||||
],
|
||||
"dependencies": {
|
||||
"async": "0.9.0",
|
||||
"body-parser": "^1.9.2",
|
||||
"coffee-script": "1.8.0",
|
||||
"express": "4.10.2",
|
||||
"lodash": "2.4.1",
|
||||
"redis": "0.12.1",
|
||||
"request": "2.47.0",
|
||||
"sha1": "1.1.0",
|
||||
"winston": "0.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "0.10.26"
|
||||
}
|
||||
}
|
62
labs/bbb-webhooks/utils.coffee
Normal file
62
labs/bbb-webhooks/utils.coffee
Normal file
@ -0,0 +1,62 @@
|
||||
sha1 = require("sha1")
|
||||
url = require("url")
|
||||
|
||||
config = require("./config")
|
||||
|
||||
Utils = exports
|
||||
|
||||
# Calculates the checksum given a url `fullUrl` and a `salt`, as calculate by bbb-web.
|
||||
Utils.checksumAPI = (fullUrl, salt) ->
|
||||
query = Utils.queryFromUrl(fullUrl)
|
||||
method = Utils.methodFromUrl(fullUrl)
|
||||
Utils.checksum(method + query + salt)
|
||||
|
||||
# Calculates the checksum for a string.
|
||||
# Just a wrapper for the method that actually does it.
|
||||
Utils.checksum = (string) ->
|
||||
sha1(string)
|
||||
|
||||
# Get the query of an API call from the url object (from url.parse())
|
||||
# Example:
|
||||
#
|
||||
# * `fullUrl` = `http://bigbluebutton.org/bigbluebutton/api/create?name=Demo+Meeting&meetingID=Demo`
|
||||
# * returns: `name=Demo+Meeting&meetingID=Demo`
|
||||
Utils.queryFromUrl = (fullUrl) ->
|
||||
|
||||
# Returns the query without the checksum.
|
||||
# We can't use url.parse() because it would change the encoding
|
||||
# and the checksum wouldn't match. We need the url exactly as
|
||||
# the client sent us.
|
||||
query = fullUrl.replace(/&checksum=[^&]*/, '')
|
||||
query = query.replace(/checksum=[^&]*&/, '')
|
||||
query = query.replace(/checksum=[^&]*$/, '')
|
||||
matched = query.match(/\?(.*)/)
|
||||
if matched?
|
||||
matched[1]
|
||||
else
|
||||
''
|
||||
|
||||
# Get the method name of an API call from the url object (from url.parse())
|
||||
# Example:
|
||||
#
|
||||
# * `fullUrl` = `http://mconf.org/bigbluebutton/api/create?name=Demo+Meeting&meetingID=Demo`
|
||||
# * returns: `create`
|
||||
Utils.methodFromUrl = (fullUrl) ->
|
||||
urlObj = url.parse(fullUrl, true)
|
||||
urlObj.pathname.substr (config.bbb.apiPath + "/").length
|
||||
|
||||
# Returns the IP address of the client that made a request `req`.
|
||||
# If can not determine the IP, returns `127.0.0.1`.
|
||||
Utils.ipFromRequest = (req) ->
|
||||
|
||||
# the first ip in the list if the ip of the client
|
||||
# the others are proxys between him and us
|
||||
if req.headers?["x-forwarded-for"]?
|
||||
ips = req.headers["x-forwarded-for"].split(",")
|
||||
ipAddress = ips[0]?.trim()
|
||||
|
||||
# fallbacks
|
||||
ipAddress ||= req.headers?["x-real-ip"] # when behind nginx
|
||||
ipAddress ||= req.connection?.remoteAddress
|
||||
ipAddress ||= "127.0.0.1"
|
||||
ipAddress
|
88
labs/bbb-webhooks/web_hooks.coffee
Normal file
88
labs/bbb-webhooks/web_hooks.coffee
Normal file
@ -0,0 +1,88 @@
|
||||
_ = require("lodash")
|
||||
async = require("async")
|
||||
redis = require("redis")
|
||||
request = require("request")
|
||||
|
||||
config = require("./config")
|
||||
Hook = require("./hook")
|
||||
IDMapping = require("./id_mapping")
|
||||
Logger = require("./logger")
|
||||
|
||||
# Web hooks will listen for events on redis coming from BigBlueButton and
|
||||
# perform HTTP calls with them to all registered hooks.
|
||||
module.exports = class WebHooks
|
||||
|
||||
constructor: ->
|
||||
@subscriberEvents = redis.createClient()
|
||||
|
||||
start: ->
|
||||
@_subscribeToEvents()
|
||||
|
||||
# Subscribe to the events on pubsub that might need to be sent in callback calls.
|
||||
_subscribeToEvents: ->
|
||||
@subscriberEvents.on "psubscribe", (channel, count) ->
|
||||
Logger.info "WebHooks: subscribed to " + channel
|
||||
|
||||
@subscriberEvents.on "pmessage", (pattern, channel, message) =>
|
||||
|
||||
processMessage = =>
|
||||
if @_filterMessage(channel, message)
|
||||
Logger.info "WebHooks: processing message on [#{channel}]:", JSON.stringify(message)
|
||||
@_processEvent(message)
|
||||
|
||||
try
|
||||
message = JSON.parse(message)
|
||||
if message?
|
||||
id = message.payload?.meeting_id
|
||||
IDMapping.reportActivity(id)
|
||||
|
||||
# First treat meeting events to add/remove ID mappings
|
||||
if message.header?.name is "meeting_created_message"
|
||||
Logger.info "WebHooks: got create message on meetings channel [#{channel}]", message
|
||||
IDMapping.addOrUpdateMapping message.payload?.meeting_id, message.payload?.external_meeting_id, (error, result) ->
|
||||
# has to be here, after the meeting was created, otherwise create calls won't generate
|
||||
# callback calls for meeting hooks
|
||||
processMessage()
|
||||
|
||||
# TODO: Temporarily commented because we still need the mapping for recording events,
|
||||
# after the meeting ended.
|
||||
# else if message.header?.name is "meeting_destroyed_event"
|
||||
# Logger.info "WebHooks: got destroy message on meetings channel [#{channel}]", message
|
||||
# IDMapping.removeMapping message.payload?.meeting_id, (error, result) ->
|
||||
# processMessage()
|
||||
|
||||
else
|
||||
processMessage()
|
||||
|
||||
catch e
|
||||
Logger.error "WebHooks: error processing the message", message, ":", e
|
||||
|
||||
@subscriberEvents.psubscribe config.hooks.pchannel
|
||||
|
||||
# Returns whether the message read from redis should generate a callback
|
||||
# call or not.
|
||||
_filterMessage: (channel, message) ->
|
||||
for event in config.hooks.events
|
||||
if channel? and message.header?.name? and
|
||||
event.channel.match(channel) and event.name.match(message.header?.name)
|
||||
return true
|
||||
false
|
||||
|
||||
# Processes an event received from redis. Will get all hook URLs that
|
||||
# should receive this event and start the process to perform the callback.
|
||||
_processEvent: (message) ->
|
||||
hooks = Hook.allGlobalSync()
|
||||
|
||||
# TODO: events that happen after the meeting ended will never trigger the hooks
|
||||
# below, since the mapping is removed when the meeting ends
|
||||
|
||||
# filter the hooks that need to receive this event
|
||||
# only global hooks or hooks for this specific meeting
|
||||
idFromMessage = message.payload?.meeting_id
|
||||
if idFromMessage?
|
||||
eMeetingID = IDMapping.getExternalMeetingID(idFromMessage)
|
||||
hooks = hooks.concat(Hook.findByExternalMeetingIDSync(eMeetingID))
|
||||
|
||||
hooks.forEach (hook) ->
|
||||
Logger.info "WebHooks: enqueueing a message in the hook:", hook.callbackURL
|
||||
hook.enqueue message
|
127
labs/bbb-webhooks/web_server.coffee
Normal file
127
labs/bbb-webhooks/web_server.coffee
Normal file
@ -0,0 +1,127 @@
|
||||
_ = require("lodash")
|
||||
express = require("express")
|
||||
url = require("url")
|
||||
|
||||
config = require("./config")
|
||||
Hook = require("./hook")
|
||||
Logger = require("./logger")
|
||||
Utils = require("./utils")
|
||||
|
||||
# Web server that listens for API calls and process them.
|
||||
module.exports = class WebServer
|
||||
|
||||
constructor: ->
|
||||
@app = express()
|
||||
@_registerRoutes()
|
||||
|
||||
start: (port) ->
|
||||
@server = @app.listen(port)
|
||||
unless @server.address()?
|
||||
Logger.error "Could not bind to port", port
|
||||
Logger.error "Aborting."
|
||||
process.exit(1)
|
||||
Logger.info "Server listening on port", port, "in", @app.settings.env.toUpperCase(), "mode"
|
||||
|
||||
_registerRoutes: ->
|
||||
# Request logger
|
||||
@app.all "*", (req, res, next) ->
|
||||
unless fromMonit(req)
|
||||
Logger.info "<==", req.method, "request to", req.url, "from:", clientDataSimple(req)
|
||||
next()
|
||||
|
||||
@app.get "/bigbluebutton/api/hooks/create", @_validateChecksum, @_create
|
||||
@app.get "/bigbluebutton/api/hooks/destroy", @_validateChecksum, @_destroy
|
||||
@app.get "/bigbluebutton/api/hooks/list", @_validateChecksum, @_list
|
||||
@app.get "/bigbluebutton/api/hooks/ping", (req, res) ->
|
||||
res.write "bbb-webhooks up!"
|
||||
res.end()
|
||||
|
||||
_create: (req, res, next) ->
|
||||
urlObj = url.parse(req.url, true)
|
||||
callbackURL = urlObj.query["callbackURL"]
|
||||
meetingID = urlObj.query["meetingID"]
|
||||
|
||||
unless callbackURL?
|
||||
respondWithXML(res, config.api.responses.missingParamCallbackURL)
|
||||
else
|
||||
Hook.addSubscription callbackURL, meetingID, (error, hook) ->
|
||||
if error? # the only error for now is for duplicated callbackURL
|
||||
msg = config.api.responses.createDuplicated(hook.id)
|
||||
else if hook?
|
||||
msg = config.api.responses.createSuccess(hook.id)
|
||||
else
|
||||
msg = config.api.responses.createFailure
|
||||
respondWithXML(res, msg)
|
||||
|
||||
_destroy: (req, res, next) ->
|
||||
urlObj = url.parse(req.url, true)
|
||||
hookID = urlObj.query["hookID"]
|
||||
|
||||
unless hookID?
|
||||
respondWithXML(res, config.api.responses.missingParamHookID)
|
||||
else
|
||||
Hook.removeSubscription hookID, (error, result) ->
|
||||
if error?
|
||||
msg = config.api.responses.destroyFailure
|
||||
else if !result
|
||||
msg = config.api.responses.destroyNoHook
|
||||
else
|
||||
msg = config.api.responses.destroySuccess
|
||||
respondWithXML(res, msg)
|
||||
|
||||
_list: (req, res, next) ->
|
||||
urlObj = url.parse(req.url, true)
|
||||
meetingID = urlObj.query["meetingID"]
|
||||
|
||||
if meetingID?
|
||||
# all the hooks that receive events from this meeting
|
||||
hooks = Hook.allGlobalSync()
|
||||
hooks = hooks.concat(Hook.findByExternalMeetingIDSync(meetingID))
|
||||
hooks = _.sortBy(hooks, (hook) -> hook.id)
|
||||
else
|
||||
# no meetingID, return all hooks
|
||||
hooks = Hook.allSync()
|
||||
|
||||
msg = "<response><returncode>SUCCESS</returncode><hooks>"
|
||||
hooks.forEach (hook) ->
|
||||
msg += "<hook>"
|
||||
msg += "<hookID>#{hook.id}</hookID>"
|
||||
msg += "<callbackURL><![CDATA[#{hook.callbackURL}]]></callbackURL>"
|
||||
msg += "<meetingID><![CDATA[#{hook.externalMeetingID}]]></meetingID>" unless hook.isGlobal()
|
||||
msg += "</hook>"
|
||||
msg += "</hooks></response>"
|
||||
|
||||
respondWithXML(res, msg)
|
||||
|
||||
# Validates the checksum in the request `req`.
|
||||
# If it doesn't match BigBlueButton's shared secret, will send an XML response
|
||||
# with an error code just like BBB does.
|
||||
_validateChecksum: (req, res, next) =>
|
||||
urlObj = url.parse(req.url, true)
|
||||
checksum = urlObj.query["checksum"]
|
||||
|
||||
if checksum is Utils.checksumAPI(req.url, config.bbb.sharedSecret)
|
||||
next()
|
||||
else
|
||||
Logger.info "checksum check failed, sending a checksumError response"
|
||||
res.setHeader("Content-Type", "text/xml")
|
||||
res.send cleanupXML(config.api.responses.checksumError)
|
||||
|
||||
respondWithXML = (res, msg) ->
|
||||
msg = cleanupXML(msg)
|
||||
Logger.info "==> respond with:", msg
|
||||
res.setHeader("Content-Type", "text/xml")
|
||||
res.send msg
|
||||
|
||||
# Returns a simple string with a description of the client that made
|
||||
# the request. It includes the IP address and the user agent.
|
||||
clientDataSimple = (req) ->
|
||||
"ip " + Utils.ipFromRequest(req) + ", using " + req.headers["user-agent"]
|
||||
|
||||
# Cleans up a string with an XML in it removing spaces and new lines from between the tags.
|
||||
cleanupXML = (string) ->
|
||||
string.trim().replace(/>\s*/g, '>')
|
||||
|
||||
# Was this request made by monit?
|
||||
fromMonit = (req) ->
|
||||
req.headers["user-agent"]? and req.headers["user-agent"].match(/^monit/)
|
@ -126,6 +126,13 @@ if not FileTest.directory?(target_dir)
|
||||
# we will abort the archiving if there's no marks to start and stop the recording
|
||||
if not archive_has_recording_marks?(meeting_id, raw_archive_dir)
|
||||
BigBlueButton.logger.info("There's no recording marks for #{meeting_id}, aborting the archive process")
|
||||
|
||||
# we need to delete the keys here because the sanity phase won't never happen for this recording
|
||||
BigBlueButton.logger.info("Deleting keys")
|
||||
redis = BigBlueButton::RedisWrapper.new(redis_host, redis_port)
|
||||
events_archiver = BigBlueButton::RedisEventsArchiver.new redis
|
||||
events_archiver.delete_events(meeting_id)
|
||||
|
||||
BigBlueButton.logger.info("Removing events.xml")
|
||||
FileUtils.rm_r target_dir
|
||||
BigBlueButton.logger.info("Removing the recorded flag")
|
||||
|
@ -55,6 +55,11 @@ def archive_recorded_meeting(recording_dir)
|
||||
step_stop_time = BigBlueButton.monotonic_clock
|
||||
step_time = step_stop_time - step_start_time
|
||||
|
||||
if not File.exists?(recorded_done)
|
||||
BigBlueButton.logger.info("There's no recording marks in #{meeting_id}, skipping it")
|
||||
return
|
||||
end
|
||||
|
||||
step_succeeded = (ret == 0 && File.exists?(archived_done))
|
||||
|
||||
BigBlueButton.redis_publisher.put_archive_ended meeting_id, {
|
||||
|
@ -302,7 +302,7 @@ function checkUrl(url)
|
||||
var http = new XMLHttpRequest();
|
||||
http.open('HEAD', url, false);
|
||||
http.send();
|
||||
return http.status!=404;
|
||||
return http.status==200;
|
||||
}
|
||||
|
||||
load_video = function(){
|
||||
|
Loading…
Reference in New Issue
Block a user