From a61030a95f788133d4feb9d3836ab37484fd20ae Mon Sep 17 00:00:00 2001
From: Richard Alam <ritzalam@gmail.com>
Date: Tue, 18 Jul 2017 14:47:42 -0700
Subject: [PATCH]  - track meeting inactivity and expiry in akka-apps

---
 .../users/LogoutAndEndMeetingCmdMsgHdlr.scala |  15 +--
 .../MeetingActivityResponseCmdMsgHdlr.scala   |  31 +++++-
 .../domain/MeetingInactivityTracker.scala     |  57 ++++++----
 .../core/domain/MeetingState2x.scala          |   8 ++
 .../core/running/HandlerHelpers.scala         |  30 ++++--
 .../core/running/MeetingActor.scala           |  56 ++--------
 .../running/MeetingExpiryTrackerHelper.scala  |  72 ++++++++-----
 .../MeetingInactivityTrackerHelper.scala      | 100 ------------------
 .../meeting/EndMeetingSysCmdMsgHdlr.scala     |  13 +--
 .../users/ValidateAuthTokenReqMsgHdlr.scala   |   1 +
 .../core2/message/senders/MsgBuilder.scala    |  20 ----
 .../bigbluebutton/client/MsgToClientGW.scala  |   2 -
 .../common2/msgs/SystemMsgs.scala             |   2 +-
 .../messaging/ConnectionInvokerService.java   |   4 +-
 14 files changed, 160 insertions(+), 251 deletions(-)
 delete mode 100755 akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingInactivityTrackerHelper.scala

diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/LogoutAndEndMeetingCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/LogoutAndEndMeetingCmdMsgHdlr.scala
index 90a21bead6..c13562218f 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/LogoutAndEndMeetingCmdMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/LogoutAndEndMeetingCmdMsgHdlr.scala
@@ -2,7 +2,8 @@ package org.bigbluebutton.core.apps.users
 
 import org.bigbluebutton.common2.msgs._
 import org.bigbluebutton.core.OutMessageGateway
-import org.bigbluebutton.core.bus.{ IncomingEventBus }
+import org.bigbluebutton.core.bus.IncomingEventBus
+import org.bigbluebutton.core.domain.MeetingEndReason
 import org.bigbluebutton.core.models.{ Roles, Users2x }
 import org.bigbluebutton.core.running.LiveMeeting
 
@@ -18,17 +19,7 @@ trait LogoutAndEndMeetingCmdMsgHdlr {
       u <- Users2x.findWithIntId(liveMeeting.users2x, msg.body.userId)
     } yield {
       if (u.role == Roles.MODERATOR_ROLE) {
-        endMeeting(outGW, liveMeeting)
-
-        if (liveMeeting.props.meetingProp.isBreakout) {
-          log.info(
-            "Informing parent meeting {} that a breakout room has been ended {}",
-            liveMeeting.props.breakoutProps.parentId, liveMeeting.props.meetingProp.intId
-          )
-          notifyParentThatBreakoutEnded(eventBus, liveMeeting)
-        }
-
-        destroyMeeting(eventBus, liveMeeting.props.meetingProp.intId)
+        sendEndMeetingDueToExpiry(MeetingEndReason.ENDED_AFTER_USER_LOGGED_OUT, eventBus, outGW, liveMeeting)
       }
     }
   }
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/MeetingActivityResponseCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/MeetingActivityResponseCmdMsgHdlr.scala
index a6f52251ce..ac22a7d39d 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/MeetingActivityResponseCmdMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/MeetingActivityResponseCmdMsgHdlr.scala
@@ -1,9 +1,10 @@
 package org.bigbluebutton.core.apps.users
 
+import org.bigbluebutton.common2.domain.DefaultProps
 import org.bigbluebutton.common2.msgs._
 import org.bigbluebutton.core.OutMessageGateway
 import org.bigbluebutton.core.domain.{ MeetingInactivityTracker, MeetingState2x }
-import org.bigbluebutton.core.running.{ BaseMeetingActor, LiveMeeting, MeetingInactivityTrackerHelper }
+import org.bigbluebutton.core.running.{ LiveMeeting }
 
 trait MeetingActivityResponseCmdMsgHdlr {
   this: UsersApp =>
@@ -12,11 +13,31 @@ trait MeetingActivityResponseCmdMsgHdlr {
   val outGW: OutMessageGateway
 
   def handleMeetingActivityResponseCmdMsg(
-    msg:    MeetingActivityResponseCmdMsg,
-    state:  MeetingState2x,
-    helper: MeetingInactivityTrackerHelper
+    msg:   MeetingActivityResponseCmdMsg,
+    state: MeetingState2x
   ): MeetingState2x = {
-    helper.processMeetingActivityResponse(liveMeeting.props, outGW, msg)
+    processMeetingActivityResponse(liveMeeting.props, outGW, msg)
     MeetingInactivityTracker.resetWarningSentAndTimestamp(state)
   }
+
+  def processMeetingActivityResponse(
+    props: DefaultProps,
+    outGW: OutMessageGateway,
+    msg:   MeetingActivityResponseCmdMsg
+  ): Unit = {
+
+    def buildMeetingIsActiveEvtMsg(meetingId: String): BbbCommonEnvCoreMsg = {
+      val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, "not-used")
+      val envelope = BbbCoreEnvelope(MeetingIsActiveEvtMsg.NAME, routing)
+      val body = MeetingIsActiveEvtMsgBody(meetingId)
+      val header = BbbClientMsgHeader(MeetingIsActiveEvtMsg.NAME, meetingId, "not-used")
+      val event = MeetingIsActiveEvtMsg(header, body)
+
+      BbbCommonEnvCoreMsg(envelope, event)
+    }
+
+    val event = buildMeetingIsActiveEvtMsg(props.meetingProp.intId)
+    outGW.send(event)
+
+  }
 }
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/domain/MeetingInactivityTracker.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/domain/MeetingInactivityTracker.scala
index d0d59f0e2d..248ba33c02 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/domain/MeetingInactivityTracker.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/domain/MeetingInactivityTracker.scala
@@ -3,6 +3,14 @@ package org.bigbluebutton.core.domain
 import com.softwaremill.quicklens._
 import org.bigbluebutton.core.util.TimeUtil
 
+case class MeetingInactivityTracker(
+  val maxInactivityTimeoutMinutes: Int,
+  val warningMinutesBeforeMax:     Int,
+  lastActivityTimestamp:           Long,
+  warningSent:                     Boolean,
+  warningSentOnTimestamp:          Long
+)
+
 object MeetingInactivityTracker {
 
   def warningHasBeenSent(state: MeetingState2x): Boolean = {
@@ -28,7 +36,8 @@ object MeetingInactivityTracker {
 
   def hasRecentActivity(state: MeetingState2x, nowInSeconds: Long): Boolean = {
     nowInSeconds - state.inactivityTracker.lastActivityTimestamp <
-      TimeUtil.minutesToSeconds(state.inactivityTracker.maxInactivityTimeoutMinutes)
+      TimeUtil.minutesToSeconds(state.inactivityTracker.maxInactivityTimeoutMinutes) -
+      TimeUtil.minutesToSeconds(state.inactivityTracker.warningMinutesBeforeMax)
   }
 
   def isMeetingInactive(state: MeetingState2x, nowInSeconds: Long): Boolean = {
@@ -43,12 +52,13 @@ object MeetingInactivityTracker {
   }
 }
 
-case class MeetingInactivityTracker(
-  val maxInactivityTimeoutMinutes: Int,
-  val warningMinutesBeforeMax:     Int,
-  lastActivityTimestamp:           Long,
-  warningSent:                     Boolean,
-  warningSentOnTimestamp:          Long
+case class MeetingExpiryTracker(
+  startedOn:                              Long,
+  userHasJoined:                          Boolean,
+  lastUserLeftOn:                         Option[Long],
+  durationInMinutes:                      Int,
+  meetingExpireIfNoUserJoinedInMinutes:   Int,
+  meetingExpireWhenLastUserLeftInMinutes: Int
 )
 
 object MeetingExpiryTracker {
@@ -59,21 +69,38 @@ object MeetingExpiryTracker {
 
   def setUserHasJoined(state: MeetingState2x): MeetingState2x = {
     val tracker = state.expiryTracker.modify(_.userHasJoined).setTo(true)
+      .modify(_.lastUserLeftOn).setTo(None)
     state.modify(_.expiryTracker).setTo(tracker)
   }
 
+  def hasMeetingExpiredAfterLastUserLeft(state: MeetingState2x, timestampInSeconds: Long): Boolean = {
+    val expire = for {
+      lastUserLeftOn <- state.expiryTracker.lastUserLeftOn
+    } yield {
+      timestampInSeconds - lastUserLeftOn >
+        TimeUtil.minutesToSeconds(state.expiryTracker.meetingExpireWhenLastUserLeftInMinutes)
+    }
+
+    expire.getOrElse(false)
+  }
+
   def setLastUserLeftOn(state: MeetingState2x, timestampInSeconds: Long): MeetingState2x = {
-    val tracker = state.expiryTracker.modify(_.lastUserLeftOn).setTo(timestampInSeconds)
+    val tracker = state.expiryTracker.modify(_.lastUserLeftOn).setTo(Some(timestampInSeconds))
     state.modify(_.expiryTracker).setTo(tracker)
   }
 
   def hasMeetingExpiredNeverBeenJoined(state: MeetingState2x, nowInSeconds: Long): Boolean = {
-    nowInSeconds - state.expiryTracker.startedOn >
-      TimeUtil.minutesToSeconds(state.expiryTracker.meetingExpireIfNoUserJoinedInMinutes)
+    !state.expiryTracker.userHasJoined &&
+      (nowInSeconds - state.expiryTracker.startedOn >
+        TimeUtil.minutesToSeconds(state.expiryTracker.meetingExpireIfNoUserJoinedInMinutes))
   }
 
   def meetingOverDuration(state: MeetingState2x, nowInSeconds: Long): Boolean = {
-    nowInSeconds > state.expiryTracker.startedOn + TimeUtil.minutesToSeconds(state.expiryTracker.durationInMinutes)
+    if (state.expiryTracker.durationInMinutes == 0) {
+      false
+    } else {
+      nowInSeconds > state.expiryTracker.startedOn + TimeUtil.minutesToSeconds(state.expiryTracker.durationInMinutes)
+    }
   }
 
   def endMeetingTime(state: MeetingState2x): Int = {
@@ -81,11 +108,3 @@ object MeetingExpiryTracker {
   }
 }
 
-case class MeetingExpiryTracker(
-  startedOn:                              Long,
-  userHasJoined:                          Boolean,
-  lastUserLeftOn:                         Long,
-  durationInMinutes:                      Int,
-  meetingExpireIfNoUserJoinedInMinutes:   Int,
-  meetingExpireWhenLastUserLeftInMinutes: Int
-)
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/domain/MeetingState2x.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/domain/MeetingState2x.scala
index f86cedfdd7..bb880efc73 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/domain/MeetingState2x.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/domain/MeetingState2x.scala
@@ -9,3 +9,11 @@ case class MeetingState2x(
   expiryTracker:     MeetingExpiryTracker
 )
 
+object MeetingEndReason {
+  val ENDED_FROM_API = "ENDED_FROM_API"
+  val ENDED_DUE_TO_INACTIVITY = "ENDED_DUE_TO_ACTIVITY"
+  val ENDED_WHEN_NOT_JOINED = "ENDED_WHEN_NOT_JOINED"
+  val ENDED_WHEN_LAST_USER_LEFT = "ENDED_WHEN_LAST_USER_LEFT"
+  val ENDED_AFTER_USER_LOGGED_OUT = "ENDED_AFTER_USER_LOGGED_OUT"
+  val ENDED_AFTER_EXCEEDING_DURATION = "ENDED_AFTER_USER_LOGGED_OUT"
+}
\ No newline at end of file
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/HandlerHelpers.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/HandlerHelpers.scala
index 3e43a45ce9..746ac79428 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/HandlerHelpers.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/HandlerHelpers.scala
@@ -1,12 +1,14 @@
 package org.bigbluebutton.core.running
 
 import org.bigbluebutton.SystemConfiguration
+import org.bigbluebutton.common2.domain.DefaultProps
 import org.bigbluebutton.common2.msgs._
 import org.bigbluebutton.core.api.{ BreakoutRoomEndedInternalMsg, DestroyMeetingInternalMsg, RecordingStatusChanged }
 import org.bigbluebutton.core.bus.{ BigBlueButtonEvent, IncomingEventBus }
 import org.bigbluebutton.core.domain.{ MeetingExpiryTracker, MeetingState2x }
-import org.bigbluebutton.core.{ OutMessageGateway }
+import org.bigbluebutton.core.OutMessageGateway
 import org.bigbluebutton.core.models._
+import org.bigbluebutton.core.util.TimeUtil
 import org.bigbluebutton.core2.MeetingStatus2x
 import org.bigbluebutton.core2.message.senders.{ MsgBuilder, Sender, UserJoinedMeetingEvtMsgBuilder }
 
@@ -178,11 +180,11 @@ trait HandlerHelpers extends SystemConfiguration {
     outGW.send(event)
   }
 
-  def endMeeting(outGW: OutMessageGateway, liveMeeting: LiveMeeting): Unit = {
+  def endMeeting(outGW: OutMessageGateway, liveMeeting: LiveMeeting, reason: String): Unit = {
     def buildMeetingEndingEvtMsg(meetingId: String): BbbCommonEnvCoreMsg = {
       val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, "not-used")
       val envelope = BbbCoreEnvelope(MeetingEndingEvtMsg.NAME, routing)
-      val body = MeetingEndingEvtMsgBody(meetingId)
+      val body = MeetingEndingEvtMsgBody(meetingId, reason)
       val header = BbbClientMsgHeader(MeetingEndingEvtMsg.NAME, meetingId, "not-used")
       val event = MeetingEndingEvtMsg(header, body)
 
@@ -215,9 +217,23 @@ trait HandlerHelpers extends SystemConfiguration {
   }
 
   def notifyParentThatBreakoutEnded(eventBus: IncomingEventBus, liveMeeting: LiveMeeting): Unit = {
-    eventBus.publish(BigBlueButtonEvent(
-      liveMeeting.props.breakoutProps.parentId,
-      new BreakoutRoomEndedInternalMsg(liveMeeting.props.meetingProp.intId)
-    ))
+    if (liveMeeting.props.meetingProp.isBreakout) {
+      eventBus.publish(BigBlueButtonEvent(
+        liveMeeting.props.breakoutProps.parentId,
+        new BreakoutRoomEndedInternalMsg(liveMeeting.props.meetingProp.intId)
+      ))
+    }
+  }
+
+  def ejectAllUsersFromVoiceConf(outGW: OutMessageGateway, liveMeeting: LiveMeeting): Unit = {
+    val event = MsgBuilder.buildEjectAllFromVoiceConfMsg(liveMeeting.props.meetingProp.intId, liveMeeting.props.voiceProp.voiceConf)
+    outGW.send(event)
+  }
+
+  def sendEndMeetingDueToExpiry(reason: String, eventBus: IncomingEventBus, outGW: OutMessageGateway, liveMeeting: LiveMeeting): Unit = {
+    endMeeting(outGW, liveMeeting, reason)
+    notifyParentThatBreakoutEnded(eventBus, liveMeeting)
+    ejectAllUsersFromVoiceConf(outGW, liveMeeting)
+    destroyMeeting(eventBus, liveMeeting.props.meetingProp.intId)
   }
 }
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 c83dc77699..3490373742 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
@@ -104,7 +104,6 @@ class MeetingActor(
   val chatApp2x = new ChatApp2x(liveMeeting, outGW)
   val usersApp = new UsersApp(liveMeeting, outGW, eventBus)
 
-  val inactivityTrackerHelper = new MeetingInactivityTrackerHelper(liveMeeting, outGW)
   val expiryTrackerHelper = new MeetingExpiryTrackerHelper(liveMeeting, outGW)
 
   val inactivityTracker = new MeetingInactivityTracker(
@@ -118,7 +117,7 @@ class MeetingActor(
   val expiryTracker = new MeetingExpiryTracker(
     startedOn = TimeUtil.timeNowInSeconds(),
     userHasJoined = false,
-    lastUserLeftOn = 0L,
+    lastUserLeftOn = None,
     durationInMinutes = props.durationProps.duration,
     meetingExpireIfNoUserJoinedInMinutes = props.durationProps.meetingExpireIfNoUserJoinedInMinutes,
     meetingExpireWhenLastUserLeftInMinutes = props.durationProps.meetingExpireWhenLastUserLeftInMinutes
@@ -161,19 +160,21 @@ class MeetingActor(
   }
 
   private def handleBbbCommonEnvCoreMsg(msg: BbbCommonEnvCoreMsg): Unit = {
-    // TODO: Update meeting activity status here
-    // updateActivityStatus(msg)
+    state = MeetingInactivityTracker.updateLastActivityTimestamp(state, TimeUtil.timeNowInSeconds())
 
     msg.core match {
       // Users
-      case m: ValidateAuthTokenReqMsg   => state = usersApp.handleValidateAuthTokenReqMsg(m, state)
-      case m: UserJoinMeetingReqMsg     => state = handleUserJoinMeetingReqMsg(m, state)
-      case m: UserLeaveReqMsg           => state = handleUserLeaveReqMsg(m, state)
+      case m: ValidateAuthTokenReqMsg =>
+        state = usersApp.handleValidateAuthTokenReqMsg(m, state)
+      case m: UserJoinMeetingReqMsg =>
+        state = handleUserJoinMeetingReqMsg(m, state)
+      case m: UserLeaveReqMsg =>
+        state = handleUserLeaveReqMsg(m, state)
       case m: UserBroadcastCamStartMsg  => handleUserBroadcastCamStartMsg(m)
       case m: UserBroadcastCamStopMsg   => handleUserBroadcastCamStopMsg(m)
       case m: UserJoinedVoiceConfEvtMsg => handleUserJoinedVoiceConfEvtMsg(m)
       case m: MeetingActivityResponseCmdMsg =>
-        state = usersApp.handleMeetingActivityResponseCmdMsg(m, state, inactivityTrackerHelper)
+        state = usersApp.handleMeetingActivityResponseCmdMsg(m, state)
       case m: LogoutAndEndMeetingCmdMsg => usersApp.handleLogoutAndEndMeetingCmdMsg(m)
       case m: SetRecordingStatusCmdMsg => usersApp.handleSetRecordingStatusCmdMsg(m)
       case m: GetRecordingStatusReqMsg => usersApp.handleGetRecordingStatusReqMsg(m)
@@ -326,45 +327,8 @@ class MeetingActor(
   }
 
   def handleMonitorNumberOfUsers(msg: MonitorNumberOfUsersInternalMsg) {
-    state = inactivityTrackerHelper.processMeetingInactivityAudit(
-      props = liveMeeting.props,
-      outGW,
-      eventBus,
-      state
-    )
-
+    state = expiryTrackerHelper.processMeetingInactivityAudit(liveMeeting.props, outGW, eventBus, state)
     state = expiryTrackerHelper.processMeetingExpiryAudit(liveMeeting.props, state, eventBus)
-
-    monitorNumberOfWebUsers()
-    monitorNumberOfUsers()
-  }
-
-  def monitorNumberOfWebUsers() {
-
-    def buildEjectAllFromVoiceConfMsg(meetingId: String, voiceConf: String): BbbCommonEnvCoreMsg = {
-      val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
-      val envelope = BbbCoreEnvelope(EjectAllFromVoiceConfMsg.NAME, routing)
-      val body = EjectAllFromVoiceConfMsgBody(voiceConf)
-      val header = BbbCoreHeaderWithMeetingId(EjectAllFromVoiceConfMsg.NAME, meetingId)
-      val event = EjectAllFromVoiceConfMsg(header, body)
-
-      BbbCommonEnvCoreMsg(envelope, event)
-    }
-
-    if (Users2x.numUsers(liveMeeting.users2x) == 0 &&
-      state.expiryTracker.lastUserLeftOn > 0) {
-      if (TimeUtil.timeNowInMinutes - state.expiryTracker.lastUserLeftOn > 2) {
-        log.info("Empty meeting. Ejecting all users from voice. meetingId={}", props.meetingProp.intId)
-        val event = buildEjectAllFromVoiceConfMsg(props.meetingProp.intId, props.voiceProp.voiceConf)
-        outGW.send(event)
-      }
-    }
-  }
-
-  def monitorNumberOfUsers() {
-    val hasUsers = Users2x.numUsers(liveMeeting.users2x) != 0
-    // TODO: We could use a better control over this message to send it just when it really matters :)
-    eventBus.publish(BigBlueButtonEvent(props.meetingProp.intId, UpdateMeetingExpireMonitor(props.meetingProp.intId, hasUsers)))
   }
 
   def handleExtendMeetingDuration(msg: ExtendMeetingDuration) {
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingExpiryTrackerHelper.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingExpiryTrackerHelper.scala
index 3aa361d986..994c23492e 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingExpiryTrackerHelper.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingExpiryTrackerHelper.scala
@@ -3,9 +3,10 @@ package org.bigbluebutton.core.running
 import akka.actor.ActorContext
 import akka.event.Logging
 import org.bigbluebutton.common2.domain.DefaultProps
+import org.bigbluebutton.common2.msgs._
 import org.bigbluebutton.core.OutMessageGateway
 import org.bigbluebutton.core.bus.IncomingEventBus
-import org.bigbluebutton.core.domain.{ MeetingExpiryTracker, MeetingState2x }
+import org.bigbluebutton.core.domain.{ MeetingEndReason, MeetingExpiryTracker, MeetingInactivityTracker, MeetingState2x }
 import org.bigbluebutton.core.util.TimeUtil
 
 class MeetingExpiryTrackerHelper(
@@ -15,45 +16,64 @@ class MeetingExpiryTrackerHelper(
 
   val log = Logging(context.system, getClass)
 
-  def processNeverBeenJoinedExpiry(nowInSeconds: Long, props: DefaultProps, state: MeetingState2x,
-                                   eventBus: IncomingEventBus): MeetingState2x = {
+  def processMeetingExpiryAudit(props: DefaultProps, state: MeetingState2x, eventBus: IncomingEventBus): MeetingState2x = {
+    val nowInSeconds = TimeUtil.timeNowInSeconds()
+
     if (MeetingExpiryTracker.hasMeetingExpiredNeverBeenJoined(state, nowInSeconds)) {
-      log.info("Ending meeting as it has never been joined.")
-      sendEndMeetingDueToExpiry(props, eventBus)
-      state
-    } else {
-      state
+      sendEndMeetingDueToExpiry(MeetingEndReason.ENDED_WHEN_NOT_JOINED, eventBus, outGW, liveMeeting)
+    } else if (MeetingExpiryTracker.meetingOverDuration(state, nowInSeconds)) {
+      sendEndMeetingDueToExpiry(MeetingEndReason.ENDED_AFTER_EXCEEDING_DURATION, eventBus, outGW, liveMeeting)
+    } else if (MeetingExpiryTracker.hasMeetingExpiredAfterLastUserLeft(state, nowInSeconds)) {
+      sendEndMeetingDueToExpiry(MeetingEndReason.ENDED_WHEN_LAST_USER_LEFT, eventBus, outGW, liveMeeting)
     }
+
+    state
   }
 
-  def processMeetingExpiryAudit(props: DefaultProps, state: MeetingState2x, eventBus: IncomingEventBus): MeetingState2x = {
-    val nowInSeconds = TimeUtil.timeNowInSeconds()
+  def processMeetingInactivityAudit(
+    props:    DefaultProps,
+    outGW:    OutMessageGateway,
+    eventBus: IncomingEventBus,
+    state:    MeetingState2x
+  ): MeetingState2x = {
 
-    if (!state.expiryTracker.userHasJoined) {
-      processNeverBeenJoinedExpiry(nowInSeconds, props, state, eventBus)
-    } else {
-      if (props.durationProps.duration != 0 && MeetingExpiryTracker.meetingOverDuration(state, nowInSeconds)) {
-        log.info("Ending meeting as it has passed duration.")
-        sendEndMeetingDueToExpiry(props, eventBus)
+    val nowInSeconds = TimeUtil.timeNowInSeconds()
+    if (!MeetingInactivityTracker.hasRecentActivity(state, nowInSeconds)) {
+      if (MeetingInactivityTracker.isMeetingInactive(state, nowInSeconds)) {
+        sendEndMeetingDueToExpiry(MeetingEndReason.ENDED_DUE_TO_INACTIVITY, eventBus, outGW, liveMeeting)
         state
       } else {
-        state
+        if (!MeetingInactivityTracker.warningHasBeenSent(state)) {
+          warnOfMeetingInactivity(props, outGW, nowInSeconds, state)
+          MeetingInactivityTracker.setWarningSentAndTimestamp(state, nowInSeconds)
+        } else {
+          state
+        }
       }
+    } else {
+      state
     }
   }
 
-  def sendEndMeetingDueToExpiry(props: DefaultProps, eventBus: IncomingEventBus): Unit = {
+  def warnOfMeetingInactivity(props: DefaultProps, outGW: OutMessageGateway,
+                              nowInSeconds: Long, state: MeetingState2x): Unit = {
+    val timeLeftSeconds = MeetingInactivityTracker.timeLeftInSeconds(state, nowInSeconds)
+    sendMeetingInactivityWarning(props, outGW, timeLeftSeconds)
+  }
 
-    endMeeting(outGW, liveMeeting)
+  def sendMeetingInactivityWarning(props: DefaultProps, outGW: OutMessageGateway, timeLeftSeconds: Long): Unit = {
+    def build(meetingId: String, timeLeftInSec: Long): BbbCommonEnvCoreMsg = {
+      val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, "not-used")
+      val envelope = BbbCoreEnvelope(MeetingInactivityWarningEvtMsg.NAME, routing)
+      val body = MeetingInactivityWarningEvtMsgBody(timeLeftInSec)
+      val header = BbbClientMsgHeader(MeetingInactivityWarningEvtMsg.NAME, meetingId, "not-used")
+      val event = MeetingInactivityWarningEvtMsg(header, body)
 
-    if (liveMeeting.props.meetingProp.isBreakout) {
-      log.info(
-        "Informing parent meeting {} that a breakout room has been ended {}",
-        liveMeeting.props.breakoutProps.parentId, liveMeeting.props.meetingProp.intId
-      )
-      notifyParentThatBreakoutEnded(eventBus, liveMeeting)
+      BbbCommonEnvCoreMsg(envelope, event)
     }
 
-    destroyMeeting(eventBus, liveMeeting.props.meetingProp.intId)
+    val event = build(props.meetingProp.intId, timeLeftSeconds)
+    outGW.send(event)
   }
+
 }
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingInactivityTrackerHelper.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingInactivityTrackerHelper.scala
deleted file mode 100755
index eaa592636a..0000000000
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingInactivityTrackerHelper.scala
+++ /dev/null
@@ -1,100 +0,0 @@
-package org.bigbluebutton.core.running
-
-import akka.actor.ActorContext
-import akka.event.Logging
-import org.bigbluebutton.core.domain.{ MeetingInactivityTracker, MeetingState2x }
-import com.softwaremill.quicklens._
-import org.bigbluebutton.common2.domain.DefaultProps
-import org.bigbluebutton.common2.msgs._
-import org.bigbluebutton.core.OutMessageGateway
-import org.bigbluebutton.core.bus.IncomingEventBus
-import org.bigbluebutton.core.util.TimeUtil
-
-class MeetingInactivityTrackerHelper(
-    val liveMeeting: LiveMeeting,
-    val outGW:       OutMessageGateway
-)(implicit val context: ActorContext) extends HandlerHelpers {
-
-  val log = Logging(context.system, getClass)
-
-  def processMeetingInactivityAudit(
-    props:    DefaultProps,
-    outGW:    OutMessageGateway,
-    eventBus: IncomingEventBus,
-    state:    MeetingState2x
-  ): MeetingState2x = {
-
-    val nowInSeconds = TimeUtil.timeNowInSeconds()
-    if (!MeetingInactivityTracker.hasRecentActivity(state, nowInSeconds)) {
-      if (MeetingInactivityTracker.isMeetingInactive(state, nowInSeconds)) {
-        sendEndMeetingDueToInactivity(props, eventBus)
-        state
-      } else {
-        if (!MeetingInactivityTracker.warningHasBeenSent(state)) {
-          warnOfMeetingInactivity(props, outGW, nowInSeconds, state)
-          MeetingInactivityTracker.setWarningSentAndTimestamp(state, nowInSeconds)
-        } else {
-          state
-        }
-      }
-    } else {
-      state
-    }
-  }
-
-  def warnOfMeetingInactivity(props: DefaultProps, outGW: OutMessageGateway,
-                              nowInSeconds: Long, state: MeetingState2x): Unit = {
-    val timeLeftSeconds = MeetingInactivityTracker.timeLeftInSeconds(state, nowInSeconds)
-    sendMeetingInactivityWarning(props, outGW, timeLeftSeconds)
-  }
-
-  def sendEndMeetingDueToInactivity(props: DefaultProps, eventBus: IncomingEventBus): Unit = {
-    endMeeting(outGW, liveMeeting)
-
-    if (liveMeeting.props.meetingProp.isBreakout) {
-      log.info(
-        "Informing parent meeting {} that a breakout room has been ended {}",
-        liveMeeting.props.breakoutProps.parentId, liveMeeting.props.meetingProp.intId
-      )
-      notifyParentThatBreakoutEnded(eventBus, liveMeeting)
-    }
-
-    destroyMeeting(eventBus, liveMeeting.props.meetingProp.intId)
-  }
-
-  def sendMeetingInactivityWarning(props: DefaultProps, outGW: OutMessageGateway, timeLeftSeconds: Long): Unit = {
-    def build(meetingId: String, timeLeftInSec: Long): BbbCommonEnvCoreMsg = {
-      val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, "not-used")
-      val envelope = BbbCoreEnvelope(MeetingInactivityWarningEvtMsg.NAME, routing)
-      val body = MeetingInactivityWarningEvtMsgBody(timeLeftInSec)
-      val header = BbbClientMsgHeader(MeetingInactivityWarningEvtMsg.NAME, meetingId, "not-used")
-      val event = MeetingInactivityWarningEvtMsg(header, body)
-
-      BbbCommonEnvCoreMsg(envelope, event)
-    }
-
-    val event = build(props.meetingProp.intId, timeLeftSeconds)
-    outGW.send(event)
-  }
-
-  def processMeetingActivityResponse(
-    props: DefaultProps,
-    outGW: OutMessageGateway,
-    msg:   MeetingActivityResponseCmdMsg
-  ): Unit = {
-
-    def buildMeetingIsActiveEvtMsg(meetingId: String): BbbCommonEnvCoreMsg = {
-      val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, "not-used")
-      val envelope = BbbCoreEnvelope(MeetingIsActiveEvtMsg.NAME, routing)
-      val body = MeetingIsActiveEvtMsgBody(meetingId)
-      val header = BbbClientMsgHeader(MeetingIsActiveEvtMsg.NAME, meetingId, "not-used")
-      val event = MeetingIsActiveEvtMsg(header, body)
-
-      BbbCommonEnvCoreMsg(envelope, event)
-    }
-
-    val event = buildMeetingIsActiveEvtMsg(props.meetingProp.intId)
-    outGW.send(event)
-
-  }
-}
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/meeting/EndMeetingSysCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/meeting/EndMeetingSysCmdMsgHdlr.scala
index 597f0e7ed8..1358e94059 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/meeting/EndMeetingSysCmdMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/meeting/EndMeetingSysCmdMsgHdlr.scala
@@ -3,6 +3,7 @@ package org.bigbluebutton.core2.message.handlers.meeting
 import org.bigbluebutton.common2.msgs._
 import org.bigbluebutton.core.OutMessageGateway
 import org.bigbluebutton.core.bus.IncomingEventBus
+import org.bigbluebutton.core.domain.MeetingEndReason
 import org.bigbluebutton.core.running.{ BaseMeetingActor, HandlerHelpers, LiveMeeting }
 
 trait EndMeetingSysCmdMsgHdlr extends HandlerHelpers {
@@ -13,17 +14,7 @@ trait EndMeetingSysCmdMsgHdlr extends HandlerHelpers {
   val eventBus: IncomingEventBus
 
   def handleEndMeeting(msg: EndMeetingSysCmdMsg) {
-    endMeeting(outGW, liveMeeting)
-
-    if (liveMeeting.props.meetingProp.isBreakout) {
-      log.info(
-        "Informing parent meeting {} that a breakout room has been ended {}",
-        liveMeeting.props.breakoutProps.parentId, liveMeeting.props.meetingProp.intId
-      )
-      notifyParentThatBreakoutEnded(eventBus, liveMeeting)
-    }
-
-    destroyMeeting(eventBus, liveMeeting.props.meetingProp.intId)
+    sendEndMeetingDueToExpiry(MeetingEndReason.ENDED_FROM_API, eventBus, outGW, liveMeeting)
   }
 
 }
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/users/ValidateAuthTokenReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/users/ValidateAuthTokenReqMsgHdlr.scala
index cae96d5cd4..00e4297a66 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/users/ValidateAuthTokenReqMsgHdlr.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/handlers/users/ValidateAuthTokenReqMsgHdlr.scala
@@ -36,4 +36,5 @@ trait ValidateAuthTokenReqMsgHdlr extends HandlerHelpers {
           userId = msg.body.userId, authToken = msg.body.authToken, valid = false, waitForApproval = false, state)
     }
   }
+
 }
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/senders/MsgBuilder.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/senders/MsgBuilder.scala
index e817177c33..928ba490a3 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/senders/MsgBuilder.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/message/senders/MsgBuilder.scala
@@ -227,26 +227,6 @@ object MsgBuilder {
     BbbCommonEnvCoreMsg(envelope, event)
   }
 
-  def buildMeetingEndingEvtMsg(meetingId: String): BbbCommonEnvCoreMsg = {
-    val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, "not-used")
-    val envelope = BbbCoreEnvelope(MeetingEndingEvtMsg.NAME, routing)
-    val body = MeetingEndingEvtMsgBody(meetingId)
-    val header = BbbClientMsgHeader(MeetingEndingEvtMsg.NAME, meetingId, "not-used")
-    val event = MeetingEndingEvtMsg(header, body)
-
-    BbbCommonEnvCoreMsg(envelope, event)
-  }
-
-  def buildMeetingEndedEvtMsg(meetingId: String): BbbCommonEnvCoreMsg = {
-    val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
-    val envelope = BbbCoreEnvelope(MeetingEndedEvtMsg.NAME, routing)
-    val body = MeetingEndedEvtMsgBody(meetingId)
-    val header = BbbCoreBaseHeader(MeetingEndedEvtMsg.NAME)
-    val event = MeetingEndedEvtMsg(header, body)
-
-    BbbCommonEnvCoreMsg(envelope, event)
-  }
-
   def buildBreakoutRoomEndedEvtMsg(meetingId: String, userId: String, breakoutRoomId: String): BbbCommonEnvCoreMsg = {
     val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId)
     val envelope = BbbCoreEnvelope(BreakoutRoomEndedEvtMsg.NAME, routing)
diff --git a/bbb-apps-common/src/main/scala/org/bigbluebutton/client/MsgToClientGW.scala b/bbb-apps-common/src/main/scala/org/bigbluebutton/client/MsgToClientGW.scala
index bec29b9fa7..ab7e762470 100755
--- a/bbb-apps-common/src/main/scala/org/bigbluebutton/client/MsgToClientGW.scala
+++ b/bbb-apps-common/src/main/scala/org/bigbluebutton/client/MsgToClientGW.scala
@@ -9,12 +9,10 @@ case class DisconnectAllConnections(scope: String) extends SystemMessage
 class MsgToClientGW(val connInvokerService: IConnectionInvokerService) {
 
   def broadcastToMeeting(msg: BroadcastToMeetingMsg): Unit = {
-    //println("**** MsgToClientGW broadcastToMeeting " + msg.json)
     connInvokerService.sendMessage(msg)
   }
 
   def directToClient(msg: DirectToClientMsg): Unit = {
-    //println("**** MsgToClientGW directToClient " + msg.json)
     connInvokerService.sendMessage(msg)
   }
 
diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/SystemMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/SystemMsgs.scala
index 755b0cd6f3..82f2aa47c2 100755
--- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/SystemMsgs.scala
+++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/SystemMsgs.scala
@@ -59,7 +59,7 @@ case class MeetingEndedEvtMsgBody(meetingId: String)
 object MeetingEndingEvtMsg { val NAME = "MeetingEndingEvtMsg"}
 case class MeetingEndingEvtMsg(header: BbbClientMsgHeader,
                               body: MeetingEndingEvtMsgBody) extends BbbCoreMsg
-case class MeetingEndingEvtMsgBody(meetingId: String)
+case class MeetingEndingEvtMsgBody(meetingId: String, reason: String)
 
 
 /**
diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/red5/client/messaging/ConnectionInvokerService.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/red5/client/messaging/ConnectionInvokerService.java
index 86bc8b12ae..d15d351dd5 100755
--- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/red5/client/messaging/ConnectionInvokerService.java
+++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/red5/client/messaging/ConnectionInvokerService.java
@@ -150,14 +150,14 @@ public class ConnectionInvokerService implements IConnectionInvokerService {
   }
 
   private void handleCloseMeetingAllConnectionsMsg(CloseMeetingAllConnectionsMsg msg) {
+    log.info("Disconnecting all clients for meeting {}", msg.meetingId);
+
     IScope meetingScope = getScope(msg.meetingId);
     if (meetingScope != null) {
       Set<IConnection> conns = meetingScope.getClientConnections();
 
       for (IConnection conn : conns) {
         if (conn.isConnected()) {
-          String connId = (String) conn.getAttribute("INTERNAL_USER_ID");
-          log.info("Disconnecting client=[{}] from meeting=[{}]", connId, msg.meetingId);
           conn.close();
         }
       }
-- 
GitLab