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:
Anton Georgiev 2014-11-28 20:33:11 +00:00
commit 4172537f24
91 changed files with 1627 additions and 1075 deletions

View File

@ -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.");
}
}
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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";

View 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);
}
}
}
}

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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";

View File

@ -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;

View File

@ -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);
}

View File

@ -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() {

View File

@ -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() + "] *********");
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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))
}
}

View File

@ -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))
}

View File

@ -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)

View File

@ -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)

View File

@ -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"

View File

@ -15,6 +15,7 @@ case class KeepAliveMessage
case class CreateMeeting
(
meetingID: String,
externalMeetingID: String,
meetingName: String,
recorded: Boolean,
voiceBridge: String,

View File

@ -44,6 +44,7 @@ case class GetRecordingStatusReply(
case class MeetingCreated(
meetingID: String,
externalMeetingID: String,
recorded: Boolean,
name: String,
voiceBridge: String,

View File

@ -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);
}

View File

@ -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)

View 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)
}
}
}

View 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)
}

View File

@ -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)

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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
View 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
View 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
View 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);
}

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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)
{

View File

@ -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);
}
}
}

View File

@ -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);

View 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}"/>

View File

@ -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);
}
}
}

View 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;
}

View File

@ -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;
}

View File

@ -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;

View 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;

View File

@ -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;

View 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.

View File

@ -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>

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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 '{}' \;

View File

@ -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());
}

View File

@ -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";

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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");

View File

@ -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

View File

@ -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
View File

@ -0,0 +1,6 @@
*~
**/#*#
*.log
node_modules/
config_local.coffee
log/*

1
labs/bbb-webhooks/.nvmrc Normal file
View File

@ -0,0 +1 @@
0.10.33

322
labs/bbb-webhooks/README.md Normal file
View 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":{}}&timestamp=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
View 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();

View 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()

View 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}"

View 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

View File

@ -0,0 +1,8 @@
/usr/local/bigbluebutton/bbb-webhooks/log/*.log {
size 300M
copytruncate
rotate 30
compress
missingok
notifempty
}

View 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

View 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

View 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;
}

View 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

View 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;
}

View 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);
});

View 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?()

View 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?()

View File

View 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
View 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"
}
}

View 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

View 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

View 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/)

View File

@ -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")

View File

@ -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, {

View File

@ -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(){