From 5af63742655f9e4b9e37bb427f86ed1c68153761 Mon Sep 17 00:00:00 2001
From: Richard Alam <ritzalam@gmail.com>
Date: Wed, 5 Feb 2020 14:58:47 -0800
Subject: [PATCH]  Track call session state

 Currently, we user DTMF to inform the client when the call session is in echo test and when entering the voice conference.
 Unfortunately, sometimes when FS sends the DTMF, FS crashes.

 Monitor the progress of the call session using ESL events and propagate to the client.

 The client would be informed of these call states: CALL_STARTED, IN_ECHO_TEST, IN_CONFERENCE, CALL_ENDED.
---
 .../core/apps/voice/VoiceApp2x.scala          |   8 +
 .../voice/VoiceConfCallStateEvtMsgHdlr.scala  |  42 ++++
 .../senders/ReceivedJsonMsgHandlerActor.scala |   2 +
 .../core/running/MeetingActor.scala           |   1 +
 .../bigbluebutton/core2/AnalyticsActor.scala  |   2 +
 .../FreeswitchConferenceEventListener.java    |  13 ++
 .../voice/IVoiceConferenceService.java        |  10 +
 .../events/FreeswitchStatusReplyEvent.java    |  11 +
 .../voice/events/VoiceCallStateEvent.java     |  30 +++
 .../voice/freeswitch/ConnectionManager.java   |  17 +-
 .../voice/freeswitch/ESLEventListener.java    | 209 +++++++++++++++++-
 .../actions/CheckFreeswitchStatusCommand.java |   6 +-
 .../freeswitch/VoiceConferenceService.scala   |  37 ++++
 .../common2/msgs/VoiceConfMsgs.scala          |  38 +++-
 .../freeswitch/esl/client/inbound/Client.java |   5 +-
 15 files changed, 416 insertions(+), 15 deletions(-)
 create mode 100755 akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceConfCallStateEvtMsgHdlr.scala
 create mode 100755 akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/FreeswitchStatusReplyEvent.java
 create mode 100755 akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/VoiceCallStateEvent.java

diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp2x.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp2x.scala
index 571c4db2bd..c5f0662066 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp2x.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceApp2x.scala
@@ -3,6 +3,13 @@ package org.bigbluebutton.core.apps.voice
 import org.bigbluebutton.core.running.MeetingActor
 import org.bigbluebutton.core2.message.handlers.RecordingStartedVoiceConfEvtMsgHdlr
 
+object VoiceCallState {
+  val NOT_IN_CALL = "NOT_IN_CALL"
+  val IN_ECHO_TEST = "IN_ECHO_TEST"
+  val IN_CONFERENCE = "IN_CONFERENCE"
+  val CALL_STARTED = "CALL_STARTED"
+  val CALL_ENDED = "CALL_ENDED"
+}
 trait VoiceApp2x extends UserJoinedVoiceConfEvtMsgHdlr
   with UserJoinedVoiceConfMessageHdlr
   with UserLeftVoiceConfEvtMsgHdlr
@@ -11,6 +18,7 @@ trait VoiceApp2x extends UserJoinedVoiceConfEvtMsgHdlr
   with RecordingStartedVoiceConfEvtMsgHdlr
   with VoiceConfRunningEvtMsgHdlr
   with SyncGetVoiceUsersMsgHdlr
+  with VoiceConfCallStateEvtMsgHdlr
   with UserStatusVoiceConfEvtMsgHdlr {
 
   this: MeetingActor =>
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceConfCallStateEvtMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceConfCallStateEvtMsgHdlr.scala
new file mode 100755
index 0000000000..b82ddc6468
--- /dev/null
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/VoiceConfCallStateEvtMsgHdlr.scala
@@ -0,0 +1,42 @@
+package org.bigbluebutton.core.apps.voice
+
+import org.bigbluebutton.common2.msgs.{ BbbClientMsgHeader, BbbCommonEnvCoreMsg, BbbCoreEnvelope, MessageTypes, Routing, VoiceCallStateEvtMsg, VoiceCallStateEvtMsgBody, VoiceConfCallStateEvtMsg }
+import org.bigbluebutton.core.models.{ VoiceUserState, VoiceUsers }
+import org.bigbluebutton.core.running.{ LiveMeeting, MeetingActor, OutMsgRouter }
+
+trait VoiceConfCallStateEvtMsgHdlr {
+  this: MeetingActor =>
+
+  val liveMeeting: LiveMeeting
+  val outGW: OutMsgRouter
+
+  def handleVoiceConfCallStateEvtMsg(msg: VoiceConfCallStateEvtMsg): Unit = {
+    val routing = Routing.addMsgToClientRouting(
+      MessageTypes.BROADCAST_TO_MEETING,
+      liveMeeting.props.meetingProp.intId,
+      msg.body.voiceConf
+    )
+    val envelope = BbbCoreEnvelope(
+      VoiceCallStateEvtMsg.NAME,
+      routing
+    )
+    val header = BbbClientMsgHeader(
+      VoiceCallStateEvtMsg.NAME,
+      liveMeeting.props.meetingProp.intId,
+      msg.body.voiceConf
+    )
+
+    val body = VoiceCallStateEvtMsgBody(
+      meetingId = liveMeeting.props.meetingProp.intId,
+      voiceConf = msg.body.voiceConf,
+      clientSession = msg.body.callSession,
+      userId = msg.body.userId,
+      callerName = msg.body.callerName,
+      callState = msg.body.callState
+    )
+
+    val event = VoiceCallStateEvtMsg(header, body)
+    val msgEvent = BbbCommonEnvCoreMsg(envelope, event)
+    outGW.send(msgEvent)
+  }
+}
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala
index 7dd24a7fa9..2db88ad68a 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala
@@ -152,6 +152,8 @@ class ReceivedJsonMsgHandlerActor(
         routeVoiceMsg[CheckRunningAndRecordingVoiceConfEvtMsg](envelope, jsonNode)
       case UserStatusVoiceConfEvtMsg.NAME =>
         routeVoiceMsg[UserStatusVoiceConfEvtMsg](envelope, jsonNode)
+      case VoiceConfCallStateEvtMsg.NAME =>
+        routeVoiceMsg[VoiceConfCallStateEvtMsg](envelope, jsonNode)
 
       // Breakout rooms
       case BreakoutRoomsListMsg.NAME =>
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala
index b6e32aab55..5ea402cdde 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala
@@ -384,6 +384,7 @@ class MeetingActor(
         state = updateInactivityTracker(state)
         updateVoiceUserLastActivity(m.body.voiceUserId)
         handleUserTalkingInVoiceConfEvtMsg(m)
+      case m: VoiceConfCallStateEvtMsg        => handleVoiceConfCallStateEvtMsg(m)
 
       case m: RecordingStartedVoiceConfEvtMsg => handleRecordingStartedVoiceConfEvtMsg(m)
       case m: MuteUserCmdMsg =>
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala
index c7b4b09486..f65e7bf727 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/AnalyticsActor.scala
@@ -93,6 +93,8 @@ class AnalyticsActor extends Actor with ActorLogging {
       // Voice
       case m: UserMutedVoiceEvtMsg =>
         logMessage(msg)
+      case m: VoiceConfCallStateEvtMsg => logMessage(msg)
+      case m: VoiceCallStateEvtMsg => logMessage(msg)
 
       // Breakout
       case m: BreakoutRoomEndedEvtMsg => logMessage(msg)
diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/FreeswitchConferenceEventListener.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/FreeswitchConferenceEventListener.java
index 0d165a9a5c..48012683bf 100755
--- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/FreeswitchConferenceEventListener.java
+++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/FreeswitchConferenceEventListener.java
@@ -100,6 +100,19 @@ public class FreeswitchConferenceEventListener implements ConferenceEventListene
         } else if (event instanceof VoiceUsersStatusEvent) {
           VoiceUsersStatusEvent evt = (VoiceUsersStatusEvent) event;
           vcs.voiceUsersStatus(evt.getRoom(), evt.confMembers, evt.confRecordings);
+        } else if (event instanceof VoiceCallStateEvent) {
+          VoiceCallStateEvent evt = (VoiceCallStateEvent) event;
+          vcs.voiceCallStateEvent(evt.getRoom(),
+                  evt.callSession,
+                  evt.clientSession,
+                  evt.userId,
+                  evt.callerName,
+                  evt.callState,
+                  evt.origCallerIdName,
+                  evt.origCalledDest);
+        } else if (event instanceof FreeswitchStatusReplyEvent) {
+          FreeswitchStatusReplyEvent evt = (FreeswitchStatusReplyEvent) event;
+          vcs.freeswitchStatusReplyEvent(evt.jsonResponse);
         }
 
       }
diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/IVoiceConferenceService.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/IVoiceConferenceService.java
index e916924939..cd42bfbaa1 100755
--- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/IVoiceConferenceService.java
+++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/IVoiceConferenceService.java
@@ -65,4 +65,14 @@ public interface IVoiceConferenceService {
                                     Boolean isRecording,
                                     java.util.List<ConfRecording> confRecording);
 
+  void voiceCallStateEvent(String conf,
+                           String callSession,
+                           String clientSession,
+                           String userId,
+                           String callerName,
+                           String callState,
+                           String origCallerIdName,
+                           String origCalledDest);
+
+  void freeswitchStatusReplyEvent(String json);
 }
diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/FreeswitchStatusReplyEvent.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/FreeswitchStatusReplyEvent.java
new file mode 100755
index 0000000000..d8ae7f3541
--- /dev/null
+++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/FreeswitchStatusReplyEvent.java
@@ -0,0 +1,11 @@
+package org.bigbluebutton.freeswitch.voice.events;
+
+public class FreeswitchStatusReplyEvent extends VoiceConferenceEvent {
+
+    public final String jsonResponse;
+
+    public FreeswitchStatusReplyEvent(String json) {
+        super("unused");
+        this.jsonResponse = json;
+    }
+}
diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/VoiceCallStateEvent.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/VoiceCallStateEvent.java
new file mode 100755
index 0000000000..9d0a9277e6
--- /dev/null
+++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/events/VoiceCallStateEvent.java
@@ -0,0 +1,30 @@
+package org.bigbluebutton.freeswitch.voice.events;
+
+public class VoiceCallStateEvent extends VoiceConferenceEvent {
+    public final String callSession;
+    public final String clientSession;
+    public final String userId;
+    public final String callerName;
+    public final String callState;
+    public final String origCallerIdName;
+    public final String origCalledDest;
+
+    public VoiceCallStateEvent(
+            String conf,
+            String callSession,
+            String clientSession,
+            String userId,
+            String callerName,
+            String callState,
+            String origCallerIdName,
+            String origCalledDest) {
+        super(conf);
+        this.callSession = callSession;
+        this.clientSession = clientSession;
+        this.userId = userId;
+        this.callerName = callerName;
+        this.callState = callState;
+        this.origCallerIdName = origCallerIdName;
+        this.origCalledDest = origCalledDest;
+    }
+}
diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/ConnectionManager.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/ConnectionManager.java
index 47a6b8e32e..a194ffa16c 100755
--- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/ConnectionManager.java
+++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/ConnectionManager.java
@@ -34,6 +34,7 @@ import org.bigbluebutton.freeswitch.voice.freeswitch.actions.*;
 import org.freeswitch.esl.client.inbound.Client;
 import org.freeswitch.esl.client.inbound.InboundConnectionFailure;
 import org.freeswitch.esl.client.manager.ManagerConnection;
+import org.freeswitch.esl.client.transport.CommandResponse;
 import org.freeswitch.esl.client.transport.message.EslMessage;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -81,10 +82,18 @@ public class ConnectionManager {
 				if (!subscribed) {
 					log.info("Subscribing for ESL events.");
 					c.cancelEventSubscriptions();
-					c.setEventSubscriptions("plain", "all");
-					//c.addEventFilter(EVENT_NAME, "heartbeat");
-					c.addEventFilter(EVENT_NAME, "custom");
-					c.addEventFilter(EVENT_NAME, "background_job");
+					CommandResponse response = c.setEventSubscriptions("plain", "all");
+					if (response.isOk()) {
+						log.info("Subscribed to ESL events." +
+								" Command: [" + response.getCommand() + "] " +
+								" Response: [" + response.getReplyText() + "]");
+					}
+
+					//c.addEventFilter(EVENT_NAME, "HEARTBEAT");
+					//c.addEventFilter(EVENT_NAME, "custom");
+					//c.addEventFilter(EVENT_NAME, "background_job");
+					c.addEventFilter(EVENT_NAME, "CHANNEL_EXECUTE");
+					c.addEventFilter(EVENT_NAME, "CHANNEL_STATE");
 					subscribed = true;
 				} else {
 					// Let's check for status every minute.
diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/ESLEventListener.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/ESLEventListener.java
index d18b994b21..fb204b984d 100755
--- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/ESLEventListener.java
+++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/ESLEventListener.java
@@ -1,6 +1,7 @@
 package org.bigbluebutton.freeswitch.voice.freeswitch;
 
 
+import java.util.Iterator;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import java.util.regex.Matcher;
@@ -52,6 +53,8 @@ public class ESLEventListener implements IEslEventListener {
     private static final Pattern GLOBAL_AUDION_PATTERN = Pattern.compile("(GLOBAL_AUDIO)_(.*)$");
     private static final Pattern CALLERNAME_PATTERN = Pattern.compile("(.*)-bbbID-(.*)$");
     private static final Pattern CALLERNAME_WITH_SESS_INFO_PATTERN = Pattern.compile("^(.*)_(\\d+)-bbbID-(.*)$");
+    private static final Pattern CALLERNAME_LISTENONLY_PATTERN = Pattern.compile("^(.*)_(\\d+)-bbbID-LISTENONLY-(.*)$");
+    private static final Pattern ECHO_TEST_DEST_PATTERN = Pattern.compile("^9196(\\d+)$");
     
     @Override
     public void conferenceEventJoin(String uniqueId, String confName, int confSize, EslEvent event) {
@@ -92,6 +95,22 @@ public class ESLEventListener implements IEslEventListener {
                 voiceUserId = "v_" + memberId.toString();
             }
 
+            String coreuuid = headers.get("Core-UUID");
+            String clientSession = "0";
+            String callState = "IN_CONFERENCE";
+            String origCallerIdName = headers.get("Caller-Caller-ID-Name");
+            String origCallerDestNumber = headers.get("Caller-Destination-Number");
+            VoiceCallStateEvent csEvent = new VoiceCallStateEvent(
+                    confName,
+                    coreuuid,
+                    clientSession,
+                    voiceUserId,
+                    callerIdName,
+                    callState,
+                    origCallerIdName,
+                    origCallerDestNumber);
+            conferenceEventListener.handleConferenceEvent(csEvent);
+
             String callerUUID = this.getMemberUUIDFromEvent(event);
             log.info("Caller joined: conf=" + confName +
                     ",uuid=" + callerUUID +
@@ -111,6 +130,8 @@ public class ESLEventListener implements IEslEventListener {
                     speaking,
                     "none");
             conferenceEventListener.handleConferenceEvent(pj);
+
+
         }
     }
 
@@ -243,13 +264,187 @@ public class ESLEventListener implements IEslEventListener {
     
     @Override
     public void eventReceived(EslEvent event) {
-        //System.out.println("ESL Event Listener received event=[" + event.getEventName() + "]" +
-        //        event.getEventHeaders().toString());
-        if (event.getEventName().equals("heartbeat")) {
-            log.info("Received heartbeat from FreeSWITCH");
-////           setChanged();
-//           notifyObservers(event);
-//           return; 
+//        System.out.println("*********** ESL Event Listener received event=[" + event.getEventName() + "]" +
+//                event.getEventHeaders().toString());
+
+        /**
+        Map<String, String> eventHeaders1 = event.getEventHeaders();
+         StringBuilder sb = new StringBuilder("");
+         sb.append("\n");
+         for (Iterator it = eventHeaders1.entrySet().iterator(); it.hasNext(); ) {
+         Map.Entry entry = (Map.Entry)it.next();
+         sb.append(entry.getKey());
+         sb.append(" => '");
+         sb.append(entry.getValue());
+         sb.append("'\n");
+         }
+
+         System.out.println("##### ===>>> " + sb.toString());
+         System.out.println("<<<=== #####");
+        **/
+
+        if (event.getEventName().equals("HEARTBEAT")) {
+            //log.info("Received heartbeat from FreeSWITCH");
+        } else if (event.getEventName().equals( "CHANNEL_EXECUTE" )) {
+            Map<String, String> eventHeaders = event.getEventHeaders();
+
+            String application = (eventHeaders.get("Application") == null) ? "" : eventHeaders.get("Application");
+            String channelCallState = (eventHeaders.get("Channel-Call-State") == null) ? "" : eventHeaders.get("Channel-Call-State");
+            String varvBridge = (eventHeaders.get("variable_vbridge") == null) ? "" : eventHeaders.get("variable_vbridge");
+
+            if ("echo".equalsIgnoreCase(application) && !varvBridge.isEmpty()) {
+                String origCallerIdName = eventHeaders.get("Caller-Caller-ID-Name");
+                String origCallerDestNumber = eventHeaders.get("Caller-Destination-Number");
+                String coreuuid = eventHeaders.get("Core-UUID");
+
+                //System.out.println("******** uuid=" + coreuuid + " " + origCallerIdName + " is in echo test " + origCallerDestNumber + " vbridge=" + varvBridge);
+
+                String voiceUserId = "";
+                String callerName = origCallerIdName;
+                String clientSession = "0";
+                String callState = "IN_ECHO_TEST";
+
+                Matcher callerListenOnly = CALLERNAME_LISTENONLY_PATTERN.matcher(origCallerIdName);
+                Matcher callWithSess = CALLERNAME_WITH_SESS_INFO_PATTERN.matcher(origCallerIdName);
+                if (callWithSess.matches()) {
+                    voiceUserId = callWithSess.group(1).trim();
+                    clientSession = callWithSess.group(2).trim();
+                    callerName = callWithSess.group(3).trim();
+                } else if (callerListenOnly.matches()) {
+                    voiceUserId = callerListenOnly.group(1).trim();
+                    clientSession = callWithSess.group(2).trim();
+                    callerName = callerListenOnly.group(3).trim();
+                }
+
+                VoiceCallStateEvent csEvent = new VoiceCallStateEvent(varvBridge,
+                        coreuuid,
+                        clientSession,
+                        voiceUserId,
+                        callerName,
+                        callState,
+                        origCallerIdName,
+                        origCallerDestNumber);
+                conferenceEventListener.handleConferenceEvent(csEvent);
+
+            } else if ("RINGING".equalsIgnoreCase(channelCallState) && !varvBridge.isEmpty()) {
+                String origCallerIdName = eventHeaders.get("Caller-Caller-ID-Name");
+                String origCallerDestNumber = eventHeaders.get("Caller-Destination-Number");
+                String coreuuid = eventHeaders.get("Core-UUID");
+                //System.out.println("******** uuid=" + coreuuid + " " + origCallerIdName + " is in ringing " + origCallerDestNumber + " vbridge=" + varvBridge);
+
+                String voiceUserId = "";
+                String callerName = origCallerIdName;
+                String clientSession = "0";
+                String callState = "CALL_STARTED";
+
+                Matcher callerListenOnly = CALLERNAME_LISTENONLY_PATTERN.matcher(origCallerIdName);
+                Matcher callWithSess = CALLERNAME_WITH_SESS_INFO_PATTERN.matcher(origCallerIdName);
+                if (callWithSess.matches()) {
+                    voiceUserId = callWithSess.group(1).trim();
+                    clientSession = callWithSess.group(2).trim();
+                    callerName = callWithSess.group(3).trim();
+                } else if (callerListenOnly.matches()) {
+                    voiceUserId = callerListenOnly.group(1).trim();
+                    clientSession = callWithSess.group(2).trim();
+                    callerName = callerListenOnly.group(3).trim();
+                }
+
+                VoiceCallStateEvent csEvent = new VoiceCallStateEvent(varvBridge,
+                        coreuuid,
+                        clientSession,
+                        voiceUserId,
+                        callerName,
+                        callState,
+                        origCallerIdName,
+                        origCallerDestNumber);
+                conferenceEventListener.handleConferenceEvent(csEvent);
+            }
+        } else if (event.getEventName().equalsIgnoreCase("CHANNEL_STATE")) {
+            Map<String, String> eventHeaders = event.getEventHeaders();
+            String channelCallState = (eventHeaders.get("Channel-Call-State") == null) ? "" : eventHeaders.get("Channel-Call-State");
+            String channelState = (eventHeaders.get("Channel-State") == null) ? "" : eventHeaders.get("Channel-State");
+
+            if ("HANGUP".equalsIgnoreCase(channelCallState) && "CS_DESTROY".equalsIgnoreCase(channelState)) {
+                String origCallerIdName = eventHeaders.get("Caller-Caller-ID-Name");
+                String origCallerDestNumber = eventHeaders.get("Caller-Destination-Number");
+                String coreuuid = eventHeaders.get("Core-UUID");
+                //System.out.println("******** uuid=" + coreuuid + " " + origCallerIdName + " is hanging up " + origCallerDestNumber);
+
+                String voiceUserId = "";
+                String callerName = origCallerIdName;
+                String clientSession = "0";
+                String callState = "CALL_ENDED";
+
+                Matcher callerListenOnly = CALLERNAME_LISTENONLY_PATTERN.matcher(origCallerIdName);
+                Matcher callWithSess = CALLERNAME_WITH_SESS_INFO_PATTERN.matcher(origCallerIdName);
+                if (callWithSess.matches()) {
+                    voiceUserId = callWithSess.group(1).trim();
+                    clientSession = callWithSess.group(2).trim();
+                    callerName = callWithSess.group(3).trim();
+                } else if (callerListenOnly.matches()) {
+                    voiceUserId = callerListenOnly.group(1).trim();
+                    clientSession = callWithSess.group(2).trim();
+                    callerName = callerListenOnly.group(3).trim();
+                }
+
+                String conf = origCallerDestNumber;
+                Matcher callerDestNumberMatcher = ECHO_TEST_DEST_PATTERN.matcher(origCallerDestNumber);
+                if (callerDestNumberMatcher.matches()) {
+                    conf = callerDestNumberMatcher.group(1).trim();
+                }
+
+                VoiceCallStateEvent csEvent = new VoiceCallStateEvent(conf,
+                        coreuuid,
+                        clientSession,
+                        voiceUserId,
+                        callerName,
+                        callState,
+                        origCallerIdName,
+                        origCallerDestNumber
+                        );
+                conferenceEventListener.handleConferenceEvent(csEvent);
+
+            } else if ("RINGING".equalsIgnoreCase(channelCallState) && "CS_EXECUTE".equalsIgnoreCase(channelState)) {
+                String origCallerIdName = eventHeaders.get("Caller-Caller-ID-Name");
+                String origCallerDestNumber = eventHeaders.get("Caller-Destination-Number");
+                String coreuuid = eventHeaders.get("Core-UUID");
+                //System.out.println("******** uuid=" + coreuuid + " " + origCallerIdName + " is ringing " + origCallerDestNumber);
+
+                String voiceUserId = "";
+                String callerName = origCallerIdName;
+                String clientSession = "0";
+                String callState = "CALL_STARTED";
+
+                Matcher callerListenOnly = CALLERNAME_LISTENONLY_PATTERN.matcher(origCallerIdName);
+                Matcher callWithSess = CALLERNAME_WITH_SESS_INFO_PATTERN.matcher(origCallerIdName);
+                if (callWithSess.matches()) {
+                    voiceUserId = callWithSess.group(1).trim();
+                    clientSession = callWithSess.group(2).trim();
+                    callerName = callWithSess.group(3).trim();
+                } else if (callerListenOnly.matches()) {
+                    voiceUserId = callerListenOnly.group(1).trim();
+                    clientSession = callWithSess.group(2).trim();
+                    callerName = callerListenOnly.group(3).trim();
+                }
+
+                String conf = origCallerDestNumber;
+                Matcher callerDestNumberMatcher = ECHO_TEST_DEST_PATTERN.matcher(origCallerDestNumber);
+                if (callerDestNumberMatcher.matches()) {
+                    conf = callerDestNumberMatcher.group(1).trim();
+                }
+
+                VoiceCallStateEvent csEvent = new VoiceCallStateEvent(conf,
+                        coreuuid,
+                        clientSession,
+                        voiceUserId,
+                        callerName,
+                        callState,
+                        origCallerIdName,
+                        origCallerDestNumber
+                        );
+                conferenceEventListener.handleConferenceEvent(csEvent);
+            }
+
         }
     }
 
diff --git a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/actions/CheckFreeswitchStatusCommand.java b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/actions/CheckFreeswitchStatusCommand.java
index d0d2aa67f0..2e8e4f912d 100755
--- a/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/actions/CheckFreeswitchStatusCommand.java
+++ b/akka-bbb-fsesl/src/main/java/org/bigbluebutton/freeswitch/voice/freeswitch/actions/CheckFreeswitchStatusCommand.java
@@ -3,6 +3,7 @@ package org.bigbluebutton.freeswitch.voice.freeswitch.actions;
 import com.google.gson.Gson;
 import org.apache.commons.lang3.StringUtils;
 import org.bigbluebutton.freeswitch.voice.events.ConferenceEventListener;
+import org.bigbluebutton.freeswitch.voice.events.FreeswitchStatusReplyEvent;
 import org.freeswitch.esl.client.transport.message.EslMessage;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -26,10 +27,11 @@ public class CheckFreeswitchStatusCommand extends FreeswitchCommand {
     }
 
     public void handleResponse(EslMessage response, ConferenceEventListener eventListener) {
-
         Gson gson = new Gson();
         log.info(gson.toJson(response.getBodyLines()));
-
+        FreeswitchStatusReplyEvent statusEvent = new FreeswitchStatusReplyEvent(
+                gson.toJson(response.getBodyLines()));
+        eventListener.handleConferenceEvent(statusEvent);
     }
 
 }
diff --git a/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/VoiceConferenceService.scala b/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/VoiceConferenceService.scala
index 2e186f1990..07ff1be12d 100755
--- a/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/VoiceConferenceService.scala
+++ b/akka-bbb-fsesl/src/main/scala/org/bigbluebutton/freeswitch/VoiceConferenceService.scala
@@ -273,4 +273,41 @@ class VoiceConferenceService(sender: RedisPublisher) extends IVoiceConferenceSer
     sender.publish(fromVoiceConfRedisChannel, json)
   }
 
+  def voiceCallStateEvent(
+      conf:             String,
+      callSession:      String,
+      clientSession:    String,
+      userId:           String,
+      callerName:       String,
+      callState:        String,
+      origCallerIdName: String,
+      origCalledDest:   String
+  ): Unit = {
+    val header = BbbCoreVoiceConfHeader(VoiceConfCallStateEvtMsg.NAME, conf)
+    val body = VoiceConfCallStateEvtMsgBody(
+      voiceConf = conf,
+      callSession = callSession,
+      clientSession = clientSession,
+      userId = userId,
+      callerName = callerName,
+      callState = callState,
+      origCallerIdName = origCallerIdName,
+      origCalledDest = origCalledDest
+    )
+    val envelope = BbbCoreEnvelope(VoiceConfCallStateEvtMsg.NAME, Map("voiceConf" -> conf))
+
+    val msg = new VoiceConfCallStateEvtMsg(header, body)
+    val msgEvent = BbbCommonEnvCoreMsg(envelope, msg)
+
+    val json = JsonUtil.toJson(msgEvent)
+    sender.publish(fromVoiceConfRedisChannel, json)
+  }
+
+  def freeswitchStatusReplyEvent(json: String): Unit = {
+    // Placeholder so we can add a /healthz check endpoint to
+    // monitor akka-fsesl (ralam feb 5, 2020)
+    //println("***** >>>>")
+    //println(json)
+    //println("<<<< *****")
+  }
 }
diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala
index 83f90ead4d..4d92363940 100755
--- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala
+++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/VoiceConfMsgs.scala
@@ -433,4 +433,40 @@ case class UserDisconnectedFromGlobalAudioMsgBody(userId: String, name: String)
  */
 object SyncGetVoiceUsersRespMsg { val NAME = "SyncGetVoiceUsersRespMsg" }
 case class SyncGetVoiceUsersRespMsg(header: BbbClientMsgHeader, body: SyncGetVoiceUsersRespMsgBody) extends BbbCoreMsg
-case class SyncGetVoiceUsersRespMsgBody(voiceUsers: Vector[VoiceConfUser])
\ No newline at end of file
+case class SyncGetVoiceUsersRespMsgBody(voiceUsers: Vector[VoiceConfUser])
+
+/**
+ * Received from FS call state events.
+ */
+object VoiceConfCallStateEvtMsg { val NAME = "VoiceConfCallStateEvtMsg" }
+case class VoiceConfCallStateEvtMsg(
+    header: BbbCoreVoiceConfHeader,
+    body:   VoiceConfCallStateEvtMsgBody
+) extends VoiceStandardMsg
+case class VoiceConfCallStateEvtMsgBody(
+    voiceConf:        String,
+    callSession:      String,
+    clientSession:    String,
+    userId:           String,
+    callerName:       String,
+    callState:        String,
+    origCallerIdName: String,
+    origCalledDest:   String
+)
+
+/**
+ * Sent to interested parties call state events.
+ */
+object VoiceCallStateEvtMsg { val NAME = "VoiceCallStateEvtMsg" }
+case class VoiceCallStateEvtMsg(
+    header: BbbClientMsgHeader,
+    body:   VoiceCallStateEvtMsgBody
+) extends BbbCoreMsg
+case class VoiceCallStateEvtMsgBody(
+    meetingId:     String,
+    voiceConf:     String,
+    clientSession: String,
+    userId:        String,
+    callerName:    String,
+    callState:     String
+)
\ No newline at end of file
diff --git a/bbb-fsesl-client/src/main/java/org/freeswitch/esl/client/inbound/Client.java b/bbb-fsesl-client/src/main/java/org/freeswitch/esl/client/inbound/Client.java
index 482067ed8e..dc5d0f6d46 100755
--- a/bbb-fsesl-client/src/main/java/org/freeswitch/esl/client/inbound/Client.java
+++ b/bbb-fsesl-client/src/main/java/org/freeswitch/esl/client/inbound/Client.java
@@ -428,6 +428,7 @@ public class Client
         public void eventReceived( final EslEvent event )
         {
             log.debug( "Event received [{}]", event );
+
             /*
              *  Notify listeners in a different thread in order to:
              *    - not to block the IO threads with potentially long-running listeners
@@ -537,8 +538,10 @@ public class Client
                                         System.out.println("##### " + sb.toString());
                                          **/
                                     }
+                                } else {
+                                    listener.eventReceived( event );
                                 }
-                                listener.eventReceived( event );
+
                             } catch ( Throwable t ) {
                                 log.error( "Error caught notifying listener of event [" + event + ']', t );
                             }
-- 
GitLab