diff --git a/akka-bbb-apps/.dockerignore b/akka-bbb-apps/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..c9c56767351f2752f2fad6e75af920655d3efa8d --- /dev/null +++ b/akka-bbb-apps/.dockerignore @@ -0,0 +1,2 @@ +Dockerfile + diff --git a/akka-bbb-apps/Dockerfile b/akka-bbb-apps/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..e8adc296c1cbe6f283c5c44dea86fe2a01a90019 --- /dev/null +++ b/akka-bbb-apps/Dockerfile @@ -0,0 +1,24 @@ +FROM bbb-common-message AS builder + +ARG COMMON_VERSION=0.0.1-SNAPSHOT + +COPY . /source + +RUN cd /source \ + && find -name build.sbt -exec sed -i "s|\(.*org.bigbluebutton.*bbb-common-message[^\"]*\"[ ]*%[ ]*\)\"[^\"]*\"\(.*\)|\1\"$COMMON_VERSION\"\2|g" {} \; \ + && sbt compile + +RUN apt-get update \ + && apt-get -y install fakeroot + +RUN cd /source \ + && sbt debian:packageBin + +# FROM ubuntu:16.04 +FROM openjdk:8-jre-slim-stretch + +COPY --from=builder /source/target/*.deb /root/ + +RUN dpkg -i /root/*.deb + +CMD ["/usr/share/bbb-apps-akka/bin/bbb-apps-akka"] diff --git a/akka-bbb-apps/src/main/resources/application.conf b/akka-bbb-apps/src/main/resources/application.conf index dfcf1bb5826282edcba0838cd189562417d1377d..e6904f7e9d99b9aae44fb0428072f15ebef56a5c 100755 --- a/akka-bbb-apps/src/main/resources/application.conf +++ b/akka-bbb-apps/src/main/resources/application.conf @@ -86,3 +86,8 @@ voiceConf { recording { chapterBreakLengthInMinutes = 180 } + +whiteboard { + multiUserDefault = false +} + diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/SystemConfiguration.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/SystemConfiguration.scala index 131336f55193023b5c7f75f2b9ae07102e8ef116..d2986d262a88d84363f7870e563f11d981312a6f 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/SystemConfiguration.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/SystemConfiguration.scala @@ -63,4 +63,6 @@ trait SystemConfiguration { lazy val endMeetingWhenNoMoreAuthedUsers = Try(config.getBoolean("apps.endMeetingWhenNoMoreAuthedUsers")).getOrElse(false) lazy val endMeetingWhenNoMoreAuthedUsersAfterMinutes = Try(config.getInt("apps.endMeetingWhenNoMoreAuthedUsersAfterMinutes")).getOrElse(2) + lazy val multiUserWhiteboardDefault = Try(config.getBoolean("whiteboard.multiUserDefault")).getOrElse(false) } + diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/WhiteboardModel.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/WhiteboardModel.scala index a3c2b8e1f231c98069d110b84b770fc21088a0d7..f73d74a4e5537e794c42c7ce3635d841c1adf4d8 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/WhiteboardModel.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/WhiteboardModel.scala @@ -7,8 +7,9 @@ import scala.collection.immutable.HashMap import scala.collection.JavaConverters._ import org.bigbluebutton.common2.msgs.AnnotationVO import org.bigbluebutton.core.apps.whiteboard.Whiteboard +import org.bigbluebutton.SystemConfiguration -class WhiteboardModel { +class WhiteboardModel extends SystemConfiguration { private var _whiteboards = new HashMap[String, Whiteboard]() private def saveWhiteboard(wb: Whiteboard) { @@ -24,7 +25,7 @@ class WhiteboardModel { } private def createWhiteboard(wbId: String): Whiteboard = { - new Whiteboard(wbId, false, System.currentTimeMillis(), 0, new HashMap[String, List[AnnotationVO]]()) + new Whiteboard(wbId, multiUserWhiteboardDefault, System.currentTimeMillis(), 0, new HashMap[String, List[AnnotationVO]]()) } private def getAnnotationsByUserId(wb: Whiteboard, id: String): List[AnnotationVO] = { diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/chat/ChatApp2x.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/chat/ChatApp2x.scala index 927acbcfcbff7b0e2c4e2a3f9b1c84b9f69a8d1f..c1aecafa35c5b15ec4d5204d7cbfc567616a8d7d 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/chat/ChatApp2x.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/chat/ChatApp2x.scala @@ -6,6 +6,7 @@ class ChatApp2x(implicit val context: ActorContext) extends GetChatHistoryReqMsgHdlr with SendPublicMessagePubMsgHdlr with SendPrivateMessagePubMsgHdlr - with ClearPublicChatHistoryPubMsgHdlr { + with ClearPublicChatHistoryPubMsgHdlr + with UserTypingPubMsgHdlr { } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/chat/UserTypingPubMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/chat/UserTypingPubMsgHdlr.scala new file mode 100644 index 0000000000000000000000000000000000000000..47dae9fa7e71d7c7415919628cd63d79414d4251 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/chat/UserTypingPubMsgHdlr.scala @@ -0,0 +1,21 @@ +package org.bigbluebutton.core.apps.chat + +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.bus.MessageBus +import org.bigbluebutton.core.running.{ LiveMeeting, LogHelper } + +trait UserTypingPubMsgHdlr extends LogHelper { + def handle(msg: UserTypingPubMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { + def broadcastEvent(msg: UserTypingPubMsg): Unit = { + val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, liveMeeting.props.meetingProp.intId, msg.header.userId) + val envelope = BbbCoreEnvelope(UserTypingEvtMsg.NAME, routing) + val header = BbbClientMsgHeader(UserTypingEvtMsg.NAME, liveMeeting.props.meetingProp.intId, msg.header.userId) + + val body = UserTypingEvtMsgBody(msg.body.chatId, msg.header.userId) + val event = UserTypingEvtMsg(header, body) + val msgEvent = BbbCommonEnvCoreMsg(envelope, event) + bus.outGW.send(msgEvent) + } + broadcastEvent(msg) + } +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/RecordAndClearPreviousMarkersCmdMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/RecordAndClearPreviousMarkersCmdMsgHdlr.scala index b179127d03105ac8fd56e152ab8d8987bcbb97e9..7d8558a5a4f00cbaf42d585da56dbbedfe81d79e 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/RecordAndClearPreviousMarkersCmdMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/RecordAndClearPreviousMarkersCmdMsgHdlr.scala @@ -5,6 +5,7 @@ import org.bigbluebutton.core.domain.MeetingState2x import org.bigbluebutton.core.running.{ LiveMeeting, OutMsgRouter } import org.bigbluebutton.core2.MeetingStatus2x import org.bigbluebutton.core.util.TimeUtil +import org.bigbluebutton.core2.message.senders.MsgBuilder trait RecordAndClearPreviousMarkersCmdMsgHdlr { this: UsersApp => @@ -35,9 +36,11 @@ trait RecordAndClearPreviousMarkersCmdMsgHdlr { val event = buildRecordingStatusChangedEvtMsg(liveMeeting.props.meetingProp.intId, msg.body.setBy, msg.body.recording) outGW.send(event) + outGW.send(MsgBuilder.buildRecordStatusResetSysMsg(liveMeeting.props.meetingProp.intId, msg.body.recording, msg.body.setBy)) + state.update(tracker) } else { state } } -} \ No newline at end of file +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/SyncGetVoiceUsersMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/SyncGetVoiceUsersMsgHdlr.scala new file mode 100644 index 0000000000000000000000000000000000000000..60dd8cec881b924fab108b679041219bc5d132f3 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/voice/SyncGetVoiceUsersMsgHdlr.scala @@ -0,0 +1,34 @@ +package org.bigbluebutton.core.apps.voice + +import org.bigbluebutton.core.running.{ BaseMeetingActor, LiveMeeting } +import org.bigbluebutton.common2.msgs._ +import org.bigbluebutton.core.bus.MessageBus +import org.bigbluebutton.core.models.VoiceUsers +import org.bigbluebutton.core.domain.MeetingState2x + +trait SyncGetVoiceUsersMsgHdlr { + this: BaseMeetingActor => + + def handleSyncGetVoiceUsersMsg(state: MeetingState2x, liveMeeting: LiveMeeting, bus: MessageBus): MeetingState2x = { + + def buildSyncGetVoiceUsersRespMsg(): BbbCommonEnvCoreMsg = { + val voiceUsers = VoiceUsers.findAll(liveMeeting.voiceUsers).map { u => + VoiceConfUser(intId = u.intId, voiceUserId = u.voiceUserId, callingWith = u.callingWith, callerName = u.callerName, + callerNum = u.callerNum, muted = u.muted, talking = u.talking, listenOnly = u.listenOnly) + } + + val routing = Routing.addMsgToClientRouting(MessageTypes.DIRECT, liveMeeting.props.meetingProp.intId, "nodeJSapp") + val envelope = BbbCoreEnvelope(SyncGetVoiceUsersRespMsg.NAME, routing) + val header = BbbClientMsgHeader(SyncGetVoiceUsersRespMsg.NAME, liveMeeting.props.meetingProp.intId, "nodeJSapp") + val body = SyncGetVoiceUsersRespMsgBody(voiceUsers) + val event = SyncGetVoiceUsersRespMsg(header, body) + + BbbCommonEnvCoreMsg(envelope, event) + } + + val respMsg = buildSyncGetVoiceUsersRespMsg() + bus.outGW.send(respMsg) + + state + } +} 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 a7f07a4c0a385acaf6a22155685d7bd382109b90..b9096d96786f33b481679c51e934e331defa23b4 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 @@ -9,7 +9,8 @@ trait VoiceApp2x extends UserJoinedVoiceConfEvtMsgHdlr with UserMutedInVoiceConfEvtMsgHdlr with UserTalkingInVoiceConfEvtMsgHdlr with RecordingStartedVoiceConfEvtMsgHdlr - with VoiceConfRunningEvtMsgHdlr { + with VoiceConfRunningEvtMsgHdlr + with SyncGetVoiceUsersMsgHdlr { this: MeetingActor => } 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 464e40471f667051606649e09e6284442866ea4a..d2970f9331b5f7ec695d7f35a06c006d19d1742b 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 @@ -255,6 +255,8 @@ class ReceivedJsonMsgHandlerActor( routeGenericMsg[SendPrivateMessagePubMsg](envelope, jsonNode) case ClearPublicChatHistoryPubMsg.NAME => routeGenericMsg[ClearPublicChatHistoryPubMsg](envelope, jsonNode) + case UserTypingPubMsg.NAME => + routeGenericMsg[UserTypingPubMsg](envelope, jsonNode) // Meeting case EndMeetingSysCmdMsg.NAME => diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/RecordStatusResetEvent.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/RecordStatusResetEvent.scala new file mode 100644 index 0000000000000000000000000000000000000000..c9c21daa147ea9ccfb5604fbf31b007520b5ccf3 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/record/events/RecordStatusResetEvent.scala @@ -0,0 +1,39 @@ +/** + * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ + * + * Copyright (c) 2017 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.core.record.events + +class RecordStatusResetEvent extends AbstractParticipantRecordEvent { + import RecordStatusResetEvent._ + + setEvent("RecordStatusReset") + + def setUserId(userId: String) { + eventMap.put(USER_ID, userId) + } + + def setRecordingStatus(status: Boolean) { + eventMap.put(STATUS, status.toString) + } +} + +object RecordStatusResetEvent { + protected final val USER_ID = "userId" + protected final val STATUS = "status" +} 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 5409a1d520b6a6e75983dcd3e719a9f661bdc462..ad5dced59d563eaf6e3b92f0b936c73166803b36 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 @@ -42,8 +42,7 @@ object MeetingActor { props: DefaultProps, eventBus: InternalEventBus, outGW: OutMsgRouter, - liveMeeting: LiveMeeting - ): Props = + liveMeeting: LiveMeeting): Props = Props(classOf[MeetingActor], props, eventBus, outGW, liveMeeting) } @@ -51,36 +50,35 @@ class MeetingActor( val props: DefaultProps, val eventBus: InternalEventBus, val outGW: OutMsgRouter, - val liveMeeting: LiveMeeting -) - extends BaseMeetingActor - with SystemConfiguration - with GuestsApp - with LayoutApp2x - with VoiceApp2x - with BreakoutApp2x - with UsersApp2x - - with UserBroadcastCamStartMsgHdlr - with UserJoinMeetingReqMsgHdlr - with UserJoinMeetingAfterReconnectReqMsgHdlr - with UserBroadcastCamStopMsgHdlr - with UserConnectedToGlobalAudioMsgHdlr - with UserDisconnectedFromGlobalAudioMsgHdlr - with MuteAllExceptPresentersCmdMsgHdlr - with MuteMeetingCmdMsgHdlr - with IsMeetingMutedReqMsgHdlr - - with EjectUserFromVoiceCmdMsgHdlr - with EndMeetingSysCmdMsgHdlr - with DestroyMeetingSysCmdMsgHdlr - with SendTimeRemainingUpdateHdlr - with SendBreakoutTimeRemainingMsgHdlr - with ChangeLockSettingsInMeetingCmdMsgHdlr - with SyncGetMeetingInfoRespMsgHdlr - with ClientToServerLatencyTracerMsgHdlr - with ValidateConnAuthTokenSysMsgHdlr - with UserActivitySignCmdMsgHdlr { + val liveMeeting: LiveMeeting) + extends BaseMeetingActor + with SystemConfiguration + with GuestsApp + with LayoutApp2x + with VoiceApp2x + with BreakoutApp2x + with UsersApp2x + + with UserBroadcastCamStartMsgHdlr + with UserJoinMeetingReqMsgHdlr + with UserJoinMeetingAfterReconnectReqMsgHdlr + with UserBroadcastCamStopMsgHdlr + with UserConnectedToGlobalAudioMsgHdlr + with UserDisconnectedFromGlobalAudioMsgHdlr + with MuteAllExceptPresentersCmdMsgHdlr + with MuteMeetingCmdMsgHdlr + with IsMeetingMutedReqMsgHdlr + + with EjectUserFromVoiceCmdMsgHdlr + with EndMeetingSysCmdMsgHdlr + with DestroyMeetingSysCmdMsgHdlr + with SendTimeRemainingUpdateHdlr + with SendBreakoutTimeRemainingMsgHdlr + with ChangeLockSettingsInMeetingCmdMsgHdlr + with SyncGetMeetingInfoRespMsgHdlr + with ClientToServerLatencyTracerMsgHdlr + with ValidateConnAuthTokenSysMsgHdlr + with UserActivitySignCmdMsgHdlr { override val supervisorStrategy = OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) { case e: Exception => { @@ -98,8 +96,7 @@ class MeetingActor( */ var actorMonitor = context.actorOf( MeetingActorAudit.props(props, eventBus, outGW), - "actorMonitor-" + props.meetingProp.intId - ) + "actorMonitor-" + props.meetingProp.intId) val msgBus = MessageBus(eventBus, outGW) @@ -121,8 +118,7 @@ class MeetingActor( TimeUtil.minutesToMillis(props.durationProps.warnMinutesBeforeMax), lastActivityTimestampInMs = TimeUtil.timeNowInMs(), warningSent = false, - warningSentOnTimestampInMs = 0L - ) + warningSentOnTimestampInMs = 0L) val expiryTracker = new MeetingExpiryTracker( startedOnInMs = TimeUtil.timeNowInMs(), @@ -134,8 +130,7 @@ class MeetingActor( meetingExpireWhenLastUserLeftInMs = TimeUtil.minutesToMillis(props.durationProps.meetingExpireWhenLastUserLeftInMinutes), userInactivityInspectTimerInMs = TimeUtil.minutesToMillis(props.durationProps.userInactivityInspectTimerInMinutes), userInactivityThresholdInMs = TimeUtil.minutesToMillis(props.durationProps.userInactivityInspectTimerInMinutes), - userActivitySignResponseDelayInMs = TimeUtil.minutesToMillis(props.durationProps.userActivitySignResponseDelayInMinutes) - ) + userActivitySignResponseDelayInMs = TimeUtil.minutesToMillis(props.durationProps.userActivitySignResponseDelayInMinutes)) val recordingTracker = new MeetingRecordingTracker(startedOnInMs = 0L, previousDurationInMs = 0L, currentDurationInMs = 0L) @@ -145,8 +140,7 @@ class MeetingActor( None, inactivityTracker, expiryTracker, - recordingTracker - ) + recordingTracker) var lastRttTestSentOn = System.currentTimeMillis() @@ -402,6 +396,7 @@ class MeetingActor( chatApp2x.handle(m, liveMeeting, msgBus) updateUserLastActivity(m.body.message.fromUserId) case m: ClearPublicChatHistoryPubMsg => state = chatApp2x.handle(m, state, liveMeeting, msgBus) + case m: UserTypingPubMsg => chatApp2x.handle(m, liveMeeting, msgBus) // Screenshare case m: ScreenshareStartedVoiceConfEvtMsg => screenshareApp2x.handle(m, liveMeeting, msgBus) @@ -441,8 +436,12 @@ class MeetingActor( // sync all group chats and group chat messages groupChatApp.handleSyncGetGroupChatsInfo(state, liveMeeting, msgBus) + // sync all voice users + handleSyncGetVoiceUsersMsg(state, liveMeeting, msgBus) + // TODO send all lock settings // TODO send all screen sharing info + } def handlePresenterChange(msg: AssignPresenterReqMsg, state: MeetingState2x): MeetingState2x = { @@ -456,8 +455,7 @@ class MeetingActor( screenshareApp2x.handleScreenshareStoppedVoiceConfEvtMsg( liveMeeting.props.voiceProp.voiceConf, liveMeeting.props.screenshareProps.screenshareConf, - liveMeeting, msgBus - ) + liveMeeting, msgBus) newState @@ -548,8 +546,7 @@ class MeetingActor( if (authedUsers.isEmpty) { sendEndMeetingDueToExpiry( MeetingEndReason.ENDED_DUE_TO_NO_AUTHED_USER, - eventBus, outGW, liveMeeting - ) + eventBus, outGW, liveMeeting) } } } @@ -577,8 +574,7 @@ class MeetingActor( val event = buildRecordingStatusChangedEvtMsg( liveMeeting.props.meetingProp.intId, - "system", MeetingStatus2x.isRecording(liveMeeting.status) - ) + "system", MeetingStatus2x.isRecording(liveMeeting.status)) outGW.send(event) } @@ -602,8 +598,7 @@ class MeetingActor( val event = buildRecordingStatusChangedEvtMsg( liveMeeting.props.meetingProp.intId, - "system", MeetingStatus2x.isRecording(liveMeeting.status) - ) + "system", MeetingStatus2x.isRecording(liveMeeting.status)) outGW.send(event) } } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/FromAkkaAppsMsgSenderActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/FromAkkaAppsMsgSenderActor.scala index e891c445f0d52a24623ccf839d84f288f0072b54..2dcaaf0f1f44a96019243bbd968727a8ecc684ff 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/FromAkkaAppsMsgSenderActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core2/FromAkkaAppsMsgSenderActor.scala @@ -27,6 +27,7 @@ class FromAkkaAppsMsgSenderActor(msgSender: MessageSender) case SyncGetUsersMeetingRespMsg.NAME => msgSender.send(toHTML5RedisChannel, json) case SyncGetGroupChatsRespMsg.NAME => msgSender.send(toHTML5RedisChannel, json) case SyncGetGroupChatMsgsRespMsg.NAME => msgSender.send(toHTML5RedisChannel, json) + case SyncGetVoiceUsersRespMsg.NAME => msgSender.send(toHTML5RedisChannel, json) // Sent to FreeSWITCH case ScreenshareStartRtmpBroadcastVoiceConfMsg.NAME => @@ -96,7 +97,7 @@ class FromAkkaAppsMsgSenderActor(msgSender: MessageSender) //================================================================== //================================================================== - // Some events are only intended for recording and shouldn't be + // Some events are only intended for recording and shouldn't be // sent past akka-apps // Poll Record Event case UserRespondedToPollRecordMsg.NAME => 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 a977d582fa7660302545c21b96b1f404982011f3..220314a53a929a27268d77a6c40eb684c51bee63 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 @@ -156,6 +156,16 @@ object MsgBuilder { BbbCommonEnvCoreMsg(envelope, event) } + def buildRecordStatusResetSysMsg(meetingId: String, recording: Boolean, setBy: String): BbbCommonEnvCoreMsg = { + val routing = Routing.addMsgToClientRouting(MessageTypes.SYSTEM, meetingId, setBy) + val envelope = BbbCoreEnvelope(RecordStatusResetSysMsg.NAME, routing) + val body = RecordStatusResetSysMsgBody(recording, setBy) + val header = BbbCoreHeaderWithMeetingId(RecordStatusResetSysMsg.NAME, meetingId) + val event = RecordStatusResetSysMsg(header, body) + + BbbCommonEnvCoreMsg(envelope, event) + } + def buildDisconnectAllClientsSysMsg(meetingId: String, reason: String): BbbCommonEnvCoreMsg = { val routing = Routing.addMsgToClientRouting(MessageTypes.SYSTEM, meetingId, "not-used") val envelope = BbbCoreEnvelope(DisconnectAllClientsSysMsg.NAME, routing) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala index 832ebdaae5453e28fca505cc9a4583246a708103..4a0d9c5f485979eb0887f2c9fa363547947c569d 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/endpoint/redis/RedisRecorderActor.scala @@ -95,6 +95,7 @@ class RedisRecorderActor(val system: ActorSystem) // Meeting case m: RecordingStatusChangedEvtMsg => handleRecordingStatusChangedEvtMsg(m) + case m: RecordStatusResetSysMsg => handleRecordStatusResetSysMsg(m) case m: WebcamsOnlyForModeratorChangedEvtMsg => handleWebcamsOnlyForModeratorChangedEvtMsg(m) case m: EndAndKickAllSysMsg => handleEndAndKickAllSysMsg(m) @@ -463,6 +464,15 @@ class RedisRecorderActor(val system: ActorSystem) record(msg.header.meetingId, ev.toMap) } + + private def handleRecordStatusResetSysMsg(msg: RecordStatusResetSysMsg) { + val ev = new RecordStatusResetEvent() + ev.setMeetingId(msg.header.meetingId) + ev.setUserId(msg.body.setBy) + ev.setRecordingStatus(msg.body.recording) + + record(msg.header.meetingId, ev.toMap) + } private def handleWebcamsOnlyForModeratorChangedEvtMsg(msg: WebcamsOnlyForModeratorChangedEvtMsg) { val ev = new WebcamsOnlyForModeratorRecordEvent() diff --git a/akka-bbb-fsesl/Dockerfile b/akka-bbb-fsesl/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..ff06041d93c9a190dfd4b85ae11368ae5f76ad3b --- /dev/null +++ b/akka-bbb-fsesl/Dockerfile @@ -0,0 +1,26 @@ +FROM bbb-fsesl-client AS builder + +ARG COMMON_VERSION=0.0.1-SNAPSHOT + +COPY . /source + +RUN cd /source \ + && find -name build.sbt -exec sed -i "s|\(.*org.bigbluebutton.*bbb-common-message[^\"]*\"[ ]*%[ ]*\)\"[^\"]*\"\(.*\)|\1\"$COMMON_VERSION\"\2|g" {} \; \ + && find -name build.sbt -exec sed -i "s|\(.*org.bigbluebutton.*bbb-fsesl-client[^\"]*\"[ ]*%[ ]*\)\"[^\"]*\"\(.*\)|\1\"$COMMON_VERSION\"\2|g" {} \; \ + && sbt compile + +RUN apt-get update \ + && apt-get -y install fakeroot + +RUN cd /source \ + && sbt debian:packageBin + +FROM openjdk:8-jre-slim-stretch + +COPY --from=builder /source/target/*.deb /root/ + +RUN dpkg -i /root/*.deb + +COPY wait-for-it.sh /usr/local/bin/ + +CMD ["/usr/share/bbb-fsesl-akka/bin/bbb-fsesl-akka"] diff --git a/akka-bbb-fsesl/wait-for-it.sh b/akka-bbb-fsesl/wait-for-it.sh new file mode 100755 index 0000000000000000000000000000000000000000..bbe404324bce73f9ca9999dd2d6936cf90e5c184 --- /dev/null +++ b/akka-bbb-fsesl/wait-for-it.sh @@ -0,0 +1,177 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +cmdname=$(basename $0) + +echoerr() { if [[ $QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $TIMEOUT -gt 0 ]]; then + echoerr "$cmdname: waiting $TIMEOUT seconds for $HOST:$PORT" + else + echoerr "$cmdname: waiting for $HOST:$PORT without a timeout" + fi + start_ts=$(date +%s) + while : + do + if [[ $ISBUSY -eq 1 ]]; then + nc -z $HOST $PORT + result=$? + else + (echo > /dev/tcp/$HOST/$PORT) >/dev/null 2>&1 + result=$? + fi + if [[ $result -eq 0 ]]; then + end_ts=$(date +%s) + echoerr "$cmdname: $HOST:$PORT is available after $((end_ts - start_ts)) seconds" + break + fi + sleep 1 + done + return $result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $QUIET -eq 1 ]]; then + timeout $BUSYTIMEFLAG $TIMEOUT $0 --quiet --child --host=$HOST --port=$PORT --timeout=$TIMEOUT & + else + timeout $BUSYTIMEFLAG $TIMEOUT $0 --child --host=$HOST --port=$PORT --timeout=$TIMEOUT & + fi + PID=$! + trap "kill -INT -$PID" INT + wait $PID + RESULT=$? + if [[ $RESULT -ne 0 ]]; then + echoerr "$cmdname: timeout occurred after waiting $TIMEOUT seconds for $HOST:$PORT" + fi + return $RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + hostport=(${1//:/ }) + HOST=${hostport[0]} + PORT=${hostport[1]} + shift 1 + ;; + --child) + CHILD=1 + shift 1 + ;; + -q | --quiet) + QUIET=1 + shift 1 + ;; + -s | --strict) + STRICT=1 + shift 1 + ;; + -h) + HOST="$2" + if [[ $HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + HOST="${1#*=}" + shift 1 + ;; + -p) + PORT="$2" + if [[ $PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + PORT="${1#*=}" + shift 1 + ;; + -t) + TIMEOUT="$2" + if [[ $TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$HOST" == "" || "$PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +TIMEOUT=${TIMEOUT:-15} +STRICT=${STRICT:-0} +CHILD=${CHILD:-0} +QUIET=${QUIET:-0} + +# check to see if timeout is from busybox? +# check to see if timeout is from busybox? +TIMEOUT_PATH=$(realpath $(which timeout)) +if [[ $TIMEOUT_PATH =~ "busybox" ]]; then + ISBUSY=1 + BUSYTIMEFLAG="-t" +else + ISBUSY=0 + BUSYTIMEFLAG="" +fi + +if [[ $CHILD -gt 0 ]]; then + wait_for + RESULT=$? + exit $RESULT +else + if [[ $TIMEOUT -gt 0 ]]; then + wait_for_wrapper + RESULT=$? + else + wait_for + RESULT=$? + fi +fi + +if [[ $CLI != "" ]]; then + if [[ $RESULT -ne 0 && $STRICT -eq 1 ]]; then + echoerr "$cmdname: strict mode, refusing to execute subprocess" + exit $RESULT + fi + exec "${CLI[@]}" +else + exit $RESULT +fi diff --git a/bbb-apps-common/src/main/scala/org/bigbluebutton/client/meeting/AllowedMessageNames.scala b/bbb-apps-common/src/main/scala/org/bigbluebutton/client/meeting/AllowedMessageNames.scala index 9899f62ff98de32a3908fc5880a6412d63c1bfd3..f79075a7f4cb98f0b2b5404262859b2ddbf004b0 100644 --- a/bbb-apps-common/src/main/scala/org/bigbluebutton/client/meeting/AllowedMessageNames.scala +++ b/bbb-apps-common/src/main/scala/org/bigbluebutton/client/meeting/AllowedMessageNames.scala @@ -51,6 +51,7 @@ object AllowedMessageNames { SendGroupChatMessageMsg.NAME, ClearPublicChatHistoryPubMsg.NAME, CreateGroupChatReqMsg.NAME, + UserTypingPubMsg.NAME, // Presentation Messages ResizeAndMovePagePubMsg.NAME, diff --git a/bbb-common-message/Dockerfile b/bbb-common-message/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..223159915350214fdaf6c9f9493bf52c6f7e9580 --- /dev/null +++ b/bbb-common-message/Dockerfile @@ -0,0 +1,13 @@ +FROM sbt:0.13.8 + +ARG COMMON_VERSION + +COPY . /bbb-common-message + +RUN cd /bbb-common-message \ + && sed -i "s|\(version := \)\".*|\1\"$COMMON_VERSION\"|g" build.sbt \ + && echo 'publishTo := Some(Resolver.file("file", new File(Path.userHome.absolutePath+"/.m2/repository")))' | tee -a build.sbt \ + && sbt compile \ + && sbt publish \ + && sbt publishLocal + diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/ChatMessages.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/ChatMessages.scala index 1bd4b41895cd88247993f967eb5fb3c44403d975..541dbcfa26078c786e2d830cbfac5fd83669cd55 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/ChatMessages.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/ChatMessages.scala @@ -1,39 +1,42 @@ package org.bigbluebutton.common2.msgs - /* In Messages */ -object GetChatHistoryReqMsg { val NAME = "GetChatHistoryReqMsg"} +object GetChatHistoryReqMsg { val NAME = "GetChatHistoryReqMsg" } case class GetChatHistoryReqMsg(header: BbbClientMsgHeader, body: GetChatHistoryReqMsgBody) extends StandardMsg case class GetChatHistoryReqMsgBody() -object SendPublicMessagePubMsg { val NAME = "SendPublicMessagePubMsg"} +object SendPublicMessagePubMsg { val NAME = "SendPublicMessagePubMsg" } case class SendPublicMessagePubMsg(header: BbbClientMsgHeader, body: SendPublicMessagePubMsgBody) extends StandardMsg case class SendPublicMessagePubMsgBody(message: ChatMessageVO) -object SendPrivateMessagePubMsg { val NAME = "SendPrivateMessagePubMsg"} +object SendPrivateMessagePubMsg { val NAME = "SendPrivateMessagePubMsg" } case class SendPrivateMessagePubMsg(header: BbbClientMsgHeader, body: SendPrivateMessagePubMsgBody) extends StandardMsg case class SendPrivateMessagePubMsgBody(message: ChatMessageVO) -object ClearPublicChatHistoryPubMsg { val NAME = "ClearPublicChatHistoryPubMsg"} +object ClearPublicChatHistoryPubMsg { val NAME = "ClearPublicChatHistoryPubMsg" } case class ClearPublicChatHistoryPubMsg(header: BbbClientMsgHeader, body: ClearPublicChatHistoryPubMsgBody) extends StandardMsg case class ClearPublicChatHistoryPubMsgBody(chatId: String) /* Out Messages */ -object GetChatHistoryRespMsg { val NAME = "GetChatHistoryRespMsg"} +object GetChatHistoryRespMsg { val NAME = "GetChatHistoryRespMsg" } case class GetChatHistoryRespMsg(header: BbbClientMsgHeader, body: GetChatHistoryRespMsgBody) extends StandardMsg case class GetChatHistoryRespMsgBody(history: Array[ChatMessageVO]) -object SendPublicMessageEvtMsg { val NAME = "SendPublicMessageEvtMsg"} +object SendPublicMessageEvtMsg { val NAME = "SendPublicMessageEvtMsg" } case class SendPublicMessageEvtMsg(header: BbbClientMsgHeader, body: SendPublicMessageEvtMsgBody) extends StandardMsg case class SendPublicMessageEvtMsgBody(message: ChatMessageVO) -object SendPrivateMessageEvtMsg { val NAME = "SendPrivateMessageEvtMsg"} +object SendPrivateMessageEvtMsg { val NAME = "SendPrivateMessageEvtMsg" } case class SendPrivateMessageEvtMsg(header: BbbClientMsgHeader, body: SendPrivateMessageEvtMsgBody) extends StandardMsg case class SendPrivateMessageEvtMsgBody(message: ChatMessageVO) -object ClearPublicChatHistoryEvtMsg { val NAME = "ClearPublicChatHistoryEvtMsg"} +object ClearPublicChatHistoryEvtMsg { val NAME = "ClearPublicChatHistoryEvtMsg" } case class ClearPublicChatHistoryEvtMsg(header: BbbClientMsgHeader, body: ClearPublicChatHistoryEvtMsgBody) extends StandardMsg case class ClearPublicChatHistoryEvtMsgBody(chatId: String) +object UserTypingEvtMsg { val NAME = "UserTypingEvtMsg" } +case class UserTypingEvtMsg(header: BbbClientMsgHeader, body: UserTypingEvtMsgBody) extends StandardMsg +case class UserTypingEvtMsgBody(chatId: String, userId: String) + case class ChatMessageVO(fromUserId: String, fromUsername: String, fromColor: String, fromTime: Long, fromTimezoneOffset: Int, toUserId: String, toUsername: String, message: String) \ No newline at end of file diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/GoupChatMsg.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/GoupChatMsg.scala index 0005720a24f4ddc9bd4db97bba330ed46f9eec22..44c8046a94de1cfacfdb57fcb9c3ea7090235e79 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/GoupChatMsg.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/GoupChatMsg.scala @@ -86,6 +86,10 @@ object GroupChatMessageBroadcastEvtMsg { val NAME = "GroupChatMessageBroadcastEv case class GroupChatMessageBroadcastEvtMsg(header: BbbClientMsgHeader, body: GroupChatMessageBroadcastEvtMsgBody) extends BbbCoreMsg case class GroupChatMessageBroadcastEvtMsgBody(chatId: String, msg: GroupChatMsgToUser) +object UserTypingPubMsg { val NAME = "UserTypingPubMsg"} +case class UserTypingPubMsg(header: BbbClientMsgHeader, body: UserTypingPubMsgBody) extends StandardMsg +case class UserTypingPubMsgBody(chatId: String) + // html5 client only object SyncGetGroupChatsRespMsg { val NAME = "SyncGetGroupChatsRespMsg"} case class SyncGetGroupChatsRespMsg(header: BbbClientMsgHeader, body: SyncGetGroupChatsRespMsgBody) extends BbbCoreMsg 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 b94e33391858510bbf85ea0c373d069648fa40f1..02fa08817044243ee197b0231ca3cd8b15614d14 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 @@ -92,6 +92,10 @@ case class EndAndKickAllSysMsg(header: BbbCoreHeaderWithMeetingId, body: EndAndKickAllSysMsgBody) extends BbbCoreMsg case class EndAndKickAllSysMsgBody(meetingId: String) +object RecordStatusResetSysMsg {val NAME = "RecordStatusResetSysMsg"} +case class RecordStatusResetSysMsg(header: BbbCoreHeaderWithMeetingId, + body: RecordStatusResetSysMsgBody) extends BbbCoreMsg +case class RecordStatusResetSysMsgBody(recording: Boolean, setBy: String) object SyncGetMeetingInfoRespMsg { val NAME = "SyncGetMeetingInfoRespMsg"} case class SyncGetMeetingInfoRespMsg(header: BbbCoreBaseHeader, 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 c8d6a80acc72353983f4832d0ced45de8f18efa4..831136c03477e4d6cf44140bafe1174c0aa0de2f 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 @@ -315,3 +315,10 @@ object UserDisconnectedFromGlobalAudioMsg { val NAME = "UserDisconnectedFromGlob case class UserDisconnectedFromGlobalAudioMsg(header: BbbCoreVoiceConfHeader, body: UserDisconnectedFromGlobalAudioMsgBody) extends VoiceStandardMsg case class UserDisconnectedFromGlobalAudioMsgBody(userId: String, name: String) + +/** + * Sync voice users with html5 client + */ +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 diff --git a/bbb-common-web/Dockerfile b/bbb-common-web/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..874219a3ba36e6b2ef2ed34321cdaa7734155c4f --- /dev/null +++ b/bbb-common-web/Dockerfile @@ -0,0 +1,13 @@ +FROM bbb-common-message + +ARG COMMON_VERSION + +COPY . /bbb-common-web + +RUN cd /bbb-common-web \ + && sed -i "s|\(version := \)\".*|\1\"$COMMON_VERSION\"|g" build.sbt \ + && find -name build.sbt -exec sed -i "s|\(.*org.bigbluebutton.*bbb-common-message[^\"]*\"[ ]*%[ ]*\)\"[^\"]*\"\(.*\)|\1\"$COMMON_VERSION\"\2|g" {} \; \ + && echo 'publishTo := Some(Resolver.file("file", new File(Path.userHome.absolutePath+"/.m2/repository")))' | tee -a build.sbt \ + && sbt compile \ + && sbt publish \ + && sbt publishLocal diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/DocumentConversionServiceImp.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/DocumentConversionServiceImp.java index 2ac2edce21f99199e0f8cf33f8aaa4310aec979a..b80c5d6a36750095e28dc0f20d28f0d8034ac0c6 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/DocumentConversionServiceImp.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/DocumentConversionServiceImp.java @@ -72,11 +72,29 @@ public class DocumentConversionServiceImp implements DocumentConversionService { } else if (SupportedFileTypes.isImageFile(fileType)) { imageToSwfSlidesGenerationService.generateSlides(pres); } else { - + logData = new HashMap<String, Object>(); + logData.put("podId", pres.getPodId()); + logData.put("meetingId", pres.getMeetingId()); + logData.put("presId", pres.getId()); + logData.put("filename", pres.getName()); + logData.put("current", pres.isCurrent()); + logData.put("message", "Supported file not handled."); + gson = new Gson(); + logStr = gson.toJson(logData); + log.warn("-- analytics -- {}", logStr); } } else { - // TODO: error log + logData = new HashMap<String, Object>(); + logData.put("podId", pres.getPodId()); + logData.put("meetingId", pres.getMeetingId()); + logData.put("presId", pres.getId()); + logData.put("filename", pres.getName()); + logData.put("current", pres.isCurrent()); + logData.put("message", "Unsupported file format"); + gson = new Gson(); + logStr = gson.toJson(logData); + log.error("-- analytics -- {}", logStr); } logData = new HashMap<String, Object>(); diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/ImageResizer.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/ImageResizer.java new file mode 100644 index 0000000000000000000000000000000000000000..0453352080674c949fc6910a45aae9e86309a3ee --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/ImageResizer.java @@ -0,0 +1,23 @@ +/** + * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ + * <p> + * Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below). + * <p> + * 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. + * <p> + * 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. + * <p> + * 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.presentation; + +public interface ImageResizer { + boolean resize(UploadedPresentation pres, String ratio); +} diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/SupportedFileTypes.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/SupportedFileTypes.java index 50069c14a4f49073e237f795d0dd5339a39c0c91..abed05cb321d2b911c268098baf2a229288cf634 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/SupportedFileTypes.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/SupportedFileTypes.java @@ -48,7 +48,7 @@ public final class SupportedFileTypes { private static final List<String> IMAGE_FILE_LIST = Collections.unmodifiableList(new ArrayList<String>(3) { { // Add all image file types - add(JPEG); add(JPG); add(PNG); + add(JPEG); add(JPG); add(PNG); } }); diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/events/UserTalkingEvent.as b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/handlers/ImageResizerHandler.java old mode 100755 new mode 100644 similarity index 62% rename from bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/events/UserTalkingEvent.as rename to bbb-common-web/src/main/java/org/bigbluebutton/presentation/handlers/ImageResizerHandler.java index 0080f521fac7411b351b2561a5c29bc6817fbbfb..53cf1a8898f3fca01dd779f4723d1ff6342b762d --- a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/events/UserTalkingEvent.as +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/handlers/ImageResizerHandler.java @@ -1,32 +1,30 @@ -/* - * 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.videoconf.events -{ - import flash.events.Event; - - public class UserTalkingEvent extends Event - { - public static const TALKING:String = "USER TALKING PRIORITIZE"; - - public function UserTalkingEvent(type:String, bubbles:Boolean=true, cancelable:Boolean=false) - { - super(type, bubbles, cancelable); - } - } -} \ No newline at end of file +/** + * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ + * + * Copyright (c) 2015 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.presentation.handlers; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ImageResizerHandler extends AbstractCommandHandler { + + private static Logger log = LoggerFactory + .getLogger(ImageResizerHandler.class); + +} diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/ImageResizerImp.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/ImageResizerImp.java new file mode 100644 index 0000000000000000000000000000000000000000..96917228af233112a4bf6e37af05db9b3f44d1a8 --- /dev/null +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/ImageResizerImp.java @@ -0,0 +1,59 @@ +/** + * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ + * <p> + * Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below). + * <p> + * 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. + * <p> + * 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. + * <p> + * 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.presentation.imp; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.bigbluebutton.presentation.ImageResizer; +import org.bigbluebutton.presentation.UploadedPresentation; +import org.bigbluebutton.presentation.handlers.ImageResizerHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.zaxxer.nuprocess.NuProcess; +import com.zaxxer.nuprocess.NuProcessBuilder; + +public class ImageResizerImp implements ImageResizer { + private static Logger log = LoggerFactory.getLogger(ImageResizerImp.class); + + private static int waitForSec = 7; + + public boolean resize(UploadedPresentation pres, String ratio) { + Boolean conversionSuccess = true; + + log.debug("Rescaling file {} with {} ratio", pres.getUploadedFile().getAbsolutePath(), ratio); + NuProcessBuilder imgResize = new NuProcessBuilder(Arrays.asList("convert", "-resize", ratio, + pres.getUploadedFile().getAbsolutePath(), pres.getUploadedFile().getAbsolutePath())); + + ImageResizerHandler pHandler = new ImageResizerHandler(); + imgResize.setProcessListener(pHandler); + + NuProcess process = imgResize.start(); + try { + process.waitFor(waitForSec, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.error(e.getMessage()); + conversionSuccess = false; + } + + return conversionSuccess; + } + +} diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/ImageToSwfSlidesGenerationService.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/ImageToSwfSlidesGenerationService.java index 64fe4143f38b8da3e6768dc0641359c29c4163ae..0dd1d34df87a0351db6d0cf527fe400656a26461 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/ImageToSwfSlidesGenerationService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/ImageToSwfSlidesGenerationService.java @@ -19,6 +19,7 @@ package org.bigbluebutton.presentation.imp; +import java.text.DecimalFormat; import java.util.concurrent.Callable; import java.util.concurrent.CompletionService; import java.util.concurrent.ExecutionException; @@ -30,6 +31,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.bigbluebutton.presentation.FileTypeConstants; +import org.bigbluebutton.presentation.ImageResizer; import org.bigbluebutton.presentation.ImageToSwfSlide; import org.bigbluebutton.presentation.PageConverter; import org.bigbluebutton.presentation.SvgImageCreator; @@ -50,6 +52,8 @@ public class ImageToSwfSlidesGenerationService { private SvgImageCreator svgImageCreator; private ThumbnailCreator thumbnailCreator; private TextFileCreator textFileCreator; + private ImageResizer imageResizer; + private Long maxImageSize; private long MAX_CONVERSION_TIME = 5*60*1000L; private String BLANK_SLIDE; @@ -102,12 +106,24 @@ public class ImageToSwfSlidesGenerationService { } private void convertImageToSwf(UploadedPresentation pres, PageConverter pageConverter) { - int numPages = pres.getNumberOfPages(); + int numPages = pres.getNumberOfPages(); + // A better implementation is described at the link below + // https://stackoverflow.com/questions/4513648/how-to-estimate-the-size-of-jpeg-image-which-will-be-scaled-down + if (pres.getUploadedFile().length() > maxImageSize) { + DecimalFormat percentFormat= new DecimalFormat("#.##%"); + // Resize the image and overwrite it + resizeImage(pres, percentFormat + .format(Double.valueOf(maxImageSize) / Double.valueOf(pres.getUploadedFile().length()))); + } ImageToSwfSlide[] slides = setupSlides(pres, numPages, pageConverter); generateSlides(slides); handleSlideGenerationResult(pres, slides); } + private void resizeImage(UploadedPresentation pres, String ratio) { + imageResizer.resize(pres, ratio); + } + private void handleSlideGenerationResult(UploadedPresentation pres, ImageToSwfSlide[] slides) { long endTime = System.currentTimeMillis() + MAX_CONVERSION_TIME; int slideGenerated = 0; @@ -193,4 +209,12 @@ public class ImageToSwfSlidesGenerationService { public void setSwfSlidesGenerationProgressNotifier(SwfSlidesGenerationProgressNotifier notifier) { this.notifier = notifier; } + + public void setImageResizer(ImageResizer imageResizer) { + this.imageResizer = imageResizer; + } + + public void setMaxImageSize(Long maxImageSize) { + this.maxImageSize = maxImageSize; + } } diff --git a/bbb-fsesl-client/Dockerfile b/bbb-fsesl-client/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..c9fd93a8417e592f09e68ec1e258e0b16d139279 --- /dev/null +++ b/bbb-fsesl-client/Dockerfile @@ -0,0 +1,13 @@ +FROM bbb-common-message + +ARG COMMON_VERSION + +COPY . /bbb-fsesl-client + +RUN cd /bbb-fsesl-client \ + && sed -i "s|\(version := \)\".*|\1\"$COMMON_VERSION\"|g" build.sbt \ + && find -name build.sbt -exec sed -i "s|\(.*org.bigbluebutton.*bbb-common-message[^\"]*\"[ ]*%[ ]*\)\"[^\"]*\"\(.*\)|\1\"$COMMON_VERSION\"\2|g" {} \; \ + && echo 'publishTo := Some(Resolver.file("file", new File(Path.userHome.absolutePath+"/.m2/repository")))' | tee -a build.sbt \ + && sbt compile \ + && sbt publish \ + && sbt publishLocal diff --git a/bbb-lti/.dockerignore b/bbb-lti/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..94143827ed065ca0d7d5be1b765d255c5c32cd9a --- /dev/null +++ b/bbb-lti/.dockerignore @@ -0,0 +1 @@ +Dockerfile diff --git a/bbb-lti/Dockerfile b/bbb-lti/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..4503f2d172e525085026332f16b97e38b46f4a2c --- /dev/null +++ b/bbb-lti/Dockerfile @@ -0,0 +1,52 @@ +FROM java:8-jdk AS builder + +RUN mkdir -p /root/tools \ + && cd /root/tools \ + && wget http://services.gradle.org/distributions/gradle-2.12-bin.zip \ + && unzip gradle-2.12-bin.zip \ + && ln -s gradle-2.12 gradle + +RUN mkdir -p /root/tools \ + && cd /root/tools \ + && wget https://github.com/grails/grails-core/releases/download/v2.5.2/grails-2.5.2.zip \ + && unzip grails-2.5.2.zip \ + && ln -s grails-2.5.2 grails + +ENV PATH="/root/tools/gradle/bin:/root/tools/grails/bin:${PATH}" + +COPY . /source + +# build with: +# docker build -t mconftec/bbb-lti --build-arg title=Mconf --build-arg description='Single Sign On into Mconf' --build-arg vendor_code=mconf --build-arg vendor_name=Mconf --build-arg vendor_description='Mconf web conferencing' --build-arg vendor_url=https://mconf.com . + +ARG title=BigBlueButton +ARG description='Single Sign On into BigBlueButton' +ARG vendor_code=bigbluebutton +ARG vendor_name=BigBlueButton +ARG vendor_description='Open source web conferencing system for distance learning.' +ARG vendor_url=http://www.bigbluebutton.org/ + +RUN cd /source \ + && sed -i "s|\(<blti:title>\)[^<]*|\1$title|g" grails-app/controllers/org/bigbluebutton/ToolController.groovy \ + && sed -i "s|\(<blti:description>\)[^<]*|\1$description|g" grails-app/controllers/org/bigbluebutton/ToolController.groovy \ + && sed -i "s|\(<lticp:code>\)[^<]*|\1$vendor_code|g" grails-app/controllers/org/bigbluebutton/ToolController.groovy \ + && sed -i "s|\(<lticp:name>\)[^<]*|\1$vendor_name|g" grails-app/controllers/org/bigbluebutton/ToolController.groovy \ + && sed -i "s|\(<lticp:description>\)[^<]*|\1$vendor_description|g" grails-app/controllers/org/bigbluebutton/ToolController.groovy \ + && sed -i "s|\(<lticp:url>\)[^<]*|\1$vendor_url|g" grails-app/controllers/org/bigbluebutton/ToolController.groovy \ + && grails war + +FROM tomcat:7-jre8 + +WORKDIR $CATALINA_HOME + +# clean default webapps +RUN rm -r webapps/* + +COPY --from=builder /source/target/lti-*.war webapps/lti.war + +RUN unzip -q webapps/lti.war -d webapps/lti \ + && rm webapps/lti.war + +COPY docker-entrypoint.sh /usr/local/bin/ + +CMD ["docker-entrypoint.sh"] diff --git a/bbb-lti/docker-entrypoint.sh b/bbb-lti/docker-entrypoint.sh new file mode 100755 index 0000000000000000000000000000000000000000..608af0aa7ad42fa92ec1d1b73f85021484974b78 --- /dev/null +++ b/bbb-lti/docker-entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/bash -xe + +export JAVA_OPTS="$JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -DbigbluebuttonSalt=$BIGBLUEBUTTON_SHARED_SECRET -DbigbluebuttonURL=$BIGBLUEBUTTON_URL -DltiEndPoint=$LTI_ENDPOINT -DltiConsumers=$LTI_CONSUMERS -DltiAllRecordedByDefault=$RECORDED_BY_DEFAULT" +sed -i "s|^securerandom\.source=.*|securerandom.source=file:/dev/./urandom|g" $JAVA_HOME/lib/security/java.security + +catalina.sh run + diff --git a/bbb-webhooks/Dockerfile b/bbb-webhooks/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..afa9470fdc7330c434f6433c62ce1179b7dd3042 --- /dev/null +++ b/bbb-webhooks/Dockerfile @@ -0,0 +1,16 @@ +FROM node:8 + +ADD . app + +WORKDIR app + +RUN cp config_local.js.example config_local.js + +ENV NODE_ENV production + +RUN npm install \ + && npm cache clear --force + +EXPOSE 3005 + +CMD ["node", "app.js"] diff --git a/bbb-webhooks/application.js b/bbb-webhooks/application.js index 2817826712746cc2f49e2819e2c2482b84d56e79..b6e9985b1254bf729a2d226204d680600ead0903 100644 --- a/bbb-webhooks/application.js +++ b/bbb-webhooks/application.js @@ -13,8 +13,12 @@ const async = require("async"); module.exports = class Application { constructor() { - config.redis.pubSubClient = redis.createClient(); - config.redis.client = redis.createClient() + const options = { + host : process.env.REDIS_HOST || config.redis.host, + port : process.env.REDIS_PORT || config.redis.port + }; + config.redis.pubSubClient = redis.createClient(options); + config.redis.client = redis.createClient(options); this.webHooks = new WebHooks(); this.webServer = new WebServer(); } diff --git a/bbb-webhooks/callback_emitter.js b/bbb-webhooks/callback_emitter.js index 11416774cb1461cb5a37009d868dc497f13c7ce2..6ec7d553632c42d0c4a71da85bfb371cd80c59c6 100644 --- a/bbb-webhooks/callback_emitter.js +++ b/bbb-webhooks/callback_emitter.js @@ -62,11 +62,19 @@ module.exports = class CallbackEmitter extends EventEmitter { _emitMessage(callback) { let data,requestOptions; + const serverDomain = process.env.SERVER_DOMAIN || config.bbb.serverDomain; + const sharedSecret = process.env.SHARED_SECRET || config.bbb.sharedSecret; + const bearerAuth = process.env.BEARER_AUTH || config.bbb.auth2_0; + + // data to be sent + // note: keep keys in alphabetical order + data = { + event: "[" + this.message + "]", + timestamp: this.timestamp, + domain: serverDomain + }; - if (config.bbb.auth2_0) { - // Send data as a JSON - data = "[" + this.message + "]"; - + if (bearerAuth) { const callbackURL = this.callbackURL; requestOptions = { @@ -76,20 +84,13 @@ module.exports = class CallbackEmitter extends EventEmitter { method: "POST", form: data, auth: { - bearer: config.bbb.sharedSecret + bearer: sharedSecret } }; } else { - // data to be sent - // note: keep keys in alphabetical order - data = { - event: "[" + this.message + "]", - timestamp: this.timestamp - }; - // calculate the checksum - const checksum = Utils.checksum(`${this.callbackURL}${JSON.stringify(data)}${config.bbb.sharedSecret}`); + const checksum = Utils.checksum(`${this.callbackURL}${JSON.stringify(data)}${sharedSecret}`); // get the final callback URL, including the checksum const urlObj = url.parse(this.callbackURL, true); diff --git a/bbb-webhooks/config.js b/bbb-webhooks/config.js index 80845f1fc9183e360f06b21d470aa922442451df..cfc7ccda07e0a8f5f33c3f695da7c6e9d274d9b9 100644 --- a/bbb-webhooks/config.js +++ b/bbb-webhooks/config.js @@ -51,6 +51,8 @@ config.mappings.timeout = 1000*60*60*24; // 24 hours, in ms // Redis config.redis = {}; +config.redis.host = '127.0.0.1'; +config.redis.port = 6379; config.redis.keys = {}; config.redis.keys.hook = id => `bigbluebutton:webhooks:hook:${id}`; config.redis.keys.hooks = "bigbluebutton:webhooks:hooks"; diff --git a/bbb-webhooks/config/monit-bbb-webhooks b/bbb-webhooks/config/monit-bbb-webhooks deleted file mode 100755 index 18e1f1e445dfa9f30f84907d7d8312e48f0bcb91..0000000000000000000000000000000000000000 --- a/bbb-webhooks/config/monit-bbb-webhooks +++ /dev/null @@ -1,12 +0,0 @@ -#!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 diff --git a/bbb-webhooks/config/upstart-bbb-webhooks.conf b/bbb-webhooks/config/upstart-bbb-webhooks.conf deleted file mode 100644 index af2e5069340f1650edcc42dede5ece593d76d1e1..0000000000000000000000000000000000000000 --- a/bbb-webhooks/config/upstart-bbb-webhooks.conf +++ /dev/null @@ -1,34 +0,0 @@ -# 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 diff --git a/bbb-webhooks/config_local.js.example b/bbb-webhooks/config_local.js.example index 0a64ac0d5fc504b12c270cead9d4cd99d0e4f7ea..0bebd13924dafc0e7f86e9cfb5d84ebe8a390551 100644 --- a/bbb-webhooks/config_local.js.example +++ b/bbb-webhooks/config_local.js.example @@ -4,6 +4,7 @@ const config = {}; // Shared secret of your BigBlueButton server. config.bbb = {}; +config.bbb.serverDomain = "myserver.com"; config.bbb.sharedSecret = "mysharedsecret"; // Whether to use Auth2.0 or not, Auth2.0 sends the sharedSecret whithin an Authorization header as a bearer config.bbb.auth2_0 = false; diff --git a/bbb-webhooks/extra/events.js b/bbb-webhooks/extra/events.js index 9abaf828d5622b2b69aed49f16e58bd5df8f5add..c527879ac95f1d8062087c1ecedfbb9581ea38a9 100644 --- a/bbb-webhooks/extra/events.js +++ b/bbb-webhooks/extra/events.js @@ -6,7 +6,7 @@ const redis = require("redis"); const config = require('../config.js'); var target_meeting = null; var events_printed = []; -var subscriber = redis.createClient(); +var subscriber = redis.createClient(process.env.REDIS_PORT || config.redis.port, process.env.REDIS_HOST || config.redis.host); subscriber.on("psubscribe", function(channel, count) { console.log("subscribed to " + channel); diff --git a/bbb-webhooks/messageMapping.js b/bbb-webhooks/messageMapping.js index f79d9410b932661866acf0bdb5ebfdf933a68a4b..7ea2062c8a022894279ad9f4f15c20f82a4aac7e 100644 --- a/bbb-webhooks/messageMapping.js +++ b/bbb-webhooks/messageMapping.js @@ -7,8 +7,8 @@ module.exports = class MessageMapping { constructor() { this.mappedObject = {}; this.mappedMessage = {}; - this.meetingEvents = ["MeetingCreatedEvtMsg","MeetingDestroyedEvtMsg"]; - this.userEvents = ["UserJoinedMeetingEvtMsg","UserLeftMeetingEvtMsg","UserJoinedVoiceConfToClientEvtMsg","UserLeftVoiceConfToClientEvtMsg","PresenterAssignedEvtMsg", "PresenterUnassignedEvtMsg"]; + this.meetingEvents = ["MeetingCreatedEvtMsg","MeetingDestroyedEvtMsg", "ScreenshareRtmpBroadcastStartedEvtMsg", "ScreenshareRtmpBroadcastStoppedEvtMsg", "SetCurrentPresentationEvtMsg", "RecordingStatusChangedEvtMsg"]; + this.userEvents = ["UserJoinedMeetingEvtMsg","UserLeftMeetingEvtMsg","UserJoinedVoiceConfToClientEvtMsg","UserLeftVoiceConfToClientEvtMsg","PresenterAssignedEvtMsg", "PresenterUnassignedEvtMsg", "UserBroadcastCamStartedEvtMsg", "UserBroadcastCamStoppedEvtMsg", "UserEmojiChangedEvtMsg"]; this.chatEvents = ["SendPublicMessageEvtMsg","SendPrivateMessageEvtMsg"]; this.rapEvents = ["archive_started","archive_ended","sanity_started","sanity_ended","post_archive_started","post_archive_ended","process_started","process_ended","post_process_started","post_process_ended","publish_started","publish_ended","post_publish_started","post_publish_ended"]; } @@ -41,13 +41,14 @@ module.exports = class MessageMapping { // Map internal to external message for meeting information meetingTemplate(messageObj) { const props = messageObj.core.body.props; + const meetingId = messageObj.core.body.meetingId || messageObj.core.header.meetingId; this.mappedObject.data = { "type": "event", "id": this.mapInternalMessage(messageObj), "attributes":{ "meeting":{ - "internal-meeting-id": messageObj.core.body.meetingId, - "external-meeting-id": IDMapping.getExternalMeetingID(messageObj.core.body.meetingId) + "internal-meeting-id": meetingId, + "external-meeting-id": IDMapping.getExternalMeetingID(meetingId) } }, "event":{ @@ -82,7 +83,7 @@ module.exports = class MessageMapping { userTemplate(messageObj) { const msgBody = messageObj.core.body; const msgHeader = messageObj.core.header; - const extId = UserMapping.getExternalUserID(msgHeader.userId) ? UserMapping.getExternalUserID(msgHeader.userId) : msgBody.extId; + const extId = UserMapping.getExternalUserID(msgHeader.userId) || msgBody.extId || ""; this.mappedObject.data = { "type": "event", "id": this.mapInternalMessage(messageObj), @@ -93,7 +94,7 @@ module.exports = class MessageMapping { }, "user":{ "internal-user-id": msgHeader.userId, - "external-user-id": extId ? extId : "", + "external-user-id": extId, "sharing-mic": msgBody.muted, "name": msgBody.name, "role": msgBody.role, @@ -146,31 +147,36 @@ module.exports = class MessageMapping { } rapTemplate(messageObj) { - data = messageObj.payload + const data = messageObj.payload; this.mappedObject.data = { "type": "event", "id": this.mapInternalMessage(messageObj.header.name), "attributes": { "meeting": { "internal-meeting-id": data.meeting_id, - "external-meeting-id": IDMapping.getExternalMeetingID(data.meeting_id) + "external-meeting-id": data.external_meeting_id }, - "recording": { - "name": data.metadata.meetingName, - "isBreakout": data.metadata.isBreakout, - "startTime": data.startTime, - "endTime": data.endTime, - "size": data.playback.size, - "rawSize": data.rawSize, - "metadata": data.metadata, - "playback": data.playback, - "download": data.download - } + "success": data.success, + "step-time": data.step_time }, "event": { "ts": messageObj.header.current_time } }; + + if (this.mappedObject.data["id"] == "rap-publish-ended") { + this.mappedObject.data["attributes"]["recording"] = { + "name": data.metadata.meetingName, + "isBreakout": data.metadata.isBreakout, + "startTime": data.startTime, + "endTime": data.endTime, + "size": data.playback.size, + "rawSize": data.rawSize, + "metadata": data.metadata, + "playback": data.playback, + "download": data.download + } + } this.mappedMessage = JSON.stringify(this.mappedObject); Logger.info("[MessageMapping] Mapped message:", this.mappedMessage); } @@ -186,6 +192,10 @@ module.exports = class MessageMapping { const mappedMsg = (() => { switch (message) { case "MeetingCreatedEvtMsg": return "meeting-created"; case "MeetingDestroyedEvtMsg": return "meeting-ended"; + case "RecordingStatusChangedEvtMsg": return "meeting-recording-changed"; + case "ScreenshareRtmpBroadcastStartedEvtMsg": return "meeting-screenshare-started"; + case "ScreenshareRtmpBroadcastStoppedEvtMsg": return "meeting-screenshare-stopped"; + case "SetCurrentPresentationEvtMsg": return "meeting-presentation-changed"; case "UserJoinedMeetingEvtMsg": return "user-joined"; case "UserLeftMeetingEvtMsg": return "user-left"; case "UserJoinedVoiceConfToClientEvtMsg": return "user-audio-voice-enabled"; @@ -193,7 +203,8 @@ module.exports = class MessageMapping { case "UserBroadcastCamStartedEvtMsg": return "user-cam-broadcast-start"; case "UserBroadcastCamStoppedEvtMsg": return "user-cam-broadcast-end"; case "PresenterAssignedEvtMsg": return "user-presenter-assigned"; - case "PresenterUnassignedEvtMsg": return "user-presenter-unassigned" + case "PresenterUnassignedEvtMsg": return "user-presenter-unassigned"; + case "UserEmojiChangedEvtMsg": return "user-emoji-changed"; case "SendPublicMessageEvtMsg": return "chat-public-message-sent"; case "SendPrivateMessageEvtMsg": return "chat-private-message-sent"; case "archive_started": return "rap-archive-started"; diff --git a/bbb-webhooks/package-lock.json b/bbb-webhooks/package-lock.json index 756667bad9b6e41d2e885a38a2ee14ed62a66509..d219d53434a1cd81410f9b651b99fd5f20f757e5 100644 --- a/bbb-webhooks/package-lock.json +++ b/bbb-webhooks/package-lock.json @@ -4,42 +4,14 @@ "lockfileVersion": 1, "requires": true, "dependencies": { - "accepts": { - "version": "https://registry.npmjs.org/accepts/-/accepts-1.1.4.tgz", - "integrity": "sha1-1xyW99QdD+2iw4zRToonwEFY30o=", - "requires": { - "mime-types": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz", - "negotiator": "https://registry.npmjs.org/negotiator/-/negotiator-0.4.9.tgz" - }, - "dependencies": { - "mime-db": { - "version": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz", - "integrity": "sha1-PQxjGA9FjrENMlqqN9fFiuMS6dc=" - }, - "mime-types": { - "version": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz", - "integrity": "sha1-MQ4VnbI+B3+Lsit0jav6SVcUCqY=", - "requires": { - "mime-db": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz" - } - } - } - }, - "asn1": { - "version": "https://registry.npmjs.org/asn1/-/asn1-0.1.11.tgz", - "integrity": "sha1-VZvhg3bQik7E2+gId9J4GGObLfc=" - }, - "assert-plus": { - "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz", - "integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA=" - }, "assertion-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=" }, "async": { - "version": "https://registry.npmjs.org/async/-/async-0.9.0.tgz", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.0.tgz", "integrity": "sha1-rDYTsdqb7RtHUQu0ZRuJMeRxRsc=" }, "asynckit": { @@ -48,44 +20,152 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "dev": true }, - "aws-sign2": { - "version": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.5.0.tgz", - "integrity": "sha1-xXED96F/wDfwLXwuZLYC6iI/fWM=" - }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, - "bl": { - "version": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz", - "integrity": "sha1-wGt5evCF6gC8Unr8jvzxHeIjIFQ=", - "requires": { - "readable-stream": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz" - } - }, "body-parser": { - "version": "https://registry.npmjs.org/body-parser/-/body-parser-1.17.2.tgz", - "integrity": "sha1-+IkqvI+eYn1Crtr7yma/WrmRBO4=", - "requires": { - "bytes": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz", - "content-type": "https://registry.npmjs.org/content-type/-/content-type-1.0.2.tgz", - "debug": "https://registry.npmjs.org/debug/-/debug-2.6.7.tgz", - "depd": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", - "http-errors": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", - "iconv-lite": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz", - "on-finished": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "qs": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", - "raw-body": "https://registry.npmjs.org/raw-body/-/raw-body-2.2.0.tgz", - "type-is": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz" - } - }, - "boom": { - "version": "https://registry.npmjs.org/boom/-/boom-0.4.2.tgz", - "integrity": "sha1-emNune1O/O+xnO9JR6PGffrukRs=", + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", "requires": { - "hoek": "https://registry.npmjs.org/hoek/-/hoek-0.9.1.tgz" + "bytes": "3.0.0", + "content-type": "1.0.4", + "debug": "2.6.9", + "depd": "1.1.2", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", + "on-finished": "2.3.0", + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "1.6.16" + }, + "dependencies": { + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "requires": { + "depd": "1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": "1.5.0" + } + }, + "iconv-lite": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "requires": { + "safer-buffer": "2.1.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "mime-db": { + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", + "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==" + }, + "mime-types": { + "version": "2.1.19", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz", + "integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==", + "requires": { + "mime-db": "1.35.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "raw-body": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", + "unpipe": "1.0.0" + } + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "type-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "2.1.19" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + } } }, "brace-expansion": { @@ -104,45 +184,27 @@ "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", "dev": true }, - "bytes": { - "version": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz", - "integrity": "sha1-fZcZb51br39pNeJZhVSe3SpsIzk=" - }, - "caseless": { - "version": "https://registry.npmjs.org/caseless/-/caseless-0.6.0.tgz", - "integrity": "sha1-gWfBq4OX+1u5X5bSjlqBxQ8kesQ=" - }, - "chai": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz", - "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=", - "requires": { - "assertion-error": "1.0.2", - "deep-eql": "0.1.3", - "type-detect": "1.0.0" - } - }, - "charenc": { - "version": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" - }, - "coffee-script": { - "version": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.8.0.tgz", - "integrity": "sha1-nJ8dK0pSoADe0Vtll5FwNkgmPB0=", + "build": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/build/-/build-0.1.4.tgz", + "integrity": "sha1-cH/gJv/O3crL/c3zVur9pk8VEEY=", "requires": { - "mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" + "cssmin": "0.3.2", + "jsmin": "1.0.1", + "jxLoader": "0.1.1", + "moo-server": "1.3.0", + "promised-io": "0.3.5", + "timespan": "2.3.0", + "uglify-js": "1.3.5", + "walker": "1.0.7", + "winston": "0.8.3", + "wrench": "1.3.9" } }, - "colors": { - "version": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", - "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=" - }, - "combined-stream": { - "version": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz", - "integrity": "sha1-ATfmV7qlp1QcV6w3rF/AfXO03B8=", - "requires": { - "delayed-stream": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz" - } + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=" }, "commander": { "version": "2.9.0", @@ -165,22 +227,6 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, - "content-disposition": { - "version": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.0.tgz", - "integrity": "sha1-QoT+auBjCHRjnkToCkGMKTQTXp4=" - }, - "content-type": { - "version": "https://registry.npmjs.org/content-type/-/content-type-1.0.2.tgz", - "integrity": "sha1-t9ETrueo3Se9IRM8TcJSnfFyHu0=" - }, - "cookie": { - "version": "https://registry.npmjs.org/cookie/-/cookie-0.1.2.tgz", - "integrity": "sha1-cv7D0k5Io0Mgc9kMEmQgBQYQBLE=" - }, - "cookie-signature": { - "version": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.5.tgz", - "integrity": "sha1-oSLj8VA+yg9TVXlbBxG7I2jUUPk=" - }, "cookiejar": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.1.tgz", @@ -189,82 +235,36 @@ }, "core-util-is": { "version": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "crc": { - "version": "https://registry.npmjs.org/crc/-/crc-3.2.1.tgz", - "integrity": "sha1-XZyPt3okXNXsopHl0tAFM0urAII=" - }, - "crypt": { - "version": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" - }, - "cryptiles": { - "version": "https://registry.npmjs.org/cryptiles/-/cryptiles-0.2.2.tgz", - "integrity": "sha1-7ZH/HxetE9N0gohZT4pIoNJvMlw=", - "requires": { - "boom": "https://registry.npmjs.org/boom/-/boom-0.4.2.tgz" - } - }, - "ctype": { - "version": "https://registry.npmjs.org/ctype/-/ctype-0.5.3.tgz", - "integrity": "sha1-gsGMJGH3QRTvFsE1IkrQuRRMoS8=" + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true }, - "cycle": { - "version": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", - "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=" + "cssmin": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/cssmin/-/cssmin-0.3.2.tgz", + "integrity": "sha1-3c5MVHtRCuDVlKjx+/iq+OLFwA0=" }, "debug": { "version": "https://registry.npmjs.org/debug/-/debug-2.6.7.tgz", "integrity": "sha1-krrR9tBbu2u6Isyoi80OyJTChh4=", + "dev": true, "requires": { "ms": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" } }, - "deep-eql": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", - "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", - "requires": { - "type-detect": "0.1.1" - }, - "dependencies": { - "type-detect": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", - "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=" - } - } - }, "deep-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" }, - "delayed-stream": { - "version": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz", - "integrity": "sha1-1LH0OpPoKW3+AmlPRoC8N6MTxz8=" - }, - "depd": { - "version": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", - "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" - }, - "destroy": { - "version": "https://registry.npmjs.org/destroy/-/destroy-1.0.3.tgz", - "integrity": "sha1-tDO0ck5x/YVR2YhRdIUcX8N34sk=" - }, "diff": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=" }, - "ee-first": { - "version": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "escape-html": { - "version": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.1.tgz", - "integrity": "sha1-GBoobq05ejmpKFfPsdQwUuNWv/A=" + "double-ended-queue": { + "version": "2.1.0-0", + "resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz", + "integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw=" }, "escape-string-regexp": { "version": "1.0.5", @@ -272,91 +272,260 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "dev": true }, - "etag": { - "version": "https://registry.npmjs.org/etag/-/etag-1.5.1.tgz", - "integrity": "sha1-VMUN4E7kJpVWKSWsVmWIKRvn6eo=", - "requires": { - "crc": "https://registry.npmjs.org/crc/-/crc-3.2.1.tgz" - } - }, "express": { - "version": "https://registry.npmjs.org/express/-/express-4.10.2.tgz", + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.10.2.tgz", "integrity": "sha1-3wbd6U2WiTKCnUQKIATF7+ZElbA=", "requires": { - "accepts": "https://registry.npmjs.org/accepts/-/accepts-1.1.4.tgz", - "content-disposition": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.0.tgz", - "cookie": "https://registry.npmjs.org/cookie/-/cookie-0.1.2.tgz", - "cookie-signature": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.5.tgz", - "debug": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz", - "depd": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz", - "escape-html": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.1.tgz", - "etag": "https://registry.npmjs.org/etag/-/etag-1.5.1.tgz", - "finalhandler": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.3.2.tgz", - "fresh": "https://registry.npmjs.org/fresh/-/fresh-0.2.4.tgz", - "media-typer": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "merge-descriptors": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-0.0.2.tgz", - "methods": "https://registry.npmjs.org/methods/-/methods-1.1.0.tgz", - "on-finished": "https://registry.npmjs.org/on-finished/-/on-finished-2.1.1.tgz", - "parseurl": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz", - "path-to-regexp": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.3.tgz", - "proxy-addr": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.0.10.tgz", - "qs": "https://registry.npmjs.org/qs/-/qs-2.3.2.tgz", - "range-parser": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz", - "send": "https://registry.npmjs.org/send/-/send-0.10.1.tgz", - "serve-static": "https://registry.npmjs.org/serve-static/-/serve-static-1.7.2.tgz", - "type-is": "https://registry.npmjs.org/type-is/-/type-is-1.5.7.tgz", - "utils-merge": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz", - "vary": "https://registry.npmjs.org/vary/-/vary-1.0.1.tgz" + "accepts": "1.1.4", + "content-disposition": "0.5.0", + "cookie": "0.1.2", + "cookie-signature": "1.0.5", + "debug": "2.1.3", + "depd": "1.0.1", + "escape-html": "1.0.1", + "etag": "1.5.1", + "finalhandler": "0.3.2", + "fresh": "0.2.4", + "media-typer": "0.3.0", + "merge-descriptors": "0.0.2", + "methods": "1.1.0", + "on-finished": "2.1.1", + "parseurl": "1.3.2", + "path-to-regexp": "0.1.3", + "proxy-addr": "1.0.10", + "qs": "2.3.2", + "range-parser": "1.0.3", + "send": "0.10.1", + "serve-static": "1.7.2", + "type-is": "1.5.7", + "utils-merge": "1.0.0", + "vary": "1.0.1" }, "dependencies": { + "accepts": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.1.4.tgz", + "integrity": "sha1-1xyW99QdD+2iw4zRToonwEFY30o=", + "requires": { + "mime-types": "2.0.14", + "negotiator": "0.4.9" + } + }, + "content-disposition": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.0.tgz", + "integrity": "sha1-QoT+auBjCHRjnkToCkGMKTQTXp4=" + }, + "cookie": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.2.tgz", + "integrity": "sha1-cv7D0k5Io0Mgc9kMEmQgBQYQBLE=" + }, + "cookie-signature": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.5.tgz", + "integrity": "sha1-oSLj8VA+yg9TVXlbBxG7I2jUUPk=" + }, + "crc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.2.1.tgz", + "integrity": "sha1-XZyPt3okXNXsopHl0tAFM0urAII=" + }, "debug": { - "version": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz", "integrity": "sha1-zoqxte6PvuK/o7Yzyrk9NmtjQY4=", "requires": { - "ms": "https://registry.npmjs.org/ms/-/ms-0.7.0.tgz" + "ms": "0.7.0" } }, "depd": { - "version": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz", "integrity": "sha1-gK7GTJ1tl+ZcwqnKqTwKpqv3Oqo=" }, + "destroy": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.3.tgz", + "integrity": "sha1-tDO0ck5x/YVR2YhRdIUcX8N34sk=" + }, "ee-first": { - "version": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.0.tgz", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.0.tgz", "integrity": "sha1-ag18YiHkkP7v2S7D9EHJzozQl/Q=" }, + "escape-html": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.1.tgz", + "integrity": "sha1-GBoobq05ejmpKFfPsdQwUuNWv/A=" + }, + "etag": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.5.1.tgz", + "integrity": "sha1-VMUN4E7kJpVWKSWsVmWIKRvn6eo=", + "requires": { + "crc": "3.2.1" + } + }, + "finalhandler": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.3.2.tgz", + "integrity": "sha1-ezibD9Nkem+QvVZOImJL+KSnf7U=", + "requires": { + "debug": "2.1.3", + "escape-html": "1.0.1", + "on-finished": "2.1.1" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.2.4.tgz", + "integrity": "sha1-NYJJkgbJcjcUGQ7ddLRgT+tKYUw=" + }, + "ipaddr.js": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.0.5.tgz", + "integrity": "sha1-X6eM8wG4JceKvDBC2BJyMEnqI8c=" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-0.0.2.tgz", + "integrity": "sha1-w2pSp4FDdRPFcnXzndnTF1FKyMc=" + }, + "methods": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.0.tgz", + "integrity": "sha1-XcpO4S31L/OwVhRZhqjwHLyGQ28=" + }, + "mime": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz", + "integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA=" + }, "mime-db": { - "version": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz", "integrity": "sha1-PQxjGA9FjrENMlqqN9fFiuMS6dc=" }, "mime-types": { - "version": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz", "integrity": "sha1-MQ4VnbI+B3+Lsit0jav6SVcUCqY=", "requires": { - "mime-db": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz" + "mime-db": "1.12.0" } }, "ms": { - "version": "https://registry.npmjs.org/ms/-/ms-0.7.0.tgz", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.0.tgz", "integrity": "sha1-hlvpTC5zl62KV9pqYzpuLzB5i4M=" }, + "negotiator": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.4.9.tgz", + "integrity": "sha1-kuRrbbU8fkIe1koryU8IvnYw3z8=" + }, "on-finished": { - "version": "https://registry.npmjs.org/on-finished/-/on-finished-2.1.1.tgz", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.1.1.tgz", "integrity": "sha1-+CyhyeOk8yhrG5k4YQ5bhja9PLI=", "requires": { - "ee-first": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.0.tgz" + "ee-first": "1.1.0" + } + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, + "path-to-regexp": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.3.tgz", + "integrity": "sha1-IbmrgidCed4lsVbqCP0SylG4rss=" + }, + "proxy-addr": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.0.10.tgz", + "integrity": "sha1-DUCoL4Afw1VWfS7LZe/j8HfxIcU=", + "requires": { + "forwarded": "0.1.2", + "ipaddr.js": "1.0.5" } }, "qs": { - "version": "https://registry.npmjs.org/qs/-/qs-2.3.2.tgz", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-2.3.2.tgz", "integrity": "sha1-1F7CSeS5sCmvAIgpoQHV/36XJ5A=" }, + "range-parser": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz", + "integrity": "sha1-aHKCNTXGkuLCoBA4Jq/YLC4P8XU=" + }, + "send": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.10.1.tgz", + "integrity": "sha1-d0XFDscvEVEVmA6PsXmuwBkA4Io=", + "requires": { + "debug": "2.1.3", + "depd": "1.0.1", + "destroy": "1.0.3", + "escape-html": "1.0.1", + "etag": "1.5.1", + "fresh": "0.2.4", + "mime": "1.2.11", + "ms": "0.6.2", + "on-finished": "2.1.1", + "range-parser": "1.0.3" + }, + "dependencies": { + "ms": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.6.2.tgz", + "integrity": "sha1-2JwhJMb9wTU9Zai3e/GqxLGTcIw=" + } + } + }, + "serve-static": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.7.2.tgz", + "integrity": "sha1-MWTOBtTmw0Wb3MnWAY+0+zXoSzk=", + "requires": { + "escape-html": "1.0.1", + "parseurl": "1.3.2", + "send": "0.10.1", + "utils-merge": "1.0.0" + } + }, "type-is": { - "version": "https://registry.npmjs.org/type-is/-/type-is-1.5.7.tgz", + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.5.7.tgz", "integrity": "sha1-uTaKWTzG730GReeLL0xky+zQXpA=", "requires": { - "media-typer": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "mime-types": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz" + "media-typer": "0.3.0", + "mime-types": "2.0.14" } + }, + "utils-merge": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz", + "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg=" + }, + "vary": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.0.1.tgz", + "integrity": "sha1-meSYFWaihhGN+yuBc1ffeZM3bRA=" } } }, @@ -366,56 +535,6 @@ "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", "dev": true }, - "eyes": { - "version": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", - "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" - }, - "finalhandler": { - "version": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.3.2.tgz", - "integrity": "sha1-ezibD9Nkem+QvVZOImJL+KSnf7U=", - "requires": { - "debug": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz", - "escape-html": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.1.tgz", - "on-finished": "https://registry.npmjs.org/on-finished/-/on-finished-2.1.1.tgz" - }, - "dependencies": { - "debug": { - "version": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz", - "integrity": "sha1-zoqxte6PvuK/o7Yzyrk9NmtjQY4=", - "requires": { - "ms": "https://registry.npmjs.org/ms/-/ms-0.7.0.tgz" - } - }, - "ee-first": { - "version": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.0.tgz", - "integrity": "sha1-ag18YiHkkP7v2S7D9EHJzozQl/Q=" - }, - "ms": { - "version": "https://registry.npmjs.org/ms/-/ms-0.7.0.tgz", - "integrity": "sha1-hlvpTC5zl62KV9pqYzpuLzB5i4M=" - }, - "on-finished": { - "version": "https://registry.npmjs.org/on-finished/-/on-finished-2.1.1.tgz", - "integrity": "sha1-+CyhyeOk8yhrG5k4YQ5bhja9PLI=", - "requires": { - "ee-first": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.0.tgz" - } - } - } - }, - "forever-agent": { - "version": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.5.2.tgz", - "integrity": "sha1-bQ4JxJIflKJ/Y9O0nF/v8epMUTA=" - }, - "form-data": { - "version": "https://registry.npmjs.org/form-data/-/form-data-0.1.4.tgz", - "integrity": "sha1-kavXiKupcCsaq/qLwBAxoqyeOxI=", - "requires": { - "async": "https://registry.npmjs.org/async/-/async-0.9.0.tgz", - "combined-stream": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz", - "mime": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz" - } - }, "formatio": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz", @@ -430,20 +549,17 @@ "integrity": "sha1-lriIb3w8NQi5Mta9cMTTqI818ak=", "dev": true }, - "forwarded": { - "version": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.0.tgz", - "integrity": "sha1-Ge+YdMSuHCl7zweP3mOgm2aoQ2M=" - }, - "fresh": { - "version": "https://registry.npmjs.org/fresh/-/fresh-0.2.4.tgz", - "integrity": "sha1-NYJJkgbJcjcUGQ7ddLRgT+tKYUw=" - }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=" + }, "glob": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", @@ -476,43 +592,6 @@ "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", "dev": true }, - "hawk": { - "version": "https://registry.npmjs.org/hawk/-/hawk-1.1.1.tgz", - "integrity": "sha1-h81JH5tG5OKurKM1QWdmiF0tHtk=", - "requires": { - "boom": "https://registry.npmjs.org/boom/-/boom-0.4.2.tgz", - "cryptiles": "https://registry.npmjs.org/cryptiles/-/cryptiles-0.2.2.tgz", - "hoek": "https://registry.npmjs.org/hoek/-/hoek-0.9.1.tgz", - "sntp": "https://registry.npmjs.org/sntp/-/sntp-0.2.4.tgz" - } - }, - "hoek": { - "version": "https://registry.npmjs.org/hoek/-/hoek-0.9.1.tgz", - "integrity": "sha1-PTIkYrrfB3Fup+uFuviAec3c5QU=" - }, - "http-errors": { - "version": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", - "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", - "requires": { - "depd": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "setprototypeof": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "statuses": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz" - } - }, - "http-signature": { - "version": "https://registry.npmjs.org/http-signature/-/http-signature-0.10.1.tgz", - "integrity": "sha1-T72sEyVZqoMjEh5UB3nAoBKyfmY=", - "requires": { - "asn1": "https://registry.npmjs.org/asn1/-/asn1-0.1.11.tgz", - "assert-plus": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz", - "ctype": "https://registry.npmjs.org/ctype/-/ctype-0.5.3.tgz" - } - }, - "iconv-lite": { - "version": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz", - "integrity": "sha1-/iZaIYrGpXz+hUkn6dBMGYJe3es=" - }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -525,23 +604,22 @@ }, "inherits": { "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ipaddr.js": { - "version": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.0.5.tgz", - "integrity": "sha1-X6eM8wG4JceKvDBC2BJyMEnqI8c=" + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true }, "isarray": { "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" }, - "isstream": { - "version": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + "js-yaml": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-0.3.7.tgz", + "integrity": "sha1-1znY7oZGHlSzVNan19HyrZoWf2I=" }, - "json-stringify-safe": { - "version": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + "jsmin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/jsmin/-/jsmin-1.0.1.tgz", + "integrity": "sha1-570NzWSWw79IYyNb9GGj2YqjuYw=" }, "json3": { "version": "3.3.2", @@ -554,8 +632,20 @@ "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-1.1.22.tgz", "integrity": "sha1-MzCvdWyralQnAMZLLk5KoGLVL/8=" }, + "jxLoader": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jxLoader/-/jxLoader-0.1.1.tgz", + "integrity": "sha1-ATTqUUTlM7WU/B/yX/GU4jXFPs0=", + "requires": { + "js-yaml": "0.3.7", + "moo-server": "1.3.0", + "promised-io": "0.3.5", + "walker": "1.0.7" + } + }, "lodash": { - "version": "https://registry.npmjs.org/lodash/-/lodash-2.4.1.tgz", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.1.tgz", "integrity": "sha1-W3cjA03aTSYuWkb7LFjXzCL3FCA=" }, "lodash._baseassign": { @@ -603,6 +693,11 @@ "lodash._isiterateecall": "3.0.9" } }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, "lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", @@ -631,29 +726,23 @@ "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.1.2.tgz", "integrity": "sha1-JpS5U8nqTQE+W4v7qJHJkQJbJik=" }, - "media-typer": { - "version": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "merge-descriptors": { - "version": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-0.0.2.tgz", - "integrity": "sha1-w2pSp4FDdRPFcnXzndnTF1FKyMc=" - }, - "methods": { - "version": "https://registry.npmjs.org/methods/-/methods-1.1.0.tgz", - "integrity": "sha1-XcpO4S31L/OwVhRZhqjwHLyGQ28=" - }, - "mime": { - "version": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz", - "integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA=" + "makeerror": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", + "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", + "requires": { + "tmpl": "1.0.4" + } }, "mime-db": { "version": "https://registry.npmjs.org/mime-db/-/mime-db-1.29.0.tgz", - "integrity": "sha1-SNJtI1WJZRcErFkWygYAGRQmaHg=" + "integrity": "sha1-SNJtI1WJZRcErFkWygYAGRQmaHg=", + "dev": true }, "mime-types": { "version": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.16.tgz", "integrity": "sha1-K4WKUuXs1RbbiXrCvodIeDBpjiM=", + "dev": true, "requires": { "mime-db": "https://registry.npmjs.org/mime-db/-/mime-db-1.29.0.tgz" } @@ -672,10 +761,6 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, - "mkdirp": { - "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", - "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=" - }, "mocha": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.0.tgz", @@ -715,19 +800,21 @@ } } }, + "moo-server": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/moo-server/-/moo-server-1.3.0.tgz", + "integrity": "sha1-XceVaVZaENbv7VQ5SR5p0jkuWPE=" + }, "ms": { "version": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true }, "native-promise-only": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz", "integrity": "sha1-IKMYwwy0X3H+et+/eyHJnBRy7xE=" }, - "negotiator": { - "version": "https://registry.npmjs.org/negotiator/-/negotiator-0.4.9.tgz", - "integrity": "sha1-kuRrbbU8fkIe1koryU8IvnYw3z8=" - }, "nise": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/nise/-/nise-1.0.1.tgz", @@ -755,25 +842,59 @@ } }, "nock": { - "version": "9.0.14", - "resolved": "https://registry.npmjs.org/nock/-/nock-9.0.14.tgz", - "integrity": "sha1-IhFVAlMXPOKYvNifyoJeg4E8pys=", + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/nock/-/nock-9.4.3.tgz", + "integrity": "sha512-inJFXR3REBvHbZy6nVVwaoKbVTR8Y4Ag051Y/pd2pNPy7HDYtQkenfilBwxToNsW9p1RTeBUml4SPK/mWrFihA==", "requires": { - "chai": "3.5.0", - "debug": "https://registry.npmjs.org/debug/-/debug-2.6.7.tgz", + "chai": "4.1.2", + "debug": "3.1.0", "deep-equal": "1.0.1", - "json-stringify-safe": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "lodash": "4.17.4", + "json-stringify-safe": "5.0.1", + "lodash": "4.17.10", "mkdirp": "0.5.1", - "propagate": "0.4.0", - "qs": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", - "semver": "5.4.1" + "propagate": "1.0.0", + "qs": "6.5.2", + "semver": "5.5.0" }, "dependencies": { + "chai": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", + "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", + "requires": { + "assertion-error": "1.0.2", + "check-error": "1.0.2", + "deep-eql": "3.0.1", + "get-func-name": "2.0.0", + "pathval": "1.1.0", + "type-detect": "4.0.8" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "requires": { + "type-detect": "4.0.8" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, "lodash": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" }, "mkdirp": { "version": "0.5.1", @@ -782,24 +903,34 @@ "requires": { "minimist": "0.0.8" } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "propagate": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz", + "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" } } }, - "node-uuid": { - "version": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", - "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=" - }, - "oauth-sign": { - "version": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.4.0.tgz", - "integrity": "sha1-8ilW8x6nFRqCHl8vsywRPK2Ln2k=" - }, - "on-finished": { - "version": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" - } - }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -809,23 +940,16 @@ "wrappy": "1.0.2" } }, - "parseurl": { - "version": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz", - "integrity": "sha1-yKuMkiO6NIiKpkopeyiFO+wY2lY=" - }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, - "path-to-regexp": { - "version": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.3.tgz", - "integrity": "sha1-IbmrgidCed4lsVbqCP0SylG4rss=" - }, - "pkginfo": { - "version": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.3.1.tgz", - "integrity": "sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE=" + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=" }, "process-nextick-args": { "version": "1.0.7", @@ -833,83 +957,259 @@ "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", "dev": true }, - "propagate": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/propagate/-/propagate-0.4.0.tgz", - "integrity": "sha1-8/zKCm/gZzanulcpZgaWF8EwtIE=" - }, - "proxy-addr": { - "version": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.0.10.tgz", - "integrity": "sha1-DUCoL4Afw1VWfS7LZe/j8HfxIcU=", - "requires": { - "forwarded": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.0.tgz", - "ipaddr.js": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.0.5.tgz" - } + "promised-io": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/promised-io/-/promised-io-0.3.5.tgz", + "integrity": "sha1-StIXuzZYvKrplGsXqGaOzYUeE1Y=" }, - "punycode": { - "version": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + "psl": { + "version": "1.1.28", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.28.tgz", + "integrity": "sha512-+AqO1Ae+N/4r7Rvchrdm432afjT9hqJRyBN3DQv9At0tPz4hIFSGKbq64fN9dVoCow4oggIIax5/iONx0r9hZw==" }, "qs": { "version": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", - "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" - }, - "range-parser": { - "version": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz", - "integrity": "sha1-aHKCNTXGkuLCoBA4Jq/YLC4P8XU=" + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=", + "dev": true }, - "raw-body": { - "version": "https://registry.npmjs.org/raw-body/-/raw-body-2.2.0.tgz", - "integrity": "sha1-mUl2z2pQlqQRYoQEkvC9xdbn+5Y=", + "redis": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz", + "integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==", "requires": { - "bytes": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz", - "iconv-lite": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz", - "unpipe": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" + "double-ended-queue": "2.1.0-0", + "redis-commands": "1.3.5", + "redis-parser": "2.6.0" } }, - "readable-stream": { - "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "isarray": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "string_decoder": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" - } + "redis-commands": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.3.5.tgz", + "integrity": "sha512-foGF8u6MXGFF++1TZVC6icGXuMYPftKXt1FBT2vrfU9ZATNtZJ8duRC5d1lEfE8hyVe3jhelHGB91oB7I6qLsA==" }, - "redis": { - "version": "https://registry.npmjs.org/redis/-/redis-0.12.1.tgz", - "integrity": "sha1-ZN92rQ/IrOuuvSoGReikj6xJGF4=" + "redis-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz", + "integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs=" }, "request": { - "version": "https://registry.npmjs.org/request/-/request-2.47.0.tgz", + "version": "2.47.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.47.0.tgz", "integrity": "sha1-Cen9Gk/tZZOoBe+CArIPDF7LSF8=", "requires": { - "aws-sign2": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.5.0.tgz", - "bl": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz", - "caseless": "https://registry.npmjs.org/caseless/-/caseless-0.6.0.tgz", - "combined-stream": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz", - "forever-agent": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.5.2.tgz", - "form-data": "https://registry.npmjs.org/form-data/-/form-data-0.1.4.tgz", - "hawk": "https://registry.npmjs.org/hawk/-/hawk-1.1.1.tgz", - "http-signature": "https://registry.npmjs.org/http-signature/-/http-signature-0.10.1.tgz", - "json-stringify-safe": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "mime-types": "https://registry.npmjs.org/mime-types/-/mime-types-1.0.2.tgz", - "node-uuid": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", - "oauth-sign": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.4.0.tgz", - "qs": "https://registry.npmjs.org/qs/-/qs-2.3.3.tgz", - "stringstream": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "tough-cookie": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", - "tunnel-agent": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz" + "aws-sign2": "0.5.0", + "bl": "0.9.5", + "caseless": "0.6.0", + "combined-stream": "0.0.7", + "forever-agent": "0.5.2", + "form-data": "0.1.4", + "hawk": "1.1.1", + "http-signature": "0.10.1", + "json-stringify-safe": "5.0.1", + "mime-types": "1.0.2", + "node-uuid": "1.4.8", + "oauth-sign": "0.4.0", + "qs": "2.3.3", + "stringstream": "0.0.6", + "tough-cookie": "2.4.3", + "tunnel-agent": "0.4.3" }, "dependencies": { + "asn1": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.1.11.tgz", + "integrity": "sha1-VZvhg3bQik7E2+gId9J4GGObLfc=" + }, + "assert-plus": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz", + "integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA=" + }, + "aws-sign2": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.5.0.tgz", + "integrity": "sha1-xXED96F/wDfwLXwuZLYC6iI/fWM=" + }, + "bl": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz", + "integrity": "sha1-wGt5evCF6gC8Unr8jvzxHeIjIFQ=", + "requires": { + "readable-stream": "1.0.34" + } + }, + "boom": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/boom/-/boom-0.4.2.tgz", + "integrity": "sha1-emNune1O/O+xnO9JR6PGffrukRs=", + "requires": { + "hoek": "0.9.1" + } + }, + "caseless": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.6.0.tgz", + "integrity": "sha1-gWfBq4OX+1u5X5bSjlqBxQ8kesQ=" + }, + "combined-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz", + "integrity": "sha1-ATfmV7qlp1QcV6w3rF/AfXO03B8=", + "requires": { + "delayed-stream": "0.0.5" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cryptiles": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-0.2.2.tgz", + "integrity": "sha1-7ZH/HxetE9N0gohZT4pIoNJvMlw=", + "requires": { + "boom": "0.4.2" + } + }, + "ctype": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/ctype/-/ctype-0.5.3.tgz", + "integrity": "sha1-gsGMJGH3QRTvFsE1IkrQuRRMoS8=" + }, + "delayed-stream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz", + "integrity": "sha1-1LH0OpPoKW3+AmlPRoC8N6MTxz8=" + }, + "forever-agent": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.5.2.tgz", + "integrity": "sha1-bQ4JxJIflKJ/Y9O0nF/v8epMUTA=" + }, + "form-data": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-0.1.4.tgz", + "integrity": "sha1-kavXiKupcCsaq/qLwBAxoqyeOxI=", + "requires": { + "async": "0.9.0", + "combined-stream": "0.0.7", + "mime": "1.2.11" + } + }, + "hawk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-1.1.1.tgz", + "integrity": "sha1-h81JH5tG5OKurKM1QWdmiF0tHtk=", + "requires": { + "boom": "0.4.2", + "cryptiles": "0.2.2", + "hoek": "0.9.1", + "sntp": "0.2.4" + } + }, + "hoek": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-0.9.1.tgz", + "integrity": "sha1-PTIkYrrfB3Fup+uFuviAec3c5QU=" + }, + "http-signature": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-0.10.1.tgz", + "integrity": "sha1-T72sEyVZqoMjEh5UB3nAoBKyfmY=", + "requires": { + "asn1": "0.1.11", + "assert-plus": "0.1.5", + "ctype": "0.5.3" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "mime": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz", + "integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA=" + }, "mime-types": { - "version": "https://registry.npmjs.org/mime-types/-/mime-types-1.0.2.tgz", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-1.0.2.tgz", "integrity": "sha1-mVrhOSq4r/y/yyZB3QVOlDwNXc4=" }, + "node-uuid": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", + "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=" + }, + "oauth-sign": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.4.0.tgz", + "integrity": "sha1-8ilW8x6nFRqCHl8vsywRPK2Ln2k=" + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, "qs": { - "version": "https://registry.npmjs.org/qs/-/qs-2.3.3.tgz", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-2.3.3.tgz", "integrity": "sha1-6eha2+ddoLvkyOBHaghikPhjtAQ=" + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "sntp": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-0.2.4.tgz", + "integrity": "sha1-+4hfGLDzqtGJ+CSGJTa87ux1CQA=", + "requires": { + "hoek": "0.9.1" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "stringstream": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", + "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==" + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "1.1.28", + "punycode": "1.4.1" + } + }, + "tunnel-agent": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=" } } }, @@ -919,142 +1219,75 @@ "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", "dev": true }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "samsam": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.2.1.tgz", "integrity": "sha1-7dOQk6MYQ3DLhZJDsr3yVefY6mc=" }, - "semver": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" - }, - "send": { - "version": "https://registry.npmjs.org/send/-/send-0.10.1.tgz", - "integrity": "sha1-d0XFDscvEVEVmA6PsXmuwBkA4Io=", + "sha1": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/sha1/-/sha1-1.1.0.tgz", + "integrity": "sha1-j8IEe+OezrHcVOv+NaUEmxigBAs=", "requires": { - "debug": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz", - "depd": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz", - "destroy": "https://registry.npmjs.org/destroy/-/destroy-1.0.3.tgz", - "escape-html": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.1.tgz", - "etag": "https://registry.npmjs.org/etag/-/etag-1.5.1.tgz", - "fresh": "https://registry.npmjs.org/fresh/-/fresh-0.2.4.tgz", - "mime": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz", - "ms": "https://registry.npmjs.org/ms/-/ms-0.6.2.tgz", - "on-finished": "https://registry.npmjs.org/on-finished/-/on-finished-2.1.1.tgz", - "range-parser": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz" + "charenc": "0.0.2", + "crypt": "0.0.2" }, "dependencies": { - "debug": { - "version": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz", - "integrity": "sha1-zoqxte6PvuK/o7Yzyrk9NmtjQY4=", - "requires": { - "ms": "https://registry.npmjs.org/ms/-/ms-0.7.0.tgz" - }, - "dependencies": { - "ms": { - "version": "https://registry.npmjs.org/ms/-/ms-0.7.0.tgz", - "integrity": "sha1-hlvpTC5zl62KV9pqYzpuLzB5i4M=" - } - } - }, - "depd": { - "version": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz", - "integrity": "sha1-gK7GTJ1tl+ZcwqnKqTwKpqv3Oqo=" + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" }, - "ee-first": { - "version": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.0.tgz", - "integrity": "sha1-ag18YiHkkP7v2S7D9EHJzozQl/Q=" - }, - "ms": { - "version": "https://registry.npmjs.org/ms/-/ms-0.6.2.tgz", - "integrity": "sha1-2JwhJMb9wTU9Zai3e/GqxLGTcIw=" - }, - "on-finished": { - "version": "https://registry.npmjs.org/on-finished/-/on-finished-2.1.1.tgz", - "integrity": "sha1-+CyhyeOk8yhrG5k4YQ5bhja9PLI=", - "requires": { - "ee-first": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.0.tgz" - } + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" } } }, - "serve-static": { - "version": "https://registry.npmjs.org/serve-static/-/serve-static-1.7.2.tgz", - "integrity": "sha1-MWTOBtTmw0Wb3MnWAY+0+zXoSzk=", - "requires": { - "escape-html": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.1.tgz", - "parseurl": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz", - "send": "https://registry.npmjs.org/send/-/send-0.10.1.tgz", - "utils-merge": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz" - } - }, - "setprototypeof": { - "version": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" - }, - "sha1": { - "version": "https://registry.npmjs.org/sha1/-/sha1-1.1.0.tgz", - "integrity": "sha1-j8IEe+OezrHcVOv+NaUEmxigBAs=", - "requires": { - "charenc": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "crypt": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz" - } - }, "sinon": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-3.2.1.tgz", - "integrity": "sha512-KY3OLOWpek/I4NGAMHetuutVgS2aRgMR5g5/1LSYvPJ3qo2BopIvk3esFztPxF40RWf/NNNJzdFPriSkXUVK3A==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-3.3.0.tgz", + "integrity": "sha512-/flfGfIxIRXSvZBHJzIf3iAyGYkmMQq6SQjA0cx9SOuVuq+4ZPPO4LJtH1Ce0Lznax1KSG1U6Dad85wIcSW19w==", "requires": { + "build": "0.1.4", "diff": "3.2.0", "formatio": "1.2.0", + "lodash.get": "4.4.2", "lolex": "2.1.2", "native-promise-only": "0.8.1", "nise": "1.0.1", "path-to-regexp": "1.7.0", "samsam": "1.2.1", "text-encoding": "0.6.4", - "type-detect": "4.0.3" + "type-detect": "4.0.8" }, "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, "path-to-regexp": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", "requires": { - "isarray": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + "isarray": "0.0.1" } }, "type-detect": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.3.tgz", - "integrity": "sha1-Dj8mcLRAmbC0bChNE2p+9Jx0wuo=" + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" } } }, - "sntp": { - "version": "https://registry.npmjs.org/sntp/-/sntp-0.2.4.tgz", - "integrity": "sha1-+4hfGLDzqtGJ+CSGJTa87ux1CQA=", - "requires": { - "hoek": "https://registry.npmjs.org/hoek/-/hoek-0.9.1.tgz" - } - }, - "stack-trace": { - "version": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" - }, - "statuses": { - "version": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "stringstream": { - "version": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" - }, "superagent": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.6.0.tgz", @@ -1175,33 +1408,20 @@ "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=" }, - "tough-cookie": { - "version": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", - "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=", - "requires": { - "punycode": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz" - } - }, - "tunnel-agent": { - "version": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", - "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=" - }, - "type-detect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz", - "integrity": "sha1-diIXzAbbJY7EiQihKY6LlRIejqI=" + "timespan": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/timespan/-/timespan-2.3.0.tgz", + "integrity": "sha1-SQLOBAvRPYRcj1myfp1ZutbzmSk=" }, - "type-is": { - "version": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", - "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", - "requires": { - "media-typer": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "mime-types": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.16.tgz" - } + "tmpl": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", + "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=" }, - "unpipe": { - "version": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + "uglify-js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-1.3.5.tgz", + "integrity": "sha1-S1v/+Rhu/7qoiOTJ6UvZ/EyUkp0=" }, "util-deprecate": { "version": "1.0.2", @@ -1209,30 +1429,62 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true }, - "utils-merge": { - "version": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz", - "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg=" - }, - "vary": { - "version": "https://registry.npmjs.org/vary/-/vary-1.0.1.tgz", - "integrity": "sha1-meSYFWaihhGN+yuBc1ffeZM3bRA=" + "walker": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", + "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", + "requires": { + "makeerror": "1.0.11" + } }, "winston": { - "version": "https://registry.npmjs.org/winston/-/winston-0.8.3.tgz", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/winston/-/winston-0.8.3.tgz", "integrity": "sha1-ZLar9M0Brcrv1QCTk7HY6L7BnbA=", "requires": { - "async": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", - "colors": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", - "cycle": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", - "eyes": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", - "isstream": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "pkginfo": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.3.1.tgz", - "stack-trace": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz" + "async": "0.2.10", + "colors": "0.6.2", + "cycle": "1.0.3", + "eyes": "0.1.8", + "isstream": "0.1.2", + "pkginfo": "0.3.1", + "stack-trace": "0.0.10" }, "dependencies": { "async": { - "version": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" + }, + "colors": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=" + }, + "cycle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=" + }, + "eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "pkginfo": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.3.1.tgz", + "integrity": "sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE=" + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" } } }, @@ -1242,6 +1494,11 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "wrench": { + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/wrench/-/wrench-1.3.9.tgz", + "integrity": "sha1-bxPsNRRTF+spLKX2UxORskQRFBE=" + }, "xmldom": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz", diff --git a/bbb-webhooks/package.json b/bbb-webhooks/package.json index cbe8a8dc77d2dd5596509a2500fb8d6f188fcd12..b6b938e669c95cd5dd0e6c0463c5897f58996698 100755 --- a/bbb-webhooks/package.json +++ b/bbb-webhooks/package.json @@ -10,11 +10,10 @@ "dependencies": { "async": "0.9.0", "body-parser": "^1.9.2", - "coffee-script": "1.8.0", "express": "4.10.2", "lodash": "2.4.1", "nock": "^9.0.14", - "redis": "0.12.1", + "redis": "^2.8.0", "request": "2.47.0", "sha1": "1.1.0", "sinon": "^3.2.1", diff --git a/bbb-webhooks/test/test.js b/bbb-webhooks/test/test.js index 51b2d0ab2037d24646ddd1efcb6ee854400c66e8..7239b9f16f3327383a2776591c735e1621bd7211 100644 --- a/bbb-webhooks/test/test.js +++ b/bbb-webhooks/test/test.js @@ -9,6 +9,8 @@ const Helpers = require('./helpers.js') const sinon = require('sinon'); const winston = require('winston'); +const sharedSecret = process.env.SHARED_SECRET || sharedSecret; + // Block winston from logging Logger.remove(winston.transports.Console); describe('bbb-webhooks tests', () => { @@ -38,7 +40,7 @@ describe('bbb-webhooks tests', () => { describe('GET /hooks/list permanent', () => { it('should list permanent hook', (done) => { - let getUrl = utils.checksumAPI(Helpers.url + Helpers.listUrl, config.bbb.sharedSecret); + let getUrl = utils.checksumAPI(Helpers.url + Helpers.listUrl, sharedSecret); getUrl = Helpers.listUrl + '?checksum=' + getUrl request(Helpers.url) @@ -62,7 +64,7 @@ describe('bbb-webhooks tests', () => { Hook.removeSubscription(hooks[hooks.length-1].id, () => { done(); }); }); it('should create a hook', (done) => { - let getUrl = utils.checksumAPI(Helpers.url + Helpers.createUrl, config.bbb.sharedSecret); + let getUrl = utils.checksumAPI(Helpers.url + Helpers.createUrl, sharedSecret); getUrl = Helpers.createUrl + '&checksum=' + getUrl request(Helpers.url) @@ -87,7 +89,7 @@ describe('bbb-webhooks tests', () => { it('should destroy a hook', (done) => { const hooks = Hook.allGlobalSync(); const hook = hooks[hooks.length-1].id; - let getUrl = utils.checksumAPI(Helpers.url + Helpers.destroyUrl(hook), config.bbb.sharedSecret); + let getUrl = utils.checksumAPI(Helpers.url + Helpers.destroyUrl(hook), sharedSecret); getUrl = Helpers.destroyUrl(hook) + '&checksum=' + getUrl request(Helpers.url) @@ -103,7 +105,7 @@ describe('bbb-webhooks tests', () => { describe('GET /hooks/destroy permanent hook', () => { it('should not destroy the permanent hook', (done) => { - let getUrl = utils.checksumAPI(Helpers.url + Helpers.destroyPermanent, config.bbb.sharedSecret); + let getUrl = utils.checksumAPI(Helpers.url + Helpers.destroyPermanent, sharedSecret); getUrl = Helpers.destroyPermanent + '&checksum=' + getUrl request(Helpers.url) .get(getUrl) @@ -126,7 +128,7 @@ describe('bbb-webhooks tests', () => { Hook.removeSubscription(hooks[hooks.length-1].id, () => { done(); }); }); it('should create a hook with getRaw=true', (done) => { - let getUrl = utils.checksumAPI(Helpers.url + Helpers.createUrl + Helpers.createRaw, config.bbb.sharedSecret); + let getUrl = utils.checksumAPI(Helpers.url + Helpers.createUrl + Helpers.createRaw, sharedSecret); getUrl = Helpers.createUrl + '&checksum=' + getUrl + Helpers.createRaw request(Helpers.url) diff --git a/bbb-webhooks/web_hooks.js b/bbb-webhooks/web_hooks.js index a91718f1e889e6724f1ec35b095f7011f5c4919c..c00a2cf0b14c5eb2af40d16eeeb399fdd089c266 100644 --- a/bbb-webhooks/web_hooks.js +++ b/bbb-webhooks/web_hooks.js @@ -69,7 +69,7 @@ module.exports = class WebHooks { } } } catch (e) { - Logger.error("[WebHooks] error processing the message:", JSON.stringify(raw), ":", e); + Logger.error("[WebHooks] error processing the message:", JSON.stringify(raw), ":", e.message); } }); diff --git a/bbb-webhooks/web_server.js b/bbb-webhooks/web_server.js index e0f77f0b405a6d7c55e3052ae6fd4d11136137dc..be5a0057836e696141adbd44e51349569678808f 100644 --- a/bbb-webhooks/web_server.js +++ b/bbb-webhooks/web_server.js @@ -144,8 +144,9 @@ module.exports = class WebServer { _validateChecksum(req, res, next) { const urlObj = url.parse(req.url, true); const checksum = urlObj.query["checksum"]; + const sharedSecret = process.env.SHARED_SECRET || config.bbb.sharedSecret; - if (checksum === Utils.checksumAPI(req.url, config.bbb.sharedSecret)) { + if (checksum === Utils.checksumAPI(req.url, sharedSecret)) { next(); } else { Logger.info("[WebServer] checksum check failed, sending a checksumError response"); diff --git a/bigbluebutton-client/branding/default/style/css/V2Theme.css b/bigbluebutton-client/branding/default/style/css/V2Theme.css index 602fcb0740e2545aee30009a69ce4582267b9e02..8fee070b290bae52942c26bc96653494d15b2508 100644 --- a/bigbluebutton-client/branding/default/style/css/V2Theme.css +++ b/bigbluebutton-client/branding/default/style/css/V2Theme.css @@ -363,15 +363,24 @@ phonecomponents|MuteMeButton { textSelectedColor : #FFFFFF; } -.helpLinkButtonStyle { +.helpLinkButtonStyle, .helpLinkButtonStyleRTL { cornerRadius : 2; rollOverColor : #1070D7; selectionColor : #0A5EAC; +} + +.helpLinkButtonStyle { icon : Embed(source="assets/swf/v2_skin.swf", symbol="Icon_Help"); overIcon : Embed(source="assets/swf/v2_skin.swf", symbol="Icon_Help_White"); downIcon : Embed(source="assets/swf/v2_skin.swf", symbol="Icon_Help_White"); } +.helpLinkButtonStyleRTL { + icon : Embed(source="assets/swf/v2_skin.swf", symbol="Icon_Help_RTL"); + overIcon : Embed(source="assets/swf/v2_skin.swf", symbol="Icon_Help_White_RTL"); + downIcon : Embed(source="assets/swf/v2_skin.swf", symbol="Icon_Help_White_RTL"); +} + /* //------------------------------ // Alert @@ -599,9 +608,7 @@ chat|ChatOptionsTab { paddingTop : 20; } -.chatControlBarSendButtonStyle { - icon : Embed(source="assets/swf/v2_skin.swf", symbol="Icon_Send"); - disabledIcon : Embed(source="assets/swf/v2_skin.swf", symbol="Icon_Send_Disabled"); +.chatControlBarSendButtonStyle, .chatControlBarSendButtonStyleRTL { cornerRadius : 20; borderStyle : none; borderThickness : 0; @@ -611,16 +618,14 @@ chat|ChatOptionsTab { fillColorDisabled : #F0F2F6; } +.chatControlBarSendButtonStyle { + icon : Embed(source="assets/swf/v2_skin.swf", symbol="Icon_Send"); + disabledIcon : Embed(source="assets/swf/v2_skin.swf", symbol="Icon_Send_Disabled"); +} + .chatControlBarSendButtonStyleRTL { icon : Embed(source="assets/swf/v2_skin.swf", symbol="Icon_Send_RTL"); disabledIcon : Embed(source="assets/swf/v2_skin.swf", symbol="Icon_Send_Disabled_RTL"); - cornerRadius : 20; - borderStyle : none; - borderThickness : 0; - fillColorUp : #1070D7; - fillColorOver : #0A5EAC; - fillColorDown : #1070D7; - fillColorDisabled : #F0F2F6; } .chatControlBarTextMsgStyle { @@ -710,6 +715,23 @@ chat|ChatMessageRenderer { paddingRight : 12; } +.userTypingCanvas { + backgroundColor : #FFFFFF; +} + +.msgTooLongLabel { + color : #DE2721; +} + +.userTypingLabel { + fontSize : 13; + color : #4E5A66; + paddingBottom : 2; + paddingTop : 2; + paddingLeft : 4; + paddingRight : 4; +} + /* //------------------------------ // CheckBox diff --git a/bigbluebutton-client/branding/default/style/css/assets/swf/v2_skin.fla b/bigbluebutton-client/branding/default/style/css/assets/swf/v2_skin.fla index e217dccf1af6e09d5b9eed81291ec7947fccd612..42efee56f5d3b5db9d8a87e2e8725c82ef8c6a46 100644 Binary files a/bigbluebutton-client/branding/default/style/css/assets/swf/v2_skin.fla and b/bigbluebutton-client/branding/default/style/css/assets/swf/v2_skin.fla differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/swf/v2_skin.swf b/bigbluebutton-client/branding/default/style/css/assets/swf/v2_skin.swf index 8eadd9637b2c100fd0a21cf1a139d005b265fc81..6593a4465e831d545fdc8a4c9ac8c781c2c97d2f 100644 Binary files a/bigbluebutton-client/branding/default/style/css/assets/swf/v2_skin.swf and b/bigbluebutton-client/branding/default/style/css/assets/swf/v2_skin.swf differ diff --git a/bigbluebutton-client/locale/en_US/bbbResources.properties b/bigbluebutton-client/locale/en_US/bbbResources.properties index 5d45826868b817e03dd08a2a0178fc86443a691e..e499618689f05e1b994c2ea35fe755466b5f271c 100644 --- a/bigbluebutton-client/locale/en_US/bbbResources.properties +++ b/bigbluebutton-client/locale/en_US/bbbResources.properties @@ -384,6 +384,9 @@ bbb.chat.fontSize = Chat Message Font Size bbb.chat.cmbFontSize.toolTip = Select Chat Message Font Size bbb.chat.messageList = Chat Messages bbb.chat.unreadMessages = You have new unread messages â–¼ +bbb.chat.usersTyping.one = {0} is typing… +bbb.chat.usersTyping.many = {0} & {1} are typing… +bbb.chat.usersTyping.multiple = Multiple users are typing… bbb.chat.minimizeBtn.accessibilityName = Minimize the Chat Window bbb.chat.maximizeRestoreBtn.accessibilityName = Maximize the Chat Window bbb.chat.closeBtn.accessibilityName = Close the Chat Window @@ -500,6 +503,7 @@ bbb.screenshareView.actualSize = Display actual size bbb.screenshareView.minimizeBtn.accessibilityName = Minimize the Screen Sharing View Window bbb.screenshareView.maximizeRestoreBtn.accessibilityName = Maximize the Screen Sharing View Window bbb.screenshareView.closeBtn.accessibilityName = Close the Screen Sharing View Window +bbb.screenshareView.warning.ffMacWebRTC = We recommend that you switch to Chrome on Mac for a better screenshare viewing quality. bbb.toolbar.phone.toolTip.start = Enable Audio (microphone or listen only) bbb.toolbar.phone.toolTip.stop = Disable Audio bbb.toolbar.phone.toolTip.mute = Stop listening the conference diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml index bf71846723f81ab3113fd723e573c9e260cd936f..352733063fe415f1e12aed960477f182e93df653 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml @@ -481,6 +481,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } btnLogout.styleName = "logoutButtonStyle" + styleNameExt; + helpBtn.styleName = "helpLinkButtonStyle" + styleNameExt; } private function openSettings(e:Event = null):void{ diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/events/UserTypingEvent.as b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/events/UserTypingEvent.as new file mode 100644 index 0000000000000000000000000000000000000000..08191b43077e75fe55def828f146f2db4e0da48a --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/events/UserTypingEvent.as @@ -0,0 +1,40 @@ +/** + * 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 UserTypingEvent extends Event { + // Used to send user typing to server + public static const USER_TYPING_EVENT:String = 'userTypingEvent'; + + // Used when receiving user typing message from the server + public static const USER_TYPING_MESSAGE:String = 'userTypingMessage'; + + public var chatId:String; + + public var userId:String; + + public function UserTypingEvent(type:String, chatId:String, userId:String) { + super(type, false, false); + this.chatId = chatId; + this.userId = userId; + } + + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/maps/ChatEventMap.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/maps/ChatEventMap.mxml index d17e542350b63ed2638ba0edee53a773b9632048..d41a99fb962f50ba6acce44845bd93c3d677f333 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/maps/ChatEventMap.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/maps/ChatEventMap.mxml @@ -25,28 +25,29 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. xmlns:fx="http://ns.adobe.com/mxml/2009"> <fx:Script> <![CDATA[ - import mx.events.FlexEvent; - - import org.bigbluebutton.core.EventConstants; - import org.bigbluebutton.main.events.BBBEvent; - import org.bigbluebutton.modules.chat.events.ChatCopyEvent; - import org.bigbluebutton.modules.chat.events.ChatHistoryEvent; - import org.bigbluebutton.modules.chat.events.ChatSaveEvent; - import org.bigbluebutton.modules.chat.events.ChatToolbarButtonEvent; - import org.bigbluebutton.modules.chat.events.CreateGroupChatReqEvent; - import org.bigbluebutton.modules.chat.events.GroupChatBoxClosedEvent; - import org.bigbluebutton.modules.chat.events.GroupChatCreatedEvent; - import org.bigbluebutton.modules.chat.events.NewGroupChatMessageEvent; - import org.bigbluebutton.modules.chat.events.OpenChatBoxEvent; - import org.bigbluebutton.modules.chat.events.ReceivedGroupChatsEvent; - import org.bigbluebutton.modules.chat.events.SendGroupChatMessageEvent; - import org.bigbluebutton.modules.chat.events.StartChatModuleEvent; - import org.bigbluebutton.modules.chat.events.StopChatModuleEvent; - import org.bigbluebutton.modules.chat.services.ChatCopy; - import org.bigbluebutton.modules.chat.services.ChatMessageService; - import org.bigbluebutton.modules.chat.services.ChatSaver; - import org.bigbluebutton.modules.chat.services.MessageReceiver; - import org.bigbluebutton.modules.chat.services.MessageSender; + import mx.events.FlexEvent; + + import org.bigbluebutton.core.EventConstants; + import org.bigbluebutton.main.events.BBBEvent; + import org.bigbluebutton.modules.chat.events.ChatCopyEvent; + import org.bigbluebutton.modules.chat.events.ChatHistoryEvent; + import org.bigbluebutton.modules.chat.events.ChatSaveEvent; + import org.bigbluebutton.modules.chat.events.ChatToolbarButtonEvent; + import org.bigbluebutton.modules.chat.events.CreateGroupChatReqEvent; + import org.bigbluebutton.modules.chat.events.GroupChatBoxClosedEvent; + import org.bigbluebutton.modules.chat.events.GroupChatCreatedEvent; + import org.bigbluebutton.modules.chat.events.NewGroupChatMessageEvent; + import org.bigbluebutton.modules.chat.events.OpenChatBoxEvent; + import org.bigbluebutton.modules.chat.events.ReceivedGroupChatsEvent; + import org.bigbluebutton.modules.chat.events.SendGroupChatMessageEvent; + import org.bigbluebutton.modules.chat.events.StartChatModuleEvent; + import org.bigbluebutton.modules.chat.events.StopChatModuleEvent; + import org.bigbluebutton.modules.chat.events.UserTypingEvent; + import org.bigbluebutton.modules.chat.services.ChatCopy; + import org.bigbluebutton.modules.chat.services.ChatMessageService; + import org.bigbluebutton.modules.chat.services.ChatSaver; + import org.bigbluebutton.modules.chat.services.MessageReceiver; + import org.bigbluebutton.modules.chat.services.MessageSender; ]]> </fx:Script> <fx:Declarations> @@ -93,6 +94,10 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <MethodInvoker generator="{ChatMessageService}" method="sendPublicMessage" arguments="{event}"/> </EventHandlers> + <EventHandlers type="{UserTypingEvent.USER_TYPING_EVENT}"> + <MethodInvoker generator="{ChatMessageService}" method="handleUserTypingMessage" arguments="{event}"/> + </EventHandlers> + <EventHandlers type="{ChatHistoryEvent.REQUEST_HISTORY}" > <MethodInvoker generator="{ChatMessageService}" method="getGroupChatHistoryMessages" arguments="{event.chatId}"/> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/services/ChatMessageService.as b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/services/ChatMessageService.as index 2d0e29287decc8686744700973abbedb8127de65..03815d30f82cbf067703400d780d4817e22aac1d 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/services/ChatMessageService.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/services/ChatMessageService.as @@ -21,18 +21,16 @@ package org.bigbluebutton.modules.chat.services import com.asfusion.mate.events.Dispatcher; import flash.events.IEventDispatcher; - import flash.external.ExternalInterface; import org.as3commons.logging.api.ILogger; import org.as3commons.logging.api.getClassLogger; - import org.bigbluebutton.core.UsersUtil; import org.bigbluebutton.core.model.LiveMeeting; import org.bigbluebutton.modules.chat.events.CreateGroupChatReqEvent; import org.bigbluebutton.modules.chat.events.OpenChatBoxEvent; import org.bigbluebutton.modules.chat.events.SendGroupChatMessageEvent; + import org.bigbluebutton.modules.chat.events.UserTypingEvent; import org.bigbluebutton.modules.chat.model.GroupChat; import org.bigbluebutton.modules.chat.vo.ChatMessageVO; - import org.bigbluebutton.util.i18n.ResourceUtil; public class ChatMessageService { @@ -75,6 +73,10 @@ package org.bigbluebutton.modules.chat.services sender.createGroupChat(event.name, event.access, event.users); } } + + public function handleUserTypingMessage(event:UserTypingEvent):void{ + sender.userTyping(event.chatId); + } public function getGroupChats():void { sender.getGroupChats(); diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/services/MessageReceiver.as b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/services/MessageReceiver.as index f59b851806ac40fd6d766ad0690417a00af38a2e..abeb352e825614c18410e450f2b524300bd56cca 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/services/MessageReceiver.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/services/MessageReceiver.as @@ -19,14 +19,18 @@ package org.bigbluebutton.modules.chat.services { import flash.events.IEventDispatcher; + import mx.collections.ArrayCollection; + import org.as3commons.logging.api.ILogger; import org.as3commons.logging.api.getClassLogger; import org.bigbluebutton.core.BBB; import org.bigbluebutton.core.EventConstants; + import org.bigbluebutton.core.UsersUtil; import org.bigbluebutton.core.events.CoreEvent; import org.bigbluebutton.core.model.LiveMeeting; import org.bigbluebutton.main.model.users.IMessageListener; + import org.bigbluebutton.modules.chat.events.UserTypingEvent; import org.bigbluebutton.modules.chat.model.GroupChat; import org.bigbluebutton.modules.chat.vo.ChatMessageVO; import org.bigbluebutton.modules.chat.vo.GroupChatUser; @@ -61,6 +65,9 @@ package org.bigbluebutton.modules.chat.services case "GroupChatCreatedEvtMsg": handleGroupChatCreatedEvtMsg(message); break; + case "UserTypingEvtMsg": + handleUserTypginEvtMsg(message); + break; default: // LogUtil.warn("Cannot handle message [" + messageName + "]"); } @@ -105,6 +112,16 @@ package org.bigbluebutton.modules.chat.services LiveMeeting.inst().chats.addGroupChatsList(chats); } + + private function handleUserTypginEvtMsg(msg:Object):void { + var body: Object = msg.body as Object; + var chatId: String = body.chatId as String; + var userId: String = body.userId as String; + // Ignore user message if it concerns myself + if (userId != UsersUtil.getMyUserID()) { + dispatcher.dispatchEvent(new UserTypingEvent(UserTypingEvent.USER_TYPING_MESSAGE, chatId, userId)); + } + } private function handleGroupChatCreatedEvtMsg(msg:Object):void { var body: Object = msg.body as Object; diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/services/MessageSender.as b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/services/MessageSender.as index 14eb368e4d83692ccae9a68842ec5988901717c4..988879be02aa68827a45b356faa6af1fb313ab81 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/services/MessageSender.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/services/MessageSender.as @@ -102,6 +102,26 @@ package org.bigbluebutton.modules.chat.services message ); } + + public function userTyping(chatId:String):void { + LOGGER.debug("Sending [chat.UserTypingMsg] to server."); + var message:Object = { + header: {name: "UserTypingPubMsg", + meetingId: UsersUtil.getInternalMeetingID(), + userId: UsersUtil.getMyUserID()}, + body: {chatId: chatId} + }; + + var _nc:ConnectionManager = BBB.initConnectionManager(); + _nc.sendMessage2x( + function(result:String):void { // On successful result + }, + function(status:String):void { // status - On error occurred + LOGGER.error(status); + }, + message + ); + } public function clearPublicChatMessages():void { LOGGER.debug("Sending [chat.clearPublicChatMessages] to server."); diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatTab.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatTab.mxml index bc26b261da8db558ab07d5b70be976673db50bb1..5c69d8d21684a287769ce8b8c68ddef0496ab838 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatTab.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatTab.mxml @@ -45,6 +45,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <mate:Listener type="{BBBEvent.RECONNECT_DISCONNECTED_EVENT}" receive="refreshChat(event)"/> <mate:Listener type="{LockControlEvent.CHANGED_LOCK_SETTINGS}" method="lockSettingsChanged" /> <mate:Listener type="{UserStatusChangedEvent.USER_STATUS_CHANGED}" method="refreshRole" /> + <mate:Listener type="{UserTypingEvent.USER_TYPING_MESSAGE}" method="handleUserTyping" /> </fx:Declarations> <fx:Script> @@ -58,6 +59,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import flashx.textLayout.formats.Direction; + import org.as3commons.lang.DictionaryUtils; import org.as3commons.lang.StringUtils; import org.as3commons.logging.api.ILogger; import org.as3commons.logging.api.getClassLogger; @@ -79,6 +81,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import org.bigbluebutton.modules.chat.events.PrivateChatMessageEvent; import org.bigbluebutton.modules.chat.events.PublicChatMessageEvent; import org.bigbluebutton.modules.chat.events.SendGroupChatMessageEvent; + import org.bigbluebutton.modules.chat.events.UserTypingEvent; import org.bigbluebutton.modules.chat.model.ChatConversation; import org.bigbluebutton.modules.chat.model.ChatModel; import org.bigbluebutton.modules.chat.model.ChatOptions; @@ -110,7 +113,13 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. public var chatMessages: ChatConversation; private var lastCount:Number = 0; + private var historyTimer:Timer; private var scrollTimer:Timer; + private var typingSenderTimer:Timer; + private var typingIndicatorTimer:Timer; + private var typingIndicatorThreshold:int = 20 * 1000; // Invalidate typing messages after 20 seconds + private var typingEventSent:Boolean; + private var typingUsersInfo : Dictionary = new Dictionary(true); private var currentMessage:int; private var latestMessage:int; @@ -138,7 +147,14 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. addContextMenuItems(); scrollTimer = new Timer(750, 1); scrollTimer.addEventListener(TimerEvent.TIMER, onScrollTimer); - + + typingSenderTimer = new Timer(2000, 1); + typingSenderTimer.addEventListener(TimerEvent.TIMER, onTypingSenderTimer); + + typingIndicatorTimer = new Timer(2000, 1); + typingSenderTimer.addEventListener(TimerEvent.TIMER_COMPLETE, onTypingIndicatorTimer); + typingSenderTimer.start(); + // Initialize the indicator for the position in the message history, // and add the listener for message history navigation currentMessage = -1; @@ -238,22 +254,19 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. historyTimer = new Timer(5000, 1); historyTimer.addEventListener(TimerEvent.TIMER, loadChatHistory); historyTimer.start(); - } - private var historyTimer:Timer; - private function loadChatHistory(event:TimerEvent):void { displayChatHistory(); } - - private function displayChatHistory():void { - var chat: GroupChat = LiveMeeting.inst().chats.getGroupChat(chatId); - if (chat != null) { - chatMessages.processChatHistory(chat.messages.source); - scrollToEndOfMessage("no-scroll"); - } + + private function displayChatHistory():void { + var chat:GroupChat = LiveMeeting.inst().chats.getGroupChat(chatId); + if (chat != null) { + chatMessages.processChatHistory(chat.messages.source); + scrollToEndOfMessage("no-scroll"); } + } private function handleUserLeftEvent(event:UserLeftEvent):void { var gc: GroupChat = LiveMeeting.inst().chats.findChatWithUser(event.userID); @@ -261,13 +274,17 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. addMessageAndScrollToEnd(createUserHasLeftMessage(), event.userID); txtMsgArea.enabled = false; } + if (DictionaryUtils.containsKey(typingUsersInfo, event.userID)) { + delete typingUsersInfo[event.userID]; + } } - - private function refreshChat(e:BBBEvent):void { - if (e.payload.type == "BIGBLUEBUTTON_CONNECTION") { - displayChatHistory(); - } - } + + private function refreshChat(e:BBBEvent):void { + if (e.payload.type == "BIGBLUEBUTTON_CONNECTION") { + displayChatHistory(); + } + } + private function handleUserJoinedEvent(event:UserJoinedEvent):void { var gc: GroupChat = LiveMeeting.inst().chats.findChatWithUser(event.userID); @@ -307,14 +324,21 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. private function handlePublicChatMessageEvent(event:PublicChatMessageEvent):void { if (chatId == event.chatId && chatMessages != null) { + if (DictionaryUtils.containsKey(typingUsersInfo, event.msg.fromUserId)) { + delete typingUsersInfo[event.msg.fromUserId]; + if (typingIndicatorTimer.running) { + typingIndicatorTimer.stop(); + } + displayTypingUsers(); + } addMessageAndScrollToEnd(event.msg, event.msg.fromUserId); } } private function handleReceivedChatHistoryEvent(event:ChatHistoryEvent):void { if (chatId == event.chatId) { - displayChatHistory(); - scrollToEndOfMessage("no-scroll"); + displayChatHistory(); + scrollToEndOfMessage("no-scroll"); } } @@ -345,7 +369,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. /** * Trigger to force the scrollbar to show the last message. */ - // @todo : scromm if + // @todo : scroll if // 1 - I am the send of the last message // 2 - If the scroll bar is at the bottom most if (UsersUtil.isMe(userID) || precheckedVScroll || (chatMessagesList.verticalScrollAtMax)) { @@ -371,6 +395,14 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. private function onScrollTimer(event:TimerEvent):void { scrollToBottom(); } + + private function onTypingSenderTimer(event:TimerEvent):void { + typingEventSent = false; + } + + private function onTypingIndicatorTimer(event:TimerEvent):void { + displayTypingUsers(); + } public function setMessageUnread():void{ this.read = false; @@ -501,8 +533,10 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. private function handleTextInput(e:TextEvent):void { if ((e.text.length == 1) && (e.text.charCodeAt(0) == 10) /*ENTER-KEY*/ && messageCanBeSent) { sendMessages(); - e.preventDefault(); - } + e.preventDefault(); + } else { + sendUserTyping(); + } } private function handleMsgAreaKeyDown(e:KeyboardEvent):void { @@ -536,14 +570,19 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. txtMsgArea.validateNow(); txtMsgArea.setSelection(0, 0); } + + private function sendUserTyping():void { + if (!typingEventSent) { + typingEventSent = true; + typingSenderTimer.start(); + globalDispatcher.dispatchEvent(new UserTypingEvent(UserTypingEvent.USER_TYPING_EVENT, chatId, UsersUtil.getMyUserID())); + } + } private function showMessageTooLong(messageLength : int):void { - msgTooLongLabel.text = ResourceUtil.getInstance().getString('bbb.chat.chatMessage.tooLong', [(messageLength - chatOptions.maxMessageLength).toString()]); - msgTooLongLabel.includeInLayout = msgTooLongLabel.visible = true; - if (chatCtrlBar.height != 80) { - chatCtrlBar.height = 80; - chatListHeight -= 20; - } + msgTooLongLabel.toolTip = ResourceUtil.getInstance().getString('bbb.chat.chatMessage.tooLong', [(messageLength - chatOptions.maxMessageLength).toString()]); + msgTooLongLabel.text = "-" + (messageLength - chatOptions.maxMessageLength).toString(); + msgTooLongLabel.visible = true; // The case where lock settings are set if (txtMsgArea.enabled) { sendBtn.enabled = false; @@ -551,11 +590,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } private function hideMessageTooLong():void { - msgTooLongLabel.includeInLayout = msgTooLongLabel.visible = false; - if (chatCtrlBar.height != 60) { - chatCtrlBar.height = 60; - chatListHeight += 20; - } + msgTooLongLabel.visible = false; // The case where lock settings are set if (txtMsgArea.enabled) { sendBtn.enabled = true; @@ -571,7 +606,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } private function applyLockSettings():void { - if (publicChat) { + if (publicChat) { txtMsgArea.enabled = sendBtn.enabled = !LiveMeeting.inst().me.disableMyPublicChat; } else { txtMsgArea.enabled = sendBtn.enabled = !LiveMeeting.inst().me.disableMyPrivateChat || LiveMeeting.inst().users.getUser(chatWithUserID).role == Role.MODERATOR; @@ -609,6 +644,56 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. validateNow(); } } + + private function handleUserTyping(event:UserTypingEvent):void { + // If not the concerned chat, we jsut ignore it + if (event.chatId == chatId) { + if (!DictionaryUtils.containsKey(typingUsersInfo, event.userId)) { + typingUsersInfo[event.userId] = {userName: UsersUtil.getUserName(event.userId), lastUpdate : new Date().time} + } else { + typingUsersInfo[event.userId]["lastUpdate"] = new Date().time; + } + } + } + + private function displayTypingUsers():void { + var userIds : Array = DictionaryUtils.getKeys(typingUsersInfo); + if (userIds.length > 0) { + var usernames : Array = []; + for (var i : int = 0; i < userIds.length; i++) { + var userId : String = userIds[i]; + if (new Date().time < typingUsersInfo[userId]["lastUpdate"] + typingIndicatorThreshold) { + usernames.push(typingUsersInfo[userId]["userName"]); + } else { + delete typingUsersInfo[userId]; + } + } + if (usernames.length > 4) { + showTypingIndicator(ResourceUtil.getInstance().getString('bbb.chat.usersTyping.multiple')); + } else if (usernames.length > 1) { + showTypingIndicator(ResourceUtil.getInstance().getString('bbb.chat.usersTyping.many', [usernames.splice(usernames.length-1,1).join(", "), usernames.pop()])); + } else if (usernames.length == 1) { + showTypingIndicator(ResourceUtil.getInstance().getString('bbb.chat.usersTyping.one', [usernames[0]])); + } else { + hideTypingIndicator(); + } + } else { + hideTypingIndicator(); + } + typingSenderTimer.start(); + } + + private function showTypingIndicator(message:String) : void { + userTypingLabel.text = message; + userTypingLabel.visible = true; + chatMessagesList.setStyle("paddingBottom", userTypingLabel.height); + } + + private function hideTypingIndicator():void { + userTypingLabel.text = ""; + userTypingLabel.visible = false; + chatMessagesList.setStyle("paddingBottom", 0); + } override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void { super.updateDisplayList(unscaledWidth, unscaledHeight); @@ -618,7 +703,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. const paddingHeight:int = 5; const paddingWidth:int = 5; - } ]]> @@ -647,16 +731,21 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. </mx:Canvas> </mx:Canvas> </mx:VBox> - <mx:HBox id="chatCtrlBar" width="100%" height="60" styleName="chatControlBarStyle" verticalScrollPolicy="off" + <mx:HBox id="chatCtrlBar" width="100%" height="80" styleName="chatControlBarStyle" + verticalScrollPolicy="off" horizontalScrollPolicy="off" paddingLeft="5" paddingRight="5"> - <mx:VBox width="100%" height="100%"> - <!-- There is a restrict in this textArea to avoid a known issue where a \u007F, which is the delete character, would be seen written in some browsers as an invalid character --> - <mx:TextArea id="txtMsgArea" width="100%" height="100%" restrict="^\u007F" - styleName="chatControlBarTextMsgStyle" - change="txtMsgAreaChangeHandler(event)" - toolTip="{ResourceUtil.getInstance().getString('bbb.accessibility.chat.chatwindow.input')}" - accessibilityName="{ResourceUtil.getInstance().getString('bbb.chat.input.accessibilityName')}" /> - <mx:Label id="msgTooLongLabel" width="100%" height="100%" includeInLayout="false" visible="false"/> + <mx:VBox width="100%" height="100%" verticalScrollPolicy="off" horizontalScrollPolicy="off"> + <mx:Canvas width="100%" height="100%" verticalScrollPolicy="off" horizontalScrollPolicy="off"> + <!-- There is a restrict in this textArea to avoid a known issue where a \u007F, which is the delete character, would be seen written in some browsers as an invalid character --> + <mx:TextArea id="txtMsgArea" width="100%" height="100%" restrict="^\u007F" + styleName="chatControlBarTextMsgStyle" + change="txtMsgAreaChangeHandler(event)" + toolTip="{ResourceUtil.getInstance().getString('bbb.accessibility.chat.chatwindow.input')}" + accessibilityName="{ResourceUtil.getInstance().getString('bbb.chat.input.accessibilityName')}" /> + <mx:Label id="msgTooLongLabel" styleName="msgTooLongLabel" bottom="-2" right="8" visible="false"/> + </mx:Canvas> + <mx:Label id="userTypingLabel" styleName="userTypingLabel" truncateToFit="true" + visible="false" width="100%" /> </mx:VBox> <mx:VBox verticalScrollPolicy="off" verticalAlign="middle" height="100%" > <mx:HBox horizontalGap="0"> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/utils/WebRTCScreenshareUtility.as b/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/utils/WebRTCScreenshareUtility.as index 060ad4e8e6437c243e269ae084f20a94b96417e5..5b21d425ac0155135b5eab3969aa970e8111e720 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/utils/WebRTCScreenshareUtility.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/utils/WebRTCScreenshareUtility.as @@ -20,13 +20,14 @@ package org.bigbluebutton.modules.screenshare.utils { import flash.external.ExternalInterface; + import flash.system.Capabilities; import org.as3commons.lang.StringUtils; import org.as3commons.logging.api.ILogger; import org.as3commons.logging.api.getClassLogger; import org.bigbluebutton.core.Options; import org.bigbluebutton.modules.screenshare.model.ScreenshareOptions; - import org.bigbluebutton.util.browser.BrowserCheck; + import org.bigbluebutton.util.browser.BrowserCheck; public class WebRTCScreenshareUtility { private static const LOGGER:ILogger = getClassLogger(WebRTCScreenshareUtility); @@ -66,7 +67,11 @@ package org.bigbluebutton.modules.screenshare.utils // if its firefox go ahead and let webrtc handle it if (BrowserCheck.isFirefox()) { - webRTCWorksAndConfigured("Firefox, lets try"); + if (Capabilities.os.indexOf("Mac") >= 0) { + cannotUseWebRTC("Firefox on Mac performs poorly fallback to Java"); + } else { + webRTCWorksAndConfigured("Firefox, lets try"); + } return; // if its chrome we need to check for the extension diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/view/components/WebRTCDesktopViewWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/view/components/WebRTCDesktopViewWindow.mxml index 52df561d2c6b4330974cd36e0e53161dd4200f01..8577bea46f66c9c5ad6a75ded5fc9d6e562349be 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/view/components/WebRTCDesktopViewWindow.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/view/components/WebRTCDesktopViewWindow.mxml @@ -69,6 +69,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import org.bigbluebutton.modules.screenshare.model.ScreenshareOptions; import org.bigbluebutton.modules.screenshare.services.red5.WebRTCConnectionEvent; import org.bigbluebutton.util.ConnUtil; + import org.bigbluebutton.util.browser.BrowserCheck; import org.bigbluebutton.util.i18n.ResourceUtil; public static const LOG:String = "Deskshare::DesktopViewWindow - "; @@ -113,6 +114,10 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. videoHolder.addEventListener(MouseEvent.MOUSE_OUT, videoHolder_mouseOutHanlder); addEventListener(MDIWindowEvent.RESIZE_END, onResizeEndEvent); + if (BrowserCheck.isFirefox() && Capabilities.os.indexOf("Mac") >= 0) { + ffMacWarning.visible = ffMacWarning.includeInLayout = true; + } + resourcesChanged(); } @@ -475,5 +480,16 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. mouseOver="btnActualSize.alpha = 1" label="{ btnActualSize.selected ? ResourceUtil.getInstance().getString('bbb.screenshareView.fitToWindow') : ResourceUtil.getInstance().getString('bbb.screenshareView.actualSize') }" toolTip="{ btnActualSize.selected ? ResourceUtil.getInstance().getString('bbb.screenshareView.fitToWindow') : ResourceUtil.getInstance().getString('bbb.screenshareView.actualSize') }"/> - + <mx:Box id="ffMacWarning" + visible="false" + includeInLayout="false" + width="260" + styleName="lockSettingsHintBoxStyle" + horizontalCenter="0" + top="{VIDEO_HEIGHT_PADDING*2+btnActualSize.height}"> + <mx:Text width="100%" + textAlign="center" + styleName="lockSettingHintTextStyle" + text="{ResourceUtil.getInstance().getString('bbb.screenshareView.warning.ffMacWebRTC')}" /> + </mx:Box> </CustomMdiWindow> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/MediaItemRenderer.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/MediaItemRenderer.mxml index 0ef86e0461adc6bf20acffb891f590c13d444bf5..2fce9888848f7e9934b866a2433f467f2657f183 100644 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/MediaItemRenderer.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/MediaItemRenderer.mxml @@ -93,8 +93,16 @@ BindingUtils.bindSetter(updateButtons, viewingStreamInd, "visible"); } + override public function set data(value:Object):void { + //reset rolledOver when the data changes because onRollOut wont be called if the row moves + if (data == null || value == null || data.userId != value.userId) { + rolledOver = false; + } + + super.data = value; + } + private function dataChangeHandler(e:Event):void { - //rest rolledOver when the data changes because onRollOut wont be called if the row moves if (data != null) { updateButtons(); validateNow(); diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/StatusItemRenderer.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/StatusItemRenderer.mxml index 95f897cf8b9f266aaf37c392f7bcce75ef44a49f..9231c052041dd3cbd189350b90097f5c0d1f0e0d 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/StatusItemRenderer.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/StatusItemRenderer.mxml @@ -84,6 +84,15 @@ validateNow(); } + override public function set data(value:Object):void { + //reset rolledOver when the data changes because onRollOut wont be called if the row moves + if (data == null || value == null || data.userId != value.userId) { + rolledOver = false; + } + + super.data = value; + } + private function dataChangeHandler(e:Event):void { if (data != null) { updateButtons(); //reassess data state on change diff --git a/bigbluebutton-config/bigbluebutton-release b/bigbluebutton-config/bigbluebutton-release index beffee177ee32c109e5ff426c38b0f2c2a0524d7..3e3fed860f984d91395ad78a951be478a636713a 100644 --- a/bigbluebutton-config/bigbluebutton-release +++ b/bigbluebutton-config/bigbluebutton-release @@ -1 +1,2 @@ BIGBLUEBUTTON_RELEASE=2.2.0-dev + diff --git a/bigbluebutton-config/bin/bbb-conf b/bigbluebutton-config/bin/bbb-conf index 7ecdf03e24698590f5cd9c74dc07967032dcab83..e3a624968a5ac1802abffbc503368e2f8dd884ec 100755 --- a/bigbluebutton-config/bin/bbb-conf +++ b/bigbluebutton-config/bin/bbb-conf @@ -79,6 +79,8 @@ FREESWITCH_EXTERNAL=/opt/freeswitch/conf/sip_profiles/external.xml FREESWITCH_PID=/opt/freeswitch/run/freeswitch.pid FREESWITCH_EVENT_SOCKET=/opt/freeswitch/conf/autoload_configs/event_socket.conf.xml +HTML5_CONFIG=/usr/share/meteor/bundle/programs/server/assets/app/config/settings-production.json + if [ -f /etc/redhat-release ]; then DISTRIB_ID=centos SERVLET_LOGS=/usr/share/tomcat/logs @@ -944,15 +946,21 @@ check_configuration() { echo "# $FREESWITCH_EXTERNAL" echo fi - if grep ws-binding $FREESWITCH_EXTERNAL > /dev/null; then - echo "# Warning: You have configured SSL, but FreeSWITCH is still listening" - echo "# on port 5066. You should edit " - echo "#" - echo "# $FREESWITCH_EXTERNAL" - echo "#" - echo "# and remove the line that contains ws-binding." - echo - fi + + if [ -f $HTML5_CONFIG ]; then + if grep \"enableListenOnly\".*true $HTML5_CONFIG > /dev/null; then + if ! grep -q ws-binding $FREESWITCH_EXTERNAL ; then + echo "# Warning: You have enabled listen-only audio via Kurento" + echo "# but FreeSWITCH is not listening on port 5066. You should edit " + echo "#" + echo "# $FREESWITCH_EXTERNAL" + echo "#" + echo "# and add a line that enables ws-binding on port 5066." + echo + fi + fi + fi + fi if [ $DISTRIB_ID != "centos" ]; then @@ -1787,23 +1795,24 @@ if [ -n "$HOST" ]; then echo "Assigning $HOST for servername in /etc/nginx/sites-available/bigbluebutton" sudo sed -i "s/server_name .*/server_name $HOST;/g" /etc/nginx/sites-available/bigbluebutton - # - # Update configuration for BigBlueButton client (and preserve hostname for chromeExtensionLink if exists) - # + # + # Update configuration for BigBlueButton client (and preserve hostname for chromeExtensionLink if exists) + # - echo "Assigning $HOST for http[s]:// in /var/www/bigbluebutton/client/conf/config.xml" - sudo sed -i "s/http[s]*:\/\/\([^\"\/]*\)\([\"\/]\)/$PROTOCOL_HTTP:\/\/$HOST\2/g" \ - /var/www/bigbluebutton/client/conf/config.xml + # Extract the chrome store URL before updating config.xml. We will be able to restore it. chromeExtensionLinkURL=$(cat /var/www/bigbluebutton/client/conf/config.xml | sed -n '/chromeExtensionLink/{s/.*https*:\/\///;s/\/.*//;p}') + + echo "Assigning $HOST for http[s]:// in /var/www/bigbluebutton/client/conf/config.xml" + sudo sed -i "s/http[s]*:\/\/\([^\"\/]*\)\([\"\/]\)/$PROTOCOL_HTTP:\/\/$HOST\2/g" \ + /var/www/bigbluebutton/client/conf/config.xml - if ! echo "$chromeExtensionLinkURL" | grep -q '""'; then - sudo sed -i "s/chromeExtensionLink=\"https:\/\/[^\/]*/chromeExtensionLink=\"https:\/\/$chromeExtensionLinkURL/g" \ - /var/www/bigbluebutton/client/conf/config.xml - fi - + if ! echo "$chromeExtensionLinkURL" | grep -q '""'; then + sudo sed -i "s/chromeExtensionLink=\"https:\/\/[^\/]*/chromeExtensionLink=\"https:\/\/$chromeExtensionLinkURL/g" \ + /var/www/bigbluebutton/client/conf/config.xml + fi - echo "Assigning $HOST for publishURI in /var/www/bigbluebutton/client/conf/config.xml" - sudo sed -i "s/publishURI=\"[^\"]*\"/publishURI=\"$HOST\"/" /var/www/bigbluebutton/client/conf/config.xml + echo "Assigning $HOST for publishURI in /var/www/bigbluebutton/client/conf/config.xml" + sudo sed -i "s/publishURI=\"[^\"]*\"/publishURI=\"$HOST\"/" /var/www/bigbluebutton/client/conf/config.xml # # Update configuration for BigBlueButton web app @@ -1893,10 +1902,9 @@ if [ -n "$HOST" ]; then # # Update HTML5 client # - if [ -f /usr/share/meteor/bundle/programs/server/assets/app/config/settings-production.json ]; then + if [ -f $HTML5_CONFIG ]; then WS=$(cat $SERVLET_DIR/bigbluebutton/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | sed -n '/^bigbluebutton.web.serverURL/{s/.*=//;p}' | sed 's/https/wss/g' | sed s'/http/ws/g') - sed -i "s|\"wsUrl.*|\"wsUrl\": \"$WS/bbb-webrtc-sfu\",|g" \ - /usr/share/meteor/bundle/programs/server/assets/app/config/settings-production.json + sed -i "s|\"wsUrl.*|\"wsUrl\": \"$WS/bbb-webrtc-sfu\",|g" $HTML5_CONFIG if [ -f /usr/local/bigbluebutton/bbb-webrtc-sfu/config/default.yml ]; then change_yml_value /usr/local/bigbluebutton/bbb-webrtc-sfu/config/default.yml kurentoUrl "ws://$HOST:8888/kurento" diff --git a/bigbluebutton-config/cron.daily/bigbluebutton b/bigbluebutton-config/cron.daily/bigbluebutton index efbe26faf67294b4616571e6873172f0d23a33d5..9e9c57ae9f3cd0a65c6ba77f85b0fbfbb1c9acbd 100755 --- a/bigbluebutton-config/cron.daily/bigbluebutton +++ b/bigbluebutton-config/cron.daily/bigbluebutton @@ -50,7 +50,7 @@ done # # Delete streams in kurento older than N days # -for app in recording screenshare; do +for app in recordings screenshare; do app_dir=/var/kurento/$app if [[ -d $app_dir ]]; then find $app_dir -name "*.mkv" -mtime +$history -delete diff --git a/bigbluebutton-html5/.dockerignore b/bigbluebutton-html5/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..bf9b07fdc25d27e22a5052d1d5801bf882fb2f88 --- /dev/null +++ b/bigbluebutton-html5/.dockerignore @@ -0,0 +1,2 @@ +Dockerfile +Dockerfile.dev diff --git a/bigbluebutton-html5/Dockerfile b/bigbluebutton-html5/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..8e4b24cb172a218115b648bd67c6002ab5ed796f --- /dev/null +++ b/bigbluebutton-html5/Dockerfile @@ -0,0 +1,38 @@ +FROM node:8 + +RUN set -x \ + && curl -sL https://install.meteor.com | sed s/--progress-bar/-sL/g | /bin/sh \ + && useradd -m -G users -s /bin/bash meteor + +RUN apt-get update && apt-get -y install jq + +COPY . /source + +RUN cd /source \ + && mv docker-entrypoint.sh /usr/local/bin/ \ + && chown -R meteor:meteor . \ + && mkdir /app \ + && chown -R meteor:meteor /app + +USER meteor + +RUN cd /source \ + && meteor npm install \ + && meteor build --directory /app + +ENV NODE_ENV production + +RUN cd /app/bundle/programs/server \ + && npm install \ + && npm cache clear --force + +WORKDIR /app/bundle + +ENV MONGO_URL=mongodb://mongo:27017/html5client \ + PORT=3000 \ + ROOT_URL=http://localhost:3000 \ + METEOR_SETTINGS_MODIFIER=. + +EXPOSE 3000 + +CMD ["docker-entrypoint.sh"] diff --git a/bigbluebutton-html5/Dockerfile.dev b/bigbluebutton-html5/Dockerfile.dev new file mode 100644 index 0000000000000000000000000000000000000000..692c200be719aca865919535210674ff871c76ee --- /dev/null +++ b/bigbluebutton-html5/Dockerfile.dev @@ -0,0 +1,24 @@ +FROM node:8 + +COPY . /source + +RUN set -x \ + && curl -sL https://install.meteor.com | sed s/--progress-bar/-sL/g | /bin/sh \ + && useradd -m -G users -s /bin/bash meteor \ + && chown -R meteor:meteor /source + +USER meteor + +RUN cd /source \ + && meteor npm install + +WORKDIR /source + +ENV MONGO_URL=mongodb://mongo:27017/html5client \ + PORT=3000 \ + ROOT_URL=http://localhost:3000 + +EXPOSE 3000 + +CMD ["npm", "start"] + diff --git a/bigbluebutton-html5/client/compatibility/kurento-extension.js b/bigbluebutton-html5/client/compatibility/kurento-extension.js index c55b99e2d164bf3c3c0fe24562825709c9a52674..4ca52d78a9b90bdf298d19a5b835d763f922e72e 100644 --- a/bigbluebutton-html5/client/compatibility/kurento-extension.js +++ b/bigbluebutton-html5/client/compatibility/kurento-extension.js @@ -13,7 +13,6 @@ Kurento = function ( onSuccess, options = {}, ) { - this.ws = null; this.video = null; this.screen = null; @@ -30,10 +29,10 @@ Kurento = function ( Object.assign(this, options); - this.SEND_ROLE = "send"; - this.RECV_ROLE = "recv"; - this.SFU_APP = "screenshare"; - this.ON_ICE_CANDIDATE_MSG = "iceCandidate"; + this.SEND_ROLE = 'send'; + this.RECV_ROLE = 'recv'; + this.SFU_APP = 'screenshare'; + this.ON_ICE_CANDIDATE_MSG = 'iceCandidate'; this.PING_INTERVAL = 15000; window.Logger = this.logger || console; @@ -45,11 +44,11 @@ Kurento = function ( } if (this.chromeScreenshareSources == null) { - this.chromeScreenshareSources = ["screen", "window"]; + this.chromeScreenshareSources = ['screen', 'window']; } if (this.firefoxScreenshareSource == null) { - this.firefoxScreenshareSource = "window"; + this.firefoxScreenshareSource = 'window'; } // Limiting max resolution to WQXGA @@ -78,7 +77,7 @@ Kurento = function ( if (onSuccess != null) { this.onSuccess = Kurento.normalizeCallback(onSuccess); } else { - var _this = this; + const _this = this; this.onSuccess = function () { _this.logSuccess('Default success handler'); }; @@ -93,7 +92,6 @@ this.KurentoManager = function () { KurentoManager.prototype.exitScreenShare = function () { if (typeof this.kurentoScreenshare !== 'undefined' && this.kurentoScreenshare) { - if (this.kurentoScreenshare.logger !== null) { this.kurentoScreenshare.logger.info(' [exitScreenShare] Exiting screensharing'); } @@ -119,6 +117,10 @@ KurentoManager.prototype.exitScreenShare = function () { KurentoManager.prototype.exitVideo = function () { if (typeof this.kurentoVideo !== 'undefined' && this.kurentoVideo) { + if(this.kurentoVideo.webRtcPeer) { + this.kurentoVideo.webRtcPeer.peerConnection.oniceconnectionstatechange = null; + } + if (this.kurentoVideo.logger !== null) { this.kurentoVideo.logger.info(' [exitScreenShare] Exiting screensharing viewing'); } @@ -139,7 +141,6 @@ KurentoManager.prototype.exitVideo = function () { KurentoManager.prototype.exitAudio = function () { if (typeof this.kurentoAudio !== 'undefined' && this.kurentoAudio) { - if (this.kurentoAudio.logger !== null) { this.kurentoAudio.logger.info(' [exitAudio] Exiting listen only audio'); } @@ -177,15 +178,15 @@ KurentoManager.prototype.joinWatchVideo = function (tag) { KurentoManager.prototype.getFirefoxScreenshareSource = function () { return this.kurentoScreenshare.firefoxScreenshareSource; -} +}; KurentoManager.prototype.getChromeScreenshareSources = function () { return this.kurentoScreenshare.chromeScreenshareSources; -} +}; KurentoManager.prototype.getChromeExtensionKey = function () { return this.kurentoScreenshare.chromeExtension; -} +}; Kurento.prototype.setScreensharing = function (tag) { @@ -201,13 +202,13 @@ Kurento.prototype.create = function (tag) { Kurento.prototype.downscaleResolution = function (oldWidth, oldHeight) { const factorWidth = this.vid_max_width / oldWidth; const factorHeight = this.vid_max_height / oldHeight; - let width, height; + let width, + height; if (factorWidth < factorHeight) { width = Math.trunc(oldWidth * factorWidth); height = Math.trunc(oldHeight * factorWidth); - } - else { + } else { width = Math.trunc(oldWidth * factorHeight); height = Math.trunc(oldHeight * factorHeight); } @@ -253,7 +254,7 @@ Kurento.prototype.onWSMessage = function (message) { this.onSuccess(parsedMessage.success); break; case 'webRTCAudioError': - this.onFail(parsedMessage.error); + this.onFail(parsedMessage); break; case 'pong': break; @@ -269,13 +270,12 @@ Kurento.prototype.setRenderTag = function (tag) { Kurento.prototype.startResponse = function (message) { if (message.response !== 'accepted') { const errorMsg = message.message ? message.message : 'Unknow error'; - this.logger.warn("Call not accepted for the following reason:", {error: errorMsg}); + this.logger.warn('Call not accepted for the following reason:', { error: errorMsg }); switch (message.type) { case 'screenshare': if (message.role === this.SEND_ROLE) { kurentoManager.exitScreenShare(); - } - else if (message.role === this.RECV_ROLE) { + } else if (message.role === this.RECV_ROLE) { kurentoManager.exitVideo(); } break; @@ -284,7 +284,7 @@ Kurento.prototype.startResponse = function (message) { break; } } else { - this.logger.debug(`Procedure for ${message.type} was accepted with SDP =>`, {sdpAnswer: message.sdpAnswer}); + this.logger.debug(`Procedure for ${message.type} was accepted with SDP =>`, { sdpAnswer: message.sdpAnswer }); this.webRtcPeer.processAnswer(message.sdpAnswer); } }; @@ -310,7 +310,7 @@ Kurento.prototype.onOfferPresenter = function (error, offerSdp) { vw: this.width, }; - this.logger.info("onOfferPresenter sending to screenshare server => ", { sdpOffer: message }); + this.logger.info('onOfferPresenter sending to screenshare server => ', { sdpOffer: message }); this.sendMessage(message); }; @@ -335,7 +335,7 @@ Kurento.prototype.startScreensharing = function () { sendSource: 'desktop', }; - this.logger.info(" Peer options =>", options); + this.logger.info(' Peer options =>', options); let resolution; this.logger.debug(`Screenshare screen dimensions are ${this.width} x ${this.height}`); @@ -343,23 +343,25 @@ Kurento.prototype.startScreensharing = function () { resolution = this.downscaleResolution(this.width, this.height); this.width = resolution.width; this.height = resolution.height; - this.logger.debug("Screenshare track dimensions have been resized to", this.width, "x", this.height); + this.logger.debug('Screenshare track dimensions have been resized to', this.width, 'x', this.height); } this.addIceServers(this.iceServers, options); this.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(options, (error) => { if (error) { - this.logger.error("WebRtcPeerSendonly constructor error:", {error}); + this.logger.error('WebRtcPeerSendonly constructor error:', { error }); this.onFail(error); return kurentoManager.exitScreenShare(); } this.webRtcPeer.generateOffer(this.onOfferPresenter.bind(this)); - this.logger.info("Generated peer offer w/ options:", {options: options}); + this.logger.info('Generated peer offer w/ options:', { options }); const localStream = this.webRtcPeer.peerConnection.getLocalStreams()[0]; + const _this = this; localStream.getVideoTracks()[0].onended = function () { + _this.webRtcPeer.peerConnection.oniceconnectionstatechange = null; return kurentoManager.exitScreenShare(); }; @@ -367,15 +369,24 @@ Kurento.prototype.startScreensharing = function () { return kurentoManager.exitScreenShare(); }; }); + this.webRtcPeer.peerConnection.oniceconnectionstatechange = () => { + if (this.webRtcPeer) { + const connectionState = this.webRtcPeer.peerConnection.iceConnectionState; + if (connectionState === 'failed' || connectionState === 'closed') { + this.webRtcPeer.peerConnection.oniceconnectionstatechange = null; + this.onFail('ICE connection failed'); + } + } + }; }; Kurento.prototype.onIceCandidate = function (candidate, role) { const self = this; - this.logger.info("Local candidate:", {candidate}); + this.logger.info('Local candidate:', { candidate }); const message = { id: this.ON_ICE_CANDIDATE_MSG, - role: role, + role, type: this.SFU_APP, voiceBridge: self.voiceBridge, candidate, @@ -398,13 +409,13 @@ Kurento.prototype.viewer = function () { if (!this.webRtcPeer) { const options = { mediaConstraints: { - audio: false + audio: false, }, remoteVideo: document.getElementById(this.renderTag), onicecandidate: (candidate) => { this.onIceCandidate(candidate, this.RECV_ROLE); - } - } + }, + }; this.addIceServers(this.iceServers, options); @@ -415,6 +426,15 @@ Kurento.prototype.viewer = function () { this.generateOffer(self.onOfferViewer.bind(self)); }); + self.webRtcPeer.peerConnection.oniceconnectionstatechange = () => { + if (this.webRtcPeer) { + const connectionState = this.webRtcPeer.peerConnection.iceConnectionState; + if (connectionState === 'failed' || connectionState === 'closed') { + this.webRtcPeer.peerConnection.oniceconnectionstatechange = null; + this.onFail('ICE connection failed'); + } + } + }; } }; @@ -434,15 +454,15 @@ Kurento.prototype.onOfferViewer = function (error, offerSdp) { sdpOffer: offerSdp, }; - this.logger.info("onOfferViewer sending to screenshare server: ", {sdpOffer: message.sdpOffer}); + this.logger.info('onOfferViewer sending to screenshare server: ', { sdpOffer: message.sdpOffer }); this.sendMessage(message); }; KurentoManager.prototype.joinAudio = function (tag) { this.exitAudio(); - var obj = Object.create(Kurento.prototype); + const obj = Object.create(Kurento.prototype); Kurento.apply(obj, arguments); - this.kurentoAudio= obj; + this.kurentoAudio = obj; this.kurentoAudio.setAudio(tag); }; @@ -453,20 +473,23 @@ Kurento.prototype.setAudio = function (tag) { Kurento.prototype.listenOnly = function () { var self = this; + const remoteVideo = document.getElementById(this.renderTag); + remoteVideo.muted = true; if (!this.webRtcPeer) { var options = { - remoteVideo: document.getElementById(this.renderTag), + audioStream: this.inputStream, + remoteVideo, onicecandidate : this.onListenOnlyIceCandidate.bind(this), mediaConstraints: { - audio:true, - video:false - } - } + audio: true, + video: false, + }, + }; this.addIceServers(this.iceServers, options); - self.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options, function(error) { - if(error) { + self.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options, function (error) { + if (error) { return self.onFail(PEER_ERROR); } @@ -476,39 +499,39 @@ Kurento.prototype.listenOnly = function () { }; Kurento.prototype.onListenOnlyIceCandidate = function (candidate) { - let self = this; - this.logger.debug("[onListenOnlyIceCandidate]", {candidate}); + const self = this; + this.logger.debug('[onListenOnlyIceCandidate]', { candidate }); - var message = { - id : this.ON_ICE_CANDIDATE_MSG, + const message = { + id: this.ON_ICE_CANDIDATE_MSG, type: 'audio', role: 'viewer', voiceBridge: self.voiceBridge, - candidate : candidate, - } + candidate, + }; this.sendMessage(message); }; Kurento.prototype.onOfferListenOnly = function (error, offerSdp) { - let self = this; - if(error) { - this.logger.error("[onOfferListenOnly]", error); + const self = this; + if (error) { + this.logger.error('[onOfferListenOnly]', error); return this.onFail(SDP_ERROR); } - let message = { - id : 'start', + const message = { + id: 'start', type: 'audio', role: 'viewer', voiceBridge: self.voiceBridge, caleeName: self.caleeName, - sdpOffer : offerSdp, + sdpOffer: offerSdp, userId: self.userId, userName: self.userName, - internalMeetingId: self.internalMeetingId + internalMeetingId: self.internalMeetingId, }; - this.logger.debug("[onOfferListenOnly]", {message}); + this.logger.debug('[onOfferListenOnly]', { message }); this.sendMessage(message); }; @@ -519,7 +542,7 @@ Kurento.prototype.pauseTrack = function (message) { if (track) { track.enabled = false; } -} +}; Kurento.prototype.resumeTrack = function (message) { const localStream = this.webRtcPeer.peerConnection.getLocalStreams()[0]; @@ -528,10 +551,10 @@ Kurento.prototype.resumeTrack = function (message) { if (track) { track.enabled = true; } -} +}; Kurento.prototype.addIceServers = function (iceServers, options) { - this.logger.debug("Adding iceServers", iceServers); + this.logger.debug('Adding iceServers', iceServers); if (iceServers && iceServers.length > 0) { options.configuration = {}; options.configuration.iceServers = iceServers; @@ -559,14 +582,14 @@ Kurento.prototype.dispose = function () { Kurento.prototype.ping = function () { const message = { - id: 'ping' + id: 'ping', }; this.sendMessage(message); -} +}; Kurento.prototype.sendMessage = function (message) { const jsonMessage = JSON.stringify(message); - this.logger.info("Sending message:", {message}); + this.logger.info('Sending message:', { message }); this.ws.send(jsonMessage); }; @@ -635,12 +658,12 @@ window.getScreenConstraints = function (sendSource, callback) { { googCpuOveruseDetection: true }, { googCpuOveruseEncodeUsage: true }, { googCpuUnderuseThreshold: 55 }, - { googCpuOveruseThreshold: 100}, + { googCpuOveruseThreshold: 100 }, { googPayloadPadding: true }, { googScreencastMinBitrate: 600 }, { googHighStartBitrate: true }, { googHighBitrate: true }, - { googVeryHighBitrate: true } + { googVeryHighBitrate: true }, ]; console.log('getScreenConstraints for Chrome returns => ', screenConstraints); @@ -659,7 +682,6 @@ window.getScreenConstraints = function (sendSource, callback) { if (isSafari) { // At this time (version 11.1), Safari doesn't support screenshare. document.dispatchEvent(new Event('safariScreenshareNotSupported')); - return; } }; @@ -736,4 +758,4 @@ window.checkChromeExtInstalled = function (callback, chromeExtensionId) { callback(true); } ); -}*/ +} */ diff --git a/bigbluebutton-html5/client/compatibility/kurento-utils.js b/bigbluebutton-html5/client/compatibility/kurento-utils.js index a5494ed6588973ba5bca3c1e10f2f43311951c7d..e3b8a003d39e81798390c59f75bbf22d5a46d8a0 100644 --- a/bigbluebutton-html5/client/compatibility/kurento-utils.js +++ b/bigbluebutton-html5/client/compatibility/kurento-utils.js @@ -287,13 +287,30 @@ function WebRtcPeer(mode, options, callback) { return pc.remoteDescription; }; function setRemoteVideo() { - if (remoteVideo) { - var stream = pc.getRemoteStreams()[0]; - remoteVideo.pause(); - remoteVideo.srcObject = stream; - remoteVideo.load(); - logger.info('Remote URL:', remoteVideo.srcObject); + if (remoteVideo) { + // TODO review the retry - prlanzarin 08/18 + let played = false; + const MAX_RETRIES = 5; + let attempt = 0; + const playVideo = () => { + if (!played && attempt < MAX_RETRIES) { + remoteVideo.play().catch(e => { + attempt++; + playVideo(remoteVideo); + }).then(() => { remoteVideo.muted = false; played = true; attempt = 0;}); + } } + var stream = pc.getRemoteStreams()[0]; + + remoteVideo.oncanplaythrough = function() { + playVideo(); + }; + + remoteVideo.pause(); + remoteVideo.srcObject = stream; + remoteVideo.load(); + logger.info('Remote URL:', remoteVideo.srcObject); + } } this.showLocalVideo = function () { localVideo.srcObject = videoStream; @@ -1045,7 +1062,7 @@ module.exports = function(stream, options) { harker.setInterval = function(i) { interval = i; }; - + harker.stop = function() { running = false; harker.emit('volume_change', -100, threshold); @@ -1063,12 +1080,12 @@ module.exports = function(stream, options) { // and emit events if changed var looper = function() { setTimeout(function() { - + //check if stop has been called if(!running) { return; } - + var currentVolume = getMaxVolume(analyser, fftBins); harker.emit('volume_change', currentVolume, threshold); @@ -4359,4 +4376,4 @@ WildEmitter.mixin = function (constructor) { WildEmitter.mixin(WildEmitter); },{}]},{},[2])(2) -}); \ No newline at end of file +}); diff --git a/bigbluebutton-html5/client/stylesheets/bbb-icons.css b/bigbluebutton-html5/client/stylesheets/bbb-icons.css index 853b0f017599c3c97acdf6c5f6a9289eb6ce87e7..8e934fa833f77188fa6ac14e206d2534e3d8692d 100755 --- a/bigbluebutton-html5/client/stylesheets/bbb-icons.css +++ b/bigbluebutton-html5/client/stylesheets/bbb-icons.css @@ -89,6 +89,9 @@ .icon-bbb-desktop:before { content: "\e928"; } +.icon-bbb-desktop_off:before { + content: "\e953"; +} .icon-bbb-logout:before { content: "\e900"; } @@ -274,3 +277,6 @@ .icon-bbb-star_filled:before { content: "\e952"; } +.icon-bbb-minus:before { + content: "\e954"; +} diff --git a/bigbluebutton-html5/docker-entrypoint.sh b/bigbluebutton-html5/docker-entrypoint.sh new file mode 100755 index 0000000000000000000000000000000000000000..951269c88166e2c753ad2996e39c5711d0685d2f --- /dev/null +++ b/bigbluebutton-html5/docker-entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/bash -e + +export METEOR_SETTINGS=` jq "${METEOR_SETTINGS_MODIFIER}" ./programs/server/assets/app/config/settings-production.json ` + +node main.js diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/kurento.js b/bigbluebutton-html5/imports/api/audio/client/bridge/kurento.js index 52b7d55cb5f054cfa9ec8cf7db25ea89985dbc82..868cd319a5afbc0261f5bb23d1647cbb56eeff71 100644 --- a/bigbluebutton-html5/imports/api/audio/client/bridge/kurento.js +++ b/bigbluebutton-html5/imports/api/audio/client/bridge/kurento.js @@ -53,6 +53,11 @@ export default class KurentoAudioBridge extends BaseAudioBridge { sessionToken }; + this.media = { + inputDevice: {}, + }; + + this.internalMeetingID = meetingId; this.voiceBridge = voiceBridge; } @@ -61,7 +66,7 @@ export default class KurentoAudioBridge extends BaseAudioBridge { window.kurentoExitAudio(); } - joinAudio({ isListenOnly }, callback) { + joinAudio({ isListenOnly, inputStream }, callback) { return new Promise(async (resolve, reject) => { this.callback = callback; let iceServers = []; @@ -78,15 +83,21 @@ export default class KurentoAudioBridge extends BaseAudioBridge { caleeName: `${GLOBAL_AUDIO_PREFIX}${this.voiceBridge}`, iceServers, logger: modLogger, + inputStream, }; const onSuccess = ack => resolve(this.callback({ status: this.baseCallStates.started })); - const onFail = error => resolve(this.callback({ - status: this.baseCallStates.failed, - error: this.baseErrorCodes.CONNECTION_ERROR, - bridgeError: error, - })); + const onFail = error => { + const { reason } = error; + this.callback({ + status: this.baseCallStates.failed, + error: this.baseErrorCodes.CONNECTION_ERROR, + bridgeError: reason, + }) + + reject(reason); + }; if (!isListenOnly) { return reject("Invalid bridge option"); @@ -105,6 +116,22 @@ export default class KurentoAudioBridge extends BaseAudioBridge { }); } + async changeOutputDevice(value) { + const audioContext = document.querySelector('#'+MEDIA_TAG); + if (audioContext.setSinkId) { + try { + await audioContext.setSinkId(value); + this.media.outputDeviceId = value; + } catch (err) { + logger.error(err); + throw new Error(this.baseErrorCodes.MEDIA_ERROR); + } + } + + return this.media.outputDeviceId || value; + } + + exitAudio() { return new Promise((resolve, reject) => { window.kurentoExitAudio(); diff --git a/bigbluebutton-html5/imports/api/chat/index.js b/bigbluebutton-html5/imports/api/chat/index.js deleted file mode 100644 index a28361d05c70d390b43ae25f3e904ef8e09c241d..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/api/chat/index.js +++ /dev/null @@ -1,19 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -const Chat = new Mongo.Collection('chat'); - -if (Meteor.isServer) { - // types of queries for the chat: - // 1. meetingId, toUsername (publishers) - // 2. meetingId, fromUserId (publishers) - // 3. meetingId, toUserId (publishers) - // 4. meetingId, fromTime, fromUserId, toUserId (addChat) - // 5. meetingId (clearChat) - // 6. meetingId, fromUserId, toUserId (clearSystemMessages) - - Chat._ensureIndex({ meetingId: 1, toUsername: 1 }); - Chat._ensureIndex({ meetingId: 1, fromUserId: 1 }); - Chat._ensureIndex({ meetingId: 1, toUserId: 1 }); -} - -export default Chat; diff --git a/bigbluebutton-html5/imports/api/chat/server/eventHandlers.js b/bigbluebutton-html5/imports/api/chat/server/eventHandlers.js deleted file mode 100755 index 342ec899aeeb62c47d45a77f0038ac08dea7696c..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/api/chat/server/eventHandlers.js +++ /dev/null @@ -1,10 +0,0 @@ -import RedisPubSub from '/imports/startup/server/redis'; -import { processForHTML5ServerOnly } from '/imports/api/common/server/helpers'; -import handleChatMessage from './handlers/chatMessage'; -import handleChatHistory from './handlers/chatHistory'; -import handleChatPublicHistoryClear from './handlers/chatPublicHistoryClear'; - -RedisPubSub.on('GetChatHistoryRespMsg', processForHTML5ServerOnly(handleChatHistory)); -RedisPubSub.on('SendPublicMessageEvtMsg', handleChatMessage); -RedisPubSub.on('SendPrivateMessageEvtMsg', processForHTML5ServerOnly(handleChatMessage)); -RedisPubSub.on('ClearPublicChatHistoryEvtMsg', handleChatPublicHistoryClear); diff --git a/bigbluebutton-html5/imports/api/chat/server/handlers/chatHistory.js b/bigbluebutton-html5/imports/api/chat/server/handlers/chatHistory.js deleted file mode 100644 index 40293145a550463438d868f4b49047f9a551934e..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/api/chat/server/handlers/chatHistory.js +++ /dev/null @@ -1,17 +0,0 @@ -import { check } from 'meteor/check'; -import addChat from '../modifiers/addChat'; - -export default function handleChatHistory({ body }, meetingId) { - const { history } = body; - - check(meetingId, String); - check(history, Array); - - const chatsAdded = []; - - history.forEach((message) => { - chatsAdded.push(addChat(meetingId, message)); - }); - - return chatsAdded; -} diff --git a/bigbluebutton-html5/imports/api/chat/server/handlers/chatMessage.js b/bigbluebutton-html5/imports/api/chat/server/handlers/chatMessage.js deleted file mode 100644 index 4ee789871cce0ef06cb75b9e0bf8fd4e9f00db14..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/api/chat/server/handlers/chatMessage.js +++ /dev/null @@ -1,11 +0,0 @@ -import { check } from 'meteor/check'; -import addChat from '../modifiers/addChat'; - -export default function handleChatMessage({ body }, meetingId) { - const { message } = body; - - check(meetingId, String); - check(message, Object); - - return addChat(meetingId, message); -} diff --git a/bigbluebutton-html5/imports/api/chat/server/handlers/chatPublicHistoryClear.js b/bigbluebutton-html5/imports/api/chat/server/handlers/chatPublicHistoryClear.js deleted file mode 100644 index 3f29aab4c1931619204c3d2b3015fbc0c6ff06b6..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/api/chat/server/handlers/chatPublicHistoryClear.js +++ /dev/null @@ -1,30 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import Chat from '/imports/api/chat'; -import Logger from '/imports/startup/server/logger'; -import addChat from './../modifiers/addChat'; - -export default function publicHistoryClear({ header }, meetingId) { - const CHAT_CONFIG = Meteor.settings.public.chat; - const PUBLIC_CHAT_USERID = CHAT_CONFIG.public_userid; - const PUBLIC_CHAT_USERNAME = CHAT_CONFIG.public_username; - const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system; - - if (meetingId) { - Chat.remove( - { meetingId, toUserId: PUBLIC_CHAT_USERID }, - Logger.info(`Cleared Chats (${meetingId})`), - ); - - addChat(meetingId, { - message: CHAT_CONFIG.system_messages_keys.chat_clear, - fromTime: new Date().getTime(), - toUserId: PUBLIC_CHAT_USERID, - toUsername: PUBLIC_CHAT_USERNAME, - fromUserId: SYSTEM_CHAT_TYPE, - fromUsername: '', - fromColor: '', - }); - } - - return null; -} diff --git a/bigbluebutton-html5/imports/api/chat/server/index.js b/bigbluebutton-html5/imports/api/chat/server/index.js deleted file mode 100644 index 92451ac76bf27410726e8f3cd2eebac46cd7b83e..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/api/chat/server/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import './eventHandlers'; -import './methods'; -import './publishers'; diff --git a/bigbluebutton-html5/imports/api/chat/server/methods.js b/bigbluebutton-html5/imports/api/chat/server/methods.js deleted file mode 100644 index f94347ddf6b23844a257bd34d2cd1f2b01333df2..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/api/chat/server/methods.js +++ /dev/null @@ -1,9 +0,0 @@ -import mapToAcl from '/imports/startup/mapToAcl'; -import { Meteor } from 'meteor/meteor'; -import sendChat from './methods/sendChat'; -import clearPublicChatHistory from './methods/clearPublicChatHistory'; - -Meteor.methods(mapToAcl(['methods.sendChat', 'methods.clearPublicChatHistory'], { - sendChat, - clearPublicChatHistory, -})); diff --git a/bigbluebutton-html5/imports/api/chat/server/methods/sendChat.js b/bigbluebutton-html5/imports/api/chat/server/methods/sendChat.js deleted file mode 100644 index 64c7b9ebf1d6480b21b0b3c55cb6906fab34c80c..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/api/chat/server/methods/sendChat.js +++ /dev/null @@ -1,59 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; -import RedisPubSub from '/imports/startup/server/redis'; -import RegexWebUrl from '/imports/utils/regex-weburl'; - -const HTML_SAFE_MAP = { - '<': '<', - '>': '>', - '"': '"', - "'": ''', -}; - -const parseMessage = (message) => { - let parsedMessage = message || ''; - parsedMessage = parsedMessage.trim(); - - // Replace <br/> with \n\r - parsedMessage = parsedMessage.replace(/<br\s*[\\/]?>/gi, '\n\r'); - - // Sanitize. See: http://shebang.brandonmintern.com/foolproof-html-escaping-in-javascript/ - parsedMessage = parsedMessage.replace(/[<>'"]/g, c => HTML_SAFE_MAP[c]); - - // Replace flash links to flash valid ones - parsedMessage = parsedMessage.replace(RegexWebUrl, "<a href='event:$&'><u>$&</u></a>"); - - return parsedMessage; -}; - -export default function sendChat(credentials, message) { - const REDIS_CONFIG = Meteor.settings.private.redis; - const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; - - const CHAT_CONFIG = Meteor.settings.public.chat; - const TO_PUBLIC_CHAT = CHAT_CONFIG.public_username; - - const { meetingId, requesterUserId, requesterToken } = credentials; - - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); - check(message, Object); - - message.fromTime = Date.now(); // server time - - let eventName = 'SendPrivateMessagePubMsg'; - - const parsedMessage = message; - - parsedMessage.message = parseMessage(message.message); - - if (message.toUsername === TO_PUBLIC_CHAT) { - eventName = 'SendPublicMessagePubMsg'; - } - - return RedisPubSub.publishUserMessage( - CHANNEL, eventName, meetingId, requesterUserId, - { message: parsedMessage }, - ); -} diff --git a/bigbluebutton-html5/imports/api/chat/server/modifiers/addChat.js b/bigbluebutton-html5/imports/api/chat/server/modifiers/addChat.js deleted file mode 100644 index 389e4557df0ca43c847f62365fc5907bfd688117..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/api/chat/server/modifiers/addChat.js +++ /dev/null @@ -1,77 +0,0 @@ -import flat from 'flat'; -import Chat from '/imports/api/chat'; -import Logger from '/imports/startup/server/logger'; -import { Match, check } from 'meteor/check'; -import { BREAK_LINE } from '/imports/utils/lineEndings'; - -const parseMessage = (message) => { - let parsedMessage = message || ''; - - // Replace \r and \n to <br/> - parsedMessage = parsedMessage.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, `$1${BREAK_LINE}$2`); - - // Replace flash links to html valid ones - parsedMessage = parsedMessage.split('<a href=\'event:').join('<a target="_blank" href=\''); - parsedMessage = parsedMessage.split('<a href="event:').join('<a target="_blank" href="'); - - return parsedMessage; -}; - -const chatType = (userName) => { - const CHAT_CONFIG = Meteor.settings.public.chat; - - const typeByUser = { - [CHAT_CONFIG.system_username]: CHAT_CONFIG.type_system, - [CHAT_CONFIG.public_username]: CHAT_CONFIG.type_public, - }; - - return userName in typeByUser ? typeByUser[userName] : CHAT_CONFIG.type_private; -}; - -export default function addChat(meetingId, chat) { - check(chat, { - message: String, - fromColor: String, - toUserId: String, - toUsername: String, - fromUserId: String, - fromUsername: Match.Maybe(String), - fromTime: Number, - fromTimezoneOffset: Match.Maybe(Number), - }); - - const selector = { - meetingId, - fromTime: chat.fromTime, - fromUserId: chat.fromUserId, - toUserId: chat.toUserId, - }; - - const modifier = { - $set: Object.assign( - flat(chat, { safe: true }), - { - meetingId, - message: parseMessage(chat.message), - type: chatType(chat.toUsername), - }, - ), - }; - - const cb = (err, numChanged) => { - if (err) { - return Logger.error(`Adding chat to collection: ${err}`); - } - - const { insertedId } = numChanged; - const to = chat.toUsername || 'PUBLIC'; - - if (insertedId) { - return Logger.info(`Added chat from=${chat.fromUsername} to=${to} time=${chat.fromTime}`); - } - - return Logger.info(`Upserted chat from=${chat.fromUsername} to=${to} time=${chat.fromTime}`); - }; - - return Chat.upsert(selector, modifier, cb); -} diff --git a/bigbluebutton-html5/imports/api/chat/server/modifiers/clearChats.js b/bigbluebutton-html5/imports/api/chat/server/modifiers/clearChats.js deleted file mode 100644 index 98d319e8b50b273892f92350aa094df9a7ae2f03..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/api/chat/server/modifiers/clearChats.js +++ /dev/null @@ -1,10 +0,0 @@ -import Chat from '/imports/api/chat'; -import Logger from '/imports/startup/server/logger'; - -export default function clearChats(meetingId) { - if (meetingId) { - return Chat.remove({ meetingId }, Logger.info(`Cleared Chats (${meetingId})`)); - } - - return Chat.remove({}, Logger.info('Cleared Chats (all)')); -} diff --git a/bigbluebutton-html5/imports/api/chat/server/modifiers/clearUserSystemMessages.js b/bigbluebutton-html5/imports/api/chat/server/modifiers/clearUserSystemMessages.js deleted file mode 100644 index 3880f3c64b835940b82b602e59e3aebf887325b6..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/api/chat/server/modifiers/clearUserSystemMessages.js +++ /dev/null @@ -1,24 +0,0 @@ -import Chat from '/imports/api/chat'; -import Logger from '/imports/startup/server/logger'; -import { check } from 'meteor/check'; - -/** - * Remove any system message from the user with userId. - * - * @param {string} meetingId - * @param {string} userId - */ -export default function clearUserSystemMessages(meetingId, userId) { - check(meetingId, String); - check(userId, String); - - const CHAT_CONFIG = Meteor.settings.public.chat; - - const selector = { - meetingId, - fromUserId: CHAT_CONFIG.type_system, - toUserId: userId, - }; - - return Chat.remove(selector, Logger.info(`Removing system messages from: (${userId})`)); -} diff --git a/bigbluebutton-html5/imports/api/chat/server/publishers.js b/bigbluebutton-html5/imports/api/chat/server/publishers.js deleted file mode 100644 index 752e9da7271ef8911ec23cbe9f97b1946be1207d..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/api/chat/server/publishers.js +++ /dev/null @@ -1,40 +0,0 @@ -import Chat from '/imports/api/chat'; -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; -import Logger from '/imports/startup/server/logger'; -import mapToAcl from '/imports/startup/mapToAcl'; - -function chat(credentials) { - const { meetingId, requesterUserId, requesterToken } = credentials; - - check(meetingId, String); - check(requesterUserId, String); - check(requesterToken, String); - - const CHAT_CONFIG = Meteor.settings.public.chat; - const PUBLIC_CHAT_USERNAME = CHAT_CONFIG.public_username; - - Logger.info(`Publishing chat for ${meetingId} ${requesterUserId} ${requesterToken}`); - - return Chat.find({ - $or: [ - { - toUsername: PUBLIC_CHAT_USERNAME, - meetingId, - }, { - fromUserId: requesterUserId, - meetingId, - }, { - toUserId: requesterUserId, - meetingId, - }, - ], - }); -} - -function publish(...args) { - const boundChat = chat.bind(this); - return mapToAcl('subscriptions.chat', boundChat)(args); -} - -Meteor.publish('chat', publish); diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/index.js b/bigbluebutton-html5/imports/api/group-chat-msg/index.js index a88d49148dd3d67845b27897b44b1b57bbd7b7d0..8a0e3b5fffe551b6d88f9dc5d51ffa0d5ff1f72c 100644 --- a/bigbluebutton-html5/imports/api/group-chat-msg/index.js +++ b/bigbluebutton-html5/imports/api/group-chat-msg/index.js @@ -1,19 +1,9 @@ import { Meteor } from 'meteor/meteor'; -const GroupChat = new Mongo.Collection('group-chat-msg'); +const GroupChatMsg = new Mongo.Collection('group-chat-msg'); if (Meteor.isServer) { - GroupChat._ensureIndex({ - meetingId: 1, chatId: 1, access: 1, users: 1, - }); + GroupChatMsg._ensureIndex({ meetingId: 1, chatId: 1 }); } -export default GroupChat; - -export const CHAT_ACCESS = { - PUBLIC: 'PUBLIC_ACCESS', - PRIVATE: 'PRIVATE_ACCESS', -}; - -export const CHAT_ACCESS_PUBLIC = CHAT_ACCESS.PUBLIC; -export const CHAT_ACCESS_PRIVATE = CHAT_ACCESS.PRIVATE; +export default GroupChatMsg; diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/eventHandlers.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/eventHandlers.js index b3525d7ecb2cf28bc017c3dac3e739c663fdaa21..acc656abb290929dfe3bfa7c7c8ac4a57c304a93 100644 --- a/bigbluebutton-html5/imports/api/group-chat-msg/server/eventHandlers.js +++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/eventHandlers.js @@ -1,6 +1,9 @@ import RedisPubSub from '/imports/startup/server/redis'; import handleGroupChatsMsgs from './handlers/groupChatsMsgs'; import handleGroupChatMsgBroadcast from './handlers/groupChatMsgBroadcast'; +import handleClearPublicGroupChat from './handlers/clearPublicGroupChat'; RedisPubSub.on('GetGroupChatMsgsRespMsg', handleGroupChatsMsgs); RedisPubSub.on('GroupChatMessageBroadcastEvtMsg', handleGroupChatMsgBroadcast); +RedisPubSub.on('ClearPublicChatHistoryEvtMsg', handleClearPublicGroupChat); +RedisPubSub.on('SyncGetGroupChatMsgsRespMsg', handleGroupChatsMsgs); diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/handlers/clearPublicGroupChat.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/handlers/clearPublicGroupChat.js new file mode 100644 index 0000000000000000000000000000000000000000..2026780b0c12ba8840d0fcff61cde154a1e32f6a --- /dev/null +++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/handlers/clearPublicGroupChat.js @@ -0,0 +1,12 @@ +import { check } from 'meteor/check'; +import clearGroupChatMsg from '../modifiers/clearGroupChatMsg'; + +export default function clearPublicChatHistory({ header, body }) { + const { meetingId } = header; + const { chatId } = body; + + check(meetingId, String); + check(chatId, String); + + return clearGroupChatMsg(meetingId, chatId); +} diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods.js index bd452e804276fdcdea98de09668a332c5caae9ef..03fe3622c54e4f52f7aa65413324cb47c209ddc8 100644 --- a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods.js +++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods.js @@ -1,7 +1,9 @@ import { Meteor } from 'meteor/meteor'; import mapToAcl from '/imports/startup/mapToAcl'; import sendGroupChatMsg from './methods/sendGroupChatMsg'; +import clearPublicChatHistory from './methods/clearPublicChatHistory'; -Meteor.methods(mapToAcl(['methods.sendGroupChatMsg'], { +Meteor.methods(mapToAcl(['methods.sendGroupChatMsg', 'methods.clearPublicChatHistory'], { sendGroupChatMsg, + clearPublicChatHistory, })); diff --git a/bigbluebutton-html5/imports/api/chat/server/methods/clearPublicChatHistory.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/clearPublicChatHistory.js similarity index 59% rename from bigbluebutton-html5/imports/api/chat/server/methods/clearPublicChatHistory.js rename to bigbluebutton-html5/imports/api/group-chat-msg/server/methods/clearPublicChatHistory.js index 6805d4b2689983f96dc3a7426129f6615c809f15..0ca003af5dec2dd6a3ae8f319601660bec0e767c 100644 --- a/bigbluebutton-html5/imports/api/chat/server/methods/clearPublicChatHistory.js +++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/clearPublicChatHistory.js @@ -5,15 +5,20 @@ import RedisPubSub from '/imports/startup/server/redis'; export default function clearPublicChatHistory(credentials) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'ClearPublicChatHistoryPubMsg'; + const CHAT_CONFIG = Meteor.settings.public.chat; + const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id; + + const { meetingId, requesterUserId, requesterToken } = credentials; check(meetingId, String); check(requesterUserId, String); check(requesterToken, String); - const eventName = 'ClearPublicChatHistoryPubMsg'; - - const payload = {}; + const payload = { + chatId: PUBLIC_GROUP_CHAT_ID, + }; - return RedisPubSub.publishUserMessage(CHANNEL, eventName, meetingId, requesterUserId, payload); + return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); } diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/sendGroupChatMsg.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/sendGroupChatMsg.js index 4bf8896b9d00f6aea0ddb1959e53b628624c7f91..5d4aa6882e926786bdbd298a2257c431126e0096 100644 --- a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/sendGroupChatMsg.js +++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/sendGroupChatMsg.js @@ -29,6 +29,7 @@ const parseMessage = (message) => { export default function sendGroupChatMsg(credentials, chatId, message) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'SendGroupChatMessageMsg'; const { meetingId, requesterUserId, requesterToken } = credentials; @@ -37,19 +38,14 @@ export default function sendGroupChatMsg(credentials, chatId, message) { check(requesterToken, String); check(message, Object); - const eventName = 'SendGroupChatMessageMsg'; + const parsedMessage = parseMessage(message.message); + + message.message = parsedMessage; - const parsedMessage = parseMessage(message); const payload = { + msg: message, chatId, - // correlationId: `${Date.now()}`, - sender: { - id: requesterUserId, - name: '', - }, - // color: '1', - message: parsedMessage, }; - return RedisPubSub.publishUserMessage(CHANNEL, eventName, meetingId, requesterUserId, payload); + return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); } diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/modifiers/addGroupChatMsg.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/modifiers/addGroupChatMsg.js index 19b96e88548e0129f4126e12985432613cb04bdb..60377b0e4580149aca01180f68f5b2ea9a693f4f 100644 --- a/bigbluebutton-html5/imports/api/group-chat-msg/server/modifiers/addGroupChatMsg.js +++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/modifiers/addGroupChatMsg.js @@ -21,7 +21,7 @@ export default function addGroupChatMsg(meetingId, chatId, msg) { check(meetingId, String); check(chatId, String); check(msg, { - id: String, + id: Match.Maybe(String), timestamp: Number, sender: Object, color: String, diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/modifiers/clearGroupChatMsg.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/modifiers/clearGroupChatMsg.js index a82f61e72eba00e52888d3eef0898c3227bfc262..2ca13c264696bb7ae1e4b300ab37a189d45559ab 100644 --- a/bigbluebutton-html5/imports/api/group-chat-msg/server/modifiers/clearGroupChatMsg.js +++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/modifiers/clearGroupChatMsg.js @@ -1,13 +1,32 @@ import GroupChatMsg from '/imports/api/group-chat-msg'; import Logger from '/imports/startup/server/logger'; +import addGroupChatMsg from '/imports/api/group-chat-msg/server/modifiers/addGroupChatMsg'; export default function clearGroupChatMsg(meetingId, chatId) { - if (meetingId) { - return GroupChatMsg.remove({ meetingId }, Logger.info(`Cleared GroupChat (${meetingId})`)); - } + const CHAT_CONFIG = Meteor.settings.public.chat; + const PUBLIC_CHAT_SYSTEM_ID = CHAT_CONFIG.system_userid; + const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id; + const CHAT_CLEAR_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_clear; if (chatId) { - return GroupChatMsg.remove({ meetingId, chatId }, Logger.info(`Cleared GroupChat (${meetingId}, ${chatId})`)); + GroupChatMsg.remove({ meetingId, chatId }, Logger.info(`Cleared GroupChat (${meetingId}, ${chatId})`)); + + const clearMsg = { + color: '0', + timestamp: Date.now(), + correlationId: `${PUBLIC_CHAT_SYSTEM_ID}-${Date.now()}`, + sender: { + id: PUBLIC_CHAT_SYSTEM_ID, + name: '', + }, + message: CHAT_CLEAR_MESSAGE, + }; + + return addGroupChatMsg(meetingId, PUBLIC_GROUP_CHAT_ID, clearMsg); + } + + if (meetingId) { + return GroupChatMsg.remove({ meetingId }, Logger.info(`Cleared GroupChat (${meetingId})`)); } return GroupChatMsg.remove({}, Logger.info('Cleared GroupChat (all)')); diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/publishers.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/publishers.js index d9f6af4f38e93d75e182dbeac80fed1537dc146f..c4c4ce3f6d787ee4bb9f0a35d4fe70dcb7975085 100644 --- a/bigbluebutton-html5/imports/api/group-chat-msg/server/publishers.js +++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/publishers.js @@ -1,29 +1,26 @@ +import GroupChatMsg from '/imports/api/group-chat-msg'; import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import Logger from '/imports/startup/server/logger'; import mapToAcl from '/imports/startup/mapToAcl'; -import { GroupChat, CHAT_ACCESS_PUBLIC } from '/imports/api/group-chat-msg'; - -function groupChatMsg(credentials) { +function groupChatMsg(credentials, chatsIds) { const { meetingId, requesterUserId, requesterToken } = credentials; check(meetingId, String); check(requesterUserId, String); check(requesterToken, String); + const CHAT_CONFIG = Meteor.settings.public.chat; + const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id; + Logger.info(`Publishing group-chat-msg for ${meetingId} ${requesterUserId} ${requesterToken}`); - return GroupChat.find({ + return GroupChatMsg.find({ $or: [ - { - access: CHAT_ACCESS_PUBLIC, - meetingId, - }, { - users: { $in: [requesterUserId] }, - meetingId, - }, + { meetingId, chatId: { $eq: PUBLIC_GROUP_CHAT_ID } }, + { chatId: { $in: chatsIds } }, ], }); } diff --git a/bigbluebutton-html5/imports/api/group-chat/index.js b/bigbluebutton-html5/imports/api/group-chat/index.js index 6d4f5c43dd8a82a8408f8fc57e28379a2b8a6f26..f26082d0a5922388ce9e6ae45a1b53f0273983b0 100644 --- a/bigbluebutton-html5/imports/api/group-chat/index.js +++ b/bigbluebutton-html5/imports/api/group-chat/index.js @@ -10,7 +10,7 @@ if (Meteor.isServer) { export default GroupChat; -export const CHAT_ACCESS = { +const CHAT_ACCESS = { PUBLIC: 'PUBLIC_ACCESS', PRIVATE: 'PRIVATE_ACCESS', }; diff --git a/bigbluebutton-html5/imports/api/group-chat/server/eventHandlers.js b/bigbluebutton-html5/imports/api/group-chat/server/eventHandlers.js index 0281ee972b530bc5e198b475bc6631cde73a0047..8d9b08f80e0930e726c9ed831c4a605fc44c3e33 100644 --- a/bigbluebutton-html5/imports/api/group-chat/server/eventHandlers.js +++ b/bigbluebutton-html5/imports/api/group-chat/server/eventHandlers.js @@ -6,3 +6,4 @@ import handleGroupChatDestroyed from './handlers/groupChatDestroyed'; RedisPubSub.on('GetGroupChatsRespMsg', handleGroupChats); RedisPubSub.on('GroupChatCreatedEvtMsg', handleGroupChatCreated); RedisPubSub.on('GroupChatDestroyedEvtMsg', handleGroupChatDestroyed); +RedisPubSub.on('SyncGetGroupChatsRespMsg', handleGroupChats); diff --git a/bigbluebutton-html5/imports/api/group-chat/server/methods/createGroupChat.js b/bigbluebutton-html5/imports/api/group-chat/server/methods/createGroupChat.js index a9b6ea40e4aec4dd995d14e5fd6f0839d9b5a778..1d80fbc07dc6f8e55dcfe56e6765e21f0d191d25 100644 --- a/bigbluebutton-html5/imports/api/group-chat/server/methods/createGroupChat.js +++ b/bigbluebutton-html5/imports/api/group-chat/server/methods/createGroupChat.js @@ -1,31 +1,28 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import RedisPubSub from '/imports/startup/server/redis'; +import { CHAT_ACCESS_PRIVATE } from '/imports/api/group-chat'; -export default function createGroupChat(credentials) { + +export default function createGroupChat(credentials, receiver) { const REDIS_CONFIG = Meteor.settings.private.redis; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'CreateGroupChatReqMsg'; + const { meetingId, requesterUserId, requesterToken } = credentials; check(meetingId, String); check(requesterUserId, String); check(requesterToken, String); - - const eventName = 'CreateGroupChatReqMsg'; + check(receiver, Object); const payload = { - // TODO: Implement this together with #4988 - // correlationId: String, - // name: String, - // access: String, - // users: Vector[String], - // msg: Vector[{ - // correlationId: String, - // sender: GroupChatUser, - // color: String, - // message: String - // }], + correlationId: `${requesterUserId}-${Date.now()}`, + msg: [], + users: [receiver.id], + access: CHAT_ACCESS_PRIVATE, + name: receiver.name, }; - return RedisPubSub.publishUserMessage(CHANNEL, eventName, meetingId, requesterUserId, payload); + return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); } diff --git a/bigbluebutton-html5/imports/api/group-chat/server/modifiers/clearGroupChat.js b/bigbluebutton-html5/imports/api/group-chat/server/modifiers/clearGroupChat.js index a3cfeb4c2ca8669e10494e302b8318320e451313..83852af9a95fe55af4237d513b3b16e9d7dd1677 100644 --- a/bigbluebutton-html5/imports/api/group-chat/server/modifiers/clearGroupChat.js +++ b/bigbluebutton-html5/imports/api/group-chat/server/modifiers/clearGroupChat.js @@ -1,13 +1,8 @@ import GroupChat from '/imports/api/group-chat'; import Logger from '/imports/startup/server/logger'; -import clearGroupChatMsg from 'imports/api/group-chat-msg/server/modifiers/clearGroupChatMsg'; +import clearGroupChatMsg from '/imports/api/group-chat-msg/server/modifiers/clearGroupChatMsg'; export default function clearGroupChat(meetingId) { - if (meetingId) { - clearGroupChatMsg(meetingId); - return GroupChat.remove({ meetingId }, Logger.info(`Cleared GroupChat (${meetingId})`)); - } - - clearGroupChatMsg(); - return GroupChat.remove({}, Logger.info('Cleared GroupChat (all)')); + clearGroupChatMsg(meetingId); + return GroupChat.remove({ meetingId }, Logger.info(`Cleared GroupChat (${meetingId})`)); } diff --git a/bigbluebutton-html5/imports/api/group-chat/server/modifiers/removeGroupChat.js b/bigbluebutton-html5/imports/api/group-chat/server/modifiers/removeGroupChat.js index dc97c1b8b668cf1d6a8cce549a66a099a187a93a..7d2da91c5ebc64df4fd9d2e49002a3e185225f48 100644 --- a/bigbluebutton-html5/imports/api/group-chat/server/modifiers/removeGroupChat.js +++ b/bigbluebutton-html5/imports/api/group-chat/server/modifiers/removeGroupChat.js @@ -1,7 +1,7 @@ import { check } from 'meteor/check'; import Logger from '/imports/startup/server/logger'; import GroupChat from '/imports/api/group-chat'; -import clearGroupChatMsg from 'imports/api/group-chat-msg/server/modifiers/clearGroupChatMsg'; +import clearGroupChatMsg from '/imports/api/group-chat-msg/server/modifiers/clearGroupChatMsg'; export default function removeGroupChat(meetingId, chatId) { check(meetingId, String); diff --git a/bigbluebutton-html5/imports/api/group-chat/server/publishers.js b/bigbluebutton-html5/imports/api/group-chat/server/publishers.js index 39657bea6810f249f076a45155b7f2089e792803..e2d1a0db23b413b7c36cb12855443c0cbf80739f 100644 --- a/bigbluebutton-html5/imports/api/group-chat/server/publishers.js +++ b/bigbluebutton-html5/imports/api/group-chat/server/publishers.js @@ -1,11 +1,10 @@ +import GroupChat from '/imports/api/group-chat'; import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import Logger from '/imports/startup/server/logger'; import mapToAcl from '/imports/startup/mapToAcl'; -import { GroupChat, CHAT_ACCESS_PUBLIC } from '/imports/api/group-chat'; - function groupChat(credentials) { const { meetingId, requesterUserId, requesterToken } = credentials; @@ -13,18 +12,17 @@ function groupChat(credentials) { check(requesterUserId, String); check(requesterToken, String); + const CHAT_CONFIG = Meteor.settings.public.chat; + const PUBLIC_CHAT_TYPE = CHAT_CONFIG.type_public; + Logger.info(`Publishing group-chat for ${meetingId} ${requesterUserId} ${requesterToken}`); return GroupChat.find({ $or: [ - { - access: CHAT_ACCESS_PUBLIC, - meetingId, - }, { - users: { $in: [requesterUserId] }, - meetingId, - }, + { meetingId, access: PUBLIC_CHAT_TYPE }, + { meetingId, users: { $all: [requesterUserId] } }, ], + }); } diff --git a/bigbluebutton-html5/imports/api/log-client/server/methods/logClient.js b/bigbluebutton-html5/imports/api/log-client/server/methods/logClient.js index ae7d8a3efc67cc242a1dd722d753a0ce7ba5d62b..d6306b06a5e66dfbd0f249b3998998217bf75215 100755 --- a/bigbluebutton-html5/imports/api/log-client/server/methods/logClient.js +++ b/bigbluebutton-html5/imports/api/log-client/server/methods/logClient.js @@ -1,13 +1,14 @@ import Logger from '/imports/startup/server/logger'; import Users from '/imports/api/users'; -const logClient = function (type, log, fullInfo) { +const logClient = function (type, log, fullInfo = {}) { const SERVER_CONN_ID = this.connection.id; const User = Users.findOne({ connectionId: SERVER_CONN_ID }); const logContents = { fullInfo }; if (User) { - if (User.meetingId === fullInfo.credentials.meetingId) { + if ((fullInfo.credentials && User.meetingId === fullInfo.credentials.meetingId) || + ((fullInfo.meetingId && User.meetingId === fullInfo.meetingId))) { logContents.validUser = 'valid'; } else { logContents.validUser = 'invalid'; diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js index a8023da3626343ddc0c50c913caba9062494b8e0..8740a523f5db8d08769ccafafd213be1a930cbde 100644 --- a/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js +++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js @@ -3,8 +3,13 @@ import { check } from 'meteor/check'; import Meetings from '/imports/api/meetings'; import Logger from '/imports/startup/server/logger'; +import addGroupChatMsg from '/imports/api/group-chat-msg/server/modifiers/addGroupChatMsg'; + export default function addMeeting(meeting) { const meetingId = meeting.meetingProp.intId; + const CHAT_CONFIG = Meteor.settings.public.chat; + const PUBLIC_CHAT_SYSTEM_ID = CHAT_CONFIG.system_userid; + const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id; check(meetingId, String); check(meeting, { @@ -76,6 +81,19 @@ export default function addMeeting(meeting) { ), }; + const welcomeMsg = { + color: '0', + timestamp: Date.now(), + correlationId: `${PUBLIC_CHAT_SYSTEM_ID}-${Date.now()}`, + sender: { + id: PUBLIC_CHAT_SYSTEM_ID, + name: '', + }, + message: meeting.welcomeProp.welcomeMsg, + }; + + addGroupChatMsg(meetingId, PUBLIC_GROUP_CHAT_ID, welcomeMsg); + const cb = (err, numChanged) => { if (err) { Logger.error(`Adding meeting to collection: ${err}`); diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/removeMeeting.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/removeMeeting.js index a1dce5f4bce9f85073f1d6778a6ad8304925e17f..26ec6f6bae04b5e991b42340b2477e4a4c9589b4 100755 --- a/bigbluebutton-html5/imports/api/meetings/server/modifiers/removeMeeting.js +++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/removeMeeting.js @@ -2,7 +2,7 @@ import Meetings from '/imports/api/meetings'; import Logger from '/imports/startup/server/logger'; import clearUsers from '/imports/api/users/server/modifiers/clearUsers'; -import clearChats from '/imports/api/chat/server/modifiers/clearChats'; +import clearGroupChat from '/imports/api/group-chat/server/modifiers/clearGroupChat'; import clearBreakouts from '/imports/api/breakouts/server/modifiers/clearBreakouts'; import clearAnnotations from '/imports/api/annotations/server/modifiers/clearAnnotations'; import clearSlides from '/imports/api/slides/server/modifiers/clearSlides'; @@ -14,7 +14,7 @@ import clearVoiceUsers from '/imports/api/voice-users/server/modifiers/clearVoic export default function removeMeeting(meetingId) { return Meetings.remove({ meetingId }, () => { clearCaptions(meetingId); - clearChats(meetingId); + clearGroupChat(meetingId); clearPresentations(meetingId); clearBreakouts(meetingId); clearPolls(meetingId); diff --git a/bigbluebutton-html5/imports/api/meetings/server/publishers.js b/bigbluebutton-html5/imports/api/meetings/server/publishers.js old mode 100644 new mode 100755 index df5b44f8dfc206f2c4ca1f0fc14287b5c7998ffc..1a1a6f52b853ab3276470add2ec922f7e20896fd --- a/bigbluebutton-html5/imports/api/meetings/server/publishers.js +++ b/bigbluebutton-html5/imports/api/meetings/server/publishers.js @@ -13,9 +13,17 @@ function meetings(credentials) { Logger.info(`Publishing meeting =${meetingId} ${requesterUserId} ${requesterToken}`); - return Meetings.find({ + const selector = { meetingId, - }); + }; + + const options = { + fields: { + password: false, + }, + }; + + return Meetings.find(selector, options); } function publish(...args) { diff --git a/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js b/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js index f266589972c617ca45545ae285d67ee46193a956..3c22b70b55a99d69445c888bd25c39d6a9c08a8a 100755 --- a/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js +++ b/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js @@ -3,6 +3,7 @@ import Auth from '/imports/ui/services/auth'; import BridgeService from './service'; import { fetchWebRTCMappedStunTurnServers } from '/imports/utils/fetchStunTurnServers'; import logger from '/imports/startup/client/logger'; +import { notify } from '/imports/ui/services/notification'; const SFU_CONFIG = Meteor.settings.public.kurento; const SFU_URL = SFU_CONFIG.wsUrl; @@ -14,6 +15,8 @@ const SCREENSHARE_VIDEO_TAG = 'screenshareVideo'; const CHROME_EXTENSION_KEY = CHROME_CUSTOM_EXTENSION_KEY === 'KEY' ? CHROME_DEFAULT_EXTENSION_KEY : CHROME_CUSTOM_EXTENSION_KEY; +const ICE_CONNECTION_FAILED = 'ICE connection failed'; + const getUserId = () => Auth.userID; const getMeetingId = () => Auth.meetingID; @@ -28,17 +31,17 @@ const logFunc = (type, message, options) => { const topic = options.topic || 'screenshare'; - logger[type]({obj: Object.assign(options, {userId, userName, topic})}, `[${topic}] ${message}`); + logger[type]({ obj: Object.assign(options, { userId, userName, topic }) }, `[${topic}] ${message}`); }; const modLogger = { - info: function (message, options = {}) { + info(message, options = {}) { logFunc('info', message, options); }, - error: function (message, options = {}) { + error(message, options = {}) { logFunc('error', message, options); }, - debug: function (message, options = {}) { + debug(message, options = {}) { logFunc('debug', message, options); }, warn: (message, options = {}) => { @@ -58,7 +61,7 @@ export default class KurentoScreenshareBridge { const options = { wsUrl: SFU_URL, iceServers, - logger: modLogger + logger: modLogger, }; window.kurentoWatchVideo( @@ -68,16 +71,16 @@ export default class KurentoScreenshareBridge { getMeetingId(), null, null, - options + options, ); - }; + } } kurentoExitVideo() { window.kurentoExitVideo(); } - async kurentoShareScreen() { + async kurentoShareScreen(onFail) { let iceServers = []; try { iceServers = await fetchWebRTCMappedStunTurnServers(getSessionToken()); @@ -92,15 +95,14 @@ export default class KurentoScreenshareBridge { iceServers, logger: modLogger, }; - window.kurentoShareScreen( SCREENSHARE_VIDEO_TAG, BridgeService.getConferenceBridge(), getUserId(), getMeetingId(), + onFail, null, - null, - options + options, ); } } diff --git a/bigbluebutton-html5/imports/api/users/server/modifiers/addDialInUser.js b/bigbluebutton-html5/imports/api/users/server/modifiers/addDialInUser.js new file mode 100644 index 0000000000000000000000000000000000000000..0c49459d942bd927020a89188e0ac51a9556cc6d --- /dev/null +++ b/bigbluebutton-html5/imports/api/users/server/modifiers/addDialInUser.js @@ -0,0 +1,31 @@ +import { check } from 'meteor/check'; +import addUser from '/imports/api/users/server/modifiers/addUser'; + + +export default function addDialInUser(meetingId, voiceUser) { + check(meetingId, String); + check(voiceUser, Object); + + const USER_CONFIG = Meteor.settings.public.user; + const ROLE_VIEWER = USER_CONFIG.role_viewer; + + const { intId, callerName } = voiceUser; + + const voiceOnlyUser = { + intId, + extId: intId, // TODO + name: callerName, + role: ROLE_VIEWER.toLowerCase(), + guest: false, + authed: true, + waitingForAcceptance: false, + guestStatus: 'ALLOW', + emoji: 'none', + presenter: false, + locked: false, // TODO + avatar: '', + clientType: 'dial-in-user', + }; + + return addUser(meetingId, voiceOnlyUser); +} diff --git a/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js b/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js index c0c165feffe47e44ea0a69122cb27fbf9f42abd5..56f5a25debdbbadc4e8049acf7349683cf339ab5 100755 --- a/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js +++ b/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js @@ -1,16 +1,14 @@ import { check } from 'meteor/check'; import Logger from '/imports/startup/server/logger'; import Users from '/imports/api/users'; +import Meetings from '/imports/api/meetings'; import stringHash from 'string-hash'; import flat from 'flat'; import addVoiceUser from '/imports/api/voice-users/server/modifiers/addVoiceUser'; import changeRole from '/imports/api/users/server/modifiers/changeRole'; - -import Meetings from '/imports/api/meetings'; -import addChat from '/imports/api/chat/server/modifiers/addChat'; -import clearUserSystemMessages from '/imports/api/chat/server/modifiers/clearUserSystemMessages'; +import setApprovedStatus from '/imports/api/users/server/modifiers/setApprovedStatus'; const COLOR_LIST = [ '#7b1fa2', '#6a1b9a', '#4a148c', '#5e35b1', '#512da8', '#4527a0', @@ -18,23 +16,6 @@ const COLOR_LIST = [ '#0d47a1', '#0277bd', '#01579b', ]; -// add some default welcoming message to the chat (welcome / mod only) -const addWelcomingChatMessage = (messageText, meetingId, userId) => { - const CHAT_CONFIG = Meteor.settings.public.chat; - - const message = { - message: messageText, - fromColor: '0x3399FF', - toUserId: userId, - toUsername: CHAT_CONFIG.type_system, - fromUserId: CHAT_CONFIG.type_system, - fromUsername: '', - fromTime: (new Date()).getTime(), - }; - - addChat(meetingId, message); -}; - export default function addUser(meetingId, user) { check(meetingId, String); @@ -45,6 +26,7 @@ export default function addUser(meetingId, user) { role: String, guest: Boolean, authed: Boolean, + waitingForAcceptance: Match.Maybe(Boolean), guestStatus: String, emoji: String, presenter: Boolean, @@ -67,7 +49,9 @@ export default function addUser(meetingId, user) { const ROLE_VIEWER = USER_CONFIG.role_viewer; const APP_CONFIG = Meteor.settings.public.app; const ALLOW_HTML5_MODERATOR = APP_CONFIG.allowHTML5Moderator; + const GUEST_ALWAYS_ACCEPT = 'ALWAYS_ACCEPT'; + const Meeting = Meetings.findOne({ meetingId }); // override moderator status of html5 client users, depending on a system flag const dummyUser = Users.findOne(selector); let userRole = user.role; @@ -116,26 +100,16 @@ export default function addUser(meetingId, user) { return Logger.error(`Adding user to collection: ${err}`); } - clearUserSystemMessages(meetingId, userId); - - const Meeting = Meetings.findOne({ meetingId }); - addWelcomingChatMessage( - Meeting.welcomeProp.welcomeMsg, - meetingId, userId, - ); - if (user.presenter) { changeRole(ROLE_PRESENTER, true, userId, meetingId); } if (userRole === ROLE_MODERATOR) { changeRole(ROLE_MODERATOR, true, userId, meetingId); - if (Meeting.welcomeProp.modOnlyMessage) { - addWelcomingChatMessage( - Meeting.welcomeProp.modOnlyMessage, - meetingId, userId, - ); - } + } + + if (Meeting.usersProp.guestPolicy === GUEST_ALWAYS_ACCEPT) { + setApprovedStatus(meetingId, userId, true); } const { insertedId } = numChanged; diff --git a/bigbluebutton-html5/imports/api/voice-users/server/eventHandlers.js b/bigbluebutton-html5/imports/api/voice-users/server/eventHandlers.js index 6f54ca836eb079903906d80ddab4ac758cd96be4..10481cda4f1914179945930e6cc9c04e7c330178 100755 --- a/bigbluebutton-html5/imports/api/voice-users/server/eventHandlers.js +++ b/bigbluebutton-html5/imports/api/voice-users/server/eventHandlers.js @@ -5,9 +5,11 @@ import handleLeftVoiceUser from './handlers/leftVoiceUser'; import handleTalkingVoiceUser from './handlers/talkingVoiceUser'; import handleMutedVoiceUser from './handlers/mutedVoiceUser'; import handleGetVoiceUsers from './handlers/getVoiceUsers'; +import handleVoiceUsers from './handlers/voiceUsers'; RedisPubSub.on('UserLeftVoiceConfToClientEvtMsg', handleLeftVoiceUser); RedisPubSub.on('UserJoinedVoiceConfToClientEvtMsg', handleJoinVoiceUser); RedisPubSub.on('UserTalkingVoiceEvtMsg', handleTalkingVoiceUser); RedisPubSub.on('UserMutedVoiceEvtMsg', handleMutedVoiceUser); RedisPubSub.on('GetVoiceUsersMeetingRespMsg', processForHTML5ServerOnly(handleGetVoiceUsers)); +RedisPubSub.on('SyncGetVoiceUsersRespMsg', handleVoiceUsers); diff --git a/bigbluebutton-html5/imports/api/voice-users/server/handlers/joinVoiceUser.js b/bigbluebutton-html5/imports/api/voice-users/server/handlers/joinVoiceUser.js index 5831c418b214e16f7d4883cd0bf89e7c5fb1fff0..90c7ea81b9c485f7c8bf7abfe4821e392ab718e0 100755 --- a/bigbluebutton-html5/imports/api/voice-users/server/handlers/joinVoiceUser.js +++ b/bigbluebutton-html5/imports/api/voice-users/server/handlers/joinVoiceUser.js @@ -1,8 +1,9 @@ import { check } from 'meteor/check'; import Users from '/imports/api/users'; -import addUser from '/imports/api/users/server/modifiers/addUser'; +import addDialInUser from '/imports/api/users/server/modifiers/addDialInUser'; import addVoiceUser from '../modifiers/addVoiceUser'; + export default function handleJoinVoiceUser({ body }, meetingId) { const voiceUser = body; voiceUser.joined = true; @@ -23,7 +24,6 @@ export default function handleJoinVoiceUser({ body }, meetingId) { const { intId, - callerName, } = voiceUser; const User = Users.findOne({ @@ -34,26 +34,7 @@ export default function handleJoinVoiceUser({ body }, meetingId) { if (!User) { /* voice-only user - called into the conference */ - - const USER_CONFIG = Meteor.settings.public.user; - const ROLE_VIEWER = USER_CONFIG.role_viewer; - - const voiceOnlyUser = { // web (Users) representation of dial-in user - intId, - extId: intId, // TODO - name: callerName, - role: ROLE_VIEWER.toLowerCase(), - guest: false, - authed: true, - waitingForAcceptance: false, - emoji: 'none', - presenter: false, - locked: false, // TODO - avatar: 'https://bbb-joao.dev.imdt.com.br/client/avatar.png', - clientType: 'dial-in-user', - }; - - addUser(meetingId, voiceOnlyUser); + addDialInUser(meetingId, voiceUser); } return addVoiceUser(meetingId, voiceUser); diff --git a/bigbluebutton-html5/imports/api/voice-users/server/handlers/voiceUsers.js b/bigbluebutton-html5/imports/api/voice-users/server/handlers/voiceUsers.js new file mode 100644 index 0000000000000000000000000000000000000000..4f65ae0f81b404d01d9979c40ddfd0bc6b3e31dd --- /dev/null +++ b/bigbluebutton-html5/imports/api/voice-users/server/handlers/voiceUsers.js @@ -0,0 +1,65 @@ +import { check } from 'meteor/check'; +import VoiceUsers from '/imports/api/voice-users/'; +import Meetings from '/imports/api/meetings'; +import addDialInUser from '/imports/api/users/server/modifiers/addDialInUser'; +import removeVoiceUser from '../modifiers/removeVoiceUser'; +import updateVoiceUser from '../modifiers/updateVoiceUser'; +import addVoiceUser from '../modifiers/addVoiceUser'; + + +export default function handleVoiceUsers({ header, body }) { + const { voiceUsers } = body; + const { meetingId } = header; + + const meeting = Meetings.findOne({ meetingId }); + const usersIds = voiceUsers.map(m => m.intId); + + const voiceUsersIdsToUpdate = VoiceUsers.find({ + meetingId, + intId: { $in: usersIds }, + }).fetch().map(m => m.intId); + + const voiceUsersUpdated = []; + voiceUsers.forEach((voice) => { + if (voiceUsersIdsToUpdate.indexOf(voice.intId) >= 0) { + // user already exist, then update + voiceUsersUpdated.push(updateVoiceUser(meetingId, { + intId: voice.intId, + voiceUserId: voice.voiceUserId, + talking: voice.talking, + muted: voice.muted, + voiceConf: meeting.voiceProp.voiceConf, + joined: true, + })); + } else { + // user doesn't exist yet, then add it + addVoiceUser(meetingId, { + voiceUserId: voice.voiceUserId, + intId: voice.intId, + callerName: voice.callerName, + callerNum: voice.callerNum, + muted: voice.muted, + talking: voice.talking, + callingWith: voice.callingWith, + listenOnly: voice.listenOnly, + voiceConf: meeting.voiceProp.voiceConf, + joined: true, + }); + + addDialInUser(meetingId, voice); + } + }); + + // removing extra users already existing in Mongo + const voiceUsersToRemove = VoiceUsers.find({ + meetingId, + intId: { $nin: usersIds }, + }).fetch(); + voiceUsersToRemove.forEach(user => removeVoiceUser(meetingId, { + voiceConf: meeting.voiceProp.voiceConf, + voiceUserId: user.voiceUserId, + intId: user.intId, + })); + + return voiceUsersUpdated; +} diff --git a/bigbluebutton-html5/imports/startup/client/auth.js b/bigbluebutton-html5/imports/startup/client/auth.js index 4a0abfb71666ab6f45287c7248f6ce80afe2698a..2621468a393f665ea7078bdc833a52e3eb65169a 100755 --- a/bigbluebutton-html5/imports/startup/client/auth.js +++ b/bigbluebutton-html5/imports/startup/client/auth.js @@ -17,7 +17,7 @@ export function joinRouteHandler(nextState, replace, callback) { replace({ pathname: '/error/404' }); callback(); } - + // Old credentials stored in memory were being used when joining a new meeting Auth.clearCredentials(); @@ -66,7 +66,7 @@ export function joinRouteHandler(nextState, replace, callback) { const key = Object.keys(data).shift(); const handledHTML5Parameters = [ - 'html5recordingbot' + 'html5recordingbot', ]; if (handledHTML5Parameters.indexOf(key) === -1) { return acc; @@ -79,7 +79,7 @@ export function joinRouteHandler(nextState, replace, callback) { log('error', `Caught: ${e.message}`); } - return { ...acc, [key]: value}; + return { ...acc, [key]: value }; }, {}) : {}; SessionStorage.setItem(METADATA_KEY, metakeys); @@ -108,7 +108,7 @@ export function joinRouteHandler(nextState, replace, callback) { replace({ pathname: path }); - logger.info(JSON.stringify(clientInfo)); + logger.info(clientInfo); return callback(); }); diff --git a/bigbluebutton-html5/imports/startup/client/base.jsx b/bigbluebutton-html5/imports/startup/client/base.jsx index 659edd7a4621293ddc588657b2963412c6fef777..afb5e63c05a41ab7bc16ff6ed9341991322c074c 100755 --- a/bigbluebutton-html5/imports/startup/client/base.jsx +++ b/bigbluebutton-html5/imports/startup/client/base.jsx @@ -13,14 +13,20 @@ import logger from '/imports/startup/client/logger'; import Users from '/imports/api/users'; import Annotations from '/imports/api/annotations'; import AnnotationsLocal from '/imports/ui/components/whiteboard/service'; +import GroupChat from '/imports/api/group-chat'; import IntlStartup from './intl'; +const CHAT_CONFIG = Meteor.settings.public.chat; +const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id; +const PUBLIC_CHAT_TYPE = CHAT_CONFIG.type_public; + const propTypes = { error: PropTypes.object, errorCode: PropTypes.number, subscriptionsReady: PropTypes.bool.isRequired, locale: PropTypes.string, endedCode: PropTypes.string, + approved: PropTypes.bool, }; const defaultProps = { @@ -28,6 +34,7 @@ const defaultProps = { errorCode: undefined, locale: undefined, endedCode: undefined, + approved: undefined, }; class Base extends Component { @@ -77,6 +84,7 @@ class Base extends Component { } if (error || errorCode) { + logger.error(`User could not log in HTML5, hit ${errorCode}`); return (<ErrorScreen code={errorCode}>{error}</ErrorScreen>); } @@ -85,6 +93,10 @@ class Base extends Component { } // this.props.annotationsHandler.stop(); + if (subscriptionsReady) { + logger.info('Client loaded successfully'); + } + return (<AppContainer {...this.props} baseControls={stateControls} />); } @@ -105,9 +117,9 @@ Base.propTypes = propTypes; Base.defaultProps = defaultProps; const SUBSCRIPTIONS_NAME = [ - 'users', 'chat', 'meetings', 'polls', 'presentations', + 'users', 'meetings', 'polls', 'presentations', 'slides', 'captions', 'breakouts', 'voiceUsers', 'whiteboard-multi-user', 'screenshare', - 'presentation-pods', + 'group-chat', 'presentation-pods', ]; const BaseContainer = withRouter(withTracker(({ params, router }) => { @@ -115,6 +127,7 @@ const BaseContainer = withRouter(withTracker(({ params, router }) => { const { locale } = Settings.application; const { credentials, loggedIn } = Auth; + const { meetingId, requesterUserId } = credentials; if (!loggedIn) { return { @@ -133,6 +146,21 @@ const BaseContainer = withRouter(withTracker(({ params, router }) => { const subscriptionsHandlers = SUBSCRIPTIONS_NAME.map(name => Meteor.subscribe(name, credentials, subscriptionErrorHandler)); + const chats = GroupChat.find({ + $or: [ + { + meetingId, + access: PUBLIC_CHAT_TYPE, + chatId: { $ne: PUBLIC_GROUP_CHAT_ID }, + }, + { meetingId, users: { $all: [requesterUserId] } }, + ], + }).fetch(); + + const chatIds = chats.map(chat => chat.chatId); + + const groupChatMessageHandler = Meteor.subscribe('group-chat-msg', credentials, chatIds, subscriptionErrorHandler); + const annotationsHandler = Meteor.subscribe('annotations', credentials, { onReady: () => { AnnotationsLocal.remove({}); @@ -154,6 +182,7 @@ const BaseContainer = withRouter(withTracker(({ params, router }) => { locale, subscriptionsReady, annotationsHandler, + groupChatMessageHandler, }; })(Base)); diff --git a/bigbluebutton-html5/imports/startup/client/intl.jsx b/bigbluebutton-html5/imports/startup/client/intl.jsx index 5b5c1b7e6a9821291c8ffc209902373c699b63f6..3602f33a625a184b04068b8f11fbb8c8d49c2d0f 100644 --- a/bigbluebutton-html5/imports/startup/client/intl.jsx +++ b/bigbluebutton-html5/imports/startup/client/intl.jsx @@ -2,13 +2,10 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { IntlProvider } from 'react-intl'; import Settings from '/imports/ui/services/settings'; +import LoadingScreen from '/imports/ui/components/loading-screen/component'; const propTypes = { - locale: PropTypes.string.isRequired, - baseControls: PropTypes.shape({ - updateErrorState: PropTypes.func.isRequired, - updateLoadingState: PropTypes.func.isRequired, - }).isRequired, + locale: PropTypes.string, children: PropTypes.element.isRequired, }; @@ -24,7 +21,8 @@ class IntlStartup extends Component { this.state = { messages: {}, - locale: DEFAULT_LANGUAGE, + normalizedLocale: null, + fetching: false, }; this.fetchLocalizedMessages = this.fetchLocalizedMessages.bind(this); @@ -34,7 +32,9 @@ class IntlStartup extends Component { } componentWillUpdate(nextProps) { - if (nextProps.locale && this.props.locale !== nextProps.locale) { + if (!this.state.fetching + && this.state.normalizedLocale + && nextProps.locale.toLowerCase() !== this.state.normalizedLocale.toLowerCase()) { this.fetchLocalizedMessages(nextProps.locale); } } @@ -42,37 +42,34 @@ class IntlStartup extends Component { fetchLocalizedMessages(locale) { const url = `/html5client/locale?locale=${locale}`; - const { baseControls } = this.props; + this.setState({ fetching: true }, () => { + fetch(url) + .then((response) => { + if (!response.ok) { + return Promise.reject(); + } - baseControls.updateLoadingState(true); - fetch(url) - .then((response) => { - if (!response.ok) { - return Promise.reject(); - } - - return response.json(); - }) - .then(({ messages, normalizedLocale }) => { - const dasherizedLocale = normalizedLocale.replace('_', '-') - this.setState({ messages, locale: dasherizedLocale }, () => { - Settings.application.locale = dasherizedLocale; - Settings.save(); - baseControls.updateLoadingState(false); - }); - }) - .catch((messages) => { - this.setState({ locale: DEFAULT_LANGUAGE }, () => { - Settings.application.locale = DEFAULT_LANGUAGE; - Settings.save(); - baseControls.updateLoadingState(false); + return response.json(); + }) + .then(({ messages, normalizedLocale }) => { + const dasherizedLocale = normalizedLocale.replace('_', '-'); + this.setState({ messages, fetching: false, normalizedLocale: dasherizedLocale }, () => { + Settings.application.locale = dasherizedLocale; + Settings.save(); + }); + }) + .catch(() => { + this.setState({ fetching: false, normalizedLocale: null }, () => { + Settings.application.locale = DEFAULT_LANGUAGE; + Settings.save(); + }); }); - }); + }); } render() { - return ( - <IntlProvider locale={this.state.locale} messages={this.state.messages}> + return this.state.fetching ? <LoadingScreen /> : ( + <IntlProvider locale={this.state.normalizedLocale} messages={this.state.messages}> {this.props.children} </IntlProvider> ); diff --git a/bigbluebutton-html5/imports/startup/client/logger.js b/bigbluebutton-html5/imports/startup/client/logger.js index a84e70bc4e064589cedb6841fe683df8d27ea6b1..6b8dc02b9be3021b9a7c282b68db1eb7c12fc4a6 100755 --- a/bigbluebutton-html5/imports/startup/client/logger.js +++ b/bigbluebutton-html5/imports/startup/client/logger.js @@ -16,25 +16,31 @@ import { nameFromLevel } from '@browser-bunyan/levels'; // externalURL is the end-point that logs will be sent to // Call the logger by doing a function call with the level name, I.e, logger.warn('Hi on warn') -const LOG_CONFIG = Meteor.settings.public.log || {}; -const { fullInfo } = Auth; +const LOG_CONFIG = Meteor.settings.public.clientLog || { console: { enabled: true, level: 'info' } }; // Custom stream that logs to an end-point class ServerLoggerStream extends ServerStream { write(rec) { + const { fullInfo } = Auth; + + this.rec = rec; if (fullInfo.meetingId != null) { - rec.clientInfo = fullInfo; + this.rec.clientInfo = fullInfo; } - return super.write(rec); + return super.write(this.rec); } } + // Custom stream to log to the meteor server class MeteorStream { write(rec) { + const { fullInfo } = Auth; + + this.rec = rec; if (fullInfo.meetingId != null) { - Meteor.call('logClient', nameFromLevel[rec.level], rec.msg, fullInfo); + Meteor.call('logClient', nameFromLevel[this.rec.level], this.rec.msg, fullInfo); } else { - Meteor.call('logClient', nameFromLevel[rec.level], rec.msg); + Meteor.call('logClient', nameFromLevel[this.rec.level], this.rec.msg); } } } @@ -55,16 +61,23 @@ function createStreamForTarget(target, options) { case TARGET_SERVER: Stream = MeteorStream; break; + default: + Stream = ConsoleFormattedStream; } return new Stream(options); } function generateLoggerStreams(config) { - return config.map(({ target, level, ...streamOptions }) => ({ - level, - stream: createStreamForTarget(target, streamOptions), - })); + let result = []; + Object.keys(config).forEach((key) => { + const logOption = config[key]; + if (logOption && logOption.enabled) { + const { level, ...streamOptions } = logOption; + result = result.concat({ level, stream: createStreamForTarget(key, streamOptions) }); + } + }); + return result; } // Creates the logger with the array of streams of the chosen targets diff --git a/bigbluebutton-html5/imports/startup/server/logger.js b/bigbluebutton-html5/imports/startup/server/logger.js old mode 100644 new mode 100755 index 98e3d2f12e9ccc447c402275ff46874556232f25..e9eacdd43fe8fdf5ad7697befcedc016ad245d80 --- a/bigbluebutton-html5/imports/startup/server/logger.js +++ b/bigbluebutton-html5/imports/startup/server/logger.js @@ -17,7 +17,7 @@ Logger.configure({ }); Meteor.startup(() => { - const LOG_CONFIG = Meteor.settings.private.log || {}; + const LOG_CONFIG = Meteor.settings.private.serverLog || {}; const { level } = LOG_CONFIG; // console logging @@ -28,7 +28,6 @@ Meteor.startup(() => { handleExceptions: true, level, }); - }); export default Logger; diff --git a/bigbluebutton-html5/imports/startup/server/redis.js b/bigbluebutton-html5/imports/startup/server/redis.js index 613c28bd740f5f86610d01b7a46bb348d4260fe3..768402d784b1963a8e939ae64762b3c9e397ebce 100644 --- a/bigbluebutton-html5/imports/startup/server/redis.js +++ b/bigbluebutton-html5/imports/startup/server/redis.js @@ -105,8 +105,9 @@ class RedisPubSub { this.config = config; this.didSendRequestEvent = false; - this.pub = Redis.createClient(); - this.sub = Redis.createClient(); + const redisHost = process.env.REDIS_HOST || Meteor.settings.private.redis.host; + this.pub = Redis.createClient(Meteor.settings.private.redis.port, redisHost); + this.sub = Redis.createClient(Meteor.settings.private.redis.port, redisHost); this.emitter = new EventEmitter2(); this.mettingsQueues = {}; diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx index 207091601ecdcda6be570cfe415db4d02d11ab6b..ed7a2222bbd15a22a0202518f367b1e330951c40 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx @@ -1,7 +1,6 @@ import React from 'react'; import cx from 'classnames'; import { styles } from './styles.scss'; -import EmojiSelect from './emoji-select/component'; import DesktopShare from './desktop-share/component'; import ActionsDropdown from './actions-dropdown/component'; import AudioControlsContainer from '../audio/audio-controls/container'; @@ -16,9 +15,6 @@ class ActionsBar extends React.PureComponent { handleShareScreen, handleUnshareScreen, isVideoBroadcasting, - emojiList, - emojiSelected, - handleEmojiChange, isUserModerator, recordSettingsList, toggleRecording, @@ -55,13 +51,12 @@ class ActionsBar extends React.PureComponent { handleCloseVideo={handleExitVideo} /> : null} - <EmojiSelect options={emojiList} selected={emojiSelected} onChange={handleEmojiChange} /> <DesktopShare {...{ -handleShareScreen, - handleUnshareScreen, - isVideoBroadcasting, - isUserPresenter, -}} + handleShareScreen, + handleUnshareScreen, + isVideoBroadcasting, + isUserPresenter, + }} /> </div> </div> diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx index ca2f85b1c1a5f6a6ba9688fdb452254e9f6375e8..ce90a94c153c5783f6e67c2fb1f101597baed895 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx @@ -10,12 +10,9 @@ const ActionsBarContainer = props => <ActionsBar {...props} />; export default withTracker(() => ({ isUserPresenter: Service.isUserPresenter(), isUserModerator: Service.isUserModerator(), - emojiList: Service.getEmojiList(), - emojiSelected: Service.getEmoji(), - handleEmojiChange: Service.setEmoji, handleExitVideo: () => VideoService.exitVideo(), handleJoinVideo: () => VideoService.joinVideo(), - handleShareScreen: () => shareScreen(), + handleShareScreen: (onFail) => shareScreen(onFail), handleUnshareScreen: () => unshareScreen(), isVideoBroadcasting: isVideoBroadcasting(), recordSettingsList: Service.recordSettingsList(), diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/desktop-share/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/desktop-share/component.jsx index 86e977deb875bdbdb329e7346a0aef9ccf8a22ae..7329d4774cc7afead29ce78a3198c97533577952 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/desktop-share/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/desktop-share/component.jsx @@ -3,6 +3,8 @@ import PropTypes from 'prop-types'; import { defineMessages, injectIntl, intlShape } from 'react-intl'; import browser from 'browser-detect'; import Button from '/imports/ui/components/button/component'; +import logger from '/imports/startup/client/logger'; +import { notify } from '/imports/ui/services/notification'; import { styles } from '../styles'; const propTypes = { @@ -30,13 +32,19 @@ const intlMessages = defineMessages({ id: 'app.actionsBar.actionsDropdown.stopDesktopShareDesc', description: 'adds context to stop desktop share option', }, - + iceConnectionStateError: { + id: 'app.deskshare.iceConnectionStateError', + description: 'Error message for ice connection state failure', + }, }); const BROWSER_RESULTS = browser(); -const isMobileBrowser = BROWSER_RESULTS.mobile || - BROWSER_RESULTS.os.includes('Android'); // mobile flag doesn't always work +const isMobileBrowser = (BROWSER_RESULTS ? BROWSER_RESULTS.mobile : false) || + (BROWSER_RESULTS && BROWSER_RESULTS.os ? + BROWSER_RESULTS.os.includes('Android') : // mobile flag doesn't always work + false); const screenSharingCheck = Meteor.settings.public.kurento.enableScreensharing; +const ICE_CONNECTION_FAILED = 'ICE connection failed'; const DesktopShare = ({ intl, @@ -44,25 +52,36 @@ const DesktopShare = ({ handleUnshareScreen, isVideoBroadcasting, isUserPresenter, -}) => ( - (screenSharingCheck && !isMobileBrowser && isUserPresenter ? +}) => { + const onFail = (error) => { + switch (error) { + case ICE_CONNECTION_FAILED: + kurentoExitScreenShare(); + logger.error('Ice connection state error'); + notify(intl.formatMessage(intlMessages.iceConnectionStateError), 'error', 'desktop'); + break; + default: + logger.error(error || 'Default error handler'); + } + }; + return (screenSharingCheck && !isMobileBrowser && isUserPresenter ? <Button className={styles.button} - icon="desktop" + icon={isVideoBroadcasting ? 'desktop_off' : 'desktop'} label={intl.formatMessage(isVideoBroadcasting ? intlMessages.stopDesktopShareLabel : intlMessages.desktopShareLabel)} description={intl.formatMessage(isVideoBroadcasting ? intlMessages.stopDesktopShareDesc : intlMessages.desktopShareDesc)} - color="primary" + color={isVideoBroadcasting ? 'danger' : 'primary'} ghost={false} hideLabel circle size="lg" - onClick={isVideoBroadcasting ? handleUnshareScreen : handleShareScreen} + onClick={isVideoBroadcasting ? handleUnshareScreen : () => handleShareScreen(onFail)} id={isVideoBroadcasting ? 'unshare-screen-button' : 'share-screen-button'} /> - : null) -); + : null); +}; DesktopShare.propTypes = propTypes; export default injectIntl(DesktopShare); diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-select/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-select/component.jsx deleted file mode 100755 index 59931acdf3361ae3730f37d48e0211fd6cd77aec..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-select/component.jsx +++ /dev/null @@ -1,105 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { defineMessages, intlShape, injectIntl } from 'react-intl'; - -import Button from '/imports/ui/components/button/component'; -import Dropdown from '/imports/ui/components/dropdown/component'; -import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component'; -import DropdownContent from '/imports/ui/components/dropdown/content/component'; -import DropdownList from '/imports/ui/components/dropdown/list/component'; -import DropdownListItem from '/imports/ui/components/dropdown/list/item/component'; -import DropdownListSeparator from '/imports/ui/components/dropdown/list/separator/component'; -import { styles } from '../styles'; - -const intlMessages = defineMessages({ - statusTriggerLabel: { - id: 'app.actionsBar.emojiMenu.statusTriggerLabel', - description: 'Emoji status button label', - }, - changeStatusLabel: { - id: 'app.actionsBar.changeStatusLabel', - description: 'Aria-label for emoji status button', - }, - currentStatusDesc: { - id: 'app.actionsBar.currentStatusDesc', - description: 'Aria description for status button', - }, -}); - -const propTypes = { - intl: intlShape.isRequired, - options: PropTypes.objectOf(PropTypes.string).isRequired, - selected: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, -}; - -const SHORTCUTS_CONFIG = Meteor.settings.public.app.shortcuts; -const OPEN_STATUS_AK = SHORTCUTS_CONFIG.openStatus.accesskey; - -const EmojiSelect = ({ - intl, - options, - selected, - onChange, -}) => { - const statuses = Object.keys(options); - const lastStatus = statuses.pop(); - - const statusLabel = intl.formatMessage(intlMessages.statusTriggerLabel); - - return ( - <Dropdown autoFocus> - <DropdownTrigger tabIndex={0}> - <Button - className={styles.button} - label={statusLabel} - aria-label={statusLabel} - aria-describedby="currentStatus" - icon={options[selected !== lastStatus ? selected : statuses[1]]} - ghost={false} - hideLabel - circle - size="lg" - color="primary" - onClick={() => null} - accessKey={OPEN_STATUS_AK} - > - <div id="currentStatus" hidden> - { intl.formatMessage(intlMessages.currentStatusDesc, { 0: selected }) } - </div> - </Button> - </DropdownTrigger> - <DropdownContent placement="top left"> - <DropdownList> - { - statuses.map(status => ( - <DropdownListItem - key={status} - className={status === selected ? styles.emojiSelected : null} - icon={options[status]} - label={intl.formatMessage({ id: `app.actionsBar.emojiMenu.${status}Label` })} - description={intl.formatMessage({ id: `app.actionsBar.emojiMenu.${status}Desc` })} - onClick={() => onChange(status)} - tabIndex={-1} - /> - )) - .concat( - <DropdownListSeparator key={-1} />, - <DropdownListItem - key={lastStatus} - icon={options[lastStatus]} - label={intl.formatMessage({ id: `app.actionsBar.emojiMenu.${lastStatus}Label` })} - description={intl.formatMessage({ id: `app.actionsBar.emojiMenu.${lastStatus}Desc` })} - onClick={() => onChange(lastStatus)} - tabIndex={-1} - />, - ) - } - </DropdownList> - </DropdownContent> - </Dropdown> - ); -}; - -EmojiSelect.propTypes = propTypes; -export default injectIntl(EmojiSelect); diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/service.js b/bigbluebutton-html5/imports/ui/components/actions-bar/service.js index 844b7f296634a5539db020ae6e945591f891c511..3eb815607df72101052fe4369cc746cb133dbbc1 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/service.js +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/service.js @@ -1,14 +1,10 @@ import Auth from '/imports/ui/services/auth'; import Users from '/imports/api/users'; import { makeCall } from '/imports/ui/services/api'; -import { EMOJI_STATUSES } from '/imports/utils/statuses'; import Meetings from '/imports/api/meetings'; export default { isUserPresenter: () => Users.findOne({ userId: Auth.userID }).presenter, - getEmoji: () => Users.findOne({ userId: Auth.userID }).emoji, - setEmoji: status => makeCall('setEmojiStatus', Auth.userID, status), - getEmojiList: () => EMOJI_STATUSES, isUserModerator: () => Users.findOne({ userId: Auth.userID }).moderator, recordSettingsList: () => Meetings.findOne({ meetingId: Auth.meetingID }).recordProp, toggleRecording: () => makeCall('toggleRecording'), diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss b/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss index 5abb5ba57052801eddefe421d197374aad621f8e..4aa485dd4231b76a85cab34a97918feac94412f2 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss @@ -44,9 +44,3 @@ box-shadow: 0 2px 5px 0 rgb(0, 0, 0); } } - -.emojiSelected { - span, i { - color: $color-primary; - } -} diff --git a/bigbluebutton-html5/imports/ui/components/app/component.jsx b/bigbluebutton-html5/imports/ui/components/app/component.jsx index b958607b2ac56863c791dc74894bf392639f03b9..d317789f84d750057cdab5436f3ddce2b5c5fe16 100755 --- a/bigbluebutton-html5/imports/ui/components/app/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/component.jsx @@ -10,7 +10,7 @@ import ToastContainer from '../toast/container'; import ModalContainer from '../modal/container'; import NotificationsBarContainer from '../notifications-bar/container'; import AudioContainer from '../audio/container'; -import ChatNotificationContainer from '../chat/notification/container'; +import ChatAlertContainer from '../chat/alert/container'; import { styles } from './styles'; const MOBILE_MEDIA = 'only screen and (max-width: 40em)'; @@ -81,8 +81,12 @@ class App extends Component { const BROWSER_RESULTS = browser(); const body = document.getElementsByTagName('body')[0]; - body.classList.add(`browser-${BROWSER_RESULTS.name}`); - body.classList.add(`os-${BROWSER_RESULTS.os.split(' ').shift().toLowerCase()}`); + if (BROWSER_RESULTS && BROWSER_RESULTS.name) { + body.classList.add(`browser-${BROWSER_RESULTS.name}`); + } + if (BROWSER_RESULTS && BROWSER_RESULTS.os) { + body.classList.add(`os-${BROWSER_RESULTS.os.split(' ').shift().toLowerCase()}`); + } this.handleWindowResize(); window.addEventListener('resize', this.handleWindowResize, false); @@ -315,7 +319,7 @@ class App extends Component { <ModalContainer /> <AudioContainer /> <ToastContainer /> - <ChatNotificationContainer currentChatID={params.chatID} /> + <ChatAlertContainer currentChatID={params.chatID} /> </main> ); } diff --git a/bigbluebutton-html5/imports/ui/components/app/container.jsx b/bigbluebutton-html5/imports/ui/components/app/container.jsx index 8548d1969ad90f0cf8a033166a50268c7bc91a8e..2f64c668357fc59503ffc4e67fe99a522e7934ee 100755 --- a/bigbluebutton-html5/imports/ui/components/app/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/container.jsx @@ -7,6 +7,7 @@ import Auth from '/imports/ui/services/auth'; import Users from '/imports/api/users'; import Breakouts from '/imports/api/breakouts'; import Meetings from '/imports/api/meetings'; +import logger from '/imports/startup/client/logger'; import ClosedCaptionsContainer from '/imports/ui/components/closed-captions/container'; @@ -73,6 +74,8 @@ export default withRouter(injectIntl(withModalMounter(withTracker(({ router, int baseControls.updateLoadingState(intl.formatMessage(intlMessages.waitingApprovalMessage)); } + logger.info('User joined meeting and subscribed to data successfully'); + // Check if user is removed out of the session Users.find({ userId: Auth.userID }).observeChanges({ changed(id, fields) { diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx index bd23bdf1e930ae4644b503a53cd9699846bd6bb2..cc9e6a3c83d84f9d7ef8ec705452f51e4ae50469 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx @@ -324,12 +324,15 @@ class AudioModal extends Component { } if (this.skipAudioOptions()) { return ( - <span className={styles.connecting} role="alert"> - {!isEchoTest ? + <div className={styles.connecting} role="alert"> + <span> + {!isEchoTest ? intl.formatMessage(intlMessages.connecting) : intl.formatMessage(intlMessages.connectingEchoTest) } - </span> + </span> + <span className={styles.connectingAnimation} /> + </div> ); } return content ? this.contents[content].component() : this.renderAudioOptions(); diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.scss b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.scss index b6b40d1bdcbb5aefc284232177cd145bf6258e7d..7c76c02db3939da9c0539310cbf5e61412550504 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.scss @@ -132,25 +132,19 @@ font-size: 2rem; } -.connecting:after { - overflow: hidden; - display: inline-block; - vertical-align: bottom; - -webkit-animation: ellipsis steps(4,end) 900ms infinite; - animation: ellipsis steps(4,end) 900ms infinite; - content: "\2026"; /* ascii code for the ellipsis character */ - width: 0; - margin-right: 1.25em; -} - -@keyframes ellipsis { - to { - width: 1.25em; - margin-right: 0; +.connectingAnimation{ + &:after { + overflow: hidden; + display: inline-block; + vertical-align: bottom; + animation: ellipsis steps(4,end) 900ms infinite; + content: "\2026"; /* ascii code for the ellipsis character */ + width: 0; + margin-right: 1.25em; } } -@-webkit-keyframes ellipsis { +@keyframes ellipsis { to { width: 1.25em; margin-right: 0; diff --git a/bigbluebutton-html5/imports/ui/components/chat/notification/audio-notification/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/alert/audio-alert/component.jsx similarity index 88% rename from bigbluebutton-html5/imports/ui/components/chat/notification/audio-notification/component.jsx rename to bigbluebutton-html5/imports/ui/components/chat/alert/audio-alert/component.jsx index 5895a2833a906c77196d62005308cf0e41706945..fad03755afaabd844b4edb50d6dd687b4779f683 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/notification/audio-notification/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/alert/audio-alert/component.jsx @@ -7,7 +7,7 @@ const propTypes = { count: PropTypes.number.isRequired, }; -class ChatAudioNotification extends React.Component { +class ChatAudioAlert extends React.Component { constructor(props) { super(props); this.audio = new Audio(`${Meteor.settings.public.app.basename}/resources/sounds/notify.mp3`); @@ -43,6 +43,6 @@ class ChatAudioNotification extends React.Component { return null; } } -ChatAudioNotification.propTypes = propTypes; +ChatAudioAlert.propTypes = propTypes; -export default ChatAudioNotification; +export default ChatAudioAlert; diff --git a/bigbluebutton-html5/imports/ui/components/chat/notification/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/alert/component.jsx similarity index 92% rename from bigbluebutton-html5/imports/ui/components/chat/notification/component.jsx rename to bigbluebutton-html5/imports/ui/components/chat/alert/component.jsx index b8a79a5052b3c731fa792dabb95fba73f60241fa..b84d4ed0c471afc4fa527e700dfbcf7b2962f543 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/notification/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/alert/component.jsx @@ -3,8 +3,8 @@ import PropTypes from 'prop-types'; import { defineMessages, injectIntl } from 'react-intl'; import _ from 'lodash'; import UnreadMessages from '/imports/ui/services/unread-messages'; -import ChatAudioNotification from './audio-notification/component'; -import ChatPushNotification from './push-notification/component'; +import ChatAudioAlert from './audio-alert/component'; +import ChatPushAlert from './push-alert/component'; import Service from '../service'; import { styles } from '../styles'; @@ -36,7 +36,7 @@ const intlMessages = defineMessages({ const PUBLIC_KEY = 'public'; const PRIVATE_KEY = 'private'; -class ChatNotification extends Component { +class ChatAlert extends Component { constructor(props) { super(props); this.state = { @@ -149,7 +149,7 @@ class ChatNotification extends Component { .filter(({ fromTime, fromUserId }) => fromTime > (this.state.notified[fromUserId] || 0)); const reduceMessages = Service - .reduceAndMapMessages(getChatmessages); + .reduceAndMapGroupMessages(getChatmessages); if (!reduceMessages.length) return null; @@ -157,7 +157,7 @@ class ChatNotification extends Component { .map(msg => this.createMessage(name, msg.content))); const limitingMessages = flatMessages; - return (<ChatPushNotification + return (<ChatPushAlert key={id} chatId={id} content={limitingMessages} @@ -188,7 +188,7 @@ class ChatNotification extends Component { } = this.props; const publicUnread = UnreadMessages.getUnreadMessages(publicUserId); - const publicUnreadReduced = Service.reduceAndMapMessages(publicUnread); + const publicUnreadReduced = Service.reduceAndMapGroupMessages(publicUnread); if (disableNotify) return; if (!Service.hasUnreadMessages(publicUserId)) return; @@ -210,7 +210,7 @@ class ChatNotification extends Component { <span> { chatsNotify.map(({ sender, time, content }) => - (<ChatPushNotification + (<ChatPushAlert key={time} chatId={PUBLIC_KEY} name={sender.name} @@ -249,13 +249,13 @@ class ChatNotification extends Component { return ( <span> - <ChatAudioNotification play={shouldPlayAudio} count={unreadMessagesCount} /> + <ChatAudioAlert play={shouldPlayAudio} count={unreadMessagesCount} /> { this.notifyPublicChat() } { this.notifyPrivateChat() } </span> ); } } -ChatNotification.propTypes = propTypes; +ChatAlert.propTypes = propTypes; -export default injectIntl(ChatNotification); +export default injectIntl(ChatAlert); diff --git a/bigbluebutton-html5/imports/ui/components/chat/notification/container.jsx b/bigbluebutton-html5/imports/ui/components/chat/alert/container.jsx similarity index 53% rename from bigbluebutton-html5/imports/ui/components/chat/notification/container.jsx rename to bigbluebutton-html5/imports/ui/components/chat/alert/container.jsx index 08e93234c9c3d6a2ffa110d186ca8026bddfab0e..b0bee3dd6be4cc6ed32f43f4d3adb455ef42f4a0 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/notification/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/alert/container.jsx @@ -2,10 +2,10 @@ import React from 'react'; import { withTracker } from 'meteor/react-meteor-data'; import UserListService from '/imports/ui/components/user-list/service'; import Settings from '/imports/ui/services/settings'; -import ChatNotification from './component'; +import ChatAlert from './component'; -const ChatNotificationContainer = props => ( - <ChatNotification {...props} /> +const ChatAlertContainer = props => ( + <ChatAlert {...props} /> ); export default withTracker(() => { @@ -13,9 +13,9 @@ export default withTracker(() => { const openChats = UserListService.getOpenChats(); return { - disableAudio: !AppSettings.chatAudioNotifications, - disableNotify: !AppSettings.chatPushNotifications, + disableAudio: !AppSettings.chatAudioAlerts, + disableNotify: !AppSettings.chatPushAlerts, openChats, - publicUserId: Meteor.settings.public.chat.public_userid, + publicUserId: Meteor.settings.public.chat.public_group_id, }; -})(ChatNotificationContainer); +})(ChatAlertContainer); diff --git a/bigbluebutton-html5/imports/ui/components/chat/notification/push-notification/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/alert/push-alert/component.jsx similarity index 64% rename from bigbluebutton-html5/imports/ui/components/chat/notification/push-notification/component.jsx rename to bigbluebutton-html5/imports/ui/components/chat/alert/push-alert/component.jsx index 78ad2d0e091d826f75989b1050f24d031b11c3a4..19a0ddc66cee0dd7181a2969b0ecd1bfb2a01d2d 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/notification/push-notification/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/alert/push-alert/component.jsx @@ -5,15 +5,15 @@ import injectNotify from '/imports/ui/components/toast/inject-notify/component'; import { Link } from 'react-router'; import { styles } from '../../styles.scss'; -const NOTIFICATION_INTERVAL = 2000; // 2 seconds -const NOTIFICATION_LIFETIME = 4000; // 4 seconds +const ALERT_INTERVAL = 2000; // 2 seconds +const ALERT_LIFETIME = 4000; // 4 seconds const propTypes = { notify: PropTypes.func.isRequired, onOpen: PropTypes.func.isRequired, }; -class ChatPushNotification extends React.Component { +class ChatPushAlert extends React.Component { static link(message, chatId) { return ( <Link className={styles.link} to={`/users/chat/${chatId}`}> @@ -24,7 +24,7 @@ class ChatPushNotification extends React.Component { constructor(props) { super(props); - this.showNotify = _.debounce(this.showNotify.bind(this), NOTIFICATION_INTERVAL); + this.showNotify = _.debounce(this.showNotify.bind(this), ALERT_INTERVAL); this.componentDidMount = this.showNotify; this.componentDidUpdate = this.showNotify; @@ -40,11 +40,11 @@ class ChatPushNotification extends React.Component { } = this.props; return notify( - ChatPushNotification.link(message, chatId), + ChatPushAlert.link(message, chatId), 'info', 'chat', - { onOpen, autoClose: NOTIFICATION_LIFETIME }, - ChatPushNotification.link(content, chatId), + { onOpen, autoClose: ALERT_LIFETIME }, + ChatPushAlert.link(content, chatId), true, ); } @@ -53,6 +53,6 @@ class ChatPushNotification extends React.Component { return null; } } -ChatPushNotification.propTypes = propTypes; +ChatPushAlert.propTypes = propTypes; -export default injectNotify(ChatPushNotification); +export default injectNotify(ChatPushAlert); diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx index 45d3a7d68cf6e4c916199f0135c5c4ed8bd32562..6de469e25dc678122815eed3dc834590d4ca7e4a 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-dropdown/component.jsx @@ -53,7 +53,7 @@ class ChatDropdown extends Component { componentDidMount() { this.clipboard = new Clipboard('#clipboardButton', { - text: () => ChatService.exportChat(ChatService.getPublicMessages()), + text: () => ChatService.exportChat(ChatService.getPublicGroupMessages()), }); } @@ -93,7 +93,7 @@ class ChatDropdown extends Component { link.setAttribute( 'href', `data: ${mimeType} ;charset=utf-8, - ${encodeURIComponent(ChatService.exportChat(ChatService.getPublicMessages()))}`, + ${encodeURIComponent(ChatService.exportChat(ChatService.getPublicGroupMessages()))}`, ); link.click(); }} diff --git a/bigbluebutton-html5/imports/ui/components/chat/container.jsx b/bigbluebutton-html5/imports/ui/components/chat/container.jsx index 37d2e99eecadf6e1c18573b4dc2d996abbae99b4..0168bb74d9090f1f6e3c645d02d9eb81cc1690f6 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/container.jsx @@ -1,4 +1,4 @@ -import React, {Component} from 'react'; +import React, { Component } from 'react'; import { defineMessages, injectIntl } from 'react-intl'; import { withTracker } from 'meteor/react-meteor-data'; import Chat from './component'; @@ -52,9 +52,10 @@ export default injectIntl(withTracker(({ params, intl }) => { let systemMessageIntl = {}; if (chatID === PUBLIC_CHAT_KEY) { - messages = ChatService.reduceAndMapMessages((ChatService.getPublicMessages())); + messages = ChatService.reduceAndMapGroupMessages(ChatService.getPublicGroupMessages()); } else { - messages = ChatService.getPrivateMessages(chatID); + messages = ChatService.getPrivateGroupMessages(chatID); + const user = ChatService.getUser(chatID); chatName = user.name; systemMessageIntl = { 0: user.name }; @@ -115,7 +116,7 @@ export default injectIntl(withTracker(({ params, intl }) => { handleSendMessage: (message) => { ChatService.updateScrollPosition(chatID, null); - return ChatService.sendMessage(chatID, message); + return ChatService.sendGroupMessage(chatID, message); }, handleScrollUpdate: position => ChatService.updateScrollPosition(chatID, position), diff --git a/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/styles.scss b/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/styles.scss index 3666f2d5d4dee7fc7180e7ac299827ccb5410294..4fb0147dbee1df87b3570f6551576e1bc2bd90f5 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/styles.scss @@ -1,7 +1,7 @@ @import "/imports/ui/stylesheets/variables/_all"; .item { - font-size: $font-size-base * .90; + font-size: $font-size-base; margin-bottom: $line-height-computed; &:last-child { diff --git a/bigbluebutton-html5/imports/ui/components/chat/service.js b/bigbluebutton-html5/imports/ui/components/chat/service.js index 085e0c2eec607ca0ea4078fee35ed1f45db73715..a203aab4981330bb88a28a8dd318b410005ab6c4 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/service.js +++ b/bigbluebutton-html5/imports/ui/components/chat/service.js @@ -1,6 +1,7 @@ -import Chats from '/imports/api/chat'; import Users from '/imports/api/users'; import Meetings from '/imports/api/meetings'; +import GroupChatMsg from '/imports/api/group-chat-msg'; +import GroupChat from '/imports/api/group-chat'; import Auth from '/imports/ui/services/auth'; import UnreadMessages from '/imports/ui/services/unread-messages'; import Storage from '/imports/ui/services/storage/session'; @@ -12,11 +13,11 @@ const CHAT_CONFIG = Meteor.settings.public.chat; const GROUPING_MESSAGES_WINDOW = CHAT_CONFIG.grouping_messages_window; const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system; -const PUBLIC_CHAT_TYPE = CHAT_CONFIG.type_public; const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id; -const PUBLIC_CHAT_USERID = CHAT_CONFIG.public_userid; -const PUBLIC_CHAT_USERNAME = CHAT_CONFIG.public_username; +const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id; +const PRIVATE_CHAT_TYPE = CHAT_CONFIG.type_private; +const PUBLIC_CHAT_USER_ID = CHAT_CONFIG.system_userid; const ScrollCollection = new Mongo.Collection(null); @@ -33,41 +34,38 @@ const getUser = (userId) => { return mapUser(user); }; -const mapMessage = (message) => { +const mapGroupMessage = (message) => { const mappedMessage = { id: message._id, content: message.content, - time: message.fromTime, // + message.from_tz_offset, + time: message.timestamp, sender: null, }; - if (message.type !== SYSTEM_CHAT_TYPE) { - mappedMessage.sender = getUser(message.fromUserId); + if (message.sender !== SYSTEM_CHAT_TYPE) { + mappedMessage.sender = getUser(message.sender); } return mappedMessage; }; -const reduceMessages = (previous, current) => { +const reduceGroupMessages = (previous, current) => { const lastMessage = previous[previous.length - 1]; const currentMessage = current; - currentMessage.content = [{ - id: current._id, + id: current.id, text: current.message, - time: current.fromTime, + time: current.timestamp, }]; - - if (!lastMessage || !currentMessage.type === SYSTEM_CHAT_TYPE) { + if (!lastMessage || !currentMessage.chatId === PUBLIC_GROUP_CHAT_ID) { return previous.concat(currentMessage); } - // Check if the last message is from the same user and time discrepancy // between the two messages exceeds window and then group current message // with the last one const timeOfLastMessage = lastMessage.content[lastMessage.content.length - 1].time; - if (lastMessage.fromUserId === currentMessage.fromUserId - && (currentMessage.fromTime - timeOfLastMessage) <= GROUPING_MESSAGES_WINDOW) { + if (lastMessage.sender === currentMessage.sender + && (currentMessage.timestamp - timeOfLastMessage) <= GROUPING_MESSAGES_WINDOW) { lastMessage.content.push(currentMessage.content.pop()); return previous; } @@ -75,45 +73,56 @@ const reduceMessages = (previous, current) => { return previous.concat(currentMessage); }; -const reduceAndMapMessages = messages => - (messages.reduce(reduceMessages, []).map(mapMessage)); +const reduceAndMapGroupMessages = messages => + (messages.reduce(reduceGroupMessages, []).map(mapGroupMessage)); -const getPublicMessages = () => { - const publicMessages = Chats.find({ - type: { $in: [PUBLIC_CHAT_TYPE, SYSTEM_CHAT_TYPE] }, +const getPublicGroupMessages = () => { + const publicGroupMessages = GroupChatMsg.find({ + chatId: PUBLIC_GROUP_CHAT_ID, }, { - sort: ['fromTime'], + sort: ['timestamp'], }).fetch(); - return publicMessages; + return publicGroupMessages; }; -const getPrivateMessages = (userID) => { - const messages = Chats.find({ - toUsername: { $ne: PUBLIC_CHAT_USERNAME }, - $or: [ - { toUserId: userID }, - { fromUserId: userID }, - ], - }, { - sort: ['fromTime'], - }).fetch(); - return reduceAndMapMessages(messages); +const getPrivateGroupMessages = (chatID) => { + const sender = getUser(Auth.userID); + + const privateChat = GroupChat.findOne({ + users: { $all: [chatID, sender.id] }, + access: PRIVATE_CHAT_TYPE, + }); + + let messages = []; + + if (privateChat) { + const { + chatId, + } = privateChat; + + messages = GroupChatMsg.find({ + chatId, + }, { + sort: ['timestamp'], + }).fetch(); + } + + return reduceAndMapGroupMessages(messages, []); }; const isChatLocked = (receiverID) => { const isPublic = receiverID === PUBLIC_CHAT_ID; const meeting = Meetings.findOne({}); - const user = Users.findOne({}); + const user = Users.findOne({ userId: Auth.userID }); if (meeting.lockSettingsProp !== undefined) { const isPubChatLocked = meeting.lockSettingsProp.disablePubChat; const isPrivChatLocked = meeting.lockSettingsProp.disablePrivChat; - const isViewer = user.role === 'VIEWER'; - return (isPublic && isPubChatLocked && isViewer && user.locked) - || (!isPublic && isPrivChatLocked && isViewer && user.locked); + return mapUser(user).isLocked && + ((isPublic && isPubChatLocked) || (!isPublic && isPrivChatLocked)); } return false; @@ -121,37 +130,44 @@ const isChatLocked = (receiverID) => { const hasUnreadMessages = (receiverID) => { const isPublic = receiverID === PUBLIC_CHAT_ID; - const chatType = isPublic ? PUBLIC_CHAT_USERID : receiverID; + const chatType = isPublic ? PUBLIC_GROUP_CHAT_ID : receiverID; return UnreadMessages.count(chatType) > 0; }; const lastReadMessageTime = (receiverID) => { const isPublic = receiverID === PUBLIC_CHAT_ID; - const chatType = isPublic ? PUBLIC_CHAT_USERID : receiverID; + const chatType = isPublic ? PUBLIC_GROUP_CHAT_ID : receiverID; return UnreadMessages.get(chatType); }; -const sendMessage = (receiverID, message) => { - const isPublic = receiverID === PUBLIC_CHAT_ID; +const sendGroupMessage = (chatID, message) => { + const isPublicChat = chatID === PUBLIC_CHAT_ID; + + let chatId = PUBLIC_GROUP_CHAT_ID; const sender = getUser(Auth.userID); - const receiver = !isPublic ? getUser(receiverID) : { - id: PUBLIC_CHAT_USERID, - name: PUBLIC_CHAT_USERNAME, - }; - /* FIX: Why we need all this payload to send a message? - * The server only really needs the message, from_userid, to_userid and from_lang - */ - const messagePayload = { + const receiver = !isPublicChat ? getUser(chatID) : { id: chatID }; + + if (!isPublicChat) { + const privateChat = GroupChat.findOne({ users: { $all: [chatID, sender.id] } }); + + if (privateChat) { + const { chatId: privateChatId } = privateChat; + + chatId = privateChatId; + } + } + + const payload = { + color: '0', + correlationId: `${sender.id}-${Date.now()}`, + sender: { + id: sender.id, + name: sender.name, + }, message, - fromUserId: sender.id, - fromUsername: sender.name, - fromTimezoneOffset: (new Date()).getTimezoneOffset(), - toUsername: receiver.name, - toUserId: receiver.id, - fromColor: 0, }; const currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY); @@ -161,7 +177,7 @@ const sendMessage = (receiverID, message) => { Storage.setItem(CLOSED_CHAT_LIST_KEY, _.without(currentClosedChats, receiver.id)); } - return makeCall('sendChat', messagePayload); + return makeCall('sendGroupChatMsg', chatId, payload); }; const getScrollPosition = (receiverID) => { @@ -177,7 +193,7 @@ const updateScrollPosition = const updateUnreadMessage = (receiverID, timestamp) => { const isPublic = receiverID === PUBLIC_CHAT_ID; - const chatType = isPublic ? PUBLIC_CHAT_USERID : receiverID; + const chatType = isPublic ? PUBLIC_GROUP_CHAT_ID : receiverID; return UnreadMessages.update(chatType, timestamp); }; @@ -211,14 +227,15 @@ const htmlDecode = (input) => { // Export the chat as [Hour:Min] user: message const exportChat = messageList => ( messageList.map((message) => { - const date = new Date(message.fromTime); + const date = new Date(message.timestamp); const hour = date.getHours().toString().padStart(2, 0); const min = date.getMinutes().toString().padStart(2, 0); const hourMin = `[${hour}:${min}]`; if (message.type === SYSTEM_CHAT_TYPE) { return `${hourMin} ${message.message}`; } - return `${hourMin} ${message.fromUsername}: ${htmlDecode(message.message)}`; + const userName = message.sender === PUBLIC_CHAT_USER_ID ? '' : `${getUser(message.sender).name} :`; + return `${hourMin} ${userName} ${htmlDecode(message.message)}`; }).join('\n') ); @@ -246,9 +263,9 @@ const getNotified = (chat) => { }; export default { - reduceAndMapMessages, - getPublicMessages, - getPrivateMessages, + reduceAndMapGroupMessages, + getPublicGroupMessages, + getPrivateGroupMessages, getUser, getScrollPosition, hasUnreadMessages, @@ -256,7 +273,7 @@ export default { isChatLocked, updateScrollPosition, updateUnreadMessage, - sendMessage, + sendGroupMessage, closePrivateChat, removeFromClosedChatsSession, exportChat, diff --git a/bigbluebutton-html5/imports/ui/components/chat/styles.scss b/bigbluebutton-html5/imports/ui/components/chat/styles.scss index e33eb5c6fe36e53cbc9fdb064ad67e6520354eea..8889401573e40d5e2d2c7449052493ddaa670344 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/chat/styles.scss @@ -44,6 +44,7 @@ $background-active: darken($color-white, 5%); flex-direction: column; justify-content: space-around; overflow: hidden; + height: 100vh; } .header { diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx index bc6d9d243d32d8affcb811e435877ca9651f6b45..bd868773200d9f6cff8a63d8bff52a56c8ef6c5c 100644 --- a/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx @@ -73,15 +73,15 @@ class Dropdown extends Component { return nextState.isOpen ? screenreaderTrap.trap(this.dropdown) : screenreaderTrap.untrap(); } - componentDidUpdate(prevProps, prevState) { - if (this.state.isOpen && !prevState.isOpen) { - this.props.onShow(); - } + const { + onShow, + onHide, + } = this.props; - if (!this.state.isOpen && prevState.isOpen) { - this.props.onHide(); - } + if (this.state.isOpen && !prevState.isOpen) { onShow(); } + + if (!this.state.isOpen && prevState.isOpen) { onHide(); } } handleShow() { @@ -98,14 +98,17 @@ class Dropdown extends Component { }); } - handleWindowClick(event) { + handleWindowClick() { const triggerElement = findDOMNode(this.trigger); + const contentElement = findDOMNode(this.content); + const closeDropdown = this.props.isOpen && this.state.isOpen && triggerElement.contains(event.target); + const preventHide = this.props.isOpen && contentElement.contains(event.target) || !triggerElement; - if (!triggerElement) return; + if (closeDropdown) { + return this.props.onHide(); + } - if (!this.state.isOpen - || triggerElement === event.target - || triggerElement.contains(event.target)) { + if (contentElement && preventHide) { return; } diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/list/component.jsx b/bigbluebutton-html5/imports/ui/components/dropdown/list/component.jsx index 744331f4197d0bc0d77609c0ed57deebee15aed3..95496e1bad2d39b88a75fd830c92a45cb8405884 100755 --- a/bigbluebutton-html5/imports/ui/components/dropdown/list/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/dropdown/list/component.jsx @@ -6,7 +6,6 @@ import { styles } from './styles'; import ListItem from './item/component'; import ListSeparator from './separator/component'; import ListTitle from './title/component'; -import UserActions from '../../user-list/user-list-content/user-participants/user-list-item/user-action/component'; const propTypes = { /* We should recheck this proptype, sometimes we need to create an container and send to dropdown, @@ -15,8 +14,7 @@ const propTypes = { children: PropTypes.arrayOf((propValue, key, componentName, location, propFullName) => { if (propValue[key].type !== ListItem && propValue[key].type !== ListSeparator && - propValue[key].type !== ListTitle && - propValue[key].type !== UserActions) { + propValue[key].type !== ListTitle) { return new Error(`Invalid prop \`${propFullName}\` supplied to` + ` \`${componentName}\`. Validation failed.`); } diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/list/item/component.jsx b/bigbluebutton-html5/imports/ui/components/dropdown/list/item/component.jsx index cb7f49b57ee9b11d10c8c1431fe3ce57161e4826..41c158f9dadd9f22fd97153df0ccdcec7119ab51 100644 --- a/bigbluebutton-html5/imports/ui/components/dropdown/list/item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/dropdown/list/item/component.jsx @@ -26,11 +26,12 @@ export default class DropdownListItem extends Component { } renderDefault() { - const { icon, label } = this.props; + const { icon, label, iconRight } = this.props; return [ (icon ? <Icon iconName={icon} key="icon" className={styles.itemIcon} /> : null), (<span className={styles.itemLabel} key="label">{label}</span>), + (iconRight ? <Icon iconName={iconRight} key="iconRight" className={styles.iconRight} /> : null), ]; } diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/list/styles.scss b/bigbluebutton-html5/imports/ui/components/dropdown/list/styles.scss index 991fc52f73855432ee8a0f46c0e3df155130b888..06f65f09c8bbb9d8525a60b2f68aeb60cc6b75fd 100755 --- a/bigbluebutton-html5/imports/ui/components/dropdown/list/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/dropdown/list/styles.scss @@ -1,5 +1,8 @@ @import "/imports/ui/stylesheets/variables/_all"; +$more-icon-font-size: 12px; +$more-icon-line-height: 16px; + %list { list-style: none; font-size: $font-size-base; @@ -107,6 +110,7 @@ border-radius: 0.2rem; } + .iconRight, .itemIcon, .itemLabel { color: inherit; @@ -118,12 +122,20 @@ } } +.iconRight, .itemIcon { margin-right: ($line-height-computed / 2); color: $color-text; flex: 0 0; } +.iconRight { + margin-right: -$indicator-padding-left; + margin-left: $sm-padding-x; + font-size: $more-icon-font-size; + line-height: $more-icon-line-height; +} + .itemLabel { color: $color-gray-dark; font-size: 90%; diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx index a2c025fca6a203b3fc71fef70dda650a5ac53869..f9ef5610dba9e01767d1008fb48b889bd7af74ad 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx @@ -11,7 +11,7 @@ import Service from './service'; import NavBar from './component'; const PUBLIC_CONFIG = Meteor.settings.public; -const PUBLIC_CHAT_KEY = PUBLIC_CONFIG.chat.public_id; +const PUBLIC_GROUP_CHAT_ID = PUBLIC_CONFIG.chat.public_group_id; const CLIENT_TITLE = PUBLIC_CONFIG.app.clientTitle; const NavBarContainer = ({ children, ...props }) => ( @@ -45,7 +45,7 @@ export default withRouter(withTracker(({ location, router }) => { return users .map(user => user.id) .filter(userID => userID !== Auth.userID) - .concat(PUBLIC_CHAT_KEY) + .concat(PUBLIC_GROUP_CHAT_ID) .some(receiverID => ChatService.hasUnreadMessages(receiverID)); }; diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/styles.scss b/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/styles.scss index b569419dc2a30586a3be5378971080b33be7d62b..1be766e2959a60b4efd4f25707cb060ef251f6ea 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/styles.scss @@ -30,6 +30,8 @@ overflow: hidden; text-overflow: ellipsis; vertical-align: middle; + max-width: 20vw; + } .recordIndicator { diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx index 66d9ff5458ca768fe6520dbc0a5eb6cc8499eb0e..50f3859de848779befcd699a07df2e32c40a09ca 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx @@ -73,6 +73,14 @@ const intlMessages = defineMessages({ id: 'app.navBar.settingsDropdown.hotkeysDesc', description: 'Describes hotkeys option', }, + helpLabel: { + id: 'app.navBar.settingsDropdown.helpLabel', + description: 'Help options label', + }, + helpDesc: { + id: 'app.navBar.settingsDropdown.helpDesc', + description: 'Describes help option', + }, }); const SHORTCUTS_CONFIG = Meteor.settings.public.app.shortcuts; @@ -93,8 +101,9 @@ class SettingsDropdown extends Component { componentWillMount() { const { intl, mountModal, isAndroid } = this.props; const { fullscreenLabel, fullscreenDesc, fullscreenIcon } = this.checkFullscreen(this.props); + const { showHelpButton: helpButton } = Meteor.settings.public.app; - this.menuItems = [(<DropdownListItem + this.menuItems =_.compact( [(<DropdownListItem key={_.uniqueId('list-item-')} icon={fullscreenIcon} label={fullscreenLabel} @@ -115,9 +124,17 @@ class SettingsDropdown extends Component { description={intl.formatMessage(intlMessages.aboutDesc)} onClick={() => mountModal(<AboutContainer />)} />), + !helpButton ? null : (<DropdownListItem key={_.uniqueId('list-item-')} - icon="about" + icon="help" + label={intl.formatMessage(intlMessages.helpLabel)} + description={intl.formatMessage(intlMessages.helpDesc)} + onClick={() => window.open('https://bigbluebutton.org/videos/')} + />), + (<DropdownListItem + key={_.uniqueId('list-item-')} + icon="shortcuts" label={intl.formatMessage(intlMessages.hotkeysLabel)} description={intl.formatMessage(intlMessages.hotkeysDesc)} onClick={() => mountModal(<ShortcutHelpComponent />)} @@ -130,12 +147,14 @@ class SettingsDropdown extends Component { description={intl.formatMessage(intlMessages.leaveSessionDesc)} onClick={() => mountModal(<LogoutConfirmationContainer />)} />), - ]; + ]) // Removes fullscreen button if not on Android if (!isAndroid) { this.menuItems.shift(); } + + } componentWillReceiveProps(nextProps) { diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/container.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/container.jsx index b068f1faa22bb7df870e252dbb0444ebd516a2ff..6a13b39a6e9d209806b96997dfa2f84dd4bff391 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/container.jsx @@ -53,7 +53,7 @@ export default class SettingsDropdownContainer extends Component { const handleToggleFullscreen = toggleFullScreen; const isFullScreen = this.state.isFullScreen; const result = browser(); - const isAndroid = result.os.includes('Android'); + const isAndroid = (result && result.os) ? result.os.includes('Android') : false; return ( <SettingsDropdown diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/styles.scss b/bigbluebutton-html5/imports/ui/components/nav-bar/styles.scss old mode 100644 new mode 100755 index d9f228f7a6491a93d54a30e8b300cde869a921bf..473924509538585cf89c471cb247a757f0f7eeaa --- a/bigbluebutton-html5/imports/ui/components/nav-bar/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/styles.scss @@ -33,7 +33,8 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - + max-width: 30vw; + > [class^="icon-bbb-"] { font-size: 75%; } diff --git a/bigbluebutton-html5/imports/ui/components/polling/styles.scss b/bigbluebutton-html5/imports/ui/components/polling/styles.scss index d1208f739e5ba11b704e27ee1d752db82f004eae..3a6173c67c397a8a4746a50ca79c77422c39518c 100644 --- a/bigbluebutton-html5/imports/ui/components/polling/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/polling/styles.scss @@ -1,5 +1,11 @@ @import "../../stylesheets/variables/_all"; +// pollingAnswer position offsets +$xs-portrait-offset: 8.75em; +$xs-landscape-offset: 4.75em; +$s-portrait-offset: 11.75em; +$s-landscape-offset: 1.75em; + .pollingContainer { order: 2; width: 100%; @@ -20,12 +26,29 @@ } .pollingAnswers { + align-items: center; display: grid; grid-auto-flow: column; grid-auto-columns: auto; justify-content: center; - align-items: center; + position: relative; width: 100%; + z-index: 1; + + :global(.browser-safari) & { + @include mq($ip5-portrait) { + bottom: $xs-portrait-offset; + } + @include mq($ip678-portrait) { + bottom: $s-portrait-offset; + } + @include mq($ip5-landscape) { + bottom: $xs-landscape-offset; + } + @include mq($ip678-landscape) { + bottom: $s-landscape-offset; + } + } } .pollButtonWrapper { diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.scss b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.scss index d9d863df263d716d28f08487c3be46bb9bc5a6f2..63b7cadc87464cc58a02465a02e1429fd0f99b09 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.scss @@ -3,6 +3,7 @@ $controls-color: $color-gray !default; $controls-background: $color-white !default; +$toolbar-button-border-radius: 5px; .presentationToolbarWrapper, .zoomWrapper { @@ -16,6 +17,7 @@ $controls-background: $color-white !default; position: absolute; bottom: .8rem; box-shadow: 0 0 10px -2px rgba(0, 0, 0, .25); + border-radius: $toolbar-button-border-radius; align-self: center; justify-content: center; z-index: 1; diff --git a/bigbluebutton-html5/imports/ui/components/screenshare/service.js b/bigbluebutton-html5/imports/ui/components/screenshare/service.js index 5c7965dcaa919e317b4784ea75a5634b8a5926a0..99e61aae0b1506660d33dcb707793e1756b87852 100644 --- a/bigbluebutton-html5/imports/ui/components/screenshare/service.js +++ b/bigbluebutton-html5/imports/ui/components/screenshare/service.js @@ -13,8 +13,9 @@ const isVideoBroadcasting = () => { return false; } + const hasStream = ds.screenshare.stream ? true : false; // TODO commented out isPresenter to enable screen viewing to the presenter - return ds.screenshare.stream; // && !PresentationService.isPresenter(); + return hasStream; // && !PresentationService.isPresenter(); } // if remote screenshare has been ended disconnect and hide the video stream @@ -32,8 +33,8 @@ const presenterScreenshareHasStarted = () => { KurentoBridge.kurentoWatchVideo(); } -const shareScreen = () => { - KurentoBridge.kurentoShareScreen(); +const shareScreen = (onFail) => { + KurentoBridge.kurentoShareScreen(onFail); } const unshareScreen = () => { diff --git a/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx b/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx index 010bed8fb264748c57e2727aba33ef6ed2a5d8c6..cbd75e98268a1b093451823559d97b2102a84837 100644 --- a/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx @@ -14,12 +14,12 @@ const intlMessages = defineMessages({ id: 'app.submenu.application.applicationSectionTitle', description: 'Application section title', }, - audioNotifyLabel: { - id: 'app.submenu.application.audioNotifyLabel', + audioAlertLabel: { + id: 'app.submenu.application.audioAlertLabel', description: 'audio notification label', }, - pushNotifyLabel: { - id: 'app.submenu.application.pushNotifyLabel', + pushAlertLabel: { + id: 'app.submenu.application.pushAlertLabel', description: 'push notifiation label', }, fontSizeControlLabel: { @@ -117,14 +117,6 @@ class ApplicationMenu extends BaseMenu { this.handleUpdateSettings('application', obj.settings); } - // Adjust the locale format to be able to display the locale names properly in the client - formatLocale(locale) { - return locale - .split('-') - .map((val, idx) => (idx == 1 ? val.toUpperCase() : val)) - .join('_'); - } - render() { const { availableLocales, intl } = this.props; const { isLargestFontSize, isSmallestFontSize } = this.state; @@ -141,7 +133,7 @@ class ApplicationMenu extends BaseMenu { <div className={styles.col} aria-hidden="true"> <div className={styles.formElement}> <label className={styles.label}> - {intl.formatMessage(intlMessages.audioNotifyLabel)} + {intl.formatMessage(intlMessages.audioAlertLabel)} </label> </div> </div> @@ -149,9 +141,9 @@ class ApplicationMenu extends BaseMenu { <div className={cx(styles.formElement, styles.pullContentRight)}> <Toggle icons={false} - defaultChecked={this.state.settings.chatAudioNotifications} - onChange={() => this.handleToggle('chatAudioNotifications')} - ariaLabel={intl.formatMessage(intlMessages.audioNotifyLabel)} + defaultChecked={this.state.settings.chatAudioAlerts} + onChange={() => this.handleToggle('chatAudioAlerts')} + ariaLabel={intl.formatMessage(intlMessages.audioAlertLabel)} /> </div> </div> @@ -161,7 +153,7 @@ class ApplicationMenu extends BaseMenu { <div className={styles.col} aria-hidden="true"> <div className={styles.formElement}> <label className={styles.label}> - {intl.formatMessage(intlMessages.pushNotifyLabel)} + {intl.formatMessage(intlMessages.pushAlertLabel)} </label> </div> </div> @@ -169,9 +161,9 @@ class ApplicationMenu extends BaseMenu { <div className={cx(styles.formElement, styles.pullContentRight)}> <Toggle icons={false} - defaultChecked={this.state.settings.chatPushNotifications} - onChange={() => this.handleToggle('chatPushNotifications')} - ariaLabel={intl.formatMessage(intlMessages.pushNotifyLabel)} + defaultChecked={this.state.settings.chatPushAlerts} + onChange={() => this.handleToggle('chatPushAlerts')} + ariaLabel={intl.formatMessage(intlMessages.pushAlertLabel)} /> </div> </div> diff --git a/bigbluebutton-html5/imports/ui/components/user-avatar/component.jsx b/bigbluebutton-html5/imports/ui/components/user-avatar/component.jsx old mode 100644 new mode 100755 index 3db4cf1e280ddb0a9261d3fe0c3391b6f7689337..a0ab4b21c1ce9ca26945f538a120a0705488fd97 --- a/bigbluebutton-html5/imports/ui/components/user-avatar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-avatar/component.jsx @@ -38,27 +38,34 @@ const UserAvatar = ({ voice, className, }) => ( - <div - aria-hidden="true" - data-test="userAvatar" - className={cx(styles.avatar, { - [styles.moderator]: moderator, - [styles.presenter]: presenter, - [styles.muted]: muted, - [styles.listenOnly]: listenOnly, - [styles.talking]: (talking && !muted), - [styles.voice]: voice, - }, className)} - style={{ - backgroundColor: color, - color, // We need the same color on both for the border - }} - > - <div className={styles.content}> - {children} + + <div + aria-hidden="true" + data-test="userAvatar" + className={cx(styles.avatar, { + [styles.moderator]: moderator, + [styles.presenter]: presenter, + [styles.muted]: muted, + [styles.listenOnly]: listenOnly, + [styles.voice]: voice, + }, className)} + style={{ + backgroundColor: color, + color, // We need the same color on both for the border + }} + > + + <div className={cx({ + [styles.talking]: (talking && !muted), + })} + /> + + + <div className={styles.content}> + {children} + </div> </div> - </div> -); + ); UserAvatar.propTypes = propTypes; UserAvatar.defaultProps = defaultProps; diff --git a/bigbluebutton-html5/imports/ui/components/user-avatar/styles.scss b/bigbluebutton-html5/imports/ui/components/user-avatar/styles.scss index 2dd42cbc15353c2d365e5f761d358058204102f7..77f652bd10f9468a070c5a6fbcc90aa84ddde85b 100755 --- a/bigbluebutton-html5/imports/ui/components/user-avatar/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/user-avatar/styles.scss @@ -46,22 +46,30 @@ $user-color: currentColor; //picks the current color reference in the class text-align: center; vertical-align: middle; letter-spacing: -.65rem; + z-index: 1; } } .talking { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: $user-color; + border-radius: inherit; animation: pulse 1s infinite ease-in; } + @keyframes pulse { 0% { - box-shadow: 0 0 0 0 $user-color; - } - 70% { - box-shadow: 0 0 0 0.5625rem transparent; + opacity: 1; + transform: scale(1); } 100% { - box-shadow: 0 0 0 0 transparent; + opacity: 0; + transform: scale(1.5); } } @@ -92,6 +100,7 @@ $user-color: currentColor; //picks the current color reference in the class } } + .listenOnly { &:after { content: "\00a0\e90c\00a0"; diff --git a/bigbluebutton-html5/imports/ui/components/user-list/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/component.jsx index 27c04e3ac70ebb9be7aa233fe45c24642f181e79..948345a9546702533ab75175250fdfef0b25edb7 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/component.jsx @@ -27,6 +27,7 @@ const propTypes = { toggleVoice: PropTypes.func.isRequired, changeRole: PropTypes.func.isRequired, roving: PropTypes.func.isRequired, + getGroupChatPrivate: PropTypes.func.isRequired, }; const SHOW_BRANDING = Meteor.settings.public.app.branding.displayBrandingArea; const defaultProps = { @@ -62,6 +63,10 @@ class UserList extends Component { isPublicChat, roving, CustomLogoUrl, + getGroupChatPrivate, + handleEmojiChange, + getEmojiList, + getEmoji, } = this.props; return ( @@ -91,6 +96,10 @@ class UserList extends Component { isMeetingLocked, isPublicChat, roving, + getGroupChatPrivate, + handleEmojiChange, + getEmojiList, + getEmoji, } } />} diff --git a/bigbluebutton-html5/imports/ui/components/user-list/container.jsx b/bigbluebutton-html5/imports/ui/components/user-list/container.jsx index f30d12964cec76792ee4e045df61b718339d6a3e..6681437009194483abc1b3b59fe424bf3acae5b7 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/container.jsx @@ -22,6 +22,7 @@ const propTypes = { toggleVoice: PropTypes.func.isRequired, changeRole: PropTypes.func.isRequired, roving: PropTypes.func.isRequired, + getGroupChatPrivate: PropTypes.func.isRequired, }; const UserListContainer = props => <UserList {...props} />; @@ -46,4 +47,8 @@ export default withTracker(({ chatID, compact }) => ({ roving: Service.roving, CustomLogoUrl: Service.getCustomLogoUrl(), compact, + getGroupChatPrivate: Service.getGroupChatPrivate, + handleEmojiChange: Service.setEmojiStatus, + getEmojiList: Service.getEmojiList(), + getEmoji: Service.getEmoji(), }))(UserListContainer); diff --git a/bigbluebutton-html5/imports/ui/components/user-list/service.js b/bigbluebutton-html5/imports/ui/components/user-list/service.js index 11290c48f1b8bcd3a54386695423814089627525..b54412be33cc08a3d24abe26550e0583bf685222 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/service.js +++ b/bigbluebutton-html5/imports/ui/components/user-list/service.js @@ -1,5 +1,6 @@ import Users from '/imports/api/users'; -import Chat from '/imports/api/chat'; +import GroupChat from '/imports/api/group-chat'; +import GroupChatMsg from '/imports/api/group-chat-msg'; import Meetings from '/imports/api/meetings'; import Auth from '/imports/ui/services/auth'; import UnreadMessages from '/imports/ui/services/unread-messages'; @@ -14,17 +15,23 @@ const APP_CONFIG = Meteor.settings.public.app; const ALLOW_MODERATOR_TO_UNMUTE_AUDIO = APP_CONFIG.allowModeratorToUnmuteAudio; const CHAT_CONFIG = Meteor.settings.public.chat; -const PRIVATE_CHAT_TYPE = CHAT_CONFIG.type_private; -const PUBLIC_CHAT_USERID = CHAT_CONFIG.public_userid; +const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id; // session for closed chat list const CLOSED_CHAT_LIST_KEY = 'closedChatList'; const mapOpenChats = (chat) => { const currentUserId = Auth.userID; - return chat.fromUserId !== currentUserId - ? chat.fromUserId - : chat.toUserId; + + if (chat.sender !== currentUserId) { + return chat.sender; + } + + const { chatId } = chat; + + const userId = GroupChat.findOne({ chatId }).users.filter(user => user !== currentUserId); + + return userId[0]; }; const CUSTOM_LOGO_URL_KEY = 'CustomLogoUrl'; @@ -54,15 +61,15 @@ const sortUsersByEmoji = (a, b) => { const emojiA = statusA in EMOJI_STATUSES ? EMOJI_STATUSES[statusA] : statusA; const emojiB = statusB in EMOJI_STATUSES ? EMOJI_STATUSES[statusB] : statusB; - if (emojiA && emojiB && (emojiA !== EMOJI_STATUSES.none && emojiB !== EMOJI_STATUSES.none)) { + if (emojiA && emojiB && (emojiA !== 'none' && emojiB !== 'none')) { if (a.emoji.changedAt < b.emoji.changedAt) { return -1; } else if (a.emoji.changedAt > b.emoji.changedAt) { return 1; } - } else if (emojiA && emojiA !== EMOJI_STATUSES.none) { + } else if (emojiA && emojiA !== 'none') { return -1; - } else if (emojiB && emojiB !== EMOJI_STATUSES.none) { + } else if (emojiB && emojiB !== 'none') { return 1; } return 0; @@ -184,8 +191,21 @@ const getUsers = () => { }; const getOpenChats = (chatID) => { - let openChats = Chat - .find({ type: PRIVATE_CHAT_TYPE }) + const privateChat = GroupChat + .find({ users: { $all: [Auth.userID] } }) + .fetch() + .map(chat => chat.chatId); + + const filter = { + chatId: { $ne: PUBLIC_GROUP_CHAT_ID }, + }; + + if (privateChat) { + filter.chatId = { $in: privateChat }; + } + + let openChats = GroupChatMsg + .find(filter) .fetch() .map(mapOpenChats); @@ -193,7 +213,7 @@ const getOpenChats = (chatID) => { openChats.push(chatID); } - openChats = _.uniq(openChats); + openChats = _.uniq(_.compact(openChats)); openChats = Users .find({ userId: { $in: openChats } }) @@ -229,7 +249,7 @@ const getOpenChats = (chatID) => { id: 'public', name: 'Public Chat', icon: 'group_chat', - unreadCounter: UnreadMessages.count(PUBLIC_CHAT_USERID), + unreadCounter: UnreadMessages.count(PUBLIC_GROUP_CHAT_ID), }); return openChats @@ -277,6 +297,8 @@ const getAvailableActions = (currentUser, user, router, isBreakoutRoom) => { && user.isModerator && !isDialInUser; + const allowedToChangeStatus = user.isCurrent; + return { allowedToChatPrivately, allowedToMuteAudio, @@ -286,6 +308,7 @@ const getAvailableActions = (currentUser, user, router, isBreakoutRoom) => { allowedToSetPresenter, allowedToPromote, allowedToDemote, + allowedToChangeStatus, }; }; @@ -318,7 +341,13 @@ const isMeetingLocked = (id) => { return isLocked; }; -const setEmojiStatus = (userId) => { makeCall('setEmojiStatus', userId, 'none'); }; +const setEmojiStatus = (data) => { + const statusAvailable = (Object.keys(EMOJI_STATUSES).includes(data)); + + return statusAvailable + ? makeCall('setEmojiStatus', Auth.userID, data) + : makeCall('setEmojiStatus', data, 'none'); +}; const assignPresenter = (userId) => { makeCall('assignPresenter', userId); }; @@ -393,6 +422,14 @@ const roving = (event, itemCount, changeState) => { } }; +const getGroupChatPrivate = (sender, receiver) => { + const privateChat = GroupChat.findOne({ users: { $all: [receiver.id, sender.id] } }); + + if (!privateChat) { + makeCall('createGroupChat', receiver); + } +}; + export default { setEmojiStatus, assignPresenter, @@ -409,4 +446,8 @@ export default { roving, setCustomLogoUrl, getCustomLogoUrl, + getGroupChatPrivate, + getEmojiList: () => EMOJI_STATUSES, + getEmoji: () => Users.findOne({ userId: Auth.userID }).emoji, }; + diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx index 74025e823c8a1c37b385f0cc03fe2be20d3e8bd8..231a8df908535c2dda4b5a337925b877241b70c1 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { styles } from './styles'; import UserParticipants from './user-participants/component'; @@ -24,6 +24,7 @@ const propTypes = { toggleVoice: PropTypes.func.isRequired, changeRole: PropTypes.func.isRequired, roving: PropTypes.func.isRequired, + getGroupChatPrivate: PropTypes.func.isRequired, }; const defaultProps = { @@ -34,8 +35,32 @@ const defaultProps = { meeting: {}, }; -class UserContent extends Component { +class UserContent extends React.PureComponent { render() { + const { + users, + compact, + intl, + currentUser, + meeting, + isBreakoutRoom, + setEmojiStatus, + assignPresenter, + removeUser, + toggleVoice, + changeRole, + getAvailableActions, + normalizeEmojiName, + isMeetingLocked, + roving, + handleEmojiChange, + getEmojiList, + getEmoji, + isPublicChat, + openChats, + getGroupChatPrivate, + } = this.props; + return ( <div data-test="userListContent" @@ -43,28 +68,36 @@ class UserContent extends Component { role="complementary" > <UserMessages - isPublicChat={this.props.isPublicChat} - openChats={this.props.openChats} - compact={this.props.compact} - intl={this.props.intl} - roving={this.props.roving} + {...{ + isPublicChat, + openChats, + compact, + intl, + roving, + }} /> <UserParticipants - users={this.props.users} - compact={this.props.compact} - intl={this.props.intl} - currentUser={this.props.currentUser} - meeting={this.props.meeting} - isBreakoutRoom={this.props.isBreakoutRoom} - setEmojiStatus={this.props.setEmojiStatus} - assignPresenter={this.props.assignPresenter} - removeUser={this.props.removeUser} - toggleVoice={this.props.toggleVoice} - changeRole={this.props.changeRole} - getAvailableActions={this.props.getAvailableActions} - normalizeEmojiName={this.props.normalizeEmojiName} - isMeetingLocked={this.props.isMeetingLocked} - roving={this.props.roving} + {...{ + users, + compact, + intl, + currentUser, + meeting, + isBreakoutRoom, + setEmojiStatus, + assignPresenter, + removeUser, + toggleVoice, + changeRole, + getAvailableActions, + normalizeEmojiName, + isMeetingLocked, + roving, + handleEmojiChange, + getEmojiList, + getEmoji, + getGroupChatPrivate, + }} /> </div> ); diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx index df0745e206c4406bf54a4131d894188f7e5ee2a2..3d45555ebee6588132879a835b2915bfd6ed4e4c 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import { TransitionGroup, CSSTransition } from 'react-transition-group'; -import PropTypes from 'prop-types'; import { defineMessages } from 'react-intl'; +import PropTypes from 'prop-types'; import cx from 'classnames'; import { styles } from '/imports/ui/components/user-list/user-list-content/styles'; import UserListItem from './user-list-item/component'; @@ -48,38 +48,6 @@ const intlMessages = defineMessages({ id: 'app.userList.usersTitle', description: 'Title for the Header', }, - ChatLabel: { - id: 'app.userList.menu.chat.label', - description: 'Save the changes and close the settings menu', - }, - ClearStatusLabel: { - id: 'app.userList.menu.clearStatus.label', - description: 'Clear the emoji status of this user', - }, - MakePresenterLabel: { - id: 'app.userList.menu.makePresenter.label', - description: 'Set this user to be the presenter in this meeting', - }, - RemoveUserLabel: { - id: 'app.userList.menu.removeUser.label', - description: 'Forcefully remove this user from the meeting', - }, - MuteUserAudioLabel: { - id: 'app.userList.menu.muteUserAudio.label', - description: 'Forcefully mute this user', - }, - UnmuteUserAudioLabel: { - id: 'app.userList.menu.unmuteUserAudio.label', - description: 'Forcefully unmute this user', - }, - PromoteUserLabel: { - id: 'app.userList.menu.promoteUser.label', - description: 'Forcefully promote this viewer to a moderator', - }, - DemoteUserLabel: { - id: 'app.userList.menu.demoteUser.label', - description: 'Forcefully demote this moderator to a viewer', - }, }); class UserParticipants extends Component { @@ -136,58 +104,17 @@ class UserParticipants extends Component { normalizeEmojiName, isMeetingLocked, users, - intl, changeRole, assignPresenter, setEmojiStatus, removeUser, toggleVoice, + getGroupChatPrivate, //// TODO check if this is used + handleEmojiChange, //// TODO add to props validation + getEmojiList, + getEmoji, } = this.props; - const userActions = - { - openChat: { - label: () => intl.formatMessage(intlMessages.ChatLabel), - handler: (router, user) => router.push(`/users/chat/${user.id}`), - icon: 'chat', - }, - clearStatus: { - label: () => intl.formatMessage(intlMessages.ClearStatusLabel), - handler: user => setEmojiStatus(user.id, 'none'), - icon: 'clear_status', - }, - setPresenter: { - label: () => intl.formatMessage(intlMessages.MakePresenterLabel), - handler: user => assignPresenter(user.id), - icon: 'presentation', - }, - remove: { - label: user => intl.formatMessage(intlMessages.RemoveUserLabel, { 0: user.name }), - handler: user => removeUser(user.id), - icon: 'circle_close', - }, - mute: { - label: () => intl.formatMessage(intlMessages.MuteUserAudioLabel), - handler: user => toggleVoice(user.id), - icon: 'mute', - }, - unmute: { - label: () => intl.formatMessage(intlMessages.UnmuteUserAudioLabel), - handler: user => toggleVoice(user.id), - icon: 'unmute', - }, - promote: { - label: () => intl.formatMessage(intlMessages.PromoteUserLabel), - handler: user => changeRole(user.id, 'MODERATOR'), - icon: 'promote', - }, - demote: { - label: () => intl.formatMessage(intlMessages.DemoteUserLabel), - handler: user => changeRole(user.id, 'VIEWER'), - icon: 'user', - }, - }; - let index = -1; return users.map(user => ( @@ -203,15 +130,24 @@ class UserParticipants extends Component { > <div ref={(node) => { this.userRefs[index += 1] = node; }}> <UserListItem - compact={compact} - isBreakoutRoom={isBreakoutRoom} - user={user} - currentUser={currentUser} - userActions={userActions} - meeting={meeting} - getAvailableActions={getAvailableActions} - normalizeEmojiName={normalizeEmojiName} - isMeetingLocked={isMeetingLocked} + {...{ + user, + currentUser, + compact, + isBreakoutRoom, + meeting, + getAvailableActions, + normalizeEmojiName, + isMeetingLocked, + handleEmojiChange, + getEmojiList, + getEmoji, + setEmojiStatus, + assignPresenter, + removeUser, + toggleVoice, + changeRole, + }} getScrollContainerRef={this.getScrollContainerRef} /> </div> @@ -220,9 +156,7 @@ class UserParticipants extends Component { } focusUserItem(index) { - if (!this.userRefs[index]) { - return; - } + if (!this.userRefs[index]) return; this.userRefs[index].firstChild.focus(); } diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx index 26d2c09dc9acf5647b53ce5f085359ab838b4be4..e3e4fc585745629138d818f7b08b6c898dabe522 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx @@ -2,9 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { withRouter } from 'react-router'; import { injectIntl } from 'react-intl'; -import _ from 'lodash'; -import UserListContent from './user-list-content/component'; -import UserAction from './user-action/component'; +import UserDropdown from './user-dropdown/component'; const propTypes = { user: PropTypes.shape({ @@ -23,7 +21,6 @@ const propTypes = { intl: PropTypes.shape({ formatMessage: PropTypes.func.isRequired, }).isRequired, - userActions: PropTypes.shape({}).isRequired, router: PropTypes.shape({}).isRequired, isBreakoutRoom: PropTypes.bool, getAvailableActions: PropTypes.func.isRequired, @@ -38,64 +35,6 @@ const defaultProps = { }; class UserListItem extends Component { - static createAction(action, ...options) { - return ( - <UserAction - key={_.uniqueId('action-item-')} - icon={action.icon} - label={action.label(...options)} - handler={action.handler} - options={[...options]} - /> - ); - } - - getUsersActions() { - const { - currentUser, - user, - userActions, - router, - isBreakoutRoom, - getAvailableActions, - } = this.props; - - const { - openChat, - clearStatus, - setPresenter, - remove, - mute, - unmute, - promote, - demote, - } = userActions; - - const actions = getAvailableActions(currentUser, user, router, isBreakoutRoom); - - const { - allowedToChatPrivately, - allowedToMuteAudio, - allowedToUnmuteAudio, - allowedToResetStatus, - allowedToRemove, - allowedToSetPresenter, - allowedToPromote, - allowedToDemote, - } = actions; - - return _.compact([ - (allowedToChatPrivately ? UserListItem.createAction(openChat, router, user) : null), - (allowedToMuteAudio ? UserListItem.createAction(mute, user) : null), - (allowedToUnmuteAudio ? UserListItem.createAction(unmute, user) : null), - (allowedToResetStatus ? UserListItem.createAction(clearStatus, user) : null), - (allowedToSetPresenter ? UserListItem.createAction(setPresenter, user) : null), - (allowedToRemove ? UserListItem.createAction(remove, user) : null), - (allowedToPromote ? UserListItem.createAction(promote, user) : null), - (allowedToDemote ? UserListItem.createAction(demote, user) : null), - ]); - } - render() { const { compact, @@ -105,19 +44,42 @@ class UserListItem extends Component { isMeetingLocked, normalizeEmojiName, getScrollContainerRef, + assignPresenter, + removeUser, + toggleVoice, + changeRole, + setEmojiStatus, + currentUser, + router, + isBreakoutRoom, + getAvailableActions, + handleEmojiChange, + getEmojiList, + getEmoji, } = this.props; - const actions = this.getUsersActions(); - - const contents = (<UserListContent - compact={compact} - user={user} - intl={intl} - normalizeEmojiName={normalizeEmojiName} - actions={actions} - meeting={meeting} - isMeetingLocked={isMeetingLocked} - getScrollContainerRef={getScrollContainerRef} + const contents = (<UserDropdown + {...{ + compact, + user, + intl, + normalizeEmojiName, + meeting, + isMeetingLocked, + getScrollContainerRef, + assignPresenter, + removeUser, + toggleVoice, + changeRole, + setEmojiStatus, + currentUser, + router, + isBreakoutRoom, + getAvailableActions, + handleEmojiChange, + getEmojiList, + getEmoji, + }} />); return contents; diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-action/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-action/component.jsx deleted file mode 100755 index 955f5a5468184548c4848947f14793c7d86f621e..0000000000000000000000000000000000000000 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-action/component.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import DropdownListItem from '/imports/ui/components/dropdown/list/item/component'; - -const propTypes = { - icon: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - handler: PropTypes.func.isRequired, - options: PropTypes.arrayOf(PropTypes.shape({})).isRequired, -}; - -export default class UserActions extends React.PureComponent { - render() { - const { - key, icon, label, handler, options, - } = this.props; - - return ( - <DropdownListItem - key={key} - icon={icon} - label={label} - defaultMessage={label} - onClick={() => handler.call(this, ...options)} - /> - ); - } -} - -UserActions.propTypes = propTypes; diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-list-content/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx similarity index 52% rename from bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-list-content/component.jsx rename to bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx index 1ba403fe4b4ce88dd032aa60ee87f614431aa4b5..b2992e16021b07f2ce42783e1bf181a6b69cce6b 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-list-content/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx @@ -8,8 +8,9 @@ import Dropdown from '/imports/ui/components/dropdown/component'; import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component'; import DropdownContent from '/imports/ui/components/dropdown/content/component'; import DropdownList from '/imports/ui/components/dropdown/list/component'; +import DropdownListItem from '/imports/ui/components/dropdown/list/item/component'; import DropdownListSeparator from '/imports/ui/components/dropdown/list/separator/component'; -import DropdownListTitle from '/imports/ui/components/dropdown/list/title/component'; +import _ from 'lodash'; import { styles } from './styles'; import UserName from './../user-name/component'; import UserIcons from './../user-icons/component'; @@ -39,6 +40,46 @@ const messages = defineMessages({ id: 'app.userList.userAriaLabel', description: 'aria label for each user in the userlist', }, + statusTriggerLabel: { + id: 'app.actionsBar.emojiMenu.statusTriggerLabel', + description: 'label for option to show emoji menu', + }, + backTriggerLabel: { + id: 'app.audio.backLabel', + description: 'label for option to hide emoji menu', + }, + ChatLabel: { + id: 'app.userList.menu.chat.label', + description: 'Save the changes and close the settings menu', + }, + ClearStatusLabel: { + id: 'app.userList.menu.clearStatus.label', + description: 'Clear the emoji status of this user', + }, + MakePresenterLabel: { + id: 'app.userList.menu.makePresenter.label', + description: 'Set this user to be the presenter in this meeting', + }, + RemoveUserLabel: { + id: 'app.userList.menu.removeUser.label', + description: 'Forcefully remove this user from the meeting', + }, + MuteUserAudioLabel: { + id: 'app.userList.menu.muteUserAudio.label', + description: 'Forcefully mute this user', + }, + UnmuteUserAudioLabel: { + id: 'app.userList.menu.unmuteUserAudio.label', + description: 'Forcefully unmute this user', + }, + PromoteUserLabel: { + id: 'app.userList.menu.promoteUser.label', + description: 'Forcefully promote this viewer to a moderator', + }, + DemoteUserLabel: { + id: 'app.userList.menu.demoteUser.label', + description: 'Forcefully demote this moderator to a viewer', + }, }); const propTypes = { @@ -48,14 +89,12 @@ const propTypes = { formatMessage: PropTypes.func.isRequired, }).isRequired, normalizeEmojiName: PropTypes.func.isRequired, - actions: PropTypes.arrayOf(PropTypes.shape({})).isRequired, meeting: PropTypes.shape({}).isRequired, isMeetingLocked: PropTypes.func.isRequired, getScrollContainerRef: PropTypes.func.isRequired, }; - -class UserListContent extends Component { +class UserDropdown extends Component { /** * Return true if the content fit on the screen, false otherwise. * @@ -75,6 +114,7 @@ class UserListContent extends Component { dropdownOffset: 0, dropdownDirection: 'top', dropdownVisible: false, + showNestedOptions: false, }; this.handleScroll = this.handleScroll.bind(this); @@ -82,6 +122,8 @@ class UserListContent extends Component { this.onActionsHide = this.onActionsHide.bind(this); this.getDropdownMenuParent = this.getDropdownMenuParent.bind(this); this.renderUserAvatar = this.renderUserAvatar.bind(this); + this.resetMenuState = this.resetMenuState.bind(this); + this.makeDropdownItem = this.makeDropdownItem.bind(this); } componentWillMount() { @@ -89,30 +131,201 @@ class UserListContent extends Component { this.seperator = _.uniqueId('action-separator-'); } - componentDidUpdate() { + componentDidUpdate(prevProps, prevState) { + if (!this.state.isActionsOpen && this.state.showNestedOptions) { + return this.resetMenuState(); + } + this.checkDropdownDirection(); } - onActionsShow() { - const dropdown = this.getDropdownMenuParent(); - const scrollContainer = this.props.getScrollContainerRef(); - const dropdownTrigger = dropdown.children[0]; - - const list = findDOMNode(this.list); - const children = [].slice.call(list.children); - children.find(child => child.getAttribute('role') === 'menuitem').focus(); + makeDropdownItem(key, label, onClick, icon = null, iconRight = null) { + return ( + <DropdownListItem + {...{ + key, + label, + onClick, + icon, + iconRight, + }} + className={key === this.props.getEmoji ? styles.emojiSelected : null} + /> + ); + } - this.setState({ - isActionsOpen: true, - dropdownVisible: false, - dropdownOffset: dropdownTrigger.offsetTop - scrollContainer.scrollTop, + resetMenuState() { + return this.setState({ + isActionsOpen: false, + dropdownOffset: 0, dropdownDirection: 'top', + dropdownVisible: false, + showNestedOptions: false, }); + } + + getUsersActions() { + const { + intl, + currentUser, + user, + router, + isBreakoutRoom, + getAvailableActions, + handleEmojiChange, + getEmojiList, + setEmojiStatus, + assignPresenter, + removeUser, + toggleVoice, + changeRole, + } = this.props; + + const actionPermissions = getAvailableActions(currentUser, user, router, isBreakoutRoom); + const actions = []; + + const { + allowedToChatPrivately, + allowedToMuteAudio, + allowedToUnmuteAudio, + allowedToResetStatus, + allowedToRemove, + allowedToSetPresenter, + allowedToPromote, + allowedToDemote, + allowedToChangeStatus, + } = actionPermissions; + + if (this.state.showNestedOptions) { + if (allowedToChangeStatus) { + actions.push(this.makeDropdownItem( + 'back', + intl.formatMessage(messages.backTriggerLabel), + () => this.setState({ showNestedOptions: false, isActionsOpen: true }), + 'left_arrow', + )); + } + + actions.push(<DropdownListSeparator key={_.uniqueId('list-separator-')} />); + + const statuses = Object.keys(getEmojiList); + statuses.map(status => actions.push(this.makeDropdownItem( + status, + intl.formatMessage({ id: `app.actionsBar.emojiMenu.${status}Label` }), + () => { handleEmojiChange(status); this.resetMenuState(); }, + getEmojiList[status], + ))); - scrollContainer.addEventListener('scroll', this.handleScroll, false); + return actions; + } + + if (allowedToChangeStatus) { + actions.push(this.makeDropdownItem( + 'setstatus', + intl.formatMessage(messages.statusTriggerLabel), + () => this.setState({ showNestedOptions: true, isActionsOpen: true }), + 'user', + 'right_arrow', + )); + } + + if (allowedToChatPrivately) { + actions.push(this.makeDropdownItem( + 'openChat', + intl.formatMessage(messages.ChatLabel), + () => this.onActionsHide(router.push(`/users/chat/${user.id}`)), + 'chat', + )); + } + + if (allowedToResetStatus && user.emoji.status !== 'none') { + actions.push(this.makeDropdownItem( + 'clearStatus', + intl.formatMessage(messages.ClearStatusLabel), + () => this.onActionsHide(setEmojiStatus(user.id, 'none')), + 'clear_status', + )); + } + + if (allowedToMuteAudio) { + actions.push(this.makeDropdownItem( + 'mute', + intl.formatMessage(messages.MuteUserAudioLabel), + () => this.onActionsHide(toggleVoice(user.id)), + 'mute', + )); + } + + if (allowedToUnmuteAudio) { + actions.push(this.makeDropdownItem( + 'unmute', + intl.formatMessage(messages.UnmuteUserAudioLabel), + () => this.onActionsHide(toggleVoice(user.id)), + 'unmute', + )); + } + + if (allowedToSetPresenter) { + actions.push(this.makeDropdownItem( + 'setPresenter', + intl.formatMessage(messages.MakePresenterLabel), + () => this.onActionsHide(assignPresenter(user.id)), + 'presentation', + )); + } + + if (allowedToRemove) { + actions.push(this.makeDropdownItem( + 'remove', + intl.formatMessage(messages.RemoveUserLabel, { 0: user.name }), + () => this.onActionsHide(removeUser(user.id)), + 'circle_close', + )); + } + + if (allowedToPromote) { + actions.push(this.makeDropdownItem( + 'promote', + intl.formatMessage(messages.PromoteUserLabel), + () => this.onActionsHide(changeRole(user.id, 'MODERATOR')), + 'promote', + )); + } + + if (allowedToDemote) { + actions.push(this.makeDropdownItem( + 'demote', + intl.formatMessage(messages.DemoteUserLabel), + () => this.onActionsHide(changeRole(user.id, 'VIEWER')), + 'user', + )); + } + + return actions; + } + + onActionsShow() { + const dropdown = this.getDropdownMenuParent(); + const scrollContainer = this.props.getScrollContainerRef(); + + if (dropdown && scrollContainer) { + const dropdownTrigger = dropdown.children[0]; + const list = findDOMNode(this.list); + const children = [].slice.call(list.children); + children.find(child => child.getAttribute('role') === 'menuitem').focus(); + + this.setState({ + isActionsOpen: true, + dropdownVisible: false, + dropdownOffset: dropdownTrigger.offsetTop - scrollContainer.scrollTop, + dropdownDirection: 'top', + }); + + scrollContainer.addEventListener('scroll', this.handleScroll, false); + } } - onActionsHide() { + onActionsHide(callback) { this.setState({ isActionsOpen: false, dropdownVisible: false, @@ -120,6 +333,10 @@ class UserListContent extends Component { const scrollContainer = this.props.getScrollContainerRef(); scrollContainer.removeEventListener('scroll', this.handleScroll, false); + + if (callback) { + return callback; + } } getDropdownMenuParent() { @@ -127,9 +344,7 @@ class UserListContent extends Component { } handleScroll() { - this.setState({ - isActionsOpen: false, - }); + this.setState({ isActionsOpen: false }); } /** @@ -148,7 +363,7 @@ class UserListContent extends Component { }; const isDropdownVisible = - UserListContent.checkIfDropdownIsVisible( + UserDropdown.checkIfDropdownIsVisible( dropdownContent.offsetTop, dropdownContent.offsetHeight, ); @@ -211,7 +426,6 @@ class UserListContent extends Component { compact, user, intl, - actions, isMeetingLocked, meeting, } = this.props; @@ -223,6 +437,8 @@ class UserListContent extends Component { dropdownOffset, } = this.state; + const actions = this.getUsersActions(); + const userItemContentsStyle = {}; userItemContentsStyle[styles.userItemContentsCompact] = compact; @@ -255,25 +471,27 @@ class UserListContent extends Component { { this.renderUserAvatar() } </div> {<UserName - user={user} - compact={compact} - intl={intl} - meeting={meeting} - isMeetingLocked={isMeetingLocked} - userAriaLabel={userAriaLabel} - isActionsOpen={isActionsOpen} + {...{ + user, + compact, + intl, + meeting, + isMeetingLocked, + userAriaLabel, + isActionsOpen, + }} />} {<UserIcons - user={user} - compact={compact} + {...{ + user, + compact, + }} />} </div> </div> ); - if (!actions.length) { - return contents; - } + if (!actions.length) return contents; return ( <Dropdown @@ -298,24 +516,12 @@ class UserListContent extends Component { className={styles.dropdownContent} placement={`right ${dropdownDirection}`} > - <DropdownList ref={(ref) => { this.list = ref; }} getDropdownMenuParent={this.getDropdownMenuParent} onActionsHide={this.onActionsHide} > - { - [ - ( - <DropdownListTitle - description={intl.formatMessage(messages.menuTitleContext)} - key={this.title} - > - {user.name} - </DropdownListTitle>), - (<DropdownListSeparator key={this.seperator} />), - ].concat(actions) - } + {actions} </DropdownList> </DropdownContent> </Dropdown> @@ -323,5 +529,5 @@ class UserListContent extends Component { } } -UserListContent.propTypes = propTypes; -export default UserListContent; +UserDropdown.propTypes = propTypes; +export default UserDropdown; diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-list-content/styles.scss b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/styles.scss similarity index 96% rename from bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-list-content/styles.scss rename to bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/styles.scss index 441d4377ac6b264410884aa1bf0738d5fdb6036c..e6e8b750e51a95e4d044ad7f847b3f4069ed25d7 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-list-content/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/styles.scss @@ -103,3 +103,9 @@ max-width: 100%; overflow: visible; } + +.emojiSelected { + span, i { + color: $color-primary; + } +} diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx index bc2c5ded07c7960f8580cdf13fda80309a014bca..7959f3ed2f160bfebc6d3129530eae142cad842a 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx @@ -9,6 +9,8 @@ import logger from '/imports/startup/client/logger'; import VideoService from './service'; import VideoList from './video-list/component'; +const VIDEO_CONSTRAINTS = Meteor.settings.public.kurento.cameraConstraints; + const intlClientErrors = defineMessages({ iceCandidateError: { id: 'app.video.iceCandidateError', @@ -46,6 +48,10 @@ const intlClientErrors = defineMessages({ id: 'app.video.notReadableError', description: 'error message When the webcam is being used by other software', }, + iceConnectionStateError: { + id: 'app.video.iceConnectionStateError', + description: 'Error message for ice connection state being failed', + }, }); const intlSFUErrors = defineMessages({ @@ -63,7 +69,7 @@ const intlSFUErrors = defineMessages({ }, 2003: { id: 'app.sfu.mediaServerRequestTimeout2003', - description: "Error message fired when requests are timing out due to lack of resources", + description: 'Error message fired when requests are timing out due to lack of resources', }, 2021: { id: 'app.sfu.serverIceGatheringFailed2021', @@ -80,7 +86,7 @@ const intlSFUErrors = defineMessages({ 2203: { id: 'app.sfu.noAvailableCodec2203', description: 'Error message fired when the server has no available codec for the client', - } + }, }); const CAMERA_SHARE_FAILED_WAIT_TIME = 15000; @@ -123,10 +129,10 @@ class VideoProvider extends Component { } logger(type, message, options = {}) { - const {userId, userName} = this.props; + const { userId, userName } = this.props; const topic = options.topic || 'video'; - logger[type]({obj: Object.assign(options, {userId, userName, topic})}, `[${topic}] ${message}`); + logger[type]({ obj: Object.assign(options, { userId, userName, topic }) }, `[${topic}] ${message}`); } _sendPauseStream(id, role, state) { @@ -215,7 +221,7 @@ class VideoProvider extends Component { } onWsOpen() { - this.logger('debug', '------ Websocket connection opened.', {topic: 'ws'}); + this.logger('debug', '------ Websocket connection opened.', { topic: 'ws' }); // -- Resend queued messages that happened when socket was not connected while (this.wsQueue.length > 0) { @@ -228,7 +234,7 @@ class VideoProvider extends Component { } onWsClose(error) { - this.logger('debug', '------ Websocket connection closed.', {topic: 'ws'}); + this.logger('debug', '------ Websocket connection closed.', { topic: 'ws' }); this.stopWebRTCPeer(this.props.userId); clearInterval(this.pingInterval); @@ -246,7 +252,7 @@ class VideoProvider extends Component { onWsMessage(msg) { const parsedMessage = JSON.parse(msg.data); - this.logger('debug', `Received new message '${parsedMessage.id}'`, {topic: 'ws', message: parsedMessage}); + this.logger('debug', `Received new message '${parsedMessage.id}'`, { topic: 'ws', message: parsedMessage }); switch (parsedMessage.id) { case 'startResponse': @@ -266,7 +272,7 @@ class VideoProvider extends Component { break; case 'pong': - this.logger('debug', 'Received pong from server', {topic: 'ws'}); + this.logger('debug', 'Received pong from server', { topic: 'ws' }); break; case 'error': @@ -281,10 +287,10 @@ class VideoProvider extends Component { if (this.connectedToMediaServer()) { const jsonMessage = JSON.stringify(message); - this.logger('debug', `Sending message '${message.id}'`, {topic: 'ws', message}); + this.logger('debug', `Sending message '${message.id}'`, { topic: 'ws', message }); ws.send(jsonMessage, (error) => { if (error) { - this.logger(`client: Websocket error '${error}' on message '${message.id}'`, {topic: 'ws'}); + this.logger(`client: Websocket error '${error}' on message '${message.id}'`, { topic: 'ws' }); } }); } else { @@ -303,12 +309,12 @@ class VideoProvider extends Component { const id = message.cameraId; const peer = this.webRtcPeers[id]; - this.logger('debug', 'SDP answer received from server. Processing ...', {cameraId: id, sdpAnswer: message.sdpAnswer}); + this.logger('debug', 'SDP answer received from server. Processing ...', { cameraId: id, sdpAnswer: message.sdpAnswer }); if (peer) { peer.processAnswer(message.sdpAnswer, (error) => { if (error) { - return this.logger('debug', JSON.stringify(error), {cameraId: id}); + return this.logger('debug', JSON.stringify(error), { cameraId: id }); } }); } else { @@ -319,13 +325,13 @@ class VideoProvider extends Component { handleIceCandidate(message) { const webRtcPeer = this.webRtcPeers[message.cameraId]; - this.logger('debug', 'Received remote ice candidate', {topic: 'ice', candidate: message.candidate}) + this.logger('debug', 'Received remote ice candidate', { topic: 'ice', candidate: message.candidate }); if (webRtcPeer) { if (webRtcPeer.didSDPAnswered) { webRtcPeer.addIceCandidate(message.candidate, (err) => { if (err) { - return this.logger('error', `Error adding candidate: ${err}`, {cameraId: message.cameraId}); + return this.logger('error', `Error adding candidate: ${err}`, { cameraId: message.cameraId }); } }); } else { @@ -335,15 +341,21 @@ class VideoProvider extends Component { webRtcPeer.iceQueue.push(message.candidate); } } else { - this.logger('warn', ' [iceCandidate] Message arrived after the peer was already thrown out, discarding it...', {cameraId: message.cameraId}); + this.logger('warn', ' [iceCandidate] Message arrived after the peer was already thrown out, discarding it...', { cameraId: message.cameraId }); } } stopWebRTCPeer(id) { - this.logger('info', 'Stopping webcam', {cameraId: id}); + this.logger('info', 'Stopping webcam', { cameraId: id }); const { userId } = this.props; const shareWebcam = id === userId; + // in this case, 'closed' state is not caused by an error; + // we stop listening to prevent this from being treated as an error + if (this.webRtcPeers[id]) { + this.webRtcPeers[id].peerConnection.oniceconnectionstatechange = null; + } + if (shareWebcam) { this.unshareWebcam(); } @@ -365,11 +377,11 @@ class VideoProvider extends Component { destroyWebRTCPeer(id) { const webRtcPeer = this.webRtcPeers[id]; if (webRtcPeer) { - this.logger('info', 'Stopping WebRTC peer', {cameraId: id}); + this.logger('info', 'Stopping WebRTC peer', { cameraId: id }); webRtcPeer.dispose(); delete this.webRtcPeers[id]; } else { - this.logger('warn', 'No WebRTC peer to stop (not an error)', {cameraId: id}); + this.logger('warn', 'No WebRTC peer to stop (not an error)', { cameraId: id }); } } @@ -382,25 +394,10 @@ class VideoProvider extends Component { } catch (error) { this.logger('error', 'Video provider failed to fetch ice servers, using default'); } finally { - const videoConstraints = { - width: { - min: 320, - max: 640, - }, - height: { - min: 180, - max: 480, - }, - }; - - if (!navigator.userAgent.match(/Version\/[\d\.]+.*Safari/)) { - videoConstraints.frameRate = { min: 5, ideal: 10 }; - } - const options = { mediaConstraints: { audio: false, - video: videoConstraints, + video: VIDEO_CONSTRAINTS, }, onicecandidate: this._getOnIceCandidateCallback(id, shareWebcam), }; @@ -436,7 +433,7 @@ class VideoProvider extends Component { return this._webRTCOnError(errorGenOffer, id, shareWebcam); } - this.logger('debug', `Invoking SDP offer callback function ${location.host}`, {cameraId: id, offerSdp}); + this.logger('debug', `Invoking SDP offer callback function ${location.host}`, { cameraId: id, offerSdp }); const message = { type: 'video', @@ -453,6 +450,8 @@ class VideoProvider extends Component { peer.didSDPAnswered = true; }); }); + this.webRtcPeers[id].peerConnection.oniceconnectionstatechange = + this._getOnIceConnectionStateChangeCallback(id); } } @@ -460,7 +459,7 @@ class VideoProvider extends Component { const { intl } = this.props; return () => { - this.logger('error', `Camera share has not suceeded in ${CAMERA_SHARE_FAILED_WAIT_TIME}`, {cameraId: id}); + this.logger('error', `Camera share has not suceeded in ${CAMERA_SHARE_FAILED_WAIT_TIME}`, { cameraId: id }); if (this.props.userId === id) { this.notifyError(intl.formatMessage(intlClientErrors.sharingError)); @@ -489,7 +488,7 @@ class VideoProvider extends Component { peer.addIceCandidate(candidate, (err) => { if (err) { this.notifyError(intl.formatMessage(intlClientErrors.iceCandidateError)); - return this.logger('error', `Error adding candidate: ${err}`, {cameraId}); + return this.logger('error', `Error adding candidate: ${err}`, { cameraId }); } }); } @@ -510,7 +509,7 @@ class VideoProvider extends Component { this.stopWebRTCPeer(id); - return this.logger('error', errorMessage, {cameraId: id}); + return this.logger('error', errorMessage, { cameraId: id }); } _getOnIceCandidateCallback(id, shareWebcam) { @@ -521,11 +520,11 @@ class VideoProvider extends Component { if (!this.restartTimeout[id]) { this.restartTimer[id] = this.restartTimer[id] || CAMERA_SHARE_FAILED_WAIT_TIME; - this.logger('debug', `Setting a camera connection restart in ${this.restartTimer[id]}`, {cameraId: id}); + this.logger('debug', `Setting a camera connection restart in ${this.restartTimer[id]}`, { cameraId: id }); this.restartTimeout[id] = setTimeout(this._getWebRTCStartTimeout(id, shareWebcam, peer), this.restartTimer[id]); } - this.logger('debug', 'Generated local ice candidate', {topic: 'ice', candidate}) + this.logger('debug', 'Generated local ice candidate', { topic: 'ice', candidate }); const message = { type: 'video', @@ -538,6 +537,24 @@ class VideoProvider extends Component { }; } + _getOnIceConnectionStateChangeCallback(id) { + const { intl } = this.props; + const peer = this.webRtcPeers[id]; + + return (event) => { + const connectionState = peer.peerConnection.iceConnectionState; + if (connectionState === 'failed' || connectionState === 'closed') { + + // prevent the same error from being detected multiple times + peer.peerConnection.oniceconnectionstatechange = null; + + this.logger('error', 'ICE connection state', id); + this.stopWebRTCPeer(id); + this.notifyError(intl.formatMessage(intlClientErrors.iceConnectionStateError)); + } + }; + } + attachVideoStream(id) { const video = this.videoTags[id]; if (video == null) { @@ -706,7 +723,7 @@ class VideoProvider extends Component { monitorTrackStart(peer, track, local, callback) { const that = this; - this.logger('info', 'Starting stats monitoring on', {cameraId: track.id}); + this.logger('info', 'Starting stats monitoring on', { cameraId: track.id }); const getStatsInterval = 2000; const callGetStats = () => { @@ -772,10 +789,10 @@ class VideoProvider extends Component { } handlePlayStop(message) { - const {cameraId} = message; + const { cameraId } = message; - this.logger('info', 'Handle play stop for camera', {cameraId}); - this.stopWebRTCPeer(id); + this.logger('info', 'Handle play stop for camera', { cameraId }); + this.stopWebRTCPeer(cameraId); } handlePlayStart(message) { @@ -784,7 +801,7 @@ class VideoProvider extends Component { const videoTag = this.videoTags[id]; if (peer) { - this.logger('info', 'Handle play start for camera', {cameraId: id}); + this.logger('info', 'Handle play start for camera', { cameraId: id }); // Clear camera shared timeout when camera succesfully starts clearTimeout(this.restartTimeout[id]); diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/styles.scss b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/styles.scss index 2e2fe1980c0503c49356360c41056f07a878307e..e5a9a27f7b650f3d9a85facd2385c1821cf80788 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/styles.scss @@ -1,5 +1,9 @@ @import "/imports/ui/stylesheets/variables/_all"; +$cam-dropdown-width: 70%; +$audio-indicator-width: 1.12rem; +$audio-indicator-fs: 75%; + .videoCanvas { position: absolute; top: 0; @@ -119,6 +123,7 @@ flex: 1; display: flex; outline: none !important; + width: $cam-dropdown-width; @include mq($medium-up) { > [aria-expanded] { @@ -164,13 +169,15 @@ .muted, .voice { display: inline-block; - width: 1.25rem; - height: 1.25rem; + width: $audio-indicator-width; + height: $audio-indicator-width; + min-width: $audio-indicator-width; + min-height: $audio-indicator-width; color: $color-white; border-radius: 50%; &::before { - font-size: 75%; + font-size: $audio-indicator-fs; } } diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/annotation-factory/reactive-annotation/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/annotation-factory/reactive-annotation/component.jsx old mode 100644 new mode 100755 diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/annotations/pencil/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/annotations/pencil/component.jsx old mode 100644 new mode 100755 diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/service.js b/bigbluebutton-html5/imports/ui/components/whiteboard/service.js index 796262fca76e4b12323418281c0c8c16bc06b94c..727e0306784275f4954e69d41f62ef2268fa5891 100755 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/service.js +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/service.js @@ -7,6 +7,14 @@ import logger from '/imports/startup/client/logger'; import { isEqual } from 'lodash'; const Annotations = new Mongo.Collection(null); +const ANNOTATION_CONFIG = Meteor.settings.public.whiteboard.annotations; +const DRAW_START = ANNOTATION_CONFIG.status.start; +const DRAW_END = ANNOTATION_CONFIG.status.end; +const discardedList = []; + +export function addAnnotationToDiscardedList(annotation) { + if (!discardedList.includes(annotation)) discardedList.push(annotation); +} function clearFakeAnnotations() { Annotations.remove({ id: /-fake/g }); @@ -25,18 +33,17 @@ function handleAddedAnnotation({ const fakeAnnotation = Annotations.findOne({ id: `${annotation.id}-fake` }); const fakePoints = fakeAnnotation.annotationInfo.points; - const lastPoints = annotation.annotationInfo.points; + const { points: lastPoints } = annotation.annotationInfo; if (annotation.annotationType !== 'pencil') { Annotations.update(fakeAnnotation._id, { $set: { position: annotation.position, - 'annotationInfo.color': isEqual(fakePoints, lastPoints) || annotation.status === 'DRAW_END' ? + 'annotationInfo.color': isEqual(fakePoints, lastPoints) || annotation.status === DRAW_END ? annotation.annotationInfo.color : fakeAnnotation.annotationInfo.color, }, $inc: { version: 1 }, // TODO: Remove all this version stuff }); - return; } @@ -47,12 +54,12 @@ function handleAddedAnnotation({ } // Remove fake annotation for pencil on draw end - if (annotation.status === 'DRAW_END') { + if (annotation.status === DRAW_END) { Annotations.remove({ id: `${annotation.id}-fake` }); return; } - if (annotation.status === 'DRAW_START') { + if (annotation.status === DRAW_START) { Annotations.update(fakeAnnotation._id, { $set: { position: annotation.position - 1, @@ -68,6 +75,8 @@ function handleRemovedAnnotation({ }) { const query = { meetingId, whiteboardId }; + addAnnotationToDiscardedList(shapeId); + if (userId) { query.userId = userId; } @@ -82,7 +91,10 @@ function handleRemovedAnnotation({ AnnotationsStreamer.on('removed', handleRemovedAnnotation); AnnotationsStreamer.on('added', ({ annotations }) => { - annotations.forEach(annotation => handleAddedAnnotation(annotation)); + // Call handleAddedAnnotation when this annotation is not in discardedList + annotations + .filter(({ annotation }) => !discardedList.includes(annotation.id)) + .forEach(annotation => handleAddedAnnotation(annotation)); }); function increaseBrightness(realHex, percent) { @@ -125,7 +137,10 @@ const proccessAnnotationsQueue = () => { } // console.log('annotationQueue.length', annotationsQueue, annotationsQueue.length); - AnnotationsStreamer.emit('publish', { credentials: Auth.credentials, payload: annotationsQueue }); + AnnotationsStreamer.emit('publish', { + credentials: Auth.credentials, + payload: annotationsQueue.filter(({ id }) => !discardedList.includes(id)), + }); annotationsQueue = []; // ask tiago const delayPerc = @@ -143,7 +158,7 @@ export function sendAnnotation(annotation) { if (!annotationsSenderIsRunning) setTimeout(proccessAnnotationsQueue, annotationsBufferTimeMin); // skip optimistic for draw end since the smoothing is done in akka - if (annotation.status === 'DRAW_END') return; + if (annotation.status === DRAW_END) return; const { position, ...relevantAnotation } = annotation; const queryFake = addAnnotationQuery( @@ -168,7 +183,6 @@ WhiteboardMultiUser.find({ meetingId: Auth.meetingID }).observeChanges({ Users.find({ userId: Auth.userID }).observeChanges({ changed(id, { presenter }) { - console.log(presenter); if (presenter === false) clearFakeAnnotations(); }, }); diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/component.jsx old mode 100644 new mode 100755 index afd48151ad422840f59d147df7d86998e780d42d..ef017649c4ca9e49d4820ffcc49e5340b951ab0c --- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/component.jsx @@ -127,6 +127,9 @@ export default class WhiteboardOverlay extends Component { sendAnnotation, resetTextShapeSession, setTextShapeActiveId, + contextMenuHandler, + addAnnotationToDiscardedList, + undoAnnotation, } = this.props; const { tool } = drawSettings; const actions = { @@ -140,6 +143,9 @@ export default class WhiteboardOverlay extends Component { sendAnnotation, resetTextShapeSession, setTextShapeActiveId, + contextMenuHandler, + addAnnotationToDiscardedList, + undoAnnotation, }; if (tool === 'triangle' || tool === 'rectangle' || tool === 'ellipse' || tool === 'line') { diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/container.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/container.jsx old mode 100644 new mode 100755 index d9580c9a8143fb4578c51d748c540478fdbbc041..eee2d97002cfa5eca59c7ca129f150827148a6d6 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/container.jsx @@ -2,6 +2,7 @@ import React from 'react'; import { withTracker } from 'meteor/react-meteor-data'; import PropTypes from 'prop-types'; import WhiteboardOverlayService from './service'; +import WhiteboardToolbarService from '../whiteboard-toolbar/service'; import WhiteboardOverlay from './component'; const WhiteboardOverlayContainer = (props) => { @@ -14,7 +15,10 @@ const WhiteboardOverlayContainer = (props) => { }; export default withTracker(() => ({ + undoAnnotation: WhiteboardToolbarService.undoAnnotation, + contextMenuHandler: WhiteboardOverlayService.contextMenuHandler, sendAnnotation: WhiteboardOverlayService.sendAnnotation, + addAnnotationToDiscardedList: WhiteboardOverlayService.addAnnotationToDiscardedList, setTextShapeActiveId: WhiteboardOverlayService.setTextShapeActiveId, resetTextShapeSession: WhiteboardOverlayService.resetTextShapeSession, drawSettings: WhiteboardOverlayService.getWhiteboardToolbarValues(), diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pencil-draw-listener/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pencil-draw-listener/component.jsx index 69d42fc5fe590fff5529adfddc504b73228dcce8..57c59ea6088941258565cb113f5a769004153259 100755 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pencil-draw-listener/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pencil-draw-listener/component.jsx @@ -24,6 +24,7 @@ export default class PencilDrawListener extends Component { this.handleTouchMove = this.handleTouchMove.bind(this); this.handleTouchEnd = this.handleTouchEnd.bind(this); this.handleTouchCancel = this.handleTouchCancel.bind(this); + this.discardAnnotation = this.discardAnnotation.bind(this); } componentDidMount() { @@ -117,17 +118,23 @@ export default class PencilDrawListener extends Component { // main mouse down handler mouseDownHandler(event) { + const isLeftClick = event.button === 0; + const isRightClick = event.button === 2; + if (!this.isDrawing) { - window.addEventListener('mouseup', this.mouseUpHandler); - window.addEventListener('mousemove', this.mouseMoveHandler, true); + if (isLeftClick) { + window.addEventListener('mouseup', this.mouseUpHandler); + window.addEventListener('mousemove', this.mouseMoveHandler, true); - const { clientX, clientY } = event; - this.commonDrawStartHandler(clientX, clientY); + const { clientX, clientY } = event; + this.commonDrawStartHandler(clientX, clientY); + } // if you switch to a different window using Alt+Tab while mouse is down and release it // it wont catch mouseUp and will keep tracking the movements. Thus we need this check. - } else { + } else if (isRightClick) { this.sendLastMessage(); + this.discardAnnotation(); } } @@ -172,7 +179,7 @@ export default class PencilDrawListener extends Component { position: 0, }; - // dimensions are added to the 'DRAW_END', last message + // dimensions are added to the 'DRAW_END', last message if (dimensions) { annotation.annotationInfo.dimensions = dimensions; } @@ -208,6 +215,14 @@ export default class PencilDrawListener extends Component { window.removeEventListener('touchcancel', this.handleTouchCancel, true); } + discardAnnotation() { + const { getCurrentShapeId, addAnnotationToDiscardedList, undoAnnotation } = this.props.actions; + const { whiteboardId } = this.props; + + undoAnnotation(whiteboardId); + addAnnotationToDiscardedList(getCurrentShapeId()); + } + render() { const baseName = Meteor.settings.public.app.basename; const pencilDrawStyle = { @@ -217,12 +232,14 @@ export default class PencilDrawListener extends Component { zIndex: 2 ** 31 - 1, // maximun value of z-index to prevent other things from overlapping cursor: `url('${baseName}/resources/images/whiteboard-cursor/pencil.png') 2 22, default`, }; + const { contextMenuHandler } = this.props.actions; return ( <div onTouchStart={this.handleTouchStart} role="presentation" style={pencilDrawStyle} onMouseDown={this.mouseDownHandler} + onContextMenu={contextMenuHandler} /> ); } diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/service.js b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/service.js index 8e61e2333db1b16470cb94c26bb682fdefa0f372..423b8965ea3fddc1f60ad98a492c845d11e3cf42 100755 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/service.js +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/service.js @@ -1,8 +1,6 @@ -import { makeCall } from '/imports/ui/services/api'; import Storage from '/imports/ui/services/storage/session'; import Auth from '/imports/ui/services/auth'; - -import { sendAnnotation } from '/imports/ui/components/whiteboard/service'; +import { sendAnnotation, addAnnotationToDiscardedList } from '/imports/ui/components/whiteboard/service'; const DRAW_SETTINGS = 'drawSettings'; @@ -49,11 +47,14 @@ const setTextShapeActiveId = (id) => { const getCurrentUserId = () => Auth.userID; +const contextMenuHandler = event => event.preventDefault(); export default { + addAnnotationToDiscardedList, sendAnnotation, getWhiteboardToolbarValues, setTextShapeActiveId, resetTextShapeSession, getCurrentUserId, + contextMenuHandler, }; diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/shape-draw-listener/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/shape-draw-listener/component.jsx index 07ec6a396e101ec03a8951fdd7cf87360de3fa45..8c17c485a7637efa1879c7e1c644ef9f89f08431 100755 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/shape-draw-listener/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/shape-draw-listener/component.jsx @@ -148,17 +148,25 @@ export default class ShapeDrawListener extends Component { // main mouse down handler handleMouseDown(event) { - // Sometimes when you Alt+Tab while drawing it can happen that your mouse is up, - // but the browser didn't catch it. So check it here. - if (this.isDrawing) { - return this.sendLastMessage(); - } + const isLeftClick = event.button === 0; + const isRightClick = event.button === 2; - window.addEventListener('mouseup', this.handleMouseUp); - window.addEventListener('mousemove', this.handleMouseMove, true); + if (!this.isDrawing) { + if (isLeftClick) { + window.addEventListener('mouseup', this.handleMouseUp); + window.addEventListener('mousemove', this.handleMouseMove, true); - const { clientX, clientY } = event; - return this.commonDrawStartHandler(clientX, clientY); + const { clientX, clientY } = event; + this.commonDrawStartHandler(clientX, clientY); + } + + // if you switch to a different window using Alt+Tab while mouse is down and release it + // it wont catch mouseUp and will keep tracking the movements. Thus we need this check. + } else if (isRightClick) { + // this.isDrawing = false; + this.sendLastMessage(); + this.discardAnnotation(); + } } // main mouse move handler @@ -280,6 +288,14 @@ export default class ShapeDrawListener extends Component { sendAnnotation(annotation, whiteboardId); } + discardAnnotation() { + const { getCurrentShapeId, addAnnotationToDiscardedList, undoAnnotation } = this.props.actions; + const { whiteboardId } = this.props; + + undoAnnotation(whiteboardId); + addAnnotationToDiscardedList(getCurrentShapeId()); + } + render() { const { tool } = this.props.drawSettings; const baseName = Meteor.settings.public.app.basename; @@ -290,12 +306,14 @@ export default class ShapeDrawListener extends Component { zIndex: 2 ** 31 - 1, // maximun value of z-index to prevent other things from overlapping cursor: `url('${baseName}/resources/images/whiteboard-cursor/${tool !== 'rectangle' ? tool : 'square'}.png'), default`, }; + const { contextMenuHandler } = this.props.actions; return ( <div onTouchStart={this.handleTouchStart} role="presentation" style={shapeDrawStyle} onMouseDown={this.handleMouseDown} + onContextMenu={contextMenuHandler} /> ); } diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/text-draw-listener/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/text-draw-listener/component.jsx index 2f155dbe71040df82c8fcc86141c22792e846042..b7d136ab9bd5dfc1bb79b5d00cb06733d453cea3 100755 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/text-draw-listener/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/text-draw-listener/component.jsx @@ -172,22 +172,30 @@ export default class TextDrawListener extends Component { // main mouse down handler handleMouseDown(event) { + const isLeftClick = event.button === 0; + const isRightClick = event.button === 2; + if (this.hasBeenTouchedRecently) { return; } // if our current drawing state is not drawing the box and not writing the text if (!this.state.isDrawing && !this.state.isWritingText) { - window.addEventListener('mouseup', this.handleMouseUp); - window.addEventListener('mousemove', this.handleMouseMove, true); + if (isLeftClick) { + window.addEventListener('mouseup', this.handleMouseUp); + window.addEventListener('mousemove', this.handleMouseMove, true); - const { clientX, clientY } = event; - this.commonDrawStartHandler(clientX, clientY); + const { clientX, clientY } = event; + this.commonDrawStartHandler(clientX, clientY); + } // second case is when a user finished writing the text and publishes the final result } else { // publishing the final shape and resetting the state this.sendLastMessage(); + if (isRightClick) { + this.discardAnnotation(); + } } } @@ -370,6 +378,14 @@ export default class TextDrawListener extends Component { sendAnnotation(annotation, whiteboardId); } + discardAnnotation() { + const { getCurrentShapeId, addAnnotationToDiscardedList, undoAnnotation } = this.props.actions; + const { whiteboardId } = this.props; + + undoAnnotation(whiteboardId); + addAnnotationToDiscardedList(getCurrentShapeId()); + } + render() { const baseName = Meteor.settings.public.app.basename; const textDrawStyle = { @@ -379,12 +395,14 @@ export default class TextDrawListener extends Component { zIndex: 2 ** 31 - 1, // maximun value of z-index to prevent other things from overlapping cursor: `url('${baseName}/resources/images/whiteboard-cursor/text.png'), default`, }; + const { contextMenuHandler } = this.props.actions; return ( <div role="presentation" style={textDrawStyle} onMouseDown={this.handleMouseDown} onTouchStart={this.handleTouchStart} + onContextMenu={contextMenuHandler} > {this.state.isDrawing ? <svg diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/styles.scss b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/styles.scss index 2878fd57bcbdfd2b2ead3c263cc4103b371b590d..60d9d7fbd1c6167322a98595736f153a27b0cc8b 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/styles.scss @@ -34,6 +34,7 @@ $toolbar-list-color: $color-gray; align-items: center; justify-content: center; box-shadow: $toolbar-box-shadow; + border-radius: $toolbar-button-border-radius; pointer-events: all; .buttonWrapper > .toolbarButton { diff --git a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js index 15873ba13aa7478c98e0b2b69d86fa04db414621..cb50880e2f6838d8f753a537dbb0d85e18be3a27 100755 --- a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js +++ b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js @@ -9,9 +9,11 @@ import logger from '/imports/startup/client/logger'; import { notify } from '/imports/ui/services/notification'; const MEDIA = Meteor.settings.public.media; +const MEDIA_TAG = MEDIA.mediaTag; const USE_SIP = MEDIA.useSIPAudio; const USE_KURENTO = Meteor.settings.public.kurento.enableListenOnly; const ECHO_TEST_NUMBER = MEDIA.echoTestNumber; +const MAX_LISTEN_ONLY_RETRIES = 2; const CALL_STATES = { STARTED: 'started', @@ -127,7 +129,7 @@ class AudioManager { .then(() => this.bridge.joinAudio(callOptions, this.callStateCallback.bind(this))); } - joinListenOnly() { + joinListenOnly(retries = 0) { this.isListenOnly = true; this.isEchoTest = false; // The kurento bridge isn't a full audio bridge yet, so we have to differ it @@ -145,24 +147,20 @@ class AudioManager { setTimeout(reject, 12000, iceGatheringErr); }); - - // Workaround to circumvent the WebKit autoplay policy without prompting - // the user to do some action again. A silent stream is played. - this.playFakeAudio(); - return this.onAudioJoining() .then(() => Promise.race([ bridge.joinAudio(callOptions, this.callStateCallback.bind(this)), iceGatheringTimeout, ])) .catch((err) => { - // If theres a iceGathering timeout we retry to join after asking device permissions - if (err === iceGatheringErr) { - return this.askDevicesPermissions() - .then(() => this.joinListenOnly()); + // If theres a iceGathering timeout we retry to join until MAX_LISTEN_ONLY_RETRIES + if (err === iceGatheringErr && retries < MAX_LISTEN_ONLY_RETRIES) { + this.joinListenOnly(++retries); + } else { + clearTimeout(iceGatheringTimeout); + logger.error('Listen only error:', err); + throw err; } - - throw err; }); } @@ -218,8 +216,6 @@ class AudioManager { }); } - clearInterval(this.fakeAudioInterval); - if (!this.isEchoTest) { this.notify(this.messages.info.JOINED_AUDIO); } @@ -269,7 +265,6 @@ class AudioManager { } else if (status === FAILED) { this.error = error; this.notify(this.messages.error[error] || this.messages.error.GENERIC_ERROR, true); - makeCall('failed callStateCallback audio', response); logger.error('Audio Error:', error, bridgeError); this.exitAudio(); this.onAudioExit(); @@ -286,7 +281,21 @@ class AudioManager { new window.AudioContext() : new window.webkitAudioContext(); - return this.listenOnlyAudioContext.createMediaStreamDestination().stream; + // Create a placeholder buffer to upstart audio context + const pBuffer = this.listenOnlyAudioContext.createBuffer(2, this.listenOnlyAudioContext.sampleRate * 3, this.listenOnlyAudioContext.sampleRate); + + var dest = this.listenOnlyAudioContext.createMediaStreamDestination(); + + let audio = document.querySelector(MEDIA_TAG); + + // Play bogus silent audio to try to circumvent autoplay policy on Safari + audio.src = 'resources/sounds/silence.mp3' + + audio.play().catch(e => { + logger.warn('Error on playing test audio:', e); + }); + + return dest.stream; } isUsingAudio() { @@ -359,19 +368,6 @@ class AudioManager { this.isListenOnly ? 'audio_on' : 'unmute', ); } - - playFakeAudio() { - const outputDeviceId = this.outputDeviceId; - const sound = new Audio('resources/sounds/silence.mp3'); - if (outputDeviceId && sound.setSinkId) { - sound.setSinkId(outputDeviceId); - } - // Hack within the hack: haven't got time to get the right timing to play - // the audio on stock listen only, but I'll get back to it - prlanzarin - this.fakeAudioInterval = setInterval(() => { - sound.play(); - }, 1000); - } } const audioManager = new AudioManager(); diff --git a/bigbluebutton-html5/imports/ui/services/auth/index.js b/bigbluebutton-html5/imports/ui/services/auth/index.js index d714597b58901dce4974b868daf0bf9d836a1887..d950defbd6f4164b26aaad269262c0a201f73f77 100755 --- a/bigbluebutton-html5/imports/ui/services/auth/index.js +++ b/bigbluebutton-html5/imports/ui/services/auth/index.js @@ -10,6 +10,17 @@ const CONNECTION_TIMEOUT = Meteor.settings.public.app.connectionTimeout; class Auth { constructor() { + this._loggedIn = { + value: false, + tracker: new Tracker.Dependency(), + }; + + const queryParams = new URLSearchParams(document.location.search); + if (queryParams.has('sessionToken') + && queryParams.get('sessionToken') !== Session.get('sessionToken')) { + return; + } + this._meetingID = Storage.getItem('meetingID'); this._userID = Storage.getItem('userID'); this._authToken = Storage.getItem('authToken'); @@ -18,10 +29,6 @@ class Auth { this._confname = Storage.getItem('confname'); this._externUserID = Storage.getItem('externUserID'); this._fullname = Storage.getItem('fullname'); - this._loggedIn = { - value: false, - tracker: new Tracker.Dependency(), - }; } get meetingID() { @@ -115,7 +122,7 @@ class Auth { sessionToken: this.sessionToken, fullname: this.fullname, externUserID: this.externUserID, - confname: this.confname + confname: this.confname, }; } @@ -149,7 +156,7 @@ class Auth { this.logoutURL = null; this.sessionToken = null; this.fullname = null; - this.externUserID = null + this.externUserID = null; this.confname = null; return Promise.resolve(...args); } diff --git a/bigbluebutton-html5/imports/ui/services/unread-messages/index.js b/bigbluebutton-html5/imports/ui/services/unread-messages/index.js index 6369f676dd48e65f19646a99797ea4d3ba518cbc..54bf967721cd737c00e6f96c83e4b11c3557a779 100755 --- a/bigbluebutton-html5/imports/ui/services/unread-messages/index.js +++ b/bigbluebutton-html5/imports/ui/services/unread-messages/index.js @@ -2,16 +2,17 @@ import { Tracker } from 'meteor/tracker'; import Storage from '/imports/ui/services/storage/session'; import Auth from '/imports/ui/services/auth'; -import Chats from '/imports/api/chat'; +import GroupChat from '/imports/api/group-chat'; +import GroupChatMsg from '/imports/api/group-chat-msg'; const CHAT_CONFIG = Meteor.settings.public.chat; const STORAGE_KEY = CHAT_CONFIG.storage_key; -const PUBLIC_CHAT_USERID = CHAT_CONFIG.public_userid; +const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id; class UnreadMessagesTracker { constructor() { this._tracker = new Tracker.Dependency(); - this._unreadChats = { ...Storage.getItem('UNREAD_CHATS'), [PUBLIC_CHAT_USERID]: (new Date()).getTime() }; + this._unreadChats = { ...Storage.getItem('UNREAD_CHATS'), [PUBLIC_GROUP_CHAT_ID]: (new Date()).getTime() }; this.get = this.get.bind(this); } @@ -33,19 +34,21 @@ class UnreadMessagesTracker { getUnreadMessages(chatID) { const filter = { - fromTime: { + timestamp: { $gt: this.get(chatID), }, - fromUserId: { $ne: Auth.userID }, + sender: { $ne: Auth.userID }, }; - // Minimongo does not support $eq. See https://github.com/meteor/meteor/issues/4142 - if (chatID === PUBLIC_CHAT_USERID) { - filter.toUserId = { $not: { $ne: chatID } }; + if (chatID === PUBLIC_GROUP_CHAT_ID) { + filter.chatId = { $not: { $ne: chatID } }; } else { - filter.toUserId = { $not: { $ne: Auth.userID } }; - filter.fromUserId.$not = { $ne: chatID }; + const privateChat = GroupChat.findOne({ users: { $all: [chatID, Auth.userID] } }); + + if (privateChat) { + filter.chatId = privateChat.chatId; + } } - const messages = Chats.find(filter).fetch(); + const messages = GroupChatMsg.find(filter).fetch(); return messages; } diff --git a/bigbluebutton-html5/imports/ui/services/user/mapUser.js b/bigbluebutton-html5/imports/ui/services/user/mapUser.js index 46f42ac6c35e0642007f3c6f4f8f25dfb2cfbf42..65b26c155f1679921ea978748ac4d43c9f4aeeb1 100755 --- a/bigbluebutton-html5/imports/ui/services/user/mapUser.js +++ b/bigbluebutton-html5/imports/ui/services/user/mapUser.js @@ -23,7 +23,7 @@ const mapUser = (user) => { isModerator: user.role === ROLE_MODERATOR, isCurrent: user.userId === userId, isVoiceUser: voiceUser ? voiceUser.joined : false, - isMuted: voiceUser ? voiceUser.muted : false, + isMuted: voiceUser ? voiceUser.muted && !voiceUser.listenOnly : false, isTalking: voiceUser ? voiceUser.talking && !voiceUser.muted : false, isListenOnly: voiceUser ? voiceUser.listenOnly : false, isSharingWebcam: user.has_stream, diff --git a/bigbluebutton-html5/imports/ui/stylesheets/variables/breakpoints.scss b/bigbluebutton-html5/imports/ui/stylesheets/variables/breakpoints.scss index 86e59b471945d25cd1396d9a7a72840164abcc4f..3ae1f452baf3d88f4b85843c5f89ef8cc5e7a2d8 100644 --- a/bigbluebutton-html5/imports/ui/stylesheets/variables/breakpoints.scss +++ b/bigbluebutton-html5/imports/ui/stylesheets/variables/breakpoints.scss @@ -41,6 +41,16 @@ $xlarge-only: "#{$screen} and (min-width:#{lower-bound($xlarge-range)}) and (max $xxlarge-up: "#{$screen} and (min-width:#{lower-bound($xxlarge-range)})"; $xxlarge-only: "#{$screen} and (min-width:#{lower-bound($xxlarge-range)}) and (max-width:#{upper-bound($xxlarge-range)})"; +//iPhone 5 size range +$ip5-range: "(min-device-width : 320px) and (max-device-width : 568px)"; +// iPhone 6, 7 and 8 size range +$ip678-range: "(min-device-width : 375px) and (max-device-width : 667px)"; + +$ip5-portrait: "#{$portrait} and #{$ip5-range}"; +$ip5-landscape: "#{$landscape} and #{$ip5-range}"; +$ip678-portrait: "#{$portrait} and #{$ip678-range}"; +$ip678-landscape: "#{$landscape} and #{$ip678-range}"; + $breakpoints: ( 'screen': $screen, 'landscape': $landscape, diff --git a/bigbluebutton-html5/imports/utils/statuses.js b/bigbluebutton-html5/imports/utils/statuses.js index 9aa785f63997e224beb0e8ab4492bdfbc6833410..848c521c4788f787de3e1c763de21cc659dfe31b 100644 --- a/bigbluebutton-html5/imports/utils/statuses.js +++ b/bigbluebutton-html5/imports/utils/statuses.js @@ -9,7 +9,6 @@ export const EMOJI_STATUSES = { applause: 'applause', thumbsUp: 'thumbs_up', thumbsDown: 'thumbs_down', - none: 'clear_status', }; export default { EMOJI_STATUSES }; diff --git a/bigbluebutton-html5/package-lock.json b/bigbluebutton-html5/package-lock.json index 3ed2bcd1716b68dfb96bd803b119efd256630b39..b4df594d445bafe538d37e4bc1d063f4de70bcde 100755 --- a/bigbluebutton-html5/package-lock.json +++ b/bigbluebutton-html5/package-lock.json @@ -11,6 +11,14 @@ "@browser-bunyan/levels": "1.3.0" } }, + "@browser-bunyan/console-plain-stream": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@browser-bunyan/console-plain-stream/-/console-plain-stream-1.4.0.tgz", + "integrity": "sha512-/8fCyMZyEN3jWToHzBsM3zf6WRUp1b4W87l298gjqPg98uws1mwVMY4Rfy4HUe/i/aDCYzDCQlxVNYXmoFzTUA==", + "requires": { + "@browser-bunyan/levels": "1.3.0" + } + }, "@browser-bunyan/console-raw-stream": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@browser-bunyan/console-raw-stream/-/console-raw-stream-1.3.0.tgz", @@ -32,8 +40,7 @@ "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "acorn": { "version": "5.3.0", @@ -68,10 +75,9 @@ "version": "5.5.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "dev": true, "requires": { "co": "4.6.0", - "fast-deep-equal": "1.0.0", + "fast-deep-equal": "1.1.0", "fast-json-stable-stringify": "2.0.0", "json-schema-traverse": "0.3.1" } @@ -85,8 +91,7 @@ "amdefine": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "dev": true + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" }, "ansi-escapes": { "version": "3.0.0", @@ -97,14 +102,12 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" }, "any-promise": { "version": "1.3.0", @@ -120,8 +123,7 @@ "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, "archiver-utils": { "version": "1.3.0", @@ -129,12 +131,12 @@ "integrity": "sha1-5QtMCccL89aA4y/xt5lOn52JUXQ=", "dev": true, "requires": { - "glob": "7.1.2", + "glob": "7.1.3", "graceful-fs": "4.1.11", "lazystream": "1.0.0", "lodash": "4.17.10", "normalize-path": "2.1.1", - "readable-stream": "2.3.3" + "readable-stream": "2.3.6" }, "dependencies": { "normalize-path": { @@ -149,13 +151,12 @@ } }, "are-we-there-yet": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", - "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", - "dev": true, + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "requires": { "delegates": "1.0.0", - "readable-stream": "2.3.3" + "readable-stream": "2.3.6" } }, "argparse": { @@ -179,8 +180,7 @@ "array-find-index": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", - "dev": true + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=" }, "array-includes": { "version": "3.0.3", @@ -231,16 +231,17 @@ "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" }, "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", - "dev": true + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "2.1.2" + } }, "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, "assertion-error": { "version": "1.1.0", @@ -262,14 +263,12 @@ "async-foreach": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", - "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", - "dev": true + "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=" }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "atob": { "version": "1.1.3", @@ -278,15 +277,17 @@ "dev": true }, "attr-accept": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-1.1.0.tgz", - "integrity": "sha1-tc01In8WOTWo8d4Q7T66FpQfa+Y=" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-1.1.3.tgz", + "integrity": "sha512-iT40nudw8zmCweivz6j58g+RT33I4KbaIvRUhjNmDwO2WmsQUxFEZZYZ5w3vXe5x5MX9D7mfvA/XaLOZYFR9EQ==", + "requires": { + "core-js": "2.5.7" + } }, "autoprefixer": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-7.1.6.tgz", "integrity": "sha512-C9yv/UF3X+eJTi/zvfxuyfxmLibYrntpF3qoJYrMeQwgUJOZrZvpJiMG2FMQ3qnhWtF/be4pYONBBw95ZGe3vA==", - "dev": true, "requires": { "browserslist": "2.11.3", "caniuse-lite": "1.0.30000792", @@ -304,14 +305,12 @@ "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", - "dev": true + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" }, "axobject-query": { "version": "0.1.0", @@ -336,29 +335,26 @@ "babel-plugin-react-remove-properties": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/babel-plugin-react-remove-properties/-/babel-plugin-react-remove-properties-0.2.5.tgz", - "integrity": "sha1-wHbhKRlAxzD0+337ZwaR/EF4fPg=", - "dev": true + "integrity": "sha1-wHbhKRlAxzD0+337ZwaR/EF4fPg=" }, "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "requires": { - "core-js": "2.5.3", + "core-js": "2.5.7", "regenerator-runtime": "0.11.1" } }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "dev": true, + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", "optional": true, "requires": { "tweetnacl": "0.14.5" @@ -376,14 +372,13 @@ "integrity": "sha1-ysMo977kVzDUBLaSID/LWQ4XLV4=", "dev": true, "requires": { - "readable-stream": "2.3.3" + "readable-stream": "2.3.6" } }, "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "dev": true, "requires": { "inherits": "2.0.3" } @@ -394,31 +389,22 @@ "integrity": "sha1-ZBE+nHzxICs3btYHvzBibr5XsYo=", "dev": true }, - "boom": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", - "dev": true, - "requires": { - "hoek": "4.2.0" - } - }, "brace-expansion": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", - "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", - "dev": true, + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "requires": { "balanced-match": "1.0.0", "concat-map": "0.0.1" } }, "browser-bunyan": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/browser-bunyan/-/browser-bunyan-1.3.0.tgz", - "integrity": "sha1-JjeNxY16mAAsyb/Pui6l1xJEmZI=", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/browser-bunyan/-/browser-bunyan-1.4.0.tgz", + "integrity": "sha512-fED2PWhdLmS2zvc+oFI+2asiV7vcqw/B4QSTKzC15pIAnk5dk7Fv2zAxOYdMnbY9rcke6FH3a/FLxSSd2ZOo9g==", "requires": { "@browser-bunyan/console-formatted-stream": "1.3.0", + "@browser-bunyan/console-plain-stream": "1.4.0", "@browser-bunyan/console-raw-stream": "1.3.0", "@browser-bunyan/levels": "1.3.0" } @@ -442,7 +428,6 @@ "version": "2.11.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.11.3.tgz", "integrity": "sha512-yWu5cXT7Av6mVwzWc8lMsJMHWn4xyjSuGYi4IozbVTLUOEYPSagUB8kiMDUHA1fS3zjr8nkxkn9jdvug4BBRmA==", - "dev": true, "requires": { "caniuse-lite": "1.0.30000792", "electron-to-chromium": "1.3.31" @@ -463,8 +448,7 @@ "builtin-modules": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", - "dev": true + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" }, "caller-path": { "version": "0.1.0", @@ -484,14 +468,12 @@ "camelcase": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", - "dev": true + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" }, "camelcase-keys": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", - "dev": true, "requires": { "camelcase": "2.1.1", "map-obj": "1.0.1" @@ -500,14 +482,12 @@ "caniuse-lite": { "version": "1.0.30000792", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000792.tgz", - "integrity": "sha1-0M6pgfgRjzlhRxr7tDyaHlu/AzI=", - "dev": true + "integrity": "sha1-0M6pgfgRjzlhRxr7tDyaHlu/AzI=" }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "chai": { "version": "4.1.2", @@ -530,9 +510,8 @@ }, "chalk": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, "requires": { "ansi-styles": "2.2.1", "escape-string-regexp": "1.0.5", @@ -566,9 +545,9 @@ "dev": true }, "classnames": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.5.tgz", - "integrity": "sha1-+zgB1FNGdknvNgPH1hoCvRKb3m0=" + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", + "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" }, "cli-cursor": { "version": "2.1.0", @@ -623,7 +602,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "dev": true, "requires": { "string-width": "1.0.2", "strip-ansi": "3.0.1", @@ -633,20 +611,17 @@ "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "color-convert": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", - "dev": true, "requires": { "color-name": "1.1.3" } @@ -654,8 +629,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "colors": { "version": "1.0.3", @@ -663,10 +637,9 @@ "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" }, "combined-stream": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", - "dev": true, + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", "requires": { "delayed-stream": "1.0.0" } @@ -686,7 +659,7 @@ "buffer-crc32": "0.2.13", "crc32-stream": "2.0.0", "normalize-path": "2.1.1", - "readable-stream": "2.3.3" + "readable-stream": "2.3.6" }, "dependencies": { "normalize-path": { @@ -708,8 +681,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { "version": "1.6.0", @@ -718,15 +690,14 @@ "dev": true, "requires": { "inherits": "2.0.3", - "readable-stream": "2.3.3", + "readable-stream": "2.3.6", "typedarray": "0.0.6" } }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, "contains-path": { "version": "0.1.0", @@ -735,15 +706,14 @@ "dev": true }, "core-js": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.3.tgz", - "integrity": "sha1-isw4NFgk8W2DZbfJtCWRaOjtYD4=" + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", + "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==" }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "cosmiconfig": { "version": "1.1.0", @@ -782,7 +752,7 @@ "dev": true, "requires": { "crc": "3.5.0", - "readable-stream": "2.3.3" + "readable-stream": "2.3.6" } }, "create-react-class": { @@ -799,30 +769,9 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", - "dev": true, "requires": { - "lru-cache": "4.1.1", - "which": "1.3.0" - } - }, - "cryptiles": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", - "dev": true, - "requires": { - "boom": "5.2.0" - }, - "dependencies": { - "boom": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", - "dev": true, - "requires": { - "hoek": "4.2.0" - } - } + "lru-cache": "4.1.3", + "which": "1.3.1" } }, "css": { @@ -884,7 +833,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", - "dev": true, "requires": { "array-find-index": "1.0.2" } @@ -909,7 +857,6 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, "requires": { "assert-plus": "1.0.0" } @@ -937,8 +884,7 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, "decompress-response": { "version": "3.3.0", @@ -996,8 +942,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "delegate": { "version": "3.2.0", @@ -1007,8 +952,7 @@ "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, "doctrine": { "version": "2.1.0", @@ -1041,13 +985,13 @@ "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" }, "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "dev": true, + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", "optional": true, "requires": { - "jsbn": "0.1.1" + "jsbn": "0.1.1", + "safer-buffer": "2.1.2" } }, "ejs": { @@ -1059,8 +1003,7 @@ "electron-to-chromium": { "version": "1.3.31", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.31.tgz", - "integrity": "sha512-XE4CLbswkZgZFn34cKFy1xaX+F5LHxeDLjY1+rsK9asDzknhbrd9g/n/01/acbU25KTsUSiLKwvlLyA+6XLUOA==", - "dev": true + "integrity": "sha512-XE4CLbswkZgZFn34cKFy1xaX+F5LHxeDLjY1+rsK9asDzknhbrd9g/n/01/acbU25KTsUSiLKwvlLyA+6XLUOA==" }, "elegant-spinner": { "version": "1.0.1", @@ -1092,10 +1035,9 @@ } }, "error-ex": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", - "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", - "dev": true, + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "requires": { "is-arrayish": "0.2.1" } @@ -1133,8 +1075,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { "version": "4.9.0", @@ -1156,7 +1097,7 @@ "esutils": "2.0.2", "file-entry-cache": "2.0.0", "functional-red-black-tree": "1.0.1", - "glob": "7.1.2", + "glob": "7.1.3", "globals": "9.18.0", "ignore": "3.3.7", "imurmurhash": "0.1.4", @@ -1174,7 +1115,7 @@ "pluralize": "7.0.0", "progress": "2.0.0", "require-uncached": "1.0.3", - "semver": "5.5.0", + "semver": "5.5.1", "strip-ansi": "4.0.0", "strip-json-comments": "2.0.1", "table": "4.0.2", @@ -1213,9 +1154,9 @@ "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", "dev": true, "requires": { - "lru-cache": "4.1.1", + "lru-cache": "4.1.3", "shebang-command": "1.2.0", - "which": "1.3.0" + "which": "1.3.1" } }, "debug": { @@ -1396,7 +1337,7 @@ "doctrine": "2.1.0", "has": "1.0.1", "jsx-ast-utils": "2.0.1", - "prop-types": "15.6.0" + "prop-types": "15.6.2" } }, "eslint-restricted-globals": { @@ -1488,9 +1429,9 @@ "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", "dev": true, "requires": { - "lru-cache": "4.1.1", + "lru-cache": "4.1.3", "shebang-command": "1.2.0", - "which": "1.3.0" + "which": "1.3.1" } } } @@ -1519,10 +1460,9 @@ "dev": true }, "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", - "dev": true + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "external-editor": { "version": "2.1.0", @@ -1538,8 +1478,7 @@ "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, "eyes": { "version": "0.1.8", @@ -1547,16 +1486,14 @@ "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" }, "fast-deep-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", - "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=", - "dev": true + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" }, "fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", - "dev": true + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, "fast-levenshtein": { "version": "2.0.6", @@ -1626,7 +1563,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, "requires": { "path-exists": "2.1.0", "pinkie-promise": "2.0.1" @@ -1655,8 +1591,7 @@ "flatten": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz", - "integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=", - "dev": true + "integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=" }, "for-each": { "version": "0.3.2", @@ -1676,18 +1611,16 @@ "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, "form-data": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", - "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", - "dev": true, + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", "requires": { "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.17" + "combined-stream": "1.0.6", + "mime-types": "2.1.20" } }, "fs-extra": { @@ -1704,14 +1637,12 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fstream": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", - "dev": true, "requires": { "graceful-fs": "4.1.11", "inherits": "2.0.3", @@ -1735,7 +1666,6 @@ "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, "requires": { "aproba": "1.2.0", "console-control-strings": "1.1.0", @@ -1744,23 +1674,21 @@ "signal-exit": "3.0.2", "string-width": "1.0.2", "strip-ansi": "3.0.1", - "wide-align": "1.1.2" + "wide-align": "1.1.3" } }, "gaze": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.2.tgz", - "integrity": "sha1-hHIkZ3rbiHDWeSV+0ziP22HkAQU=", - "dev": true, + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", "requires": { - "globule": "1.2.0" + "globule": "1.2.1" } }, "get-caller-file": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", - "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=", - "dev": true + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" }, "get-func-name": { "version": "2.0.0", @@ -1777,8 +1705,7 @@ "get-stdin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" }, "get-stream": { "version": "3.0.0", @@ -1789,16 +1716,14 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, "requires": { "assert-plus": "1.0.0" } }, "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "requires": { "fs.realpath": "1.0.0", "inflight": "1.0.6", @@ -1832,19 +1757,18 @@ "requires": { "array-union": "1.0.2", "arrify": "1.0.1", - "glob": "7.1.2", + "glob": "7.1.3", "object-assign": "4.1.1", "pify": "2.3.0", "pinkie-promise": "2.0.1" } }, "globule": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.0.tgz", - "integrity": "sha1-HcScaCLdnoovoAuiopUAboZkvQk=", - "dev": true, + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", + "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", "requires": { - "glob": "7.1.2", + "glob": "7.1.3", "lodash": "4.17.10", "minimatch": "3.0.4" } @@ -1867,8 +1791,8 @@ "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", "dev": true, "requires": { - "lru-cache": "4.1.1", - "which": "1.3.0" + "lru-cache": "4.1.3", + "which": "1.3.1" } }, "debug": { @@ -1914,20 +1838,17 @@ "graceful-fs": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, "har-validator": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "dev": true, "requires": { "ajv": "5.5.2", "har-schema": "2.0.0" @@ -1946,7 +1867,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, "requires": { "ansi-regex": "2.1.1" } @@ -1954,8 +1874,7 @@ "has-flag": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", - "dev": true + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" }, "has-symbol-support-x": { "version": "1.4.1", @@ -1979,20 +1898,7 @@ "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true - }, - "hawk": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", - "dev": true, - "requires": { - "boom": "4.3.1", - "cryptiles": "3.1.2", - "hoek": "4.2.0", - "sntp": "2.1.0" - } + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, "history": { "version": "3.3.0", @@ -2005,32 +1911,24 @@ "warning": "3.0.0" } }, - "hoek": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", - "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==", - "dev": true - }, "hoist-non-react-statics": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz", "integrity": "sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs=" }, "hosted-git-info": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", - "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==", - "dev": true + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==" }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, "requires": { "assert-plus": "1.0.0", "jsprim": "1.4.1", - "sshpk": "1.13.1" + "sshpk": "1.14.2" } }, "humanize-duration": { @@ -2098,14 +1996,12 @@ "in-publish": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", - "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=", - "dev": true + "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=" }, "indent-string": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", - "dev": true, "requires": { "repeating": "2.0.1" } @@ -2113,14 +2009,12 @@ "indexes-of": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", - "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", - "dev": true + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=" }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "1.4.0", "wrappy": "1.0.2" @@ -2258,8 +2152,7 @@ "invert-kv": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", - "dev": true + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" }, "ip-regex": { "version": "1.0.3", @@ -2270,8 +2163,7 @@ "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" }, "is-buffer": { "version": "1.1.6", @@ -2282,7 +2174,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", - "dev": true, "requires": { "builtin-modules": "1.1.1" } @@ -2318,7 +2209,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "dev": true, "requires": { "number-is-nan": "1.0.1" } @@ -2327,7 +2217,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, "requires": { "number-is-nan": "1.0.1" } @@ -2433,26 +2322,22 @@ "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, "is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "isomorphic-fetch": { "version": "2.2.1", @@ -2484,7 +2369,7 @@ "dev": true, "requires": { "exit": "0.1.2", - "glob": "7.1.2", + "glob": "7.1.3", "jasmine-core": "2.9.1" } }, @@ -2561,7 +2446,7 @@ "pixelmatch": "4.0.2", "pngjs": "3.3.1", "read-chunk": "1.0.1", - "request": "2.83.0", + "request": "2.87.0", "stream-to-buffer": "0.1.0", "tinycolor2": "1.4.1", "url-regex": "3.2.0" @@ -2582,10 +2467,9 @@ "dev": true }, "js-base64": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.3.tgz", - "integrity": "sha512-H7ErYLM34CvDMto3GbD6xD0JLUGYXR3QTcH6B/tr4Hi/QpSThnCsIp+Sy5FRTw3B0d6py4HcNkW7nO/wdtGWEw==", - "dev": true + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.9.tgz", + "integrity": "sha512-xcinL3AuDJk7VSzsHgb9DvvIXayBbadtMZ4HFPx8rUszbW1MuNMlwYVC4zzCZ6e1sqZpnNS5ZFYOhXqA39T7LQ==" }, "js-tokens": { "version": "3.0.2", @@ -2606,7 +2490,6 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true, "optional": true }, "jsesc": { @@ -2618,14 +2501,12 @@ "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, "json-schema-traverse": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", - "dev": true + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" }, "json-stable-stringify": { "version": "1.0.1", @@ -2639,8 +2520,7 @@ "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "jsonfile": { "version": "3.0.1", @@ -2661,7 +2541,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", @@ -2689,14 +2568,13 @@ "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", "dev": true, "requires": { - "readable-stream": "2.3.3" + "readable-stream": "2.3.6" } }, "lcid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "dev": true, "requires": { "invert-kv": "1.0.0" } @@ -2941,7 +2819,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, "requires": { "graceful-fs": "4.1.11", "parse-json": "2.2.0", @@ -2976,14 +2853,12 @@ "lodash.assign": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", - "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=", - "dev": true + "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=" }, "lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", - "dev": true + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" }, "lodash.cond": { "version": "4.5.2", @@ -2998,10 +2873,9 @@ "dev": true }, "lodash.mergewith": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz", - "integrity": "sha1-FQzwoWeR9ZA7iJHqsVRgknS96lU=", - "dev": true + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz", + "integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==" }, "log-symbols": { "version": "2.2.0", @@ -3098,7 +2972,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "dev": true, "requires": { "currently-unhandled": "0.4.1", "signal-exit": "3.0.2" @@ -3110,10 +2983,9 @@ "integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=" }, "lru-cache": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", - "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", - "dev": true, + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", + "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", "requires": { "pseudomap": "1.0.2", "yallist": "2.1.2" @@ -3130,8 +3002,7 @@ "map-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" }, "material-colors": { "version": "1.2.5", @@ -3142,7 +3013,6 @@ "version": "3.7.0", "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", - "dev": true, "requires": { "camelcase-keys": "2.1.0", "decamelize": "1.2.0", @@ -3154,39 +3024,31 @@ "read-pkg-up": "1.0.1", "redent": "1.0.0", "trim-newlines": "1.0.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } } }, "meteor-node-stubs": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/meteor-node-stubs/-/meteor-node-stubs-0.3.2.tgz", - "integrity": "sha512-l93SS/HutbqBRJODO2m7hup8cYI2acF5bB39+ZvP2BX8HMmCSCXeFH7v0sr4hD7zrVvHQA5UqS0pcDYKn0VM6g==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/meteor-node-stubs/-/meteor-node-stubs-0.3.3.tgz", + "integrity": "sha512-TI1aQRK0vqs94OCkUMkmf5lXNWfIsjSaEDP1inUuwRGt9w8/S2V+HdRikz9r1k/gew+7NcJieaqHsHX7pSTEgA==", "requires": { "assert": "1.4.1", "browserify-zlib": "0.1.4", "buffer": "4.9.1", "console-browserify": "1.1.0", "constants-browserify": "1.0.0", - "crypto-browserify": "3.11.1", - "domain-browser": "1.1.7", + "crypto-browserify": "3.12.0", + "domain-browser": "1.2.0", "events": "1.1.1", - "http-browserify": "1.7.0", "https-browserify": "0.0.1", "os-browserify": "0.2.1", "path-browserify": "0.0.0", "process": "0.11.10", "punycode": "1.4.1", "querystring-es3": "0.2.1", - "readable-stream": "git+https://github.com/meteor/readable-stream.git#d64a64aa6061b9b6855feff4d09e58fb3b2e4502", + "readable-stream": "git+https://github.com/meteor/readable-stream.git#c688cdd193549919b840e8d72a86682d91961e12", "stream-browserify": "2.0.1", - "string_decoder": "1.0.3", + "stream-http": "2.8.0", + "string_decoder": "1.1.0", "timers-browserify": "1.4.2", "tty-browserify": "0.0.0", "url": "0.11.0", @@ -3194,15 +3056,10 @@ "vm-browserify": "0.0.4" }, "dependencies": { - "Base64": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/Base64/-/Base64-0.2.1.tgz", - "integrity": "sha1-ujpCMHCOGGcFBl5mur3Uw1z2ACg=" - }, "asn1.js": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.9.1.tgz", - "integrity": "sha1-SLokC0WpKA6UdImQull9IWYX/UA=", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", "requires": { "bn.js": "4.11.8", "inherits": "2.0.1", @@ -3223,9 +3080,9 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base64-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz", - "integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==" + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.3.tgz", + "integrity": "sha512-MsAhsUW1GxCdgYSO6tAfZrNapmUKk7mWx/k5mFY/A1gBtkaCaNapTg+FExCw1r9yeaZhqx/xPg43xgTFH6KL5w==" }, "bn.js": { "version": "4.11.8", @@ -3233,9 +3090,9 @@ "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" }, "brace-expansion": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", - "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "requires": { "balanced-match": "1.0.0", "concat-map": "0.0.1" @@ -3247,9 +3104,9 @@ "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" }, "browserify-aes": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.1.0.tgz", - "integrity": "sha512-W2bIMLYoZ9oow7TyePpMJk9l9LY7O3R61a/68bVCDOtnJynnwe3ZeW2IzzSkrQnPKNdJrxVDn3ALZNisSBwb7g==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.1.1.tgz", + "integrity": "sha512-UGnTYAnB2a3YuYKIRy1/4FB2HdM866E0qC46JXvVTYKlBlZlnvfpSfY6OKfXZAkv70eJ2a1SqzpAo5CRhZGDFg==", "requires": { "buffer-xor": "1.0.3", "cipher-base": "1.0.4", @@ -3264,7 +3121,7 @@ "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.0.tgz", "integrity": "sha1-mYgkSHS/XtTijalWZtzWasj8Njo=", "requires": { - "browserify-aes": "1.1.0", + "browserify-aes": "1.1.1", "browserify-des": "1.0.0", "evp_bytestokey": "1.0.3" } @@ -3285,7 +3142,7 @@ "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", "requires": { "bn.js": "4.11.8", - "randombytes": "2.0.5" + "randombytes": "2.0.6" } }, "browserify-sign": { @@ -3315,7 +3172,7 @@ "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", "requires": { - "base64-js": "1.2.1", + "base64-js": "1.2.3", "ieee754": "1.1.8", "isarray": "1.0.0" } @@ -3325,6 +3182,11 @@ "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=" + }, "cipher-base": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", @@ -3369,7 +3231,7 @@ "cipher-base": "1.0.4", "inherits": "2.0.1", "ripemd160": "2.0.1", - "sha.js": "2.4.9" + "sha.js": "2.4.10" } }, "create-hmac": { @@ -3382,13 +3244,13 @@ "inherits": "2.0.1", "ripemd160": "2.0.1", "safe-buffer": "5.1.1", - "sha.js": "2.4.9" + "sha.js": "2.4.10" } }, "crypto-browserify": { - "version": "3.11.1", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.11.1.tgz", - "integrity": "sha512-Na7ZlwCOqoaW5RwUK1WpXws2kv8mNhWdTlzob0UXulk6G9BDbyiJaGTYBIX61Ozn9l1EPPJpICZb4DaOpT9NlQ==", + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", "requires": { "browserify-cipher": "1.0.0", "browserify-sign": "4.0.4", @@ -3399,7 +3261,8 @@ "inherits": "2.0.1", "pbkdf2": "3.0.14", "public-encrypt": "4.0.0", - "randombytes": "2.0.5" + "randombytes": "2.0.6", + "randomfill": "1.0.4" } }, "date-now": { @@ -3423,13 +3286,13 @@ "requires": { "bn.js": "4.11.8", "miller-rabin": "4.0.1", - "randombytes": "2.0.5" + "randombytes": "2.0.6" } }, "domain-browser": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz", - "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==" }, "elliptic": { "version": "6.4.0", @@ -3511,15 +3374,6 @@ "minimalistic-crypto-utils": "1.0.1" } }, - "http-browserify": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/http-browserify/-/http-browserify-1.7.0.tgz", - "integrity": "sha1-M3la3nLfiKz7/TZ3PO/tp2RzWyA=", - "requires": { - "Base64": "0.2.1", - "inherits": "2.0.1" - } - }, "https-browserify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-0.0.1.tgz", @@ -3598,7 +3452,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "requires": { - "brace-expansion": "1.1.8" + "brace-expansion": "1.1.11" } }, "once": { @@ -3624,8 +3478,8 @@ "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.0.tgz", "integrity": "sha1-N8T5t+06tlx0gXtfJICTf7+XxxI=", "requires": { - "asn1.js": "4.9.1", - "browserify-aes": "1.1.0", + "asn1.js": "4.10.1", + "browserify-aes": "1.1.1", "create-hash": "1.1.3", "evp_bytestokey": "1.0.3", "pbkdf2": "3.0.14" @@ -3650,7 +3504,7 @@ "create-hmac": "1.1.6", "ripemd160": "2.0.1", "safe-buffer": "5.1.1", - "sha.js": "2.4.9" + "sha.js": "2.4.10" } }, "process": { @@ -3659,9 +3513,9 @@ "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" }, "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, "public-encrypt": { "version": "4.0.0", @@ -3672,7 +3526,7 @@ "browserify-rsa": "4.0.1", "create-hash": "1.1.3", "parse-asn1": "5.1.0", - "randombytes": "2.0.5" + "randombytes": "2.0.6" } }, "punycode": { @@ -3691,21 +3545,30 @@ "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" }, "randombytes": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.5.tgz", - "integrity": "sha512-8T7Zn1AhMsQ/HI1SjcCfT/t4ii3eAqco3yOcSzS4mozsOz69lHLsoMXmF9nZgnFanYscnSlUSgs8uZyKzpE6kg==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", + "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", "requires": { + "randombytes": "2.0.6", "safe-buffer": "5.1.1" } }, "readable-stream": { - "version": "git+https://github.com/meteor/readable-stream.git#d64a64aa6061b9b6855feff4d09e58fb3b2e4502", + "version": "git+https://github.com/meteor/readable-stream.git#c688cdd193549919b840e8d72a86682d91961e12", "requires": { "inherits": "2.0.3", "isarray": "1.0.0", - "process-nextick-args": "1.0.7", + "process-nextick-args": "2.0.0", "safe-buffer": "5.1.1", - "string_decoder": "1.0.3", + "string_decoder": "1.1.0", "util-deprecate": "1.0.2" }, "dependencies": { @@ -3739,9 +3602,9 @@ "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" }, "sha.js": { - "version": "2.4.9", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.9.tgz", - "integrity": "sha512-G8zektVqbiPHrylgew9Zg1VRB1L/DtXNUVAM6q4QLy8NE3qtHlFXTf8VLL4k1Yl6c7NMjtZUTdXV+X44nFaT6A==", + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.10.tgz", + "integrity": "sha512-vnwmrFDlOExK4Nm16J2KMWHLrp14lBrjxMxBJpu++EnsuBmpiYaM/MEs46Vxxm/4FvdP5yTwuCTO9it5FSjrqA==", "requires": { "inherits": "2.0.1", "safe-buffer": "5.1.1" @@ -3753,13 +3616,25 @@ "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", "requires": { "inherits": "2.0.1", - "readable-stream": "git+https://github.com/meteor/readable-stream.git#d64a64aa6061b9b6855feff4d09e58fb3b2e4502" + "readable-stream": "git+https://github.com/meteor/readable-stream.git#c688cdd193549919b840e8d72a86682d91961e12" + } + }, + "stream-http": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.0.tgz", + "integrity": "sha512-sZOFxI/5xw058XIRHl4dU3dZ+TTOIGJR78Dvo0oEAejIt4ou27k+3ne1zYmCV+v7UucbxIFQuOgnkTVHh8YPnw==", + "requires": { + "builtin-status-codes": "3.0.0", + "inherits": "2.0.1", + "readable-stream": "git+https://github.com/meteor/readable-stream.git#c688cdd193549919b840e8d72a86682d91961e12", + "to-arraybuffer": "1.0.1", + "xtend": "4.0.1" } }, "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.0.tgz", + "integrity": "sha512-8zQpRF6juocE69ae7CSPmYEGJe4VCXwP6S6dxUWI7i53Gwv54/ec41fiUA+X7BPGGv7fRSQJjBQVa0gomGaOgg==", "requires": { "safe-buffer": "5.1.1" } @@ -3772,6 +3647,11 @@ "process": "0.11.10" } }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=" + }, "tty-browserify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", @@ -3818,6 +3698,11 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" } } }, @@ -3828,18 +3713,16 @@ "dev": true }, "mime-db": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", - "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=", - "dev": true + "version": "1.36.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz", + "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==" }, "mime-types": { - "version": "2.1.17", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", - "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", - "dev": true, + "version": "2.1.20", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz", + "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==", "requires": { - "mime-db": "1.30.0" + "mime-db": "1.36.0" } }, "mimic-fn": { @@ -3866,24 +3749,28 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, "requires": { - "brace-expansion": "1.1.8" + "brace-expansion": "1.1.11" } }, "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, "mkdirp": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, "requires": { "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + } } }, "ms": { @@ -3897,6 +3784,11 @@ "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", "dev": true }, + "nan": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.0.tgz", + "integrity": "sha512-F4miItu2rGnV2ySkXOQoA8FKz/SR2Q2sWP0sbTxNxz/tuokeC8WxOhPMcwi0qIyGtVn/rrSeLbvVkznqCdwYnw==" + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -3918,31 +3810,28 @@ } }, "node-gyp": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.6.2.tgz", - "integrity": "sha1-m/vlRWIoYoSDjnUOrAUpWFP6HGA=", - "dev": true, + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", + "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", "requires": { "fstream": "1.0.11", - "glob": "7.1.2", + "glob": "7.1.3", "graceful-fs": "4.1.11", - "minimatch": "3.0.4", "mkdirp": "0.5.1", "nopt": "3.0.6", "npmlog": "4.1.2", - "osenv": "0.1.4", - "request": "2.83.0", + "osenv": "0.1.5", + "request": "2.87.0", "rimraf": "2.6.2", "semver": "5.3.0", "tar": "2.2.1", - "which": "1.3.0" + "which": "1.3.1" }, "dependencies": { "semver": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", - "dev": true + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" } } }, @@ -3956,66 +3845,29 @@ } }, "node-sass": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.2.tgz", - "integrity": "sha512-LdxoJLZutx0aQXHtWIYwJKMj+9pTjneTcLWJgzf2XbGu0q5pRNqW5QvFCEdm3mc5rJOdru/mzln5d0EZLacf6g==", - "dev": true, + "version": "4.9.3", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.3.tgz", + "integrity": "sha512-XzXyGjO+84wxyH7fV6IwBOTrEBe2f0a6SBze9QWWYR/cL74AcQUks2AsqcCZenl/Fp/JVbuEaLpgrLtocwBUww==", "requires": { "async-foreach": "0.1.3", "chalk": "1.1.3", "cross-spawn": "3.0.1", - "gaze": "1.1.2", + "gaze": "1.1.3", "get-stdin": "4.0.1", - "glob": "7.1.2", + "glob": "7.1.3", "in-publish": "2.0.0", "lodash.assign": "4.2.0", "lodash.clonedeep": "4.5.0", - "lodash.mergewith": "4.6.0", + "lodash.mergewith": "4.6.1", "meow": "3.7.0", "mkdirp": "0.5.1", - "nan": "2.10.0", - "node-gyp": "3.6.2", + "nan": "2.11.0", + "node-gyp": "3.8.0", "npmlog": "4.1.2", "request": "2.87.0", "sass-graph": "2.2.4", - "stdout-stream": "1.4.0", - "true-case-path": "1.0.2" - }, - "dependencies": { - "nan": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", - "dev": true - }, - "request": { - "version": "2.87.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", - "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", - "dev": true, - "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.3.1", - "har-validator": "5.0.3", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.17", - "oauth-sign": "0.8.2", - "performance-now": "2.1.0", - "qs": "6.5.1", - "safe-buffer": "5.1.1", - "tough-cookie": "2.3.3", - "tunnel-agent": "0.6.0", - "uuid": "3.2.1" - } - } + "stdout-stream": "1.4.1", + "true-case-path": "1.0.3" } }, "nodeclient-spectre": { @@ -4024,7 +3876,7 @@ "integrity": "sha512-Y1oMvLmR+0P/v2pXG99RKwnC3nb4fl0vmMtbPrZkRB+FE1ADZhfZpHVbBwIcQdsnJTYOpVa7y+hIFtNSv7PYNw==", "dev": true, "requires": { - "request": "2.83.0", + "request": "2.87.0", "request-promise-native": "1.0.5" } }, @@ -4032,7 +3884,6 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "dev": true, "requires": { "abbrev": "1.1.1" } @@ -4041,12 +3892,11 @@ "version": "2.4.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", - "dev": true, "requires": { - "hosted-git-info": "2.5.0", + "hosted-git-info": "2.7.1", "is-builtin-module": "1.0.0", - "semver": "5.5.0", - "validate-npm-package-license": "3.0.1" + "semver": "5.5.1", + "validate-npm-package-license": "3.0.4" } }, "normalize-path": { @@ -4058,8 +3908,7 @@ "normalize-range": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", - "dev": true + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=" }, "npm-install-package": { "version": "2.1.0", @@ -4073,7 +3922,7 @@ "integrity": "sha512-IFsj0R9C7ZdR5cP+ET342q77uSRdtWOlWpih5eC+lu29tIDbNEgDbzgVJ5UFvYHWhxDZ5TFkJafFioO0pPQjCw==", "dev": true, "requires": { - "which": "1.3.0" + "which": "1.3.1" } }, "npm-run-path": { @@ -4093,16 +3942,15 @@ "requires": { "commander": "2.13.0", "npm-path": "2.0.4", - "which": "1.3.0" + "which": "1.3.1" } }, "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, "requires": { - "are-we-there-yet": "1.1.4", + "are-we-there-yet": "1.1.5", "console-control-strings": "1.1.0", "gauge": "2.7.4", "set-blocking": "2.0.0" @@ -4111,20 +3959,17 @@ "num2fraction": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", - "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", - "dev": true + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=" }, "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "oauth-sign": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", - "dev": true + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" }, "object-assign": { "version": "4.1.1", @@ -4153,7 +3998,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1.0.2" } @@ -4173,7 +4017,6 @@ "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", "dev": true, "requires": { - "minimist": "0.0.8", "wordwrap": "0.0.3" }, "dependencies": { @@ -4241,14 +4084,12 @@ "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, "os-locale": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", - "dev": true, "requires": { "lcid": "1.0.0" } @@ -4256,14 +4097,12 @@ "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, "osenv": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz", - "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=", - "dev": true, + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "requires": { "os-homedir": "1.0.2", "os-tmpdir": "1.0.2" @@ -4353,16 +4192,14 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, "requires": { - "error-ex": "1.3.1" + "error-ex": "1.3.2" } }, "path-exists": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, "requires": { "pinkie-promise": "2.0.1" } @@ -4370,8 +4207,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-is-inside": { "version": "1.0.2", @@ -4395,7 +4231,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, "requires": { "graceful-fs": "4.1.11", "pify": "2.3.0", @@ -4411,26 +4246,22 @@ "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" }, "pinkie": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" }, "pinkie-promise": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, "requires": { "pinkie": "2.0.4" } @@ -4488,7 +4319,6 @@ "version": "6.0.16", "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.16.tgz", "integrity": "sha512-m758RWPmSjFH/2MyyG3UOW1fgYbR9rtdzz5UNJnlm7OLtu4B2h9C6gi+bE4qFKghsBRFfZT8NzoQBs6JhLotoA==", - "dev": true, "requires": { "chalk": "2.3.0", "source-map": "0.6.1", @@ -4499,7 +4329,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, "requires": { "color-convert": "1.9.1" } @@ -4508,7 +4337,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", - "dev": true, "requires": { "ansi-styles": "3.2.0", "escape-string-regexp": "1.0.5", @@ -4519,7 +4347,6 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "dev": true, "requires": { "has-flag": "2.0.0" } @@ -4529,14 +4356,12 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "supports-color": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.1.0.tgz", "integrity": "sha512-Ry0AwkoKjDpVKK4sV4h6o3UJmNRbjYm2uXhwfj3J56lMVdvnUNqzQVRztOOMGQ++w1K/TjNDFvpJk0F/LoeBCQ==", - "dev": true, "requires": { "has-flag": "2.0.0" } @@ -4586,7 +4411,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-2.1.2.tgz", "integrity": "sha512-CU7KjbFOZSNrbFwrl8+KJHTj29GjCEhL86kCKyvf+k633fc+FQA6IuhGyPze5e+a4O5d2fP7hDlMOlVDXia1Xg==", - "dev": true, "requires": { "postcss": "6.0.16", "postcss-selector-parser": "2.2.3" @@ -4596,7 +4420,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz", "integrity": "sha1-+UN3iGBsPJrO4W/+jYsWKX8nu5A=", - "dev": true, "requires": { "flatten": "1.0.2", "indexes-of": "1.0.1", @@ -4606,8 +4429,7 @@ "postcss-value-parser": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz", - "integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=", - "dev": true + "integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=" }, "prelude-ls": { "version": "1.1.2", @@ -4667,10 +4489,9 @@ "dev": true }, "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, "progress": { "version": "2.0.0", @@ -4687,11 +4508,10 @@ } }, "prop-types": { - "version": "15.6.0", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz", - "integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=", + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", + "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", "requires": { - "fbjs": "0.8.16", "loose-envify": "1.3.1", "object-assign": "4.1.1" } @@ -4699,14 +4519,12 @@ "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" }, "q": { "version": "1.5.1", @@ -4715,10 +4533,9 @@ "dev": true }, "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", - "dev": true + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, "query-string": { "version": "4.3.4", @@ -4736,9 +4553,9 @@ "dev": true }, "re-resizable": { - "version": "4.4.8", - "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-4.4.8.tgz", - "integrity": "sha512-5Nm4FL5wz41/5SYz8yJIM1kCcftxNPXxv3Yfa5qhkrGasHPgYzmzbbu1pcYM9vuCHog79EVwKWuz7zxDH52Gfw==" + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-4.8.1.tgz", + "integrity": "sha512-73HInnxHyMh/BnJMAZUtErIWX6reY4TU9+AHC4U/VLd9s2h1SHT2VoONO8jj9pv0+e4SVhiDzV+rxOPT/BWn6Q==" }, "react": { "version": "16.0.0", @@ -4748,7 +4565,7 @@ "fbjs": "0.8.16", "loose-envify": "1.3.1", "object-assign": "4.1.1", - "prop-types": "15.6.0" + "prop-types": "15.6.2" } }, "react-autosize-textarea": { @@ -4758,7 +4575,7 @@ "requires": { "autosize": "3.0.21", "line-height": "0.3.1", - "prop-types": "15.6.0" + "prop-types": "15.6.2" } }, "react-color": { @@ -4768,29 +4585,29 @@ "requires": { "lodash": "4.17.10", "material-colors": "1.2.5", - "prop-types": "15.6.0", + "prop-types": "15.6.2", "reactcss": "1.2.3", "tinycolor2": "1.4.1" } }, "react-dom": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.0.0.tgz", - "integrity": "sha1-nMMHnD3NcNTG4BuEqrKn40wwP1g=", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.0.1.tgz", + "integrity": "sha512-gGJNmuS0VpkJsNStpzplcgc4iuHJ2X8rjiiaY/5YfHrsAd2cw1JkMXD6Z1kBOed8rDUNrRYrnDYptnhFghFFhA==", "requires": { "fbjs": "0.8.16", "loose-envify": "1.3.1", "object-assign": "4.1.1", - "prop-types": "15.6.0" + "prop-types": "15.6.2" } }, "react-dropzone": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-4.2.7.tgz", - "integrity": "sha512-BGEc/UtG0rHBEZjAkGsajPRO85d842LWeaP4CINHvXrSNyKp7Tq7s699NyZwWYHahvXaUNZzNJ17JMrfg5sxVg==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-4.2.13.tgz", + "integrity": "sha512-kqpt0Up4GZZFoz4WvBTVzMmVDUZFoGRKDXeyV+baWXZx8Gt0CZmOtV7BSMF1JaBihx6mwy+rfYVNnOKB2hrY9Q==", "requires": { - "attr-accept": "1.1.0", - "prop-types": "15.6.0" + "attr-accept": "1.1.3", + "prop-types": "15.6.2" } }, "react-intl": { @@ -4810,7 +4627,7 @@ "integrity": "sha512-IvRZxJkXiDqEIl4cxCccCzP37z+YOSN+yNOkYH99Ime+n9nPSowgxkX0KfHzR2ezP72LSS3Uln54JDZXUJmLdA==", "requires": { "exenv": "1.2.2", - "prop-types": "15.6.0" + "prop-types": "15.6.2" } }, "react-render-in-browser": { @@ -4818,9 +4635,9 @@ "resolved": "https://registry.npmjs.org/react-render-in-browser/-/react-render-in-browser-1.0.0.tgz", "integrity": "sha512-DnOYcGVfjcu13Em8Z/sNbgYSrL26NjCQhZNzOEMV3BJiZ5WfvWFqvI9P/MW2K8guAkuf+hBouQyZysJdqrVhKA==", "requires": { - "prop-types": "15.6.0", + "prop-types": "15.6.2", "react": "16.0.0", - "react-dom": "16.0.0" + "react-dom": "16.0.1" } }, "react-router": { @@ -4833,7 +4650,7 @@ "hoist-non-react-statics": "1.2.0", "invariant": "2.2.2", "loose-envify": "1.3.1", - "prop-types": "15.6.0", + "prop-types": "15.6.2", "warning": "3.0.0" } }, @@ -4842,8 +4659,8 @@ "resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-2.1.1.tgz", "integrity": "sha512-55jl6lsYmPTQarnjgrBU68WZlNtVSngpRxOc4iXm+Te27F9ixUr/IBTbhlhDCMiFJreP+cqu1OaMdNGY2Hg10A==", "requires": { - "classnames": "2.2.5", - "prop-types": "15.6.0" + "classnames": "2.2.6", + "prop-types": "15.6.2" } }, "react-toastify": { @@ -4851,7 +4668,7 @@ "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-2.1.7.tgz", "integrity": "sha512-iMS5wXiTDKXcWTIF055BmsSwJINcxs9+CUGeEPMSllU0I00IKfV2inb3xhRxrB7d+4QvPqWZAtDPTk6nz6o1nA==", "requires": { - "prop-types": "15.6.0", + "prop-types": "15.6.2", "react-transition-group": "2.2.1" } }, @@ -4860,7 +4677,7 @@ "resolved": "https://registry.npmjs.org/react-toggle/-/react-toggle-4.0.2.tgz", "integrity": "sha512-EPTWnN7gQHgEAUEmjheanZXNzY5TPnQeyyHfEs3YshaiWZf5WNjfYDrglO5F1Hl/dNveX18i4l0grTEsYH2Ccw==", "requires": { - "classnames": "2.2.5" + "classnames": "2.2.6" } }, "react-transition-group": { @@ -4869,10 +4686,10 @@ "integrity": "sha512-q54UBM22bs/CekG8r3+vi9TugSqh0t7qcEVycaRc9M0p0aCEu+h6rp/RFiW7fHfgd1IKpd9oILFTl5QK+FpiPA==", "requires": { "chain-function": "1.0.0", - "classnames": "2.2.5", + "classnames": "2.2.6", "dom-helpers": "3.3.1", "loose-envify": "1.3.1", - "prop-types": "15.6.0", + "prop-types": "15.6.2", "warning": "3.0.0" } }, @@ -4894,7 +4711,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, "requires": { "load-json-file": "1.1.0", "normalize-package-data": "2.4.0", @@ -4905,24 +4721,22 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, "requires": { "find-up": "1.1.2", "read-pkg": "1.1.0" } }, "readable-stream": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", - "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", - "dev": true, + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "1.0.2", "inherits": "2.0.3", "isarray": "1.0.0", - "process-nextick-args": "1.0.7", + "process-nextick-args": "2.0.0", "safe-buffer": "5.1.1", - "string_decoder": "1.0.3", + "string_decoder": "1.1.1", "util-deprecate": "1.0.2" } }, @@ -4935,7 +4749,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", - "dev": true, "requires": { "indent-string": "2.1.0", "strip-indent": "1.0.1" @@ -5008,39 +4821,35 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "dev": true, "requires": { "is-finite": "1.0.2" } }, "request": { - "version": "2.83.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", - "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", - "dev": true, + "version": "2.87.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", + "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", "requires": { "aws-sign2": "0.7.0", - "aws4": "1.6.0", + "aws4": "1.8.0", "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", + "combined-stream": "1.0.6", + "extend": "3.0.2", "forever-agent": "0.6.1", - "form-data": "2.3.1", + "form-data": "2.3.2", "har-validator": "5.0.3", - "hawk": "6.0.2", "http-signature": "1.2.0", "is-typedarray": "1.0.0", "isstream": "0.1.2", "json-stringify-safe": "5.0.1", - "mime-types": "2.1.17", + "mime-types": "2.1.20", "oauth-sign": "0.8.2", "performance-now": "2.1.0", - "qs": "6.5.1", + "qs": "6.5.2", "safe-buffer": "5.1.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.3", + "tough-cookie": "2.3.4", "tunnel-agent": "0.6.0", - "uuid": "3.2.1" + "uuid": "3.3.2" } }, "request-promise-core": { @@ -5060,14 +4869,13 @@ "requires": { "request-promise-core": "1.1.1", "stealthy-require": "1.1.1", - "tough-cookie": "2.3.3" + "tough-cookie": "2.3.4" } }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, "require-from-string": { "version": "1.2.1", @@ -5078,8 +4886,7 @@ "require-main-filename": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" }, "require-uncached": { "version": "1.0.3", @@ -5126,9 +4933,8 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "dev": true, "requires": { - "glob": "7.1.2" + "glob": "7.1.3" } }, "run-async": { @@ -5169,13 +4975,17 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "sass-graph": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", - "dev": true, "requires": { - "glob": "7.1.2", + "glob": "7.1.3", "lodash": "4.17.10", "scss-tokenizer": "0.2.3", "yargs": "7.1.0" @@ -5191,9 +5001,8 @@ "version": "0.2.3", "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", - "dev": true, "requires": { - "js-base64": "2.4.3", + "js-base64": "2.4.9", "source-map": "0.4.4" } }, @@ -5203,16 +5012,14 @@ "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=" }, "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", + "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==" }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "setimmediate": { "version": "1.0.5", @@ -5237,8 +5044,7 @@ "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, "slice-ansi": { "version": "1.0.0", @@ -5257,20 +5063,10 @@ } } }, - "sntp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", - "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", - "dev": true, - "requires": { - "hoek": "4.2.0" - } - }, "source-map": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "dev": true, "requires": { "amdefine": "1.0.1" } @@ -5294,25 +5090,32 @@ "dev": true }, "spdx-correct": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", - "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", - "dev": true, + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", + "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", "requires": { - "spdx-license-ids": "1.2.2" + "spdx-expression-parse": "3.0.0", + "spdx-license-ids": "3.0.1" } }, + "spdx-exceptions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", + "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==" + }, "spdx-expression-parse": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", - "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=", - "dev": true + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "requires": { + "spdx-exceptions": "2.1.0", + "spdx-license-ids": "3.0.1" + } }, "spdx-license-ids": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", - "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=", - "dev": true + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.1.tgz", + "integrity": "sha512-TfOfPcYGBB5sDuPn3deByxPhmfegAhpDYKSOXZQN81Oyrrif8ZCodOLzK3AesELnCx03kikhyDwh0pfvvQvF8w==" }, "sprintf-js": { "version": "1.0.3", @@ -5321,18 +5124,18 @@ "dev": true }, "sshpk": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", - "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", - "dev": true, + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", + "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", "requires": { - "asn1": "0.2.3", + "asn1": "0.2.4", "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", + "bcrypt-pbkdf": "1.0.2", "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", + "ecc-jsbn": "0.1.2", "getpass": "0.1.7", "jsbn": "0.1.1", + "safer-buffer": "2.1.2", "tweetnacl": "0.14.5" } }, @@ -5348,12 +5151,11 @@ "dev": true }, "stdout-stream": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz", - "integrity": "sha1-osfIWH5U2UJ+qe2zrD8s1SLfN4s=", - "dev": true, + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", + "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", "requires": { - "readable-stream": "2.3.3" + "readable-stream": "2.3.6" } }, "stealthy-require": { @@ -5405,7 +5207,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, "requires": { "code-point-at": "1.1.0", "is-fullwidth-code-point": "1.0.0", @@ -5413,10 +5214,9 @@ } }, "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "5.1.1" } @@ -5432,17 +5232,10 @@ "is-regexp": "1.0.0" } }, - "stringstream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", - "dev": true - }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, "requires": { "ansi-regex": "2.1.1" } @@ -5451,7 +5244,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, "requires": { "is-utf8": "0.2.1" } @@ -5466,7 +5258,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", - "dev": true, "requires": { "get-stdin": "4.0.1" } @@ -5480,8 +5271,7 @@ "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" }, "symbol-observable": { "version": "1.0.1", @@ -5569,7 +5359,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", - "dev": true, "requires": { "block-stream": "0.0.9", "fstream": "1.0.11", @@ -5584,7 +5373,7 @@ "requires": { "bl": "1.2.1", "end-of-stream": "1.4.1", - "readable-stream": "2.3.3", + "readable-stream": "2.3.6", "xtend": "4.0.1" } }, @@ -5633,10 +5422,9 @@ } }, "tough-cookie": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", - "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", - "dev": true, + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", "requires": { "punycode": "1.4.1" } @@ -5650,38 +5438,20 @@ "trim-newlines": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", - "dev": true + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=" }, "true-case-path": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.2.tgz", - "integrity": "sha1-fskRMJJHZsf1c74wIMNPj9/QDWI=", - "dev": true, + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", + "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", "requires": { - "glob": "6.0.4" - }, - "dependencies": { - "glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", - "dev": true, - "requires": { - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - } + "glob": "7.1.3" } }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, "requires": { "safe-buffer": "5.1.1" } @@ -5690,7 +5460,6 @@ "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true, "optional": true }, "type-check": { @@ -5722,8 +5491,7 @@ "uniq": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", - "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", - "dev": true + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=" }, "universalify": { "version": "0.1.1", @@ -5780,30 +5548,26 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "uuid": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", - "dev": true + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" }, "validate-npm-package-license": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", - "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", - "dev": true, + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "requires": { - "spdx-correct": "1.0.2", - "spdx-expression-parse": "1.0.4" + "spdx-correct": "3.0.0", + "spdx-expression-parse": "3.0.0" } }, "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, "requires": { "assert-plus": "1.0.0", "core-util-is": "1.0.2", @@ -5841,7 +5605,7 @@ "integrity": "sha1-M7mOql1IK7AajRqmtDetKwGuxBw=", "dev": true, "requires": { - "core-js": "2.5.3", + "core-js": "2.5.7", "regenerator-runtime": "0.10.5" } }, @@ -5897,8 +5661,8 @@ "image-size": "0.5.5", "jimp": "0.2.28", "lodash": "4.17.10", - "uuid": "3.2.1", - "which": "1.3.0" + "uuid": "3.3.2", + "which": "1.3.1" } }, "wdio-spec-reporter": { @@ -5960,7 +5724,7 @@ "integrity": "sha1-M7mOql1IK7AajRqmtDetKwGuxBw=", "dev": true, "requires": { - "core-js": "2.5.3", + "core-js": "2.5.7", "regenerator-runtime": "0.10.5" } }, @@ -5997,13 +5761,13 @@ "adm-zip": "0.4.7", "chalk": "1.1.3", "del": "2.2.2", - "glob": "7.1.2", + "glob": "7.1.3", "ini": "1.3.5", "minimist": "1.2.0", "q": "1.5.1", - "request": "2.83.0", + "request": "2.87.0", "rimraf": "2.6.2", - "semver": "5.5.0", + "semver": "5.5.1", "xml2js": "0.4.19" }, "dependencies": { @@ -6027,15 +5791,15 @@ "css-value": "0.0.1", "deepmerge": "2.0.1", "ejs": "2.5.7", - "gaze": "1.1.2", - "glob": "7.1.2", + "gaze": "1.1.3", + "glob": "7.1.3", "inquirer": "3.3.0", "json-stringify-safe": "5.0.1", "mkdirp": "0.5.1", "npm-install-package": "2.1.0", "optimist": "0.6.1", "q": "1.5.1", - "request": "2.83.0", + "request": "2.87.0", "rgb2hex": "0.1.9", "safe-buffer": "5.1.1", "supports-color": "5.0.1", @@ -6053,9 +5817,9 @@ "archiver-utils": "1.3.0", "async": "2.6.1", "buffer-crc32": "0.2.13", - "glob": "7.1.2", + "glob": "7.1.3", "lodash": "4.17.10", - "readable-stream": "2.3.3", + "readable-stream": "2.3.6", "tar-stream": "1.5.5", "zip-stream": "1.2.0" } @@ -6104,10 +5868,9 @@ "integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ=" }, "which": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", - "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", - "dev": true, + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "requires": { "isexe": "2.0.0" } @@ -6115,22 +5878,20 @@ "which-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", - "dev": true + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=" }, "wide-align": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", - "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", - "dev": true, + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "requires": { "string-width": "1.0.2" } }, "winston": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.0.tgz", - "integrity": "sha1-gIBQuT1SZh7Z+2wms/DIJnCLCu4=", + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.4.tgz", + "integrity": "sha512-NBo2Pepn4hK4V01UfcWcDlmiVTs7VTB1h7bgnB0rgP146bYhMxX0ypCz3lBOfNxCO4Zuek7yeT+y/zM1OfMw4Q==", "requires": { "async": "1.0.0", "colors": "1.0.3", @@ -6148,9 +5909,8 @@ }, "wrap-ansi": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, "requires": { "string-width": "1.0.2", "strip-ansi": "3.0.1" @@ -6159,8 +5919,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write": { "version": "0.2.1", @@ -6214,25 +5973,22 @@ "y18n": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", - "dev": true + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" }, "yallist": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" }, "yargs": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", - "dev": true, "requires": { "camelcase": "3.0.0", "cliui": "3.2.0", "decamelize": "1.2.0", - "get-caller-file": "1.0.2", + "get-caller-file": "1.0.3", "os-locale": "1.4.0", "read-pkg-up": "1.0.1", "require-directory": "2.1.1", @@ -6247,8 +6003,7 @@ "camelcase": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" } } }, @@ -6256,7 +6011,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", - "dev": true, "requires": { "camelcase": "3.0.0" }, @@ -6264,8 +6018,7 @@ "camelcase": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" } } }, @@ -6278,7 +6031,7 @@ "archiver-utils": "1.3.0", "compress-commons": "1.2.2", "lodash": "4.17.10", - "readable-stream": "2.3.3" + "readable-stream": "2.3.6" } } } diff --git a/bigbluebutton-html5/package.json b/bigbluebutton-html5/package.json index 9fbe89ec09709217908a34d658540fab62de5d9f..38d0354d3ff37c8f99494aa6ee48f9782b1abd7e 100755 --- a/bigbluebutton-html5/package.json +++ b/bigbluebutton-html5/package.json @@ -29,12 +29,14 @@ "need to investigate" ], "@browser-bunyan/server-stream": "^1.3.0", + "autoprefixer": "~7.1.6", + "babel-plugin-react-remove-properties": "~0.2.5", "babel-runtime": "~6.26.0", - "browser-bunyan": "^1.3.0", + "browser-bunyan": "^1.4.0", "browser-detect": "^0.2.28", - "classnames": "~2.2.5", + "classnames": "^2.2.6", "clipboard": "~1.7.1", - "core-js": "~2.5.3", + "core-js": "^2.5.7", "eventemitter2": "~4.1.2", "flat": "~4.0.0", "history": "~3.3.0", @@ -42,15 +44,17 @@ "langmap": "0.0.16", "lodash": "~4.17.10", "makeup-screenreader-trap": "0.0.5", - "meteor-node-stubs": "~0.3.2", + "meteor-node-stubs": "^0.3.3", + "node-sass": "^4.9.3", + "postcss-nested": "2.1.2", "probe-image-size": "~3.1.0", - "prop-types": "~15.6.0", - "re-resizable": "^4.4.8", + "prop-types": "^15.6.2", + "re-resizable": "^4.8.1", "react": "~16.0.0", "react-autosize-textarea": "~0.4.9", "react-color": "~2.14.1", - "react-dom": "~16.0.0", - "react-dropzone": "~4.2.1", + "react-dom": "^16.0.1", + "react-dropzone": "^4.2.13", "react-intl": "~2.4.0", "react-modal": "~3.0.4", "react-render-in-browser": "^1.0.0", @@ -63,11 +67,9 @@ "redis": "~2.8.0", "string-hash": "~1.1.3", "tippy.js": "~2.0.2", - "winston": "~2.4.0" + "winston": "^2.4.4" }, "devDependencies": { - "autoprefixer": "~7.1.6", - "babel-plugin-react-remove-properties": "~0.2.5", "chai": "~4.1.2", "eslint": "~4.9.0", "eslint-config-airbnb": "~16.1.0", @@ -77,12 +79,10 @@ "eslint-plugin-react": "~7.4.0", "husky": "~0.14.3", "lint-staged": "~4.3.0", - "node-sass": "~4.9.2", "postcss-modules-extract-imports": "1.1.0", "postcss-modules-local-by-default": "1.2.0", "postcss-modules-scope": "1.1.0", "postcss-modules-values": "1.3.0", - "postcss-nested": "2.1.2", "wdio-jasmine-framework": "~0.3.2", "wdio-junit-reporter": "~0.4.4", "wdio-spec-reporter": "~0.1.2", diff --git a/bigbluebutton-html5/private/config/settings-development.json b/bigbluebutton-html5/private/config/settings-development.json index 95f7e65c0bd883aee841564aea4425c9a9b3e97d..9f5fb78ceedb463b63fc4a886ae756ea02995b55 100755 --- a/bigbluebutton-html5/private/config/settings-development.json +++ b/bigbluebutton-html5/private/config/settings-development.json @@ -19,8 +19,8 @@ "askForFeedbackOnLogout": false, "defaultSettings": { "application": { - "chatAudioNotifications": false, - "chatPushNotifications": false, + "chatAudioAlerts": false, + "chatPushAlerts": false, "fontSize": "16px", "fallbackLocale": "en" }, @@ -67,7 +67,8 @@ "allowHTML5Moderator": true, "allowModeratorToUnmuteAudio": true, "httpsConnection": false, - "connectionTimeout": 60000 + "connectionTimeout": 60000, + "showHelpButton": true }, "kurento": { @@ -78,6 +79,14 @@ "chromeExtensionLink": "LINK", "chromeScreenshareSources": ["window", "screen"], "firefoxScreenshareSource": "window", + "cameraConstraints": { + "width": { + "max": 640 + }, + "height": { + "max": 480 + } + }, "enableScreensharing": false, "enableVideo": false, "enableVideoStats": false, @@ -150,11 +159,12 @@ "max_message_length": 5000, "grouping_messages_window": 10000, "type_system": "SYSTEM_MESSAGE", - "type_public": "PUBLIC_CHAT", - "type_private": "PRIVATE_CHAT", + "type_public": "PUBLIC_ACCESS", + "type_private": "PRIVATE_ACCESS", "system_userid": "SYSTEM_MESSAGE", "system_username": "SYSTEM_MESSAGE", "public_id": "public", + "public_group_id": "MAIN-PUBLIC-GROUP-CHAT", "public_userid": "public_chat_userid", "public_username": "public_chat_username", "storage_key": "UNREAD_CHATS", @@ -347,11 +357,12 @@ } ] } - }, - "log": [ - { "target": "server", "level": "info" } - ] + "clientLog": { + "server": { "enabled": true, "level": "info" }, + "console": { "enabled": false, "level": "debug" }, + "external": { "enabled": false, "level": "info", "url": "https://LOG_HOST/html5Log", "method": "POST" } + } }, "private": { @@ -382,8 +393,7 @@ ] }, - - "log": { + "serverLog": { "level": "info" } } diff --git a/bigbluebutton-html5/private/config/settings-production.json b/bigbluebutton-html5/private/config/settings-production.json index fa2e1341265e74ec94f8c35f847363d4f83252f8..93dbb95c08520accc3cb18ba300adfc14c1b22c1 100755 --- a/bigbluebutton-html5/private/config/settings-production.json +++ b/bigbluebutton-html5/private/config/settings-production.json @@ -19,8 +19,8 @@ "askForFeedbackOnLogout": false, "defaultSettings": { "application": { - "chatAudioNotifications": false, - "chatPushNotifications": false, + "chatAudioAlerts": false, + "chatPushAlerts": false, "fontSize": "16px", "fallbackLocale": "en" }, @@ -67,7 +67,8 @@ "allowHTML5Moderator": true, "allowModeratorToUnmuteAudio": true, "httpsConnection": true, - "connectionTimeout": 10000 + "connectionTimeout": 10000, + "showHelpButton": true }, "kurento": { @@ -78,6 +79,14 @@ "chromeExtensionLink": "LINK", "chromeScreenshareSources": ["window", "screen"], "firefoxScreenshareSource": "window", + "cameraConstraints": { + "width": { + "max": 640 + }, + "height": { + "max": 480 + } + }, "enableScreensharing": false, "enableVideo": false, "enableVideoStats": false, @@ -150,11 +159,12 @@ "max_message_length": 5000, "grouping_messages_window": 10000, "type_system": "SYSTEM_MESSAGE", - "type_public": "PUBLIC_CHAT", - "type_private": "PRIVATE_CHAT", + "type_public": "PUBLIC_ACCESS", + "type_private": "PRIVATE_ACCESS", "system_userid": "SYSTEM_MESSAGE", "system_username": "SYSTEM_MESSAGE", "public_id": "public", + "public_group_id": "MAIN-PUBLIC-GROUP-CHAT", "public_userid": "public_chat_userid", "public_username": "public_chat_username", "storage_key": "UNREAD_CHATS", @@ -348,9 +358,11 @@ ] } }, - "log": [ - { "target": "server", "level": "info" } - ] + "clientLog": { + "server": { "enabled": true, "level": "info" }, + "console": { "enabled": false, "level": "debug" }, + "external": { "enabled": false, "level": "info", "url": "https://LOG_HOST/html5Log", "method": "POST" } + } }, "private": { @@ -381,7 +393,7 @@ ] }, - "log": { + "serverLog": { "level": "info" } } diff --git a/bigbluebutton-html5/private/locales/de.json b/bigbluebutton-html5/private/locales/de.json index b7a1218f7a8fc4b0048404bee047a55b71c0b755..fa4f77db7b082607768076c1c72398c3bd0ce3ff 100644 --- a/bigbluebutton-html5/private/locales/de.json +++ b/bigbluebutton-html5/private/locales/de.json @@ -97,6 +97,8 @@ "app.navBar.settingsDropdown.exitFullscreenDesc": "Vollbildmodus beenden", "app.navBar.settingsDropdown.hotkeysLabel": "Tastaturkürzel", "app.navBar.settingsDropdown.hotkeysDesc": "Liste verfügbarer Tastaturkürzel", + "app.navBar.settingsDropdown.helpLabel": "Hilfe", + "app.navBar.settingsDropdown.helpDesc": "Verlinkt zu den Videoanleitungen", "app.navBar.userListToggleBtnLabel": "Teilnehmerliste umschalten", "app.navBar.toggleUserList.ariaLabel": "Teilnehmer/Nachrichten-Umschalter", "app.navBar.toggleUserList.newMessages": "mit Benachrichtigung für neue Nachrichten", @@ -126,8 +128,8 @@ "app.actionsBar.raiseLabel": "Hand heben", "app.actionsBar.label": "Aktionsleiste", "app.submenu.application.applicationSectionTitle": "Anwendung", - "app.submenu.application.audioNotifyLabel": "Audiobenachrichtigungen für Chat", - "app.submenu.application.pushNotifyLabel": "Push-Benachrichtigungen für Chat", + "app.submenu.application.audioAlertLabel": "Audiowarnungen für Chat", + "app.submenu.application.pushAlertLabel": "Popupwarnungen für Chat", "app.submenu.application.fontSizeControlLabel": "Schriftgröße", "app.submenu.application.increaseFontBtnLabel": "Schriftgröße erhöhen", "app.submenu.application.decreaseFontBtnLabel": "Schriftgröße verringern", @@ -320,6 +322,7 @@ "app.video.joinVideo": "Webcam freigeben", "app.video.leaveVideo": "Webcam stoppen", "app.video.iceCandidateError": "Fehler beim Hinzufügen vom ice candidate", + "app.video.iceConnectionStateError": "Fehler 1107: ICE Ãœbertragung fehlgeschlagen", "app.video.permissionError": "Fehler bei Freigabe der Webcam. Bitte Berechtigungen prüfen", "app.video.sharingError": "Fehler bei Freigabe der Webcam", "app.video.notFoundError": "Konnte keine Webcam finden. Stellen Sie sicher, dass sie angeschlossen ist", @@ -346,6 +349,7 @@ "app.video.stats.rtt": "RTT", "app.video.stats.encodeUsagePercent": "Enkodierungsnutzung", "app.video.stats.currentDelay": "Aktuelle Verzögerung", + "app.deskshare.iceConnectionStateError": "Fehler 1108: ICE Verbindung während der Bildschirmfreigabe fehlgeschlagen", "app.sfu.mediaServerConnectionError2000": "Fehler 2000: Verbindung zum Mediaserver kann nicht hergestellt werden", "app.sfu.mediaServerOffline2001": "Fehler 2001: Mediaserver ist offline. Versuchen Sie es später nochmal.", "app.sfu.mediaServerNoResources2002": "Fehler 2002: Mediaserver hat keine verfügbaren Resourcen", diff --git a/bigbluebutton-html5/private/locales/el_GR.json b/bigbluebutton-html5/private/locales/el_GR.json new file mode 100644 index 0000000000000000000000000000000000000000..1c5d01f652ee530b75800b6dd8a56e92d377aa4a --- /dev/null +++ b/bigbluebutton-html5/private/locales/el_GR.json @@ -0,0 +1,55 @@ +{ + "app.home.greeting": "Καλώς ήÏθατε {0}! Η παÏουσίαση θα ξεκινήσει σε λίγο...", + "app.chat.submitLabel": "Αποστολή ΜηνÏματος", + "app.chat.errorMinMessageLength": "Το μήνυμα είναι {0} χαÏακτήÏες(ας). Î Î¿Î»Ï Î¼Î¹ÎºÏÏŒ", + "app.chat.errorMaxMessageLength": "Το μήνυμα είναι {0} χαÏακτήÏες(ας). Î Î¿Î»Ï Î¼ÎµÎ³Î¬Î»Î¿", + "app.chat.inputPlaceholder": "Μήνυμα {0}", + "app.chat.titlePublic": "Δημόσια Συνομιλία", + "app.chat.titlePrivate": "Ιδιωτική Συνομιλία με {0}", + "app.chat.partnerDisconnected": "{0} Îχει εγκαταλείψει τη συνάντηση", + "app.chat.closeChatLabel": "Κλείσιμο {0}", + "app.chat.hideChatLabel": "ΑπόκÏυψη {0}", + "app.chat.dropdown.options": "ΕπιλογÎÏ‚ Συνομιλίας", + "app.chat.dropdown.clear": "ΚαθαÏισμός", + "app.chat.dropdown.copy": "ΑντιγÏαφή", + "app.chat.dropdown.save": "Αποθήκευση", + "app.chat.label": "Συνομιλία", + "app.userList.usersTitle": "ΧÏήστες", + "app.userList.messagesTitle": "ΜηνÏματα", + "app.userList.presenter": "ΠαÏουσιαστής", + "app.userList.you": "Εσείς", + "app.userList.label": "Λίστα ΧÏηστών", + "app.userList.guest": "ΚαλεσμÎνος", + "app.userList.menuTitleContext": "ΔιαθÎσιμες ΕπιλογÎÏ‚", + "app.userList.chatListItem.unreadSingular": "{0} ÎÎο Μήνυμα", + "app.userList.chatListItem.unreadPlural": "{0} ÎÎα ΜηνÏματα", + "app.userList.menu.makePresenter.label": "Κάνε τον/την ΠαÏουσιαστή", + "app.media.screenshare.start": "Ο διαμοιÏασμός οθόνης Îχει αÏχίσει", + "app.media.screenshare.end": "Ο διαμοιÏασμός οθόνης Îχει τελειώσει", + "app.media.screenshare.safariNotSupported": "Ο διαμοιÏασμός οθόνης δεν υποστηÏίζεται ακόμα από τον Safari.\nΠαÏακαλοÏμε, χÏησιμοποιείστε τον Firefox ή το Google Chrome.", + "app.presentation.presentationToolbar.nextSlideLabel": "Επόμενη διαφάνεια", + "app.presentation.presentationToolbar.skipSlideLabel": "ΠαÏÎλειψε διαφάνεια", + "app.presentation.presentationToolbar.goToSlide": "Διαφάνεια {0}", + "app.presentationUploder.title": "ΠαÏουσίαση", + "app.presentationUploder.confirmLabel": "Εκκίνηση", + "app.presentationUploder.dismissLabel": "ΑκÏÏωση", + "app.presentationUploder.genericError": "Ωχ, κάτι πήγε στÏαβά", + "app.presentationUploder.upload.progress": "ΜεταφόÏτωση ({0}%)", + "app.presentationUploder.upload.413": "Το μÎγεθος του αÏχείου είναι Ï€Î¿Î»Ï Î¼ÎµÎ³Î¬Î»Î¿", + "app.presentationUploder.conversion.genericConversionStatus": "ΜετατÏοπή αÏχείου...", + "app.presentationUploder.conversion.generatedSlides": "ΔημιουÏγία Διαφανειών...", + "app.waitingMessage": "Έχετε αποσυνδεθεί. Î Ïοσπαθήστε να επανασυνδεθείτε σε {0} δευτεÏόλεπτα...", + "app.navBar.settingsDropdown.optionsLabel": "ΕπιλογÎÏ‚", + "app.navBar.settingsDropdown.settingsLabel": "Άνοιγμα Ïυθμίσεων", + "app.navBar.settingsDropdown.leaveSessionLabel": "ΑποσÏνδεση", + "app.navBar.settingsDropdown.helpLabel": "Βοήθεια", + "app.navBar.recording.on": "ΕγγÏαφή", + "app.navBar.recording.off": "Μη καταγÏαφεί", + "app.actionsBar.changeStatusLabel": "Αλλαγή Κατάστασης", + "app.submenu.application.fontSizeControlLabel": "ÎœÎγεθος γÏαμματοσειÏάς", + "app.submenu.application.languageOptionLabel": "Επιλογή γλώσσας", + "app.submenu.video.videoSourceLabel": "Î Ïοβολή πηγαίου", + "app.submenu.closedCaptions.languageLabel": "Γλώσσα" + +} + diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index 38f308196217e82dbcdf6e433f04659feb83a4d5..f4a01c30d4a5840d9763a1269c6cdab8c71975a8 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -97,6 +97,8 @@ "app.navBar.settingsDropdown.exitFullscreenDesc": "Exit fullscreen mode", "app.navBar.settingsDropdown.hotkeysLabel": "Hotkeys", "app.navBar.settingsDropdown.hotkeysDesc": "Listing of available hotkeys", + "app.navBar.settingsDropdown.helpLabel": "Help", + "app.navBar.settingsDropdown.helpDesc": "Links user to video tutorials", "app.navBar.userListToggleBtnLabel": "User List Toggle", "app.navBar.toggleUserList.ariaLabel": "Users and Messages Toggle", "app.navBar.toggleUserList.newMessages": "with new message notification", @@ -126,8 +128,8 @@ "app.actionsBar.raiseLabel": "Raise", "app.actionsBar.label": "Actions Bar", "app.submenu.application.applicationSectionTitle": "Application", - "app.submenu.application.audioNotifyLabel": "Audio notifications for chat", - "app.submenu.application.pushNotifyLabel": "Push notifications for chat", + "app.submenu.application.audioAlertLabel": "Audio Alerts for Chat", + "app.submenu.application.pushAlertLabel": "Popup Alerts for Chat", "app.submenu.application.fontSizeControlLabel": "Font size", "app.submenu.application.increaseFontBtnLabel": "Increase Application Font Size", "app.submenu.application.decreaseFontBtnLabel": "Decrease Application Font Size", @@ -200,7 +202,7 @@ "app.actionsBar.actionsDropdown.stopDesktopShareDesc": "Stop sharing your screen with", "app.actionsBar.actionsDropdown.startRecording": "Start recording", "app.actionsBar.actionsDropdown.stopRecording": "Stop recording", - "app.actionsBar.emojiMenu.statusTriggerLabel": "Set a Status", + "app.actionsBar.emojiMenu.statusTriggerLabel": "Set Status", "app.actionsBar.emojiMenu.awayLabel": "Away", "app.actionsBar.emojiMenu.awayDesc": "Change your status to away", "app.actionsBar.emojiMenu.raiseHandLabel": "Raise", @@ -320,6 +322,7 @@ "app.video.joinVideo": "Share Webcam", "app.video.leaveVideo": "Unshare Webcam", "app.video.iceCandidateError": "Error on adding ice candidate", + "app.video.iceConnectionStateError": "Error 1107: ICE negotiation failed", "app.video.permissionError": "Error on sharing webcam. Please check permissions", "app.video.sharingError": "Error on sharing webcam", "app.video.notFoundError": "Could not find webcam. Please make sure it's connected", @@ -346,6 +349,7 @@ "app.video.stats.rtt": "RTT", "app.video.stats.encodeUsagePercent": "Encode usage", "app.video.stats.currentDelay": "Current delay", + "app.deskshare.iceConnectionStateError": "Error 1108: ICE connection failed when sharing screen", "app.sfu.mediaServerConnectionError2000": "Error 2000: Unable to connect to media server", "app.sfu.mediaServerOffline2001": "Error 2001: Media server is offline. Please try again later.", "app.sfu.mediaServerNoResources2002": "Error 2002: Media server has no available resources", diff --git a/bigbluebutton-html5/private/locales/es_ES.json b/bigbluebutton-html5/private/locales/es_ES.json index 65b630baf85c8892ab5786d947ad49df704b28c6..91c9a9370cbee1044dae7d52077bc0522103aba8 100644 --- a/bigbluebutton-html5/private/locales/es_ES.json +++ b/bigbluebutton-html5/private/locales/es_ES.json @@ -84,8 +84,6 @@ "app.actionsBar.raiseLabel": "Alzar", "app.actionsBar.label": "Barra de acciones", "app.submenu.application.applicationSectionTitle": "Aplicación", - "app.submenu.application.audioNotifyLabel": "Notificación de audio para el chat", - "app.submenu.application.pushNotifyLabel": "Empujar notificaciones para el chat", "app.submenu.application.fontSizeControlLabel": "Tamaño de fuente", "app.submenu.application.increaseFontBtnLabel": "Incrementar tamaño de fuente", "app.submenu.application.decreaseFontBtnLabel": "Reducir tamaño de fuente", diff --git a/bigbluebutton-html5/private/locales/fa_IR.json b/bigbluebutton-html5/private/locales/fa_IR.json new file mode 100644 index 0000000000000000000000000000000000000000..4136ce2d4c1f7786e942c26c490eaaa35b9d6b2f --- /dev/null +++ b/bigbluebutton-html5/private/locales/fa_IR.json @@ -0,0 +1,396 @@ +{ + "app.home.greeting": "خوش آمدید{0}. ارائه شما به زودی آغاز میشود.", + "app.chat.submitLabel": "ارسال پیام", + "app.chat.errorMinMessageLength": "پیام شما {0} کاراکتر(0) است، برای ارسال، کوتاه است.", + "app.chat.errorMaxMessageLength": "پیام شما {0} کارکتر(0) است Ùˆ برای ارسال خیلی طولانیست.", + "app.chat.inputLabel": "ورودی پیام برای چت{0}", + "app.chat.inputPlaceholder": "پیام{0}", + "app.chat.titlePublic": "Ú¯Ùتگوی عمومی", + "app.chat.titlePrivate": "Ú¯Ùتگوی خصوصی با{0}", + "app.chat.partnerDisconnected": "{0}کلاس را ترک کرد.", + "app.chat.closeChatLabel": "خروج{0}", + "app.chat.hideChatLabel": "مخÙÛŒ کردن{0}", + "app.chat.moreMessages": "مشاهده ادامه پیام های پایین", + "app.chat.dropdown.options": "گزینه های Ú¯Ùتگو", + "app.chat.dropdown.clear": "پاک سازی", + "app.chat.dropdown.copy": "Ú©Ù¾ÛŒ کردن", + "app.chat.dropdown.save": "ذخیره کردن", + "app.chat.label": "Ú¯Ùتگو", + "app.chat.emptyLogLabel": "پاک کردن سابقه Ú¯Ùتگو", + "app.chat.clearPublicChatMessage": "سابقه Ú¯Ùتگوها توسط مدیر Øذ٠گردید.", + "app.userList.usersTitle": "دانش آموزان", + "app.userList.participantsTitle": "دانش آموزان", + "app.userList.messagesTitle": "پیام ها", + "app.userList.presenter": "استاد", + "app.userList.you": "شما", + "app.userList.locked": "Ù‚ÙÙ„ شده", + "app.userList.label": "لیست دانش آموزان", + "app.userList.toggleCompactView.label": "مخÙÛŒ کردن نوار ابزار", + "app.userList.guest": "مهمان", + "app.userList.menuTitleContext": "گزینه های قابل اعمال", + "app.userList.chatListItem.unreadSingular": "{0} پیام جدید", + "app.userList.chatListItem.unreadPlural": "{0} پیام های جدید", + "app.userList.menu.chat.label": "Ú¯Ùتگو", + "app.userList.menu.clearStatus.label": "پاک کردن وضعیت", + "app.userList.menu.makePresenter.label": "تبدیل به استاد", + "app.userList.menu.removeUser.label": "Øذ٠کاربر", + "app.userList.menu.muteUserAudio.label": "بستن صدای کاربر", + "app.userList.menu.unmuteUserAudio.label": "Ùعال سازی میکروÙÙ† کاربر", + "app.userList.userAriaLabel": "{0} {1} {2} وضعیت {3}", + "app.userList.menu.promoteUser.label": "ارتقا نقش به مدیر", + "app.userList.menu.demoteUser.label": "تبدیل به دانش آموز", + "app.media.label": "مدیا", + "app.media.screenshare.start": "اشتراک صÙØÙ‡ نمایش شروع شد", + "app.media.screenshare.end": "اشتراک صÙØÙ‡ نمایش به پایان رسید.", + "app.media.screenshare.safariNotSupported": "در Øال Øاضر اشتراک صÙØÙ‡ نمایش در مرورگر ساÙاری پشتیبانی نمیشد، لطÙا از کروم یا ÙایرÙاکس استÙاده کنید.", + "app.meeting.ended": "جلسه پایان یاÙت", + "app.meeting.endedMessage": "شما در Øال انتقال به صÙØÙ‡ اصلی هستید.", + "app.presentation.presentationToolbar.prevSlideLabel": "اسلاید قبلی", + "app.presentation.presentationToolbar.prevSlideDesc": "تغییر ارائه به اسلاید قبلی", + "app.presentation.presentationToolbar.nextSlideLabel": "اسلاید بعدی", + "app.presentation.presentationToolbar.nextSlideDesc": "تغییر ارائه به اسلاید بعدی", + "app.presentation.presentationToolbar.skipSlideLabel": "بیرون رÙتن از اسلاید", + "app.presentation.presentationToolbar.skipSlideDesc": "تغییر ارائه به یک اسلاید خاص", + "app.presentation.presentationToolbar.fitWidthLabel": "اندازه تصویر را متناسب با عرض صÙØÙ‡ Ú©Ù†.", + "app.presentation.presentationToolbar.fitWidthDesc": "کلیه عرض اسلاید را نمایش بده", + "app.presentation.presentationToolbar.fitScreenLabel": "ابعاد را به اندازه تمام صÙØÙ‡ Ú©Ù†", + "app.presentation.presentationToolbar.fitScreenDesc": "Ú©Ù„ اسلاید را نمایش بده", + "app.presentation.presentationToolbar.zoomLabel": "بزرگ نمایی", + "app.presentation.presentationToolbar.zoomDesc": "تغییر مقدار زوم ارائه", + "app.presentation.presentationToolbar.goToSlide": "اسلاید {0}", + "app.presentationUploder.title": "ارائه", + "app.presentationUploder.message": "شما به عنوان عضوی از کلاس میتوانید جزوه خود را با Ùرمت Ùایل های Office آپلود کنید. پیشنهاد ما Ùایل های PDF است.", + "app.presentationUploder.confirmLabel": "شروع", + "app.presentationUploder.confirmDesc": "تغییرات خود را ذخیره کنید Ùˆ ارائه را آغاز نمایید.", + "app.presentationUploder.dismissLabel": "کنسل", + "app.presentationUploder.dismissDesc": "بستن پنجره Ùˆ Øذ٠تغییرات شما", + "app.presentationUploder.dropzoneLabel": "Ùایل های خود را برای آپلود به اینجا بکشید.", + "app.presentationUploder.browseFilesLabel": "یا برای انتخاب Ùایل کلیک کنید.", + "app.presentationUploder.fileToUpload": "Ùایلهایی Ú©Ù‡ آپلود خواهند شد.", + "app.presentationUploder.currentBadge": "جاری", + "app.presentationUploder.genericError": "اشتباهی اتÙاق اÙتاد.", + "app.presentationUploder.upload.progress": "در Øال آپلود ({0}%)", + "app.presentationUploder.upload.413": "Øجم Ùایل خیلی زیاد است.", + "app.presentationUploder.conversion.conversionProcessingSlides": "در Øال پردازش صÙØÙ‡ {0} از {1}", + "app.presentationUploder.conversion.genericConversionStatus": "در Øال تبدیل Ùایل", + "app.presentationUploder.conversion.generatingThumbnail": "در Øال ساخت تصاویر Ú©ÙˆÚ†Ú© از Ùایل", + "app.presentationUploder.conversion.generatedSlides": "اسلاید ها ساخته شدند.", + "app.presentationUploder.conversion.generatingSvg": "در Øال ساخت تصاویر SVG", + "app.presentationUploder.conversion.pageCountExceeded": "تعداد صÙØات از Øد اکثر صÙØات مورد پشتیبانی بیشتر است.", + "app.presentationUploder.conversion.timeout": "تبدیل Ùایل بیش از Øد طول کشید.", + "app.polling.pollingTitle": "گزینه های نظرسنجی.", + "app.polling.pollAnswerLabel": "پاسخ نظرسنجی {0}", + "app.polling.pollAnswerDesc": "این گزینه را برای رای دادن به {0} انتخاب کنید.", + "app.failedMessage": "عذر خواهی میکنیم. مشکلی در ارتباط با سرور وجود دارد.", + "app.connectingMessage": "در Øال اتصال...", + "app.waitingMessage": "ارتباط قطع شد. در Øال تلاش مجدد در {0} ثانیه", + "app.navBar.settingsDropdown.optionsLabel": "گزینه ها", + "app.navBar.settingsDropdown.fullscreenLabel": "تمام صÙØÙ‡", + "app.navBar.settingsDropdown.settingsLabel": "تنظیمات", + "app.navBar.settingsDropdown.aboutLabel": "درباره", + "app.navBar.settingsDropdown.leaveSessionLabel": "خروج", + "app.navBar.settingsDropdown.exitFullscreenLabel": "بستن Øالت تمام صÙØÙ‡", + "app.navBar.settingsDropdown.fullscreenDesc": "منوی تنظیمات را تمام صÙØÙ‡ Ú©Ù†", + "app.navBar.settingsDropdown.settingsDesc": "تغییر تنظیمات معمول", + "app.navBar.settingsDropdown.aboutDesc": "نمایش اطلاعات دانش آموز", + "app.navBar.settingsDropdown.leaveSessionDesc": "ترک جلسه", + "app.navBar.settingsDropdown.exitFullscreenDesc": "خروج از Øالت تمام صÙØÙ‡", + "app.navBar.settingsDropdown.hotkeysLabel": "کلیدهای میانبر", + "app.navBar.settingsDropdown.hotkeysDesc": "لیست کلیدهای میانبر", + "app.navBar.userListToggleBtnLabel": "نمایش لیست کاربران", + "app.navBar.toggleUserList.ariaLabel": "نمایش Ú¯Ùتگوی دانش آموزان Ùˆ اساتید", + "app.navBar.toggleUserList.newMessages": "با اعلان پیام جدید", + "app.navBar.recording": "جلسه در Øال ضبط شدن است.", + "app.navBar.recording.on": "در Øال ضبط", + "app.navBar.recording.off": "ضبط نمیشود.", + "app.leaveConfirmation.title": "ترک جلسه", + "app.leaveConfirmation.message": "آیا میخواهید جلسه را ترک کنید؟", + "app.leaveConfirmation.confirmLabel": "ترک", + "app.leaveConfirmation.confirmDesc": "پیام های شما بیرون از جلسه", + "app.leaveConfirmation.dismissLabel": "کنسل", + "app.leaveConfirmation.dismissDesc": "ترک جلسه را رد کرد Ùˆ آن را بست.", + "app.leaveConfirmation.endMeetingLabel": "بله Ùˆ بستن جلسه", + "app.leaveConfirmation.endMeetingAriaLabel": "ترک Ùˆ پایان جلسه", + "app.leaveConfirmation.endMeetingDesc": "تایید خروج از جلسه را بست.", + "app.about.title": "درباره", + "app.about.version": "نسخه کاربر", + "app.about.copyright": "Ú©Ù¾ÛŒ رایت", + "app.about.confirmLabel": "تایید", + "app.about.confirmDesc": "تایید", + "app.about.dismissLabel": "کنسل", + "app.about.dismissDesc": "بستن قسمت درباره کاربر", + "app.actionsBar.changeStatusLabel": "تغییر وضعیت دانش آموز", + "app.actionsBar.muteLabel": "Øالت بی صدا", + "app.actionsBar.unmuteLabel": "Ùعال سازی صدا", + "app.actionsBar.camOffLabel": "بستن دوربین", + "app.actionsBar.raiseLabel": "اجازه گرÙتن از استاد", + "app.actionsBar.label": "منوی امکانات", + "app.submenu.application.applicationSectionTitle": "نرم اÙزار", + "app.submenu.application.fontSizeControlLabel": "اندازه متن", + "app.submenu.application.increaseFontBtnLabel": "اÙزایش اندازه متن نرم اÙزار", + "app.submenu.application.decreaseFontBtnLabel": "کاهش اندازه متن نرم اÙزار", + "app.submenu.application.languageLabel": "زبان نرم اÙزار", + "app.submenu.application.ariaLanguageLabel": "تغییر زبان نرم اÙزار", + "app.submenu.application.languageOptionLabel": "انتخاب زبان", + "app.submenu.application.noLocaleOptionLabel": "هیچ زبان Ùعالی وجود ندارد", + "app.submenu.audio.micSourceLabel": "ورودی صدای میکروÙÙ†", + "app.submenu.audio.speakerSourceLabel": "صدای خروجی", + "app.submenu.audio.streamVolumeLabel": "ولوم صدای میکروÙÙ† شما", + "app.submenu.video.title": "تصویر کلاس", + "app.submenu.video.videoSourceLabel": "منبع تصویر", + "app.submenu.video.videoOptionLabel": "انتخاب منبع تصویر", + "app.submenu.video.videoQualityLabel": "Ú©ÛŒÙیت تصویر", + "app.submenu.video.qualityOptionLabel": "انتخاب Ú©ÛŒÙیت تصویر", + "app.submenu.video.participantsCamLabel": "مشاهده دوربین کاربران", + "app.submenu.closedCaptions.closedCaptionsLabel": "کپشن های بسته شده", + "app.submenu.closedCaptions.takeOwnershipLabel": "بدست گرÙتن کنترل", + "app.submenu.closedCaptions.languageLabel": "زبان", + "app.submenu.closedCaptions.localeOptionLabel": "انتخاب زبان", + "app.submenu.closedCaptions.noLocaleOptionLabel": "هیچ زبان Ùعالی وجود ندارد.", + "app.submenu.closedCaptions.fontFamilyLabel": "Ùونت", + "app.submenu.closedCaptions.fontFamilyOptionLabel": "انتخاب Ùونت", + "app.submenu.closedCaptions.fontSizeLabel": "اندازه Ùونت", + "app.submenu.closedCaptions.fontSizeOptionLabel": "انتخاب اندازه Ùونت", + "app.submenu.closedCaptions.backgroundColorLabel": "رنگ پس زمینه", + "app.submenu.closedCaptions.fontColorLabel": "رنگ متن", + "app.submenu.participants.muteAllLabel": "غیر Ùعال سازی صدای کاربران به جز اساتید", + "app.submenu.participants.lockAllLabel": "Ù‚ÙÙ„ کردن کابران", + "app.submenu.participants.lockItemLabel": "شرکت کنندگان{0}", + "app.submenu.participants.lockMicDesc": "غیر Ùعال سازی میکروÙÙ† شرکت کنندگان Ù‚ÙÙ„ شده", + "app.submenu.participants.lockCamDesc": "غیر Ùعال سازی دوربین کاربران Ù‚ÙÙ„ شده.", + "app.submenu.participants.lockPublicChatDesc": "غیرÙعال سازی Ú¯Ùتگوی عمومی کابران Ù‚ÙÙ„ شده", + "app.submenu.participants.lockPrivateChatDesc": "غیرÙعال سازی Ú¯Ùتگوی خصوصی برای کابران Ù‚ÙÙ„ شده.", + "app.submenu.participants.lockLayoutDesc": "Ù‚ÙÙ„ کردن چینش صÙØÙ‡ کابران Ù‚ÙÙ„ شده", + "app.submenu.participants.lockMicAriaLabel": "Ù‚ÙÙ„ کردن میکروÙÙ†", + "app.submenu.participants.lockCamAriaLabel": "Ù‚ÙÙ„ کردن دوربین", + "app.submenu.participants.lockPublicChatAriaLabel": "Ù‚ÙÙ„ کردن Ú¯Ùتگوی عمومی", + "app.submenu.participants.lockPrivateChatAriaLabel": "Ù‚ÙÙ„ کردن Ú¯Ùتگوی خصوصی", + "app.submenu.participants.lockLayoutAriaLabel": "Ù‚ÙÙ„ کردن چینش تصویر", + "app.submenu.participants.lockMicLabel": "میکروÙÙ†", + "app.submenu.participants.lockCamLabel": "دوربین", + "app.submenu.participants.lockPublicChatLabel": "Ú¯Ùتگوی عمومی", + "app.submenu.participants.lockPrivateChatLabel": "Ú¯Ùتگوی خصوصی", + "app.submenu.participants.lockLayoutLabel": "چینش تصویر", + "app.settings.applicationTab.label": "نرم اÙزار", + "app.settings.audioTab.label": "صدا", + "app.settings.videoTab.label": "تصویر کلاس", + "app.settings.closedcaptionTab.label": "کپشن های بسته شده", + "app.settings.usersTab.label": "شرکت کنندگان در کلاس", + "app.settings.main.label": "تنظیمات", + "app.settings.main.cancel.label": "کنسل", + "app.settings.main.cancel.label.description": "Øذ٠تغییرات Ùˆ بستن منوی تنظیمات", + "app.settings.main.save.label": "ذخیره", + "app.settings.main.save.label.description": "ذخیره تغییرات Ùˆ بستن منوی تنظیمات", + "app.settings.dataSavingTab.label": "کاهش مصر٠اینترنت", + "app.settings.dataSavingTab.webcam": "Ùعال سازی دوربین کلاس", + "app.settings.dataSavingTab.screenShare": "Ùعال سازی اشتراک گذاری تصویر کامپیوتر", + "app.settings.dataSavingTab.description": "برای صرÙÙ‡ جویی در مصر٠اینترنت آیتم هایی Ú©Ù‡ باید نمایش داده شوند را انتخاب کنید.", + "app.switch.onLabel": "روشن", + "app.switch.offLabel": "خاموش", + "app.actionsBar.actionsDropdown.actionsLabel": "Ùعالیت ها", + "app.actionsBar.actionsDropdown.presentationLabel": "آپلود Ùایل ارائه", + "app.actionsBar.actionsDropdown.initPollLabel": "ایجاد نظرسنجی", + "app.actionsBar.actionsDropdown.desktopShareLabel": "اشتراک صÙØÙ‡ نمایش شما", + "app.actionsBar.actionsDropdown.stopDesktopShareLabel": "متوق٠کردن اشتراک گذاری دسکتاپ شما", + "app.actionsBar.actionsDropdown.presentationDesc": "آپلود Ùایل ارائه شما", + "app.actionsBar.actionsDropdown.initPollDesc": "ایجاد نظرسنجی", + "app.actionsBar.actionsDropdown.desktopShareDesc": "اشتراک گذاری تصویر کامپیوتر شما با دیگران", + "app.actionsBar.actionsDropdown.stopDesktopShareDesc": "توق٠اشتراک گذاری تصویر کامپیوتر شما با دیگران", + "app.actionsBar.actionsDropdown.startRecording": "شروع ضبط کلاس", + "app.actionsBar.actionsDropdown.stopRecording": "توق٠ضبط کلاس", + "app.actionsBar.emojiMenu.awayLabel": "عدم Øضور در کلاس", + "app.actionsBar.emojiMenu.awayDesc": "تغییر وضعیت شما به عدم Øضور در کلاس", + "app.actionsBar.emojiMenu.raiseHandLabel": "اجازه گرÙتن از استاد", + "app.actionsBar.emojiMenu.raiseHandDesc": "دست خود را بالا ببرید تا از استاد سوال بپرسید", + "app.actionsBar.emojiMenu.neutralLabel": "تصمیم گیری نشده", + "app.actionsBar.emojiMenu.neutralDesc": "تغییر وضعیت شما به تصمیم گیری نشده", + "app.actionsBar.emojiMenu.confusedLabel": "کامل Ù†Ùهمیدم", + "app.actionsBar.emojiMenu.confusedDesc": "تغییر وضعیت شما به وضعیت کامل Ù†Ùهمیدم", + "app.actionsBar.emojiMenu.sadLabel": "ناراØت", + "app.actionsBar.emojiMenu.sadDesc": "تغییر Øالت شما به ناراØت", + "app.actionsBar.emojiMenu.happyLabel": "شاد", + "app.actionsBar.emojiMenu.happyDesc": "تغییر وضعیت شما به خوشØال", + "app.actionsBar.emojiMenu.noneLabel": "پاک کردن وضعیت", + "app.actionsBar.emojiMenu.noneDesc": "پاک کردن وضعیت شما", + "app.actionsBar.emojiMenu.applauseLabel": "تØسین کردن.", + "app.actionsBar.emojiMenu.applauseDesc": "تغییر وضعیت شما به تØسین کردن", + "app.actionsBar.emojiMenu.thumbsUpLabel": "عالی", + "app.actionsBar.emojiMenu.thumbsUpDesc": "تغییر وضعیت شما به عالی", + "app.actionsBar.emojiMenu.thumbsDownLabel": "نه چندان خوب", + "app.actionsBar.emojiMenu.thumbsDownDesc": "تغییر وضعیت شما به نه چندان خوب", + "app.actionsBar.currentStatusDesc": "وضعیت Ùعلی شما{0}", + "app.audioNotification.audioFailedError1001": "خطای 1001: سوکت شما قطع شد", + "app.audioNotification.audioFailedError1002": "خطای 1002: امکان ارتباط با سوکت برای شما Ùراهم نیست، وضعیت نت خود را بررسی کنید یا آن را تغییر دهید.", + "app.audioNotification.audioFailedError1003": "خطای 1003: نسخه مرورگر شما برای کلاس مناسب نیست.", + "app.audioNotification.audioFailedError1004": "خطای 1004: خطا در تماس", + "app.audioNotification.audioFailedError1005": "خطای 1005: تماس به صورت نامشخصی قطع شد", + "app.audioNotification.audioFailedError1006": "خطای 1006: تماس بسیار طولانی شد", + "app.audioNotification.audioFailedError1007": "خطای 1007: خطای ارتباط ice", + "app.audioNotification.audioFailedError1008": "خطای 1008: انتقال میسر نشد", + "app.audioNotification.audioFailedError1009": "خطای 1009: امکان پیدا کردن STUN/TURN وجود ندارد.", + "app.audioNotification.audioFailedError1010": "خطای 1010: ICE برقرار نشد اینترنت خود را Ú†Ú© کنید.", + "app.audioNotification.audioFailedError1011": "خطای 1011: ICE برقرار نشد", + "app.audioNotification.audioFailedMessage": "امکان اتصال صدای شما Ùراهم نیست.", + "app.audioNotification.mediaFailedMessage": "امکان گرÙتن صدای میکروÙÙ† کاربر وجود ندارد.", + "app.audioNotification.closeLabel": "بستن", + "app.audioNotificaion.reconnectingAsListenOnly": "صدای میکروÙÙ† برای مدیران Ùقط Ùعال است، شما Ùقط شنونده هستید.", + "app.breakoutJoinConfirmation.title": "رÙتن به اتاق استراØت", + "app.breakoutJoinConfirmation.message": "آیا میخواهید وارد شوید", + "app.breakoutJoinConfirmation.confirmLabel": "پیوستن", + "app.breakoutJoinConfirmation.confirmDesc": "پیوستن به اتاق استراØت", + "app.breakoutJoinConfirmation.dismissLabel": "کنسل", + "app.breakoutJoinConfirmation.dismissDesc": "بستن Ùˆ رد کردن پیوستن به اتاق استراØت", + "app.breakoutJoinConfirmation.freeJoinMessage": "انتخاب اتاق استراØت برای پیوستن", + "app.breakoutTimeRemainingMessage": "زمان باقی مانده از اتاق استراØت: {0}", + "app.breakoutWillCloseMessage": "زمان پایان یاÙت. اتاق استراØت به زودی بسته میشود", + "app.calculatingBreakoutTimeRemaining": "Ù…Øاسبه زمان باقی مانده...", + "app.audioModal.microphoneLabel": "گوینده هم باشم.", + "app.audioModal.listenOnlyLabel": "Ùقط شنونده باشم.", + "app.audioModal.audioChoiceLabel": "چگونه میخواهید صدا را دریاÙت کنید.", + "app.audioModal.iOSBrowser": "صدا/تصویر پشتیبانی نمیشود.", + "app.audioModal.iOSErrorDescription": "در Øال Øاضر صدا Ùˆ تصویر در کروم iOS پشتیبانی نمیشود.", + "app.audioModal.iOSErrorRecommendation": "پیشنهاد ما استÙاده از ساÙاری است.", + "app.audioModal.audioChoiceDesc": "انتخاب کنید چگونه صدا را در این جلسه میخواهید دریاÙت کنید", + "app.audioModal.closeLabel": "بستن", + "app.audioModal.yes": "بله", + "app.audioModal.no": "خیر", + "app.audioModal.yes.arialabel" : "اکو شنیده میشود", + "app.audioModal.no.arialabel" : "اکو شنیده نمیشود", + "app.audioModal.echoTestTitle": "این یک تست اکو خصوصی است.چند کلمه صØبت کنید.آیا صدایتان را میشنوید؟", + "app.audioModal.settingsTitle": "تغییر تنظیمات صدا", + "app.audioModal.helpTitle": "یک مشکلی در سخت اÙزار شما وجود دارد", + "app.audioModal.helpText": "آیا به مرورگر خود در قسمت تنظیمات اجازه دسترسی به میکروÙÙ† را داده اید؟ اگر اینطور نیست مطابق دستورالعمل مرورگرتان عمل کرده Ùˆ اجازه دسترسی به میکروÙÙ† را Ùعال کنید.", + "app.audioModal.connecting": "در Øال اتصال...", + "app.audioModal.connectingEchoTest": "اتصال به تست اکو", + "app.audioManager.joinedAudio": "شما به جلسه صوتی وارد شده اید", + "app.audioManager.joinedEcho": "شما به تست اکو پیوسته اید", + "app.audioManager.leftAudio": "شما جلسه صوتی را ترک کرده اید", + "app.audioManager.genericError": "خطا: مشکلی پیش آمده است لطÙا مجدد سعی کنید.", + "app.audioManager.connectionError": "خطا: خطای ارتباط", + "app.audioManager.requestTimeout": "خطا: خطای طول کشیدن بیش از اندازه در درخواست.", + "app.audioManager.invalidTarget": "خطا: تلاش برای درخواست چیزی غیر صØÛŒØ", + "app.audioManager.mediaError": "خطا: مشکلی در اتصال سخت اÙزار شما با کلاس موجود است.", + "app.audio.joinAudio": "پیوستن به صدا", + "app.audio.leaveAudio": "ترک صدا", + "app.audio.enterSessionLabel": "ورود به جلسه", + "app.audio.playSoundLabel": "پخش صدا", + "app.audio.backLabel": "بازگشت", + "app.audio.audioSettings.titleLabel": "انتخاب تنظیمات صدای شما", + "app.audio.audioSettings.descriptionLabel": "لطÙا توجه کنید، یک پیام در مرورگر شما ظاهر میشود Ùˆ از شما میخواهد اجازه اشتراک میکروÙÙ† خود را بدهید.", + "app.audio.audioSettings.microphoneSourceLabel": "منبع ورودی میکروÙÙ†", + "app.audio.audioSettings.speakerSourceLabel": "منبع خروجی صدا", + "app.audio.audioSettings.microphoneStreamLabel": "ولوم صدای میکروÙÙ† شما", + "app.audio.audioSettings.retryLabel": "تلاش مجدد", + "app.audio.listenOnly.backLabel": "بازگشت", + "app.audio.listenOnly.closeLabel": "بستن", + "app.audio.permissionsOverlay.title": "اجازه دهید کلاس به میکروÙÙ† شما دسترسی داشته باشد.", + "app.audio.permissionsOverlay.hint": "برای پیوستن شما به Ú©Ù†Ùرانس صوتی نیاز به اجازه شما Ù…ÛŒ باشد. :)", + "app.error.removed": "شما از Ú©Ù†Ùرانس کنار گذاشته شده اید.", + "app.error.meeting.ended": "شما از Ú©Ù†Ùرانس خارج شده اید", + "app.dropdown.close": "بستن", + "app.error.500": "خطای پیش آمده است.", + "app.error.404": "پیدا نشد", + "app.error.401": "شناسایی نشد", + "app.error.403": "ممنوع", + "app.error.leaveLabel": "ورود مجدد", + "app.guest.waiting": "منتظر تایید جهت پیوستن", + "app.toast.breakoutRoomEnded": "اتاق استراØت پایان یاÙت. لطÙا مجدد به Ú©Ù†Ùرانس صوتی بپیوندید.", + "app.toast.chat.public": "پیام جدید در Ú¯Ùتگوی عمومی", + "app.toast.chat.private": "پیام جدید در Ú¯Ùتگوی خصوصی", + "app.toast.chat.system": "سیستم", + "app.notification.recordingStart": "جلسه در Øال ضبط شدن است", + "app.notification.recordingStop": "جلسه دیگر ضبط نمیشود.", + "app.shortcut-help.title": "کلید های میانبر", + "app.shortcut-help.accessKeyNotAvailable": "کلیدهای دسترسی موجود نیستند.", + "app.shortcut-help.comboLabel": "کمبو", + "app.shortcut-help.functionLabel": "تابع", + "app.shortcut-help.closeLabel": "بستن", + "app.shortcut-help.closeDesc": "بستن صÙØÙ‡ کلیدهای میانبر", + "app.shortcut-help.openOptions": "باز کردن گزینه ها", + "app.shortcut-help.toggleUserList": "مشاهده لیست دانش آموزان", + "app.shortcut-help.toggleMute": "Øالت بی صدا/با صدا", + "app.shortcut-help.togglePublicChat": "نمایش Ú¯Ùتگوی عمومی(لیتس کاربران باید Ùعال باشد)", + "app.shortcut-help.hidePrivateChat": "پنهان کردن Ú¯Ùتگوی خصوصی", + "app.shortcut-help.closePrivateChat": "بستن Ú¯Ùتگوی خصوصی", + "app.shortcut-help.openActions": "باز کردن منوی کارها", + "app.shortcut-help.openStatus": "باز کردن منوی وضعیت", + "app.video.joinVideo": "اشتراک گذاری دوربین", + "app.video.leaveVideo": "بستن دوربین", + "app.video.iceCandidateError": "خطا در اÙزودن ICE", + "app.video.permissionError": "خطا در اشتراک گذاری دوربین. لطÙا دسترسی ها را Ú†Ú© کنید.", + "app.video.sharingError": "خطا در اشتراک دوربین", + "app.video.notFoundError": "دوربین یاÙت نشد. لطÙا مطمئن شوید Ú©Ù‡ دوربین متصل است.", + "app.video.notAllowed": "اجازه دسترسی مرورگر به دوربین داده نشده است. لطÙا دسترسی های مرورگر را Ú†Ú© کنید.", + "app.video.notSupportedError": "مطمئن شوید SSL Ùعال است.", + "app.video.notReadableError": "عدم امکان دسترسی به داده های دوربین، مطمئن شوید دوربین شما جای دیگری درگیر نیست.", + "app.video.swapCam": "جابجا کردن", + "app.video.swapCamDesc": "جابجا کردن موقعیت تصویر دوربین ها", + "app.video.videoMenu": "منوی دوربین", + "app.video.videoMenuDisabled": "امکان مشاهده دوربین در تنظیمات شما Ùعال نیست.لطÙا به تنظیمات مراجعه کنید.", + "app.video.videoMenuDesc": "منوی تنظیمات دوربین", + "app.video.chromeExtensionError": "شما باید نصب کنید.", + "app.video.chromeExtensionErrorLink": "این اÙزونه کروم را", + "app.video.stats.title": "وضعیت اتصال", + "app.video.stats.packetsReceived": "بسته های دریاÙت شده.", + "app.video.stats.packetsSent": "بسته های ارسال شده", + "app.video.stats.packetsLost": "بسته های از دست رÙته", + "app.video.stats.bitrate": "بیت ریت", + "app.video.stats.lostPercentage": "درصد از دست رÙته", + "app.video.stats.lostRecentPercentage": "درصد از دست رÙته اخیر", + "app.video.stats.dimensions": "ابعاد", + "app.video.stats.codec": "کدک", + "app.video.stats.decodeDelay": "تاخیر بازگشایی", + "app.video.stats.rtt": "RTT", + "app.video.stats.encodeUsagePercent": "استÙاده ENCOD", + "app.video.stats.currentDelay": "تاخیر جاری", + "app.sfu.mediaServerConnectionError2000": "خطای 2000: عدم امکان ارتباط با سرور", + "app.sfu.mediaServerOffline2001": "خطای 2001: سرور مدیا اینترنت ندارد. لطÙا بعدا Ú†Ú© کنید.", + "app.sfu.mediaServerNoResources2002": "خطای 2002: سرور مدیا منابع قابل دسترسی ندارد.", + "app.sfu.mediaServerRequestTimeout2003": "خطای 2003: درخواست های سرور مدیا TIMEOUT میشود", + "app.sfu.serverIceGatheringFailed2021": "خطای 2021: سرور مدیا ICE ندارد.", + "app.sfu.serverIceGatheringFailed2022": "خطای 2022: سرور مدیا اتصال ICE ندارد.", + "app.sfu.invalidSdp2202":"خطای 2202: کاربر SDP اشتباه ایجاد کرده است.", + "app.sfu.noAvailableCodec2203": "خطای 2203: سرور کدک متناسب را پیدا نمیکند", + + "app.meeting.endNotification.ok.label": "باشه", + "app.whiteboard.toolbar.tools": "ابزارها", + "app.whiteboard.toolbar.tools.pointer": "نشانگر", + "app.whiteboard.toolbar.tools.pencil": "مداد", + "app.whiteboard.toolbar.tools.rectangle": "چهارگوش", + "app.whiteboard.toolbar.tools.triangle": "سه گوش", + "app.whiteboard.toolbar.tools.ellipse": "بیضی", + "app.whiteboard.toolbar.tools.line": "خط", + "app.whiteboard.toolbar.tools.text": "متن", + "app.whiteboard.toolbar.thickness": "ضخامت خط", + "app.whiteboard.toolbar.thicknessDisabled": "ضخامت خط غیر Ùعال است.", + "app.whiteboard.toolbar.color": "رنگ ها", + "app.whiteboard.toolbar.colorDisabled": "رنگ ها غیر Ùعالند", + "app.whiteboard.toolbar.color.black": "مشکی", + "app.whiteboard.toolbar.color.white": "سÙید", + "app.whiteboard.toolbar.color.red": "قرمز", + "app.whiteboard.toolbar.color.orange": "نارنجی", + "app.whiteboard.toolbar.color.eletricLime": "زرد آبی", + "app.whiteboard.toolbar.color.lime": "لیمویی", + "app.whiteboard.toolbar.color.cyan": "آبی روشن", + "app.whiteboard.toolbar.color.dodgerBlue": "آبی تیره", + "app.whiteboard.toolbar.color.blue": "آبی", + "app.whiteboard.toolbar.color.violet": "بنÙØ´", + "app.whiteboard.toolbar.color.magenta": "قرمز", + "app.whiteboard.toolbar.color.silver": "نقره ای", + "app.whiteboard.toolbar.undo": "دوباره سازی Øاشیه نویسی", + "app.whiteboard.toolbar.clear": "پاک کردن همه Øاشیه نویسی ها", + "app.whiteboard.toolbar.multiUserOn": "Ùعال کردن Øالت چند کابره", + "app.whiteboard.toolbar.multiUserOff": "خاموش کردن Øالت چند کاربره", + "app.whiteboard.toolbar.fontSize": "لیست اندازه Ùونت", + "app.feedback.title": "شما از Ú©Ù†Ùرانس بیرون رÙته اید", + "app.feedback.subtitle": "بسیار ممنون میشویم نظر خود را در خصوص کلاس بÙرمایید(نظر شما)", + "app.feedback.textarea": "چگونه میتوان کلاس را بهتر کرد؟", + "app.feedback.sendFeedback": "ارسال پیشنهاد Ùˆ انتقاد", + "app.feedback.sendFeedbackDesc": "ارسال پیشنهاد Ùˆ انتقاد Ùˆ ترک جلسه", + "app.videoDock.webcamFocusLabel": "تمرکز", + "app.videoDock.webcamFocusDesc": "تمرکز در دوربین انتخاب شده", + "app.videoDock.webcamUnfocusLabel": "خروج از Øالت تمرکز", + "app.videoDock.webcamUnfocusDesc": "خروج از Øالت تمرکز در دوربین انتخاب شده" + +} + diff --git a/bigbluebutton-html5/private/locales/fr.json b/bigbluebutton-html5/private/locales/fr.json index 4196a593edb919614bd90a8378f8adeac91a645f..caf0280af266f102c2f4994c9dc26fa5b4d87e42 100644 --- a/bigbluebutton-html5/private/locales/fr.json +++ b/bigbluebutton-html5/private/locales/fr.json @@ -97,6 +97,8 @@ "app.navBar.settingsDropdown.exitFullscreenDesc": "Quitter le mode plein écran", "app.navBar.settingsDropdown.hotkeysLabel": "Raccourcis", "app.navBar.settingsDropdown.hotkeysDesc": "Liste des raccourcis disponibles", + "app.navBar.settingsDropdown.helpLabel": "Aide", + "app.navBar.settingsDropdown.helpDesc": "Liens utilisateur à des didacticiels vidéo", "app.navBar.userListToggleBtnLabel": "Basculer l'affichage de la Liste des Utilisateurs", "app.navBar.toggleUserList.ariaLabel": "Basculer l'affichage de la Liste des Utilisateurs aux messages", "app.navBar.toggleUserList.newMessages": "avec notification des nouveaux messages", @@ -126,8 +128,8 @@ "app.actionsBar.raiseLabel": "Lever la main", "app.actionsBar.label": "Barre d'actions", "app.submenu.application.applicationSectionTitle": "Application", - "app.submenu.application.audioNotifyLabel": "Notifications audio pour la discussion", - "app.submenu.application.pushNotifyLabel": "Pousser les notifications de la discussion", + "app.submenu.application.audioAlertLabel": "Alertes audio pour Discussion", + "app.submenu.application.pushAlertLabel": "Alertes Popup de Discussion", "app.submenu.application.fontSizeControlLabel": "Taille des caractères", "app.submenu.application.increaseFontBtnLabel": "Augmenter la Taille de la Police", "app.submenu.application.decreaseFontBtnLabel": "Diminuer la Taille de la Police", @@ -200,7 +202,7 @@ "app.actionsBar.actionsDropdown.stopDesktopShareDesc": "Cesser de partager votre écran avec", "app.actionsBar.actionsDropdown.startRecording": "Commencer à enregistrer", "app.actionsBar.actionsDropdown.stopRecording": "Arrêter d'enregistrer", - "app.actionsBar.emojiMenu.statusTriggerLabel": "Définir un statut", + "app.actionsBar.emojiMenu.statusTriggerLabel": "Définir le statut", "app.actionsBar.emojiMenu.awayLabel": "Éloigné", "app.actionsBar.emojiMenu.awayDesc": "Passer votre état à éloigné", "app.actionsBar.emojiMenu.raiseHandLabel": "Lever la main", @@ -223,16 +225,16 @@ "app.actionsBar.emojiMenu.thumbsDownDesc": "Passer votre statut à défavorable", "app.actionsBar.currentStatusDesc": "statut actuel {0}", "app.audioNotification.audioFailedError1001": "Erreur 1001 : WebSocket déconnecté", - "app.audioNotification.audioFailedError1002": "Erreur 1002 : Impossible d'établir une connexion WebSocket.", - "app.audioNotification.audioFailedError1003": "Erreur 1003 : Version de navigateur non supportée", - "app.audioNotification.audioFailedError1004": "Erreur 1004 : Défaut sur appel", - "app.audioNotification.audioFailedError1005": "Erreur 1005 : L'appel s'est terminé inopinément", - "app.audioNotification.audioFailedError1006": "Erreur 1006 : Délai d'appel dépassé", - "app.audioNotification.audioFailedError1007": "Erreur 1007 : La négociation ICE a échoué", - "app.audioNotification.audioFailedError1008": "Erreur 1008 : Échec du transfert", - "app.audioNotification.audioFailedError1009": "Erreur 1009 : Impossible de récupérer les informations du serveur STUN/TURN.", - "app.audioNotification.audioFailedError1010": "Erreur 1010 : Délai dépassé durant la négociation ICE", - "app.audioNotification.audioFailedError1011": "Erreur 1011 : Délai d'attente dépassé pour ICE", + "app.audioNotification.audioFailedError1002": "Erreur 1002 : impossible d'établir une connexion WebSocket.", + "app.audioNotification.audioFailedError1003": "Erreur 1003 : version de navigateur non supportée", + "app.audioNotification.audioFailedError1004": "Erreur 1004 : défaut sur appel", + "app.audioNotification.audioFailedError1005": "Erreur 1005 : l'appel s'est terminé inopinément", + "app.audioNotification.audioFailedError1006": "Erreur 1006 : délai d'appel dépassé", + "app.audioNotification.audioFailedError1007": "Erreur 1007 : la négociation ICE a échoué", + "app.audioNotification.audioFailedError1008": "Erreur 1008 : échec du transfert", + "app.audioNotification.audioFailedError1009": "Erreur 1009 : impossible de récupérer les informations du serveur STUN/TURN.", + "app.audioNotification.audioFailedError1010": "Erreur 1010 : délai dépassé durant la négociation ICE", + "app.audioNotification.audioFailedError1011": "Erreur 1011 : délai d'attente dépassé pour ICE", "app.audioNotification.audioFailedMessage": "Votre connexion audio à échoué", "app.audioNotification.mediaFailedMessage": "Votre connexion micro a échoué", "app.audioNotification.closeLabel": "Fermer", @@ -310,6 +312,9 @@ "app.shortcut-help.closeLabel": "Fermer", "app.shortcut-help.closeDesc": "Ferme la fenêtre des raccourcis", "app.shortcut-help.openOptions": "Ouvrir les options", + "app.shortcut-help.toggleUserList": "Basculer la liste d'utilisateurs", + "app.shortcut-help.toggleMute": "Assourdir / Activer", + "app.shortcut-help.togglePublicChat": "Basculer vers la Discussion Publique(La liste utilisateurs doit être ouvert)", "app.shortcut-help.hidePrivateChat": "Cacher la discussion privée", "app.shortcut-help.closePrivateChat": "Fermer la discussion privée", "app.shortcut-help.openActions": "Ouvrir le menu Actions", @@ -317,6 +322,7 @@ "app.video.joinVideo": "Partager la Webcam", "app.video.leaveVideo": "Ne plus partager la Webcam", "app.video.iceCandidateError": "Erreur lors de l'ajout d'un candidat ICE", + "app.video.iceConnectionStateError": "Erreur 1107 : négociation ICE échouée", "app.video.permissionError": "Erreur lors du partage de la webcam. Veuillez vérifier les permissions.", "app.video.sharingError": "Erreur lors du partage de la Webcam", "app.video.notFoundError": "Webcam introuvable. Assurez-vous qu'elle soit bien connectée", @@ -343,6 +349,15 @@ "app.video.stats.rtt": "RTT", "app.video.stats.encodeUsagePercent": "Usage de l'encodage", "app.video.stats.currentDelay": "Délai actuel", + "app.deskshare.iceConnectionStateError": "Erreur 1108 : connexion ICE échouée lors du partage d'écran", + "app.sfu.mediaServerConnectionError2000": "Erreur 2000 : impossible de se connecter au serveur multimédia", + "app.sfu.mediaServerOffline2001": "Erreur 2001 : le serveur multimédia est hors ligne. Veuillez réessayer plus tard.", + "app.sfu.mediaServerNoResources2002": "Erreur 2002 : le serveur de médias n'a pas de ressources disponibles", + "app.sfu.mediaServerRequestTimeout2003": "Erreur 2003 : les demandes du serveur multimédia expirent", + "app.sfu.serverIceGatheringFailed2021": "Erreur 2021 : le serveur multimédia ne peut pas rassembler de candidats ICE", + "app.sfu.serverIceGatheringFailed2022": "Erreur 2022 : la connexion ICE au serveur de médias a échoué", + "app.sfu.invalidSdp2202":"Erreur 2202 : le client a généré un SDP non valide", + "app.sfu.noAvailableCodec2203": "Erreur 2203 : le serveur n'a pas trouvé de codec approprié", "app.meeting.endNotification.ok.label": "OK", "app.whiteboard.toolbar.tools": "Outils", diff --git a/bigbluebutton-html5/private/locales/id.json b/bigbluebutton-html5/private/locales/id.json index 427b46af95449f39fe9cf5d5ed5185c71e55070a..d2a85c265e5a1293f6b0b15f25aca6774b804448 100644 --- a/bigbluebutton-html5/private/locales/id.json +++ b/bigbluebutton-html5/private/locales/id.json @@ -96,8 +96,6 @@ "app.actionsBar.camOffLabel": "Kamera Off", "app.actionsBar.raiseLabel": "Naikan", "app.submenu.application.applicationSectionTitle": "Aplikasi", - "app.submenu.application.audioNotifyLabel": "Notifikasi audio untuk Chat", - "app.submenu.application.pushNotifyLabel": "Notifikasi otomatis untuk Chat", "app.submenu.application.fontSizeControlLabel": "Ukuran Huruf", "app.submenu.application.increaseFontBtnLabel": "Besarkan Ukuran Huruf Aplikasi", "app.submenu.application.decreaseFontBtnLabel": "Kecilkan Ukuran Huruf Aplikasi", diff --git a/bigbluebutton-html5/private/locales/ja.json b/bigbluebutton-html5/private/locales/ja.json index b41d221c4782ac544d425cc37c94b3cb20e64783..43a6025d427cb8894d0846379fd0f62aee6aac6f 100644 --- a/bigbluebutton-html5/private/locales/ja.json +++ b/bigbluebutton-html5/private/locales/ja.json @@ -122,8 +122,6 @@ "app.actionsBar.raiseLabel": "手をã‚ã’ã‚‹", "app.actionsBar.label": "アクションメニュー", "app.submenu.application.applicationSectionTitle": "アプリケーション", - "app.submenu.application.audioNotifyLabel": "ãƒãƒ£ãƒƒãƒˆã®éŸ³å£°é€šçŸ¥", - "app.submenu.application.pushNotifyLabel": "ãƒãƒ£ãƒƒãƒˆã®ãƒ—ッシュ通知", "app.submenu.application.fontSizeControlLabel": "フォントサイズ", "app.submenu.application.increaseFontBtnLabel": "アプリケーションã®ãƒ•ã‚©ãƒ³ãƒˆã‚µã‚¤ã‚ºã‚’大ããã™ã‚‹", "app.submenu.application.decreaseFontBtnLabel": "アプリケーションã®ãƒ•ã‚©ãƒ³ãƒˆã‚µã‚¤ã‚ºã‚’å°ã•ãã™ã‚‹", @@ -196,7 +194,6 @@ "app.actionsBar.actionsDropdown.stopDesktopShareDesc": "ã¨ã®ç”»é¢å…±æœ‰ã‚’æ¢ã‚ã‚‹", "app.actionsBar.actionsDropdown.startRecording": "録画開始", "app.actionsBar.actionsDropdown.stopRecording": "録画åœæ¢", - "app.actionsBar.emojiMenu.statusTriggerLabel": "ステータスをè¨å®šã™ã‚‹", "app.actionsBar.emojiMenu.awayLabel": "ä¸åœ¨", "app.actionsBar.emojiMenu.awayDesc": "スタータスを「ä¸åœ¨ã€ã«ã™ã‚‹", "app.actionsBar.emojiMenu.raiseHandLabel": "挙手", diff --git a/bigbluebutton-html5/private/locales/ja_JP.json b/bigbluebutton-html5/private/locales/ja_JP.json index b41d221c4782ac544d425cc37c94b3cb20e64783..43a6025d427cb8894d0846379fd0f62aee6aac6f 100644 --- a/bigbluebutton-html5/private/locales/ja_JP.json +++ b/bigbluebutton-html5/private/locales/ja_JP.json @@ -122,8 +122,6 @@ "app.actionsBar.raiseLabel": "手をã‚ã’ã‚‹", "app.actionsBar.label": "アクションメニュー", "app.submenu.application.applicationSectionTitle": "アプリケーション", - "app.submenu.application.audioNotifyLabel": "ãƒãƒ£ãƒƒãƒˆã®éŸ³å£°é€šçŸ¥", - "app.submenu.application.pushNotifyLabel": "ãƒãƒ£ãƒƒãƒˆã®ãƒ—ッシュ通知", "app.submenu.application.fontSizeControlLabel": "フォントサイズ", "app.submenu.application.increaseFontBtnLabel": "アプリケーションã®ãƒ•ã‚©ãƒ³ãƒˆã‚µã‚¤ã‚ºã‚’大ããã™ã‚‹", "app.submenu.application.decreaseFontBtnLabel": "アプリケーションã®ãƒ•ã‚©ãƒ³ãƒˆã‚µã‚¤ã‚ºã‚’å°ã•ãã™ã‚‹", @@ -196,7 +194,6 @@ "app.actionsBar.actionsDropdown.stopDesktopShareDesc": "ã¨ã®ç”»é¢å…±æœ‰ã‚’æ¢ã‚ã‚‹", "app.actionsBar.actionsDropdown.startRecording": "録画開始", "app.actionsBar.actionsDropdown.stopRecording": "録画åœæ¢", - "app.actionsBar.emojiMenu.statusTriggerLabel": "ステータスをè¨å®šã™ã‚‹", "app.actionsBar.emojiMenu.awayLabel": "ä¸åœ¨", "app.actionsBar.emojiMenu.awayDesc": "スタータスを「ä¸åœ¨ã€ã«ã™ã‚‹", "app.actionsBar.emojiMenu.raiseHandLabel": "挙手", diff --git a/bigbluebutton-html5/private/locales/km.json b/bigbluebutton-html5/private/locales/km.json index 1aa8193acc51ac6578402f06b5eaf3517910aa26..3239abcaa143d599a0c35c8c96404518e403fadd 100644 --- a/bigbluebutton-html5/private/locales/km.json +++ b/bigbluebutton-html5/private/locales/km.json @@ -86,8 +86,6 @@ "app.actionsBar.raiseLabel": "លើកដៃ", "app.actionsBar.label": "របារ​សកម្មភាព", "app.submenu.application.applicationSectionTitle": "កម្មវិធី", - "app.submenu.application.audioNotifyLabel": "ការ​ជូន​ដំណឹង​ជា​សម្លáŸáž„​សម្រាប់​ការ​ជជែក", - "app.submenu.application.pushNotifyLabel": "ការ​ជូន​ដំណឹង​ដែល​លáŸáž…​ឡើង​សម្រាប់​ការ​ជជែក", "app.submenu.application.fontSizeControlLabel": "ទំហំ​អក្សរ​", "app.submenu.application.increaseFontBtnLabel": "បង្កើន​ទំហំ​អក្សរ​ក្នុង​កម្មវិធី", "app.submenu.application.decreaseFontBtnLabel": "បន្ážáž™â€‹áž‘ំហំ​អក្សរ​ក្នុង​កម្មវិធី", diff --git a/bigbluebutton-html5/private/locales/pt_BR.json b/bigbluebutton-html5/private/locales/pt_BR.json index 9cb1608b09f81e9217cf5f91c85f29aeb6a0850a..6450371462fdf7df52a2b65cd73e2845e2bfe19c 100644 --- a/bigbluebutton-html5/private/locales/pt_BR.json +++ b/bigbluebutton-html5/private/locales/pt_BR.json @@ -59,7 +59,7 @@ "app.presentation.presentationToolbar.zoomDesc": "Alterar o nÃvel de zoom da apresentação", "app.presentation.presentationToolbar.goToSlide": "Slide {0}", "app.presentationUploder.title": "Apresentação", - "app.presentationUploder.message": "Como apresentador no BigBlueButton, você tem a capacidade de carregar qualquer documento do Office ou arquivo PDF. Para melhores resultados, recomendamos que se carregue arquivos em PDF.", + "app.presentationUploder.message": "Como apresentador, você tem a capacidade de carregar qualquer documento do Office ou arquivo PDF. Para melhores resultados, recomendamos que se carregue arquivos em PDF.", "app.presentationUploder.confirmLabel": "Iniciar", "app.presentationUploder.confirmDesc": "Salve as alterações e inicie a apresentação", "app.presentationUploder.dismissLabel": "Cancelar", @@ -97,6 +97,8 @@ "app.navBar.settingsDropdown.exitFullscreenDesc": "Sair do modo de tela cheia", "app.navBar.settingsDropdown.hotkeysLabel": "Atalhos de teclado", "app.navBar.settingsDropdown.hotkeysDesc": "Lista de atalhos disponÃveis", + "app.navBar.settingsDropdown.helpLabel": "Ajuda", + "app.navBar.settingsDropdown.helpDesc": "VÃdeo tutoriais", "app.navBar.userListToggleBtnLabel": "Alternar lista de usuários", "app.navBar.toggleUserList.ariaLabel": "Alternar Usuários e Mensagens", "app.navBar.toggleUserList.newMessages": "com notificação para novas mensagens", @@ -126,8 +128,8 @@ "app.actionsBar.raiseLabel": "Levantar a mão", "app.actionsBar.label": "Barra de ações", "app.submenu.application.applicationSectionTitle": "Aplicação", - "app.submenu.application.audioNotifyLabel": "Notificações de áudio para o bate-papo", - "app.submenu.application.pushNotifyLabel": "Notificações para o bate-papo", + "app.submenu.application.audioAlertLabel": "Alertas de áudio para Bate-papo", + "app.submenu.application.pushAlertLabel": "Alertas de pop-up para Bate-papo", "app.submenu.application.fontSizeControlLabel": "Tamanho da fonte", "app.submenu.application.increaseFontBtnLabel": "Aumentar o tamanho da fonte da aplicação", "app.submenu.application.decreaseFontBtnLabel": "Diminuir o tamanho da fonte da aplicação", @@ -262,7 +264,7 @@ "app.audioModal.echoTestTitle": "Este é um teste privado de áudio. Fale algumas palavras. Você consegue ouvir sua voz?", "app.audioModal.settingsTitle": "Alterar as configurações de áudio", "app.audioModal.helpTitle": "Houve um problema com seus dispositivos de mÃdia", - "app.audioModal.helpText": "Você autorizou o BigBlueButton a acessar seu microfone? Observe que uma caixa de diálogo deve abrir assim que você tentar participar da conferência de áudio, pedindo as permissões de acesso aos dispositivos de mÃdia, por favor conceda essa permissão de acesso para participar da conferência. Se isso não funcionar, tente alterar a permissão do microfone nas configurações do seu navegador.", + "app.audioModal.helpText": "Você autorizou o acesso ao seu microfone? Observe que uma caixa de diálogo deve abrir assim que você tentar participar da conferência de áudio, pedindo as permissões de acesso aos dispositivos de mÃdia, por favor conceda essa permissão de acesso para participar da conferência. Se isso não funcionar, tente alterar a permissão do microfone nas configurações do seu navegador.", "app.audioModal.connecting": "Conectando", "app.audioModal.connectingEchoTest": "Conectando ao teste de áudio", "app.audioManager.joinedAudio": "Você se juntou à conferência de áudio", @@ -320,6 +322,7 @@ "app.video.joinVideo": "Ativar Webcam", "app.video.leaveVideo": "Pare a Webcam", "app.video.iceCandidateError": "Erro ao adicionar o candidato ICE", + "app.video.iceConnectionStateError": "Erro 1107: Negociação ICE falhou", "app.video.permissionError": "Erro ao compartilhar a webcam. Verifique as permissões", "app.video.sharingError": "Erro ao compartilhar a webcam", "app.video.notFoundError": "Não foi possÃvel encontrar uma webcam. Por favor, verifique se ela está conectada", @@ -346,6 +349,15 @@ "app.video.stats.rtt": "RTT", "app.video.stats.encodeUsagePercent": "Uso de codificação", "app.video.stats.currentDelay": "Atraso atual", + "app.deskshare.iceConnectionStateError": "Erro 1108: Falha na conexão ICE ao compartilhar a tela", + "app.sfu.mediaServerConnectionError2000": "Erro 2000: Não foi possÃvel conectar ao servidor de mÃdia", + "app.sfu.mediaServerOffline2001": "Erro 2001: Servidor de mÃdia está offline. Por favor, tente novamente mais tarde.", + "app.sfu.mediaServerNoResources2002": "Erro 2002: Servidor de mÃdia não possui recursos disponÃveis", + "app.sfu.mediaServerRequestTimeout2003": "Erro 2003: Tempo limite para solicitações ao servidor de mÃdia", + "app.sfu.serverIceGatheringFailed2021": "Erro 2021: O servidor de mÃdia não pode capturar candidatos ICE", + "app.sfu.serverIceGatheringFailed2022": "Erro 2022: Falha na conexão ICE do servidor de mÃdia", + "app.sfu.invalidSdp2202":"Erro 2202: Cliente gerou um SDP inválido", + "app.sfu.noAvailableCodec2203": "Erro 2203: O servidor não encontrou um codec apropriado", "app.meeting.endNotification.ok.label": "OK", "app.whiteboard.toolbar.tools": "Ferramentas", diff --git a/bigbluebutton-html5/private/locales/ru_RU.json b/bigbluebutton-html5/private/locales/ru_RU.json index ea7ce626272e42482d6518e0e6c472126a6c14ac..77f47fdf0c0fb986e0cb8fa0d631733a4f277276 100644 --- a/bigbluebutton-html5/private/locales/ru_RU.json +++ b/bigbluebutton-html5/private/locales/ru_RU.json @@ -37,6 +37,8 @@ "app.userList.menu.muteUserAudio.label": "Выключить микрофон пользователÑ", "app.userList.menu.unmuteUserAudio.label": "Включить микрофон пользователÑ", "app.userList.userAriaLabel": "{0} {1} {2} Ð¡Ñ‚Ð°Ñ‚ÑƒÑ {3}", + "app.userList.menu.promoteUser.label": "ПовыÑить до модератора", + "app.userList.menu.demoteUser.label": "Понизить до зрителÑ", "app.media.label": "Медиа", "app.media.screenshare.start": "ДемонÑÑ‚Ñ€Ð°Ñ†Ð¸Ñ Ñкрана началаÑÑŒ", "app.media.screenshare.end": "ДемонÑÑ‚Ñ€Ð°Ñ†Ð¸Ñ Ñкрана закончилаÑÑŒ", @@ -95,6 +97,8 @@ "app.navBar.settingsDropdown.exitFullscreenDesc": "Выйти из полноÑкранного режима", "app.navBar.settingsDropdown.hotkeysLabel": "БыÑтрые клавиши", "app.navBar.settingsDropdown.hotkeysDesc": "СпиÑок доÑтупных быÑтрых клавиш", + "app.navBar.settingsDropdown.helpLabel": "Помощь", + "app.navBar.settingsDropdown.helpDesc": "СÑылает пользователей на видеоуроки", "app.navBar.userListToggleBtnLabel": "Включить/выключить ÑпиÑок пользователей", "app.navBar.toggleUserList.ariaLabel": "Включить/выключить Пользователей и СообщениÑ", "app.navBar.toggleUserList.newMessages": "Ñ ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸ÐµÐ¼ о новых ÑообщениÑÑ…", @@ -124,8 +128,8 @@ "app.actionsBar.raiseLabel": "ПоднÑÑ‚ÑŒ", "app.actionsBar.label": "Панель дейÑтвий", "app.submenu.application.applicationSectionTitle": "Приложение", - "app.submenu.application.audioNotifyLabel": "Звуковые ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ñ‡Ð°Ñ‚Ð°", - "app.submenu.application.pushNotifyLabel": "Push-ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ñ‡Ð°Ñ‚Ð°", + "app.submenu.application.audioAlertLabel": "Звуковые Ð¾Ð¿Ð¾Ð²ÐµÑ‰ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ñ‡Ð°Ñ‚Ð°", + "app.submenu.application.pushAlertLabel": "Ð’Ñплывающие Ð¾Ð¿Ð¾Ð²ÐµÑ‰ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ñ‡Ð°Ñ‚Ð°", "app.submenu.application.fontSizeControlLabel": "Размер шрифта", "app.submenu.application.increaseFontBtnLabel": "Увеличить шрифт приложениÑ", "app.submenu.application.decreaseFontBtnLabel": "Уменьшить шрифт приложениÑ", @@ -234,12 +238,14 @@ "app.audioNotification.audioFailedMessage": "Ðе удалоÑÑŒ уÑтановить аудио-Ñоединение", "app.audioNotification.mediaFailedMessage": "Ошибка getUserMicMedia, разрешены только безопаÑные иÑточники", "app.audioNotification.closeLabel": "Закрыть", + "app.audioNotificaion.reconnectingAsListenOnly": "Ðудио было заблокировано модератором, вы приÑоединилиÑÑŒ в режиме проÑлушиваниÑ", "app.breakoutJoinConfirmation.title": "ПриÑоединитьÑÑ Ðº комнате групповой работы", "app.breakoutJoinConfirmation.message": "Ð’Ñ‹ хотите приÑоединитьÑÑ Ðº ", "app.breakoutJoinConfirmation.confirmLabel": "ПриÑоединитьÑÑ", "app.breakoutJoinConfirmation.confirmDesc": "ПриÑоединÑет Ð²Ð°Ñ Ðº конференции", "app.breakoutJoinConfirmation.dismissLabel": "Отмена", "app.breakoutJoinConfirmation.dismissDesc": "Закрывает и отклонÑет приÑоединение к конференции", + "app.breakoutJoinConfirmation.freeJoinMessage": "Выберите конференцию, к которой хотите подключитьÑÑ", "app.breakoutTimeRemainingMessage": "ОÑтавшееÑÑ Ð²Ñ€ÐµÐ¼Ñ ÐºÐ¾Ð½Ñ„ÐµÑ€ÐµÐ½Ñ†Ð¸Ð¸: {0}", "app.breakoutWillCloseMessage": "Ð’Ñ€ÐµÐ¼Ñ Ð²Ñ‹ÑˆÐ»Ð¾. Комната групповой работы Ñкоро закроетÑÑ.", "app.calculatingBreakoutTimeRemaining": "ПодÑчёт оÑтавшегоÑÑ Ð²Ñ€ÐµÐ¼ÐµÐ½Ð¸...", @@ -316,6 +322,7 @@ "app.video.joinVideo": "ТранÑлировать веб-камеру", "app.video.leaveVideo": "Ðе транÑлировать веб-камеру", "app.video.iceCandidateError": "Щшибка Ð´Ð¾Ð±Ð°Ð²Ð»ÐµÐ½Ð¸Ñ ice кандидата", + "app.video.iceConnectionStateError": "Ошибка 1107: ÑоглаÑование ICE провалилоÑÑŒ", "app.video.permissionError": "Ошибка. Проверьте разрешение на доÑтуп к веб-камере.", "app.video.sharingError": "Ошибка транÑлÑции веб-камеры", "app.video.notFoundError": "Ðевозможно найти веб-камеру. ПожалуйÑта, проверте приÑоединена ли она", @@ -342,6 +349,15 @@ "app.video.stats.rtt": "RTT", "app.video.stats.encodeUsagePercent": "ИÑпользование кодировкой", "app.video.stats.currentDelay": "Ð¢ÐµÐºÑƒÑ‰Ð°Ñ Ð·Ð°Ð´ÐµÑ€Ð¶ÐºÐ°", + "app.deskshare.iceConnectionStateError": "Ошибка 1108: Ñоединение Ñ ICE не удалоÑÑŒ при ÑовмеÑтном иÑпользовании Ñкрана", + "app.sfu.mediaServerConnectionError2000": "Ошибка 2000: невозможно подключитьÑÑ Ðº медиа-Ñерверу", + "app.sfu.mediaServerOffline2001": "Ошибка 2001: Медиа-Ñервер отключен. ПожалуйÑта, повторите попытку позже.", + "app.sfu.mediaServerNoResources2002": "Ошибка 2002: у медиаÑервера нет доÑтупных реÑурÑов", + "app.sfu.mediaServerRequestTimeout2003": "Ошибка 2003: Ð²Ñ€ÐµÐ¼Ñ Ð·Ð°Ð¿Ñ€Ð¾Ñа на медиа Ñервер вышло", + "app.sfu.serverIceGatheringFailed2021": "Ошибка 2021: Медиа-Ñервер не может Ñобирать кандидатов ICE", + "app.sfu.serverIceGatheringFailed2022": "Ошибка 2022: Медиа Ñерверу не удалоÑÑŒ подключитьÑÑ Ðº ICE", + "app.sfu.invalidSdp2202":"Ошибка 2202: клиент Ñоздал недопуÑтимый SDP", + "app.sfu.noAvailableCodec2203": "Ошибка 2203: Ñервер не Ñмог найти ÑоответÑтвующий кодек", "app.meeting.endNotification.ok.label": "ОК", "app.whiteboard.toolbar.tools": "ИнÑтрументы", diff --git a/bigbluebutton-html5/private/locales/tr_TR.json b/bigbluebutton-html5/private/locales/tr_TR.json index 22b7b63ab73d68512b044fa655e5e83f3b728b57..42bd9a8ba97ecd2812193316ca898cdf7386d0fc 100644 --- a/bigbluebutton-html5/private/locales/tr_TR.json +++ b/bigbluebutton-html5/private/locales/tr_TR.json @@ -121,8 +121,6 @@ "app.actionsBar.raiseLabel": "El Kaldır", "app.actionsBar.label": "Eylem ÇubuÄŸu", "app.submenu.application.applicationSectionTitle": "Uygulama", - "app.submenu.application.audioNotifyLabel": "Sohbet sesli bildirimleri", - "app.submenu.application.pushNotifyLabel": "Sohbet anlık bildirimleri", "app.submenu.application.fontSizeControlLabel": "Yazı büyüklüğü", "app.submenu.application.increaseFontBtnLabel": "Uygulama Yazı Büyüklüğünü Artır", "app.submenu.application.decreaseFontBtnLabel": "Uygulama Yazı Büyüklüğünü Azalt", @@ -195,7 +193,6 @@ "app.actionsBar.actionsDropdown.stopDesktopShareDesc": "Ekran geniÅŸliÄŸi paylaşımını sonlandır", "app.actionsBar.actionsDropdown.startRecording": "Kaydı baÅŸlat", "app.actionsBar.actionsDropdown.stopRecording": "Kaydı durdur", - "app.actionsBar.emojiMenu.statusTriggerLabel": "Durum Belirt", "app.actionsBar.emojiMenu.awayLabel": "Dışarıda", "app.actionsBar.emojiMenu.awayDesc": "Durumunu dışarıda yap", "app.actionsBar.emojiMenu.raiseHandLabel": "El Kaldır", diff --git a/bigbluebutton-html5/private/locales/uk_UA.json b/bigbluebutton-html5/private/locales/uk_UA.json index 51cafc940f77d74d0ab57a5ae42c898fb3badd14..4f2c8c65b750d41ba1e3f3d0b3857bf11c5d7dd6 100644 --- a/bigbluebutton-html5/private/locales/uk_UA.json +++ b/bigbluebutton-html5/private/locales/uk_UA.json @@ -37,6 +37,8 @@ "app.userList.menu.muteUserAudio.label": "Вимкнути мікрофон кориÑтувача", "app.userList.menu.unmuteUserAudio.label": "Увімкнути мікрофон кориÑтувача", "app.userList.userAriaLabel": "{0} {1} {2} Ð¡Ñ‚Ð°Ñ‚ÑƒÑ {3}", + "app.userList.menu.promoteUser.label": "Підвищити до модератора", + "app.userList.menu.demoteUser.label": "Понизити до глÑдача", "app.media.label": "МедіÑ", "app.media.screenshare.start": "ДемонÑÑ‚Ñ€Ð°Ñ†Ñ–Ñ ÐµÐºÑ€Ð°Ð½Ñƒ розпочалаÑÑ", "app.media.screenshare.end": "ДемонÑтрацію екрану закінчено", @@ -95,6 +97,8 @@ "app.navBar.settingsDropdown.exitFullscreenDesc": "Вийти з повноекранного режиму", "app.navBar.settingsDropdown.hotkeysLabel": "Швидкі клавіши", "app.navBar.settingsDropdown.hotkeysDesc": "Перелік наÑвних швидких клавіш", + "app.navBar.settingsDropdown.helpLabel": "Допомога", + "app.navBar.settingsDropdown.helpDesc": "ПоÑилає кориÑтувача на відеоролики", "app.navBar.userListToggleBtnLabel": "Увімкнути/вимкнути ÑпиÑок кориÑтувачів", "app.navBar.toggleUserList.ariaLabel": "Увімкнути/вимкнути КориÑтувачів та ПовідомленнÑ", "app.navBar.toggleUserList.newMessages": "зі ÑповіщеннÑм про нове повідомленнÑ", @@ -124,8 +128,8 @@ "app.actionsBar.raiseLabel": "ПіднÑти", "app.actionsBar.label": "Панель дій", "app.submenu.application.applicationSectionTitle": "ЗаÑтоÑунок", - "app.submenu.application.audioNotifyLabel": "Ðудіо ÑÐ¿Ð¾Ð²Ñ–Ñ‰ÐµÐ½Ð½Ñ Ñ‡Ð°Ñ‚Ñƒ", - "app.submenu.application.pushNotifyLabel": "Push-ÑÐ¿Ð¾Ð²Ñ–Ñ‰ÐµÐ½Ð½Ñ Ð´Ð»Ñ Ñ‡Ð°Ñ‚Ñƒ", + "app.submenu.application.audioAlertLabel": "Ðудіо ÑÐ¿Ð¾Ð²Ñ–Ñ‰ÐµÐ½Ð½Ñ Ð´Ð»Ñ Ñ‡Ð°Ñ‚Ñƒ", + "app.submenu.application.pushAlertLabel": "Спливаючі ÑÐ¿Ð¾Ð²Ñ–Ñ‰ÐµÐ½Ð½Ñ Ð´Ð»Ñ Ñ‡Ð°Ñ‚Ñƒ", "app.submenu.application.fontSizeControlLabel": "Розмір шрифту", "app.submenu.application.increaseFontBtnLabel": "Збільшити шрифт заÑтоÑунку", "app.submenu.application.decreaseFontBtnLabel": "Зменшити шрифт заÑтоÑунку", @@ -234,12 +238,14 @@ "app.audioNotification.audioFailedMessage": "Ðе вдалоÑÑ Ð²Ñтановити аудіо з'єднаннÑ", "app.audioNotification.mediaFailedMessage": "Помилка getUserMicMedia, дозволені тільки безпечні джерела", "app.audioNotification.closeLabel": "Закрити", + "app.audioNotificaion.reconnectingAsListenOnly": "Ðудіо було заблоковано модератором, ви підключилиÑÑ Ð»Ð¸ÑˆÐµ Ñк Ñлухач", "app.breakoutJoinConfirmation.title": "ПриєднатиÑÑŒ до зуÑтрічі", "app.breakoutJoinConfirmation.message": "Чи хочете ви приєднатиÑÑ Ð´Ð¾", "app.breakoutJoinConfirmation.confirmLabel": "ПриєднатиÑÑ", "app.breakoutJoinConfirmation.confirmDesc": "Приєднує Ð²Ð°Ñ Ð´Ð¾ зуÑтрічі", "app.breakoutJoinConfirmation.dismissLabel": "СкаÑувати", "app.breakoutJoinConfirmation.dismissDesc": "Закриває та відмовлÑÑ” в приєднанні до зуÑтрічі", + "app.breakoutJoinConfirmation.freeJoinMessage": "Виберіть конференцію до Ñкої бажаєте під’єднатиÑÑ", "app.breakoutTimeRemainingMessage": "Ð§Ð°Ñ Ð´Ð¾ Ð·Ð°ÐºÑ–Ð½Ñ‡ÐµÐ½Ð½Ñ ÐºÐ¾Ð½Ñ„ÐµÑ€ÐµÐ½Ñ†Ñ–Ñ—: {0}", "app.breakoutWillCloseMessage": "Ð§Ð°Ñ Ð²Ð¸Ñ‡ÐµÑ€Ð¿Ð°Ð½Ð¾. Конференцію невдовзі буде закрито", "app.calculatingBreakoutTimeRemaining": "Підрахунок чаÑу що залишивÑÑ...", @@ -316,6 +322,7 @@ "app.video.joinVideo": "ТранÑлювати веб-камеру", "app.video.leaveVideo": "Припинити транÑлювати веб-камеру", "app.video.iceCandidateError": "Помилка при додаванні ice кандидата", + "app.video.iceConnectionStateError": "Помилка 1107: ÑƒÐ·Ð³Ð¾Ð´Ð¶ÐµÐ½Ð½Ñ ICE не вдалоÑÑ", "app.video.permissionError": "Помилка при транÑлÑції веб-камерию Будь лаÑка перевірте дозволи", "app.video.sharingError": "Помилка при транÑлÑції веб-камери", "app.video.notFoundError": "Ðе вдалоÑÑ Ð·Ð½Ð°Ð¹Ñ‚Ð¸ веб-камеру. ПереконайтеÑÑ, що вона під'єднана", @@ -342,6 +349,15 @@ "app.video.stats.rtt": "RTT", "app.video.stats.encodeUsagePercent": "ВикориÑÑ‚Ð°Ð½Ð½Ñ ÐºÐ¾Ð´ÑƒÐ²Ð°Ð½Ð½Ñм", "app.video.stats.currentDelay": "Поточна затримка", + "app.deskshare.iceConnectionStateError": "Помилка 1108: при з'єднанні екрана не вдалоÑÑ Ð¿Ñ–Ð´ÐºÐ»ÑŽÑ‡Ð¸Ñ‚Ð¸ ICE", + "app.sfu.mediaServerConnectionError2000": "Помилка 2000: неможливо підключитиÑÑ Ð´Ð¾ медіаÑервера", + "app.sfu.mediaServerOffline2001": "Помилка 2001: медіаÑервер недоÑтупний. Будь-лаÑка Ñпробуйте пізніше.", + "app.sfu.mediaServerNoResources2002": "Помилка 2002: Ñервер медіа не має доÑтупних реÑурÑів", + "app.sfu.mediaServerRequestTimeout2003": "Помилка 2003: вийшов Ñ‡Ð°Ñ Ð·Ð°Ð¿Ð¸Ñ‚Ñƒ на медіа Ñервер", + "app.sfu.serverIceGatheringFailed2021": "Помилка 2021: медіа Ñервер не може збирати ICE-кандидатів", + "app.sfu.serverIceGatheringFailed2022": "Помилка 2022: Медіа-Ñерверу не вдалоÑÑ ICE Ð·â€™Ñ”Ð´Ð½Ð°Ð½Ð½Ñ ", + "app.sfu.invalidSdp2202":"Помилка 2202: Клієнт Ñтворив невірний SDP", + "app.sfu.noAvailableCodec2203": "Помилка 2203: Ñервер не зміг знайти відповідного кодека", "app.meeting.endNotification.ok.label": "OK", "app.whiteboard.toolbar.tools": "ІнÑтрументи", diff --git a/bigbluebutton-html5/private/locales/zh_CN.json b/bigbluebutton-html5/private/locales/zh_CN.json index fe09c30e61f26995a3deb3e43786653e15a8f46d..339bc296fa9f15ff7da06c6c17d2d4d9c8950194 100644 --- a/bigbluebutton-html5/private/locales/zh_CN.json +++ b/bigbluebutton-html5/private/locales/zh_CN.json @@ -124,8 +124,6 @@ "app.actionsBar.raiseLabel": "举手", "app.actionsBar.label": "æ“作æ ", "app.submenu.application.applicationSectionTitle": "应用", - "app.submenu.application.audioNotifyLabel": "èŠå¤©å£°éŸ³æ醒", - "app.submenu.application.pushNotifyLabel": "èŠå¤©æŽ¨é€æ醒", "app.submenu.application.fontSizeControlLabel": "å—å·", "app.submenu.application.increaseFontBtnLabel": "增大界é¢å—å·", "app.submenu.application.decreaseFontBtnLabel": "å‡å°ç•Œé¢å—å·", @@ -198,7 +196,6 @@ "app.actionsBar.actionsDropdown.stopDesktopShareDesc": "åœæ¢åˆ†äº«æ‚¨çš„æ¡Œé¢æ¼”示给", "app.actionsBar.actionsDropdown.startRecording": "开始录制", "app.actionsBar.actionsDropdown.stopRecording": "åœæ¢å½•åˆ¶", - "app.actionsBar.emojiMenu.statusTriggerLabel": "设置状æ€", "app.actionsBar.emojiMenu.awayLabel": "离开", "app.actionsBar.emojiMenu.awayDesc": "更改您的状æ€ä¸ºç¦»å¼€", "app.actionsBar.emojiMenu.raiseHandLabel": "举手", diff --git a/bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.eot b/bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.eot index 002765657c7b16bb5cbb69a1dc02c1236cd98ede..35752e9e8f20fe49a49d45810f179d1df2ecc403 100644 Binary files a/bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.eot and b/bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.eot differ diff --git a/bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.svg b/bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.svg index 7ad7ba23d8f2dd5070ed6dd3ea3143246346af26..6d628ddf266de26a6bf1b8ee0d7b18143b8ae92c 100644 --- a/bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.svg +++ b/bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.svg @@ -5,7 +5,7 @@ --> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"> <metadata> -Created by FontForge 20170731 at Thu Jul 05 16:25:35 2018 +Created by FontForge 20170731 at Fri Aug 17 08:12:26 2018 By BigBlueButton Inc. Copyright (c) 2016-2017, BigBlueButton Inc. </metadata> @@ -22,7 +22,7 @@ Copyright (c) 2016-2017, BigBlueButton Inc. bbox="-3 -208 1191 826" underline-thickness="51" underline-position="-102" - unicode-range="U+0020-E952" + unicode-range="U+0020-E954" /> <missing-glyph horiz-adv-x="1048" /> @@ -170,8 +170,8 @@ d="M870 385c31 0 59 -24 59 -61v-450c0 -34 -28 -59 -62 -59h-713c-31 0 -62 25 -62 d="M251 645h722c22 0 34 -18 34 -36s-12 -36 -34 -36h-722c-22 0 -34 18 -34 36s12 36 34 36zM251 135h231c22 0 34 -18 34 -36s-12 -36 -34 -36h-231c-22 0 -34 18 -34 36s12 36 34 36zM251 385h484c22 0 34 -18 34 -36s-12 -36 -34 -36h-484c-22 0 -34 18 -34 36 s12 36 34 36zM12 609c0 40 30 72 68 72s69 -32 69 -72s-31 -72 -69 -72s-68 32 -68 72zM15 349c0 38 29 69 65 69s65 -31 65 -69s-29 -68 -65 -68s-65 30 -65 68zM12 97c0 40 30 72 68 72s69 -32 69 -72s-31 -72 -69 -72s-68 32 -68 72z" /> <glyph glyph-name="desktop" unicode="" -d="M925 751c48 0 85 -38 85 -82v-598c0 -44 -38 -81 -82 -81h-269v-72h89c14 0 27 -13 27 -27s-10 -28 -27 -28h-468c-17 0 -27 14 -27 28s10 27 27 27h89v72h-270c-44 0 -82 37 -82 81v598c0 44 38 82 82 82h826zM601 -79v72h-177v-72h177zM952 75v594c0 14 -10 24 -24 24 -h-829c-14 0 -24 -10 -24 -24v-594c0 -14 10 -24 24 -24h826c14 0 27 10 27 24z" /> +d="M855 674c36 -1 66 -31 67 -67v-491c0 -37 -30 -67 -67 -68h-223v-62h73c13 0 23 -11 23 -24s-10 -23 -23 -23h-386c-13 0 -23 10 -23 23s10 24 23 24h75v60h-223c-38 0 -69 30 -69 68v493c1 37 32 67 69 67h684zM585 -12v60h-146v-60h146zM876 607c0 11 -9 19 -19 19 +h-686c-11 0 -19 -8 -19 -19v-493c0 -10 8 -19 19 -19h684c11 0 19 9 19 19z" /> <glyph glyph-name="fit_to_screen" unicode="" d="M594 -34c5 5 11 8 18 8s14 -3 19 -8c8 -8 13 -18 13 -27c0 -6 -2 -12 -6 -18l-102 -102c-5 -7 -12 -11 -20 -11s-17 4 -24 11l-106 109c-5 5 -7 12 -7 18c0 10 5 20 14 26c6 4 12 6 18 6c9 0 18 -4 26 -12l48 -48v246c0 17 13 27 30 27s31 -10 31 -27v-246zM631 645 c-5 -5 -12 -7 -19 -7s-13 2 -18 7l-48 48v-246c0 -17 -14 -27 -31 -27s-30 10 -30 27v246l-48 -48c-8 -8 -17 -12 -27 -12c-6 0 -12 1 -17 5c-9 6 -14 17 -14 27c0 6 2 12 7 17l106 110c7 7 15 10 22 10c8 0 16 -4 22 -10l102 -103c4 -6 6 -11 6 -17c0 -9 -5 -19 -13 -27z @@ -332,5 +332,11 @@ c0 -3 0 -7 -1 -10l-46 -269l242 126c8 3 18 5 28 5s20 -2 28 -6z" /> <glyph glyph-name="star_filled" unicode="" d="M243 -167c-33 1 -60 28 -60 61c0 2 1 7 1 10l46 266l-196 192c-10 9 -18 29 -18 42c0 29 24 56 52 60l269 39l121 244c9 19 33 34 54 34s45 -15 54 -34l120 -243l270 -40c28 -4 52 -31 52 -60c0 -13 -8 -33 -18 -42l-196 -192l46 -268c0 -3 1 -7 1 -10 c0 -17 -11 -39 -25 -49c-9 -6 -24 -11 -35 -11c-8 0 -21 3 -28 7l-241 127l-241 -127c-7 -3 -19 -6 -27 -6h-1z" /> + <glyph glyph-name="desktop_off" unicode="" +d="M928 707c4 -4 7 -12 7 -18s-4 -14 -8 -18l-29 -28c13 -12 23 -34 24 -51v-494c-1 -35 -31 -65 -67 -66h-223v-59h76c13 0 24 -11 24 -24s-11 -24 -24 -24h-392c-13 0 -24 11 -24 24s11 24 24 24h78v60h-128l-131 -126c-4 -4 -12 -8 -18 -8s-14 4 -18 8s-8 12 -8 18 +s4 14 8 18l92 89h-20c-37 0 -68 30 -69 66v494c2 36 32 66 69 66h669l52 49c4 4 12 8 18 8s14 -4 18 -8zM171 79h68l552 532h-620c-11 0 -19 -9 -19 -19v-494c0 -10 8 -19 19 -19zM586 -28l-1 60h-146v-60h147zM874 592c0 7 -5 14 -11 17l-551 -530h543c11 0 19 9 19 19v494 +z" /> + <glyph glyph-name="minus" unicode="" +d="M934 352c24 0 45 -20 45 -45s-21 -45 -45 -45h-844c-24 0 -45 21 -45 45c0 25 21 45 45 45h844z" /> </font> </defs></svg> diff --git a/bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.ttf b/bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.ttf index c0827970529f084e58b717cbf26ea05e1b2145ed..e2031f5e610994d92bb40aa985f5c74f16075d2c 100644 Binary files a/bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.ttf and b/bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.ttf differ diff --git a/bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.woff b/bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.woff index 81fc4c7518ff291432a360ac1e99d96206db8c84..fb192d41380e628dd91859e833e98af03c601e7a 100644 Binary files a/bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.woff and b/bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.woff differ diff --git a/bigbluebutton-html5/server/main.js b/bigbluebutton-html5/server/main.js index bc24ed1416f82ea118a8f81d3239e7f5da48f3b1..ce9e6c36feb75abb8ed6b7b398343463be606909 100755 --- a/bigbluebutton-html5/server/main.js +++ b/bigbluebutton-html5/server/main.js @@ -11,8 +11,8 @@ import '/imports/api/presentations/server'; import '/imports/api/presentation-pods/server'; import '/imports/api/slides/server'; import '/imports/api/breakouts/server'; -import '/imports/api/chat/server'; import '/imports/api/group-chat/server'; +import '/imports/api/group-chat-msg/server'; import '/imports/api/screenshare/server'; import '/imports/api/voice-users/server'; import '/imports/api/whiteboard-multi-user/server'; diff --git a/bigbluebutton-web/.dockerignore b/bigbluebutton-web/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..94143827ed065ca0d7d5be1b765d255c5c32cd9a --- /dev/null +++ b/bigbluebutton-web/.dockerignore @@ -0,0 +1 @@ +Dockerfile diff --git a/bigbluebutton-web/Dockerfile b/bigbluebutton-web/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..a980f59723ee51b198144ccc7e16ca46f36322f3 --- /dev/null +++ b/bigbluebutton-web/Dockerfile @@ -0,0 +1,64 @@ +FROM bbb-common-web AS builder + +RUN mkdir -p /root/tools \ + && cd /root/tools \ + && wget http://services.gradle.org/distributions/gradle-2.12-bin.zip \ + && unzip gradle-2.12-bin.zip \ + && ln -s gradle-2.12 gradle + +RUN mkdir -p /root/tools \ + && cd /root/tools \ + && wget https://github.com/grails/grails-core/releases/download/v2.5.2/grails-2.5.2.zip \ + && unzip grails-2.5.2.zip \ + && ln -s grails-2.5.2 grails + +ENV PATH="/root/tools/gradle/bin:/root/tools/grails/bin:${PATH}" + +ARG COMMON_VERSION=0.0.1-SNAPSHOT + +COPY . /source + +RUN cd /source \ + && find -name build.gradle -exec sed -i "s|\(.*org.bigbluebutton.*bbb-common-message[^:]*\):.*|\1:$COMMON_VERSION'|g" {} \; \ + && find -name build.gradle -exec sed -i "s|\(.*org.bigbluebutton.*bbb-common-web[^:]*\):.*|\1:$COMMON_VERSION'|g" {} \; + +RUN cd /source \ + && gradle resolveDeps \ + && grails war + +FROM tomcat:7-jre8 + +WORKDIR $CATALINA_HOME + +ENV DOCKERIZE_VERSION v0.6.1 +RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \ + && tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \ + && rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz + +ENV DEBIAN_FRONTEND noninteractive +RUN apt-get update \ + && apt-get -y install imagemagick xpdf-utils netcat libreoffice ttf-liberation fonts-crosextra-carlito fonts-crosextra-caladea unzip procps \ + && wget http://ftp.us.debian.org/debian/pool/main/libj/libjpeg8/libjpeg8_8d-1+deb7u1_amd64.deb \ + && dpkg -i libjpeg8*.deb \ + && rm libjpeg8*.deb + +RUN echo "deb http://ubuntu.bigbluebutton.org/xenial-200-dev bigbluebutton-xenial main " | tee /etc/apt/sources.list.d/bigbluebutton.list \ + && wget http://ubuntu.bigbluebutton.org/repo/bigbluebutton.asc -O- | apt-key add - \ + && apt-get update \ + && apt-get -y install bbb-swftools + +# clean default webapps +RUN rm -r webapps/* + +COPY --from=builder /source/target/bigbluebutton-0.9.0.war webapps/bigbluebutton.war + +RUN unzip -q webapps/bigbluebutton.war -d webapps/bigbluebutton \ + && rm webapps/bigbluebutton.war + +COPY turn-stun-servers.xml.tmpl turn-stun-servers.xml.tmpl + +COPY docker-entrypoint.sh /usr/local/bin/ + +CMD [ "dockerize", \ + "-template", "turn-stun-servers.xml.tmpl:webapps/bigbluebutton/WEB-INF/spring/turn-stun-servers.xml", \ + "docker-entrypoint.sh" ] diff --git a/bigbluebutton-web/docker-entrypoint.sh b/bigbluebutton-web/docker-entrypoint.sh new file mode 100755 index 0000000000000000000000000000000000000000..460ef6c5197aac1d08440c500359895ac7325d55 --- /dev/null +++ b/bigbluebutton-web/docker-entrypoint.sh @@ -0,0 +1,17 @@ +#!/bin/bash -xe + +mkdir -p /var/bigbluebutton/recording/raw +mkdir -p /var/bigbluebutton/recording/process +mkdir -p /var/bigbluebutton/recording/publish +mkdir -p /var/bigbluebutton/recording/status/recorded +mkdir -p /var/bigbluebutton/recording/status/archived +mkdir -p /var/bigbluebutton/recording/status/processed +mkdir -p /var/bigbluebutton/recording/status/sanity +mkdir -p /var/bigbluebutton/published +mkdir -p /var/bigbluebutton/deleted +mkdir -p /var/bigbluebutton/unpublished + +export JAVA_OPTS="${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -DsecuritySalt=${SHARED_SECRET} -Dredis.host=redis -DredisHost=redis -Dbigbluebutton.web.serverURL=https://${SERVER_DOMAIN} -DattendeesJoinViaHTML5Client=true -DmoderatorsJoinViaHTML5Client=true -DsvgImagesRequired=true" +sed -i "s|^securerandom\.source=.*|securerandom.source=file:/dev/urandom|g" ${JAVA_HOME}/lib/security/java.security + +catalina.sh run diff --git a/bigbluebutton-web/grails-app/conf/Config.groovy b/bigbluebutton-web/grails-app/conf/Config.groovy index cd9d36a9d343d06031c92165dfa34f6729eeaa8a..230e3f8feb790666eb17745f327a5065cce133a6 100755 --- a/bigbluebutton-web/grails-app/conf/Config.groovy +++ b/bigbluebutton-web/grails-app/conf/Config.groovy @@ -130,5 +130,7 @@ log4j = { 'org.hibernate', 'net.sf.ehcache.hibernate' - debug 'org.bigbluebutton' + debug 'org.bigbluebutton', + 'grails.app.controllers', + 'grails.app.services' } diff --git a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties index 133d6f40a6c0b37e16990bed76c1b7b386dff489..6f0565c3c8556d3b1dd6a05764c0afff2fd5f58e 100755 --- a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties +++ b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties @@ -98,6 +98,10 @@ pngSlideWidth=1200 # Default number of digits for voice conference users joining through the PSTN. defaultNumDigitsForTelVoice=5 +#---------------------------------------------------- +# Configuration for large images, 2 MB by default, if bigger it will downscaled +maxImageSize=2000000 + #---------------------------------------------------- # Default dial access number defaultDialAccessNumber=613-555-1234 diff --git a/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml b/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml index d649f48bc10482f7bf567360e917f9c990eb3c79..28d142e5cbc8aa92952165ad7b287d7067a917bb 100755 --- a/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml +++ b/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml @@ -52,6 +52,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <bean id="pageCounter" class="org.bigbluebutton.presentation.imp.PdfPageCounter"/> + <bean id="imageResizer" class="org.bigbluebutton.presentation.imp.ImageResizerImp"/> + <bean id="pageCounterService" class="org.bigbluebutton.presentation.imp.PageCounterService"> <property name="pageCounter" ref="pageCounter"/> <property name="maxNumPages" value="${maxNumPages}"/> @@ -110,6 +112,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <property name="blankSlide" value="${BLANK_SLIDE}"/> <property name="maxConversionTime" value="${maxConversionTime}"/> <property name="swfSlidesGenerationProgressNotifier" ref="swfSlidesGenerationProgressNotifier"/> + <property name="imageResizer" ref="imageResizer"/> + <property name="maxImageSize" value="${maxImageSize}"/> </bean> <bean id="swfSlidesGenerationProgressNotifier" diff --git a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy index 2e423db06c9b1c02ac0766fd9943c7e8cd9573f4..35c30a062cc68467369290699e3c11b30196284f 100755 --- a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy +++ b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy @@ -66,8 +66,8 @@ class ApiController { private static final String CONTROLLER_NAME = 'ApiController' private static final String RESP_CODE_SUCCESS = 'SUCCESS' private static final String RESP_CODE_FAILED = 'FAILED' - private static final String SECURITY_SALT = '639259d4-9dd8-4b25-bf01-95f9567eaf4b' - private static final String API_VERSION = '0.81' + private static final String ROLE_MODERATOR = "MODERATOR"; + private static final String ROLE_ATTENDEE = "VIEWER"; private static final String REDIRECT_RESPONSE = true MeetingService meetingService; @@ -77,8 +77,6 @@ class ApiController { PresentationUrlDownloadService presDownloadService StunTurnService stunTurnService - - /* general methods */ def index = { log.debug CONTROLLER_NAME + "#index" @@ -95,24 +93,6 @@ class ApiController { } } - /*********************************** - * BREAKOUT TEST (API) - ***********************************/ - def breakout = { - if(!StringUtils.isEmpty(params.meetingId)) { - String meetingId = StringUtils.strip(params.meetingId); - println("MeetingId = " + meetingId) - } else { - println("Missing meetingId") - return - } - - if (StringUtils.isEmpty(params.password)) { - println("Missing password") - return - } - } - /*********************************** * CREATE (API) ***********************************/ @@ -287,8 +267,7 @@ class ApiController { if (StringUtils.isEmpty(params.meetingID)) { errors.missingParamError("meetingID"); } - } - else { + } else { errors.missingParamError("meetingID"); } String externalMeetingId = params.meetingID @@ -331,7 +310,7 @@ class ApiController { if (params.createTime != null) { long createTime = 0; try{ - createTime=Long.parseLong(params.createTime); + createTime = Long.parseLong(params.createTime); } catch(Exception e){ log.warn("could not parse createTime param"); createTime = -1; @@ -379,16 +358,12 @@ class ApiController { return; } - String webVoice = StringUtils.isEmpty(params.webVoiceConf) ? meeting.getTelVoice() : params.webVoiceConf - - boolean redirectImm = parseBoolean(params.redirectImmediately) - // We preprend "w_" to our internal meeting Id to indicate that this is a web user. // For users joining using the phone, we will prepend "v_" so it will be easier // to distinguish users who doesn't have a web client. (ralam june 12, 2017) String internalUserID = "w_" + RandomStringUtils.randomAlphanumeric(12).toLowerCase() - String authToken = RandomStringUtils.randomAlphanumeric(12).toLowerCase() + String authToken = RandomStringUtils.randomAlphanumeric(12).toLowerCase() String sessionToken = RandomStringUtils.randomAlphanumeric(16).toLowerCase() @@ -425,28 +400,10 @@ class ApiController { } } else { Config conf = meeting.getDefaultConfig(); - if (conf == null) { - // BEGIN - backward compatibility - invalid("noConfigFound","We could not find a config for this request.", REDIRECT_RESPONSE); - return - // END - backward compatibility - - errors.noConfigFound(); - respondWithErrors(errors); - } else { - configxml = conf.config; - } + configxml = conf.config; } - if (StringUtils.isEmpty(configxml)) { - // BEGIN - backward compatibility - invalid("noConfigFound","We could not find a config for this request.", REDIRECT_RESPONSE); - return - // END - backward compatibility - - errors.noConfigFound(); - respondWithErrors(errors); - } + // Do not fail if there's no default config.xml, needed for an HTML5 client only scenario String guestStatusVal = meeting.calcGuestStatus(role, guest, authenticated) @@ -493,14 +450,14 @@ class ApiController { // when maxUsers is set to 0, the validation is ignored int maxUsers = meeting.getMaxUsers(); if (maxUsers > 0 && meeting.getRegisteredUsers().size() >= maxUsers) { - // BEGIN - backward compatibility - invalid("maxParticipantsReached","The number of participants allowed for this meeting has been reached.", REDIRECT_RESPONSE); - return - // END - backward compatibility + // BEGIN - backward compatibility + invalid("maxParticipantsReached", "The number of participants allowed for this meeting has been reached.", REDIRECT_RESPONSE); + return + // END - backward compatibility - errors.maxParticipantsReached(); - respondWithErrors(errors, REDIRECT_RESPONSE); - return; + errors.maxParticipantsReached(); + respondWithErrors(errors, REDIRECT_RESPONSE); + return; } //Identify which of these to logs should be used. sessionToken or user-token @@ -1212,37 +1169,46 @@ class ApiController { } } - def getDefaultConfigXML = { + def getDefaultConfigXML = { - String API_CALL = "getDefaultConfigXML" - ApiErrors errors = new ApiErrors(); + String API_CALL = "getDefaultConfigXML" + ApiErrors errors = new ApiErrors(); - // BEGIN - backward compatibility - if (StringUtils.isEmpty(params.checksum)) { - invalid("checksumError", "You did not pass the checksum security check") - return - } + // BEGIN - backward compatibility + if (StringUtils.isEmpty(params.checksum)) { + invalid("checksumError", "You did not pass the checksum security check") + return + } - if (!paramsProcessorUtil.isChecksumSame(API_CALL, params.checksum, request.getQueryString())) { - invalid("checksumError", "You did not pass the checksum security check") - return - } - // END - backward compatibility + if (!paramsProcessorUtil.isChecksumSame(API_CALL, params.checksum, request.getQueryString())) { + invalid("checksumError", "You did not pass the checksum security check") + return + } + // END - backward compatibility - // Do we agree on the checksum? If not, complain. - if (!paramsProcessorUtil.isChecksumSame(API_CALL, params.checksum, request.getQueryString())) { - errors.checksumError() - respondWithErrors(errors) - return - } + // Do we agree on the checksum? If not, complain. + if (!paramsProcessorUtil.isChecksumSame(API_CALL, params.checksum, request.getQueryString())) { + errors.checksumError() + respondWithErrors(errors) + return + } - String defConfigXML = paramsProcessorUtil.getDefaultConfigXML(); + String defConfigXML = paramsProcessorUtil.getDefaultConfigXML(); + if (StringUtils.isEmpty(defConfigXML)) { + // BEGIN - backward compatibility + invalid("noConfigFound","We could not find a config for this request.", REDIRECT_RESPONSE); + return + // END - backward compatibility - response.addHeader("Cache-Control", "no-cache") - render text: defConfigXML, contentType: 'text/xml' + errors.noConfigFound(); + respondWithErrors(errors); } + response.addHeader("Cache-Control", "no-cache") + render text: defConfigXML, contentType: 'text/xml' + } + def configXML = { String API_CALL = 'configXML' log.debug CONTROLLER_NAME + "#${API_CALL}" @@ -1259,7 +1225,7 @@ class ApiController { sessionToken = StringUtils.strip(params.sessionToken) log.info("Getting ConfigXml for SessionToken = " + sessionToken) if (!session[sessionToken]) { - reject = true + reject = true } else { us = meetingService.getUserSessionWithAuthToken(sessionToken); if (us == null) reject = true @@ -1280,6 +1246,16 @@ class ApiController { } } } else { + if (StringUtils.isEmpty(us.configXML)) { + // BEGIN - backward compatibility + invalid("noConfigFound","We could not find a config for this request.", REDIRECT_RESPONSE); + return + // END - backward compatibility + + errors.noConfigFound(); + respondWithErrors(errors); + } + Map<String, Object> logData = new HashMap<String, Object>(); logData.put("meetingId", us.meetingID); logData.put("externalMeetingId", us.externMeetingID); @@ -1646,7 +1622,6 @@ class ApiController { } } - /************************************************* * SIGNOUT API *************************************************/ @@ -1705,7 +1680,7 @@ class ApiController { return } - log.debug request.getQueryString() + log.debug request.getQueryString() // Do we agree on the checksum? If not, complain. if (! paramsProcessorUtil.isChecksumSame(API_CALL, params.checksum, request.getQueryString())) { @@ -1716,7 +1691,7 @@ class ApiController { List<String> externalMeetingIds = new ArrayList<String>(); if (!StringUtils.isEmpty(params.meetingID)) { - externalMeetingIds=paramsProcessorUtil.decodeIds(params.meetingID); + externalMeetingIds = paramsProcessorUtil.decodeIds(params.meetingID); } ArrayList<String> internalRecordIds = new ArrayList<String>() @@ -1730,7 +1705,7 @@ class ApiController { } // Everything is good so far. - if ( internalRecordIds.size() == 0 && externalMeetingIds.size() > 0 ) { + if (internalRecordIds.size() == 0 && externalMeetingIds.size() > 0) { // No recordIDs, process the request based on meetingID(s) // Translate the external meeting ids to internal meeting ids (which is the seed for the recordIDs). internalRecordIds = paramsProcessorUtil.convertToInternalMeetingId(externalMeetingIds); @@ -1774,7 +1749,7 @@ class ApiController { return } - if (! paramsProcessorUtil.isChecksumSame(API_CALL, params.checksum, request.getQueryString())) { + if (!paramsProcessorUtil.isChecksumSame(API_CALL, params.checksum, request.getQueryString())) { invalid("checksumError", "You did not pass the checksum security check") return } @@ -1804,7 +1779,7 @@ class ApiController { } // Do we agree on the checksum? If not, complain. - if (! paramsProcessorUtil.isChecksumSame(API_CALL, params.checksum, request.getQueryString())) { + if (!paramsProcessorUtil.isChecksumSame(API_CALL, params.checksum, request.getQueryString())) { errors.checksumError() respondWithErrors(errors) return @@ -1812,7 +1787,7 @@ class ApiController { ArrayList<String> recordIdList = new ArrayList<String>(); if (!StringUtils.isEmpty(recordId)) { - recordIdList=paramsProcessorUtil.decodeIds(recordId); + recordIdList = paramsProcessorUtil.decodeIds(recordId); } if (!meetingService.existsAnyRecording(recordIdList)) { @@ -1854,7 +1829,7 @@ class ApiController { return } - if (! paramsProcessorUtil.isChecksumSame(API_CALL, params.checksum, request.getQueryString())) { + if (!paramsProcessorUtil.isChecksumSame(API_CALL, params.checksum, request.getQueryString())) { invalid("checksumError", "You did not pass the checksum security check") return } @@ -1879,7 +1854,7 @@ class ApiController { } // Do we agree on the checksum? If not, complain. - if (! paramsProcessorUtil.isChecksumSame(API_CALL, params.checksum, request.getQueryString())) { + if (!paramsProcessorUtil.isChecksumSame(API_CALL, params.checksum, request.getQueryString())) { errors.checksumError() respondWithErrors(errors) return @@ -1887,7 +1862,7 @@ class ApiController { List<String> recordIdList = new ArrayList<String>(); if (!StringUtils.isEmpty(recordId)) { - recordIdList=paramsProcessorUtil.decodeIds(recordId); + recordIdList = paramsProcessorUtil.decodeIds(recordId); } if (!meetingService.existsAnyRecording(recordIdList)) { @@ -1913,81 +1888,81 @@ class ApiController { /****************************************************** * UPDATE_RECORDINGS API ******************************************************/ - def updateRecordingsHandler = { - String API_CALL = "updateRecordings" - log.debug CONTROLLER_NAME + "#${API_CALL}" - - // BEGIN - backward compatibility - if (StringUtils.isEmpty(params.checksum)) { - invalid("checksumError", "You did not pass the checksum security check") - return - } - - if (StringUtils.isEmpty(params.recordID)) { - invalid("missingParamRecordID", "You must specify a recordID."); - return - } - - if (! paramsProcessorUtil.isChecksumSame(API_CALL, params.checksum, request.getQueryString())) { - invalid("checksumError", "You did not pass the checksum security check") - return - } - // END - backward compatibility - - ApiErrors errors = new ApiErrors() - - // Do we have a checksum? If none, complain. - if (StringUtils.isEmpty(params.checksum)) { - errors.missingParamError("checksum"); - } - - // Do we have a recording id? If none, complain. - String recordId = params.recordID - if (StringUtils.isEmpty(recordId)) { - errors.missingParamError("recordID"); - } - - if (errors.hasErrors()) { - respondWithErrors(errors) - return - } - - // Do we agree on the checksum? If not, complain. - if (! paramsProcessorUtil.isChecksumSame(API_CALL, params.checksum, request.getQueryString())) { - errors.checksumError() - respondWithErrors(errors) - return - } - - List<String> recordIdList = new ArrayList<String>(); - if (!StringUtils.isEmpty(recordId)) { - recordIdList=paramsProcessorUtil.decodeIds(recordId); - } - - if (!meetingService.existsAnyRecording(recordIdList)) { - // BEGIN - backward compatibility - invalid("notFound", "We could not find recordings"); - return; - // END - backward compatibility - } - - //Execute code specific for this call - Map<String, String> metaParams = ParamsProcessorUtil.processMetaParam(params) - if ( !metaParams.empty ) { - //Proceed with the update - meetingService.updateRecordings(recordIdList, metaParams); - } - withFormat { - xml { - render(contentType:"text/xml") { - response() { - returncode(RESP_CODE_SUCCESS) - updated(true) - } - } - } - } - } + def updateRecordingsHandler = { + String API_CALL = "updateRecordings" + log.debug CONTROLLER_NAME + "#${API_CALL}" + + // BEGIN - backward compatibility + if (StringUtils.isEmpty(params.checksum)) { + invalid("checksumError", "You did not pass the checksum security check") + return + } + + if (StringUtils.isEmpty(params.recordID)) { + invalid("missingParamRecordID", "You must specify a recordID."); + return + } + + if (!paramsProcessorUtil.isChecksumSame(API_CALL, params.checksum, request.getQueryString())) { + invalid("checksumError", "You did not pass the checksum security check") + return + } + // END - backward compatibility + + ApiErrors errors = new ApiErrors() + + // Do we have a checksum? If none, complain. + if (StringUtils.isEmpty(params.checksum)) { + errors.missingParamError("checksum"); + } + + // Do we have a recording id? If none, complain. + String recordId = params.recordID + if (StringUtils.isEmpty(recordId)) { + errors.missingParamError("recordID"); + } + + if (errors.hasErrors()) { + respondWithErrors(errors) + return + } + + // Do we agree on the checksum? If not, complain. + if (!paramsProcessorUtil.isChecksumSame(API_CALL, params.checksum, request.getQueryString())) { + errors.checksumError() + respondWithErrors(errors) + return + } + + List<String> recordIdList = new ArrayList<String>(); + if (!StringUtils.isEmpty(recordId)) { + recordIdList = paramsProcessorUtil.decodeIds(recordId); + } + + if (!meetingService.existsAnyRecording(recordIdList)) { + // BEGIN - backward compatibility + invalid("notFound", "We could not find recordings"); + return; + // END - backward compatibility + } + + //Execute code specific for this call + Map<String, String> metaParams = ParamsProcessorUtil.processMetaParam(params) + if (!metaParams.empty) { + //Proceed with the update + meetingService.updateRecordings(recordIdList, metaParams); + } + withFormat { + xml { + render(contentType: "text/xml") { + response() { + returncode(RESP_CODE_SUCCESS) + updated(true) + } + } + } + } + } def uploadDocuments(conf) { // log.debug("ApiController#uploadDocuments(${conf.getInternalId()})"); @@ -1997,7 +1972,7 @@ class ApiController { if (requestBody == null) { downloadAndProcessDocument(presentationService.defaultUploadedPresentation, conf.getInternalId(), - true /* default presentation */ ); + true /* default presentation */); } else { log.debug "Request body: \n" + requestBody; def xml = new XmlSlurper().parseText(requestBody); @@ -2013,7 +1988,7 @@ class ApiController { def b64 = new Base64() def decodedBytes = b64.decode(document.text().getBytes()) processDocumentFromRawBytes(decodedBytes, document.@name.toString(), - conf.getInternalId(), true /* default presentation */); + conf.getInternalId(), true /* default presentation */); } else { log.debug("presentation module config found, but it did not contain url or name attributes"); } @@ -2102,9 +2077,9 @@ class ApiController { meetingID() { mkp.yield(meeting.getExternalId()) } internalMeetingID(meeting.getInternalId()) if (meeting.isBreakout()) { - parentMeetingID() { mkp.yield(meeting.getParentMeetingId()) } - sequence() { mkp.yield(meeting.getSequence()) } - freeJoin() { mkp.yield(meeting.isFreeJoin()) } + parentMeetingID() { mkp.yield(meeting.getParentMeetingId()) } + sequence() { mkp.yield(meeting.getSequence()) } + freeJoin() { mkp.yield(meeting.isFreeJoin()) } } createTime(meeting.getCreateTime()) createDate(formatPrettyDate(meeting.getCreateTime())) @@ -2179,7 +2154,7 @@ class ApiController { moderatorPW() { mkp.yield(meeting.getModeratorPassword()) } createTime(meeting.getCreateTime()) voiceBridge() { mkp.yield(meeting.getTelVoice()) } - dialNumber() { mkp.yield(meeting.getDialNumber()) } + dialNumber() { mkp.yield(meeting.getDialNumber()) } createDate(formatPrettyDate(meeting.getCreateTime())) hasUserJoined(meeting.hasUserJoined()) duration(meeting.duration) @@ -2192,87 +2167,87 @@ class ApiController { } } - private void respondWithErrors(errorList, redirectResponse=false) { + private void respondWithErrors(errorList, redirectResponse = false) { log.debug CONTROLLER_NAME + "#invalid" if (redirectResponse) { - ArrayList<Object> errors = new ArrayList<Object>(); - errorList.getErrors().each { error -> - Map<String,String> errorMap = new LinkedHashMap<String,String>() - errorMap.put("key", error[0]) - errorMap.put("message", error[1]) - errors.add(errorMap) - } + ArrayList<Object> errors = new ArrayList<Object>(); + errorList.getErrors().each { error -> + Map<String, String> errorMap = new LinkedHashMap<String, String>() + errorMap.put("key", error[0]) + errorMap.put("message", error[1]) + errors.add(errorMap) + } - JSONArray errorsJSONArray = new JSONArray(errors); - log.debug errorsJSONArray + JSONArray errorsJSONArray = new JSONArray(errors); + log.debug errorsJSONArray - respondWithRedirect(errorsJSONArray) + respondWithRedirect(errorsJSONArray) } else { - response.addHeader("Cache-Control", "no-cache") - withFormat { - xml { - render(contentType:"text/xml") { - response() { - returncode(RESP_CODE_FAILED) - errors() { - ArrayList errs = errorList.getErrors(); - Iterator itr = errs.iterator(); - while (itr.hasNext()){ - String[] er = (String[]) itr.next(); - log.debug CONTROLLER_NAME + "#invalid" + er[0] - error(key: er[0], message: er[1]) - } + response.addHeader("Cache-Control", "no-cache") + withFormat { + xml { + render(contentType: "text/xml") { + response() { + returncode(RESP_CODE_FAILED) + errors() { + ArrayList errs = errorList.getErrors(); + Iterator itr = errs.iterator(); + while (itr.hasNext()) { + String[] er = (String[]) itr.next(); + log.debug CONTROLLER_NAME + "#invalid" + er[0] + error(key: er[0], message: er[1]) } } } } - json { - log.debug "Rendering as json" - render(contentType:"text/json") { - returncode(RESP_CODE_FAILED) - messageKey(key) - message(msg) - } + } + json { + log.debug "Rendering as json" + render(contentType: "text/json") { + returncode(RESP_CODE_FAILED) + messageKey(key) + message(msg) } } + } } } //TODO: method added for backward compatibility, it will be removed in next versions after 0.8 - private void invalid(key, msg, redirectResponse=false) { + private void invalid(key, msg, redirectResponse = false) { // Note: This xml scheme will be DEPRECATED. log.debug CONTROLLER_NAME + "#invalid " + msg if (redirectResponse) { - ArrayList<Object> errors = new ArrayList<Object>(); - Map<String,String> errorMap = new LinkedHashMap<String,String>() - errorMap.put("key", key) - errorMap.put("message", msg) - errors.add(errorMap) + ArrayList<Object> errors = new ArrayList<Object>(); + Map<String, String> errorMap = new LinkedHashMap<String, String>() + errorMap.put("key", key) + errorMap.put("message", msg) + errors.add(errorMap) - JSONArray errorsJSONArray = new JSONArray(errors); - log.debug errorsJSONArray + JSONArray errorsJSONArray = new JSONArray(errors); + log.debug errorsJSONArray - respondWithRedirect(errorsJSONArray) + respondWithRedirect(errorsJSONArray) } else { - response.addHeader("Cache-Control", "no-cache") - withFormat { - xml { - render(contentType:"text/xml") { - response() { - returncode(RESP_CODE_FAILED) - messageKey(key) - message(msg) - } - } - } - json { - log.debug "Rendering as json" - render(contentType:"text/json") { + response.addHeader("Cache-Control", "no-cache") + withFormat { + xml { + render(contentType: "text/xml") { + response() { returncode(RESP_CODE_FAILED) messageKey(key) message(msg) } } } + json { + log.debug "Rendering as json" + render(contentType: "text/json") { + returncode(RESP_CODE_FAILED) + messageKey(key) + message(msg) + } + } + } } } @@ -2281,19 +2256,19 @@ class ApiController { URI oldUri = URI.create(logoutUrl) if (!StringUtils.isEmpty(params.logoutURL)) { - try { - oldUri = URI.create(params.logoutURL) - } catch ( Exception e ) { - // Do nothing, the variable oldUri was already initialized - } + try { + oldUri = URI.create(params.logoutURL) + } catch (Exception e) { + // Do nothing, the variable oldUri was already initialized + } } String newQuery = oldUri.getQuery(); if (newQuery == null) { - newQuery = "errors=" + newQuery = "errors=" } else { - newQuery += "&" + "errors=" + newQuery += "&" + "errors=" } newQuery += errorsJSONArray diff --git a/bigbluebutton-web/turn-stun-servers.xml.tmpl b/bigbluebutton-web/turn-stun-servers.xml.tmpl new file mode 100644 index 0000000000000000000000000000000000000000..534aff29501418b0e92c77d58c4ba5616a9cd93f --- /dev/null +++ b/bigbluebutton-web/turn-stun-servers.xml.tmpl @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + +BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ + +Copyright (c) 2018 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/>. + +--> +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans + http://www.springframework.org/schema/beans/spring-beans-2.5.xsd + "> + + <bean id="stun1" class="org.bigbluebutton.web.services.turn.StunServer"> + <constructor-arg index="0" value="stun:{{ .Env.TURN_DOMAIN }}:3478"/> + </bean> + + <bean id="turn1" class="org.bigbluebutton.web.services.turn.TurnServer"> + <constructor-arg index="0" value="{{ .Env.TURN_SECRET }}"/> + <constructor-arg index="1" value="turn:{{ .Env.TURN_DOMAIN }}:3478"/> + <constructor-arg index="2" value="86400"/> + </bean> + + <bean id="turn2" class="org.bigbluebutton.web.services.turn.TurnServer"> + <constructor-arg index="0" value="{{ .Env.TURN_SECRET }}"/> + <constructor-arg index="1" value="turn:{{ .Env.TURN_DOMAIN }}:3478?transport=tcp"/> + <constructor-arg index="2" value="86400"/> + </bean> + + <bean id="stunTurnService" class="org.bigbluebutton.web.services.turn.StunTurnService"> + <property name="stunServers"> + <set> + <ref bean="stun1" /> + </set> + </property> + <property name="turnServers"> + <set> + <ref bean="turn1" /> + <ref bean="turn2" /> + </set> + </property> + <property name="remoteIceCandidates"> + <set/> + </property> + </bean> +</beans> diff --git a/clients/flash/air-client/src/Default.css b/clients/flash/air-client/src/Default.css index bdbd0eb52e52c61aedf91be54de762afbe0fb6a6..0ff768a7c5d28b2fe317cb092cd95127b2ecc4af 100755 --- a/clients/flash/air-client/src/Default.css +++ b/clients/flash/air-client/src/Default.css @@ -375,3 +375,8 @@ settings|SettingsItemRenderer { borderColorOver : PropertyReference("grey700"); borderColorDown : PropertyReference("grey700"); } + +.startingScreensharingLabel { + color : PropertyReference("bbbBlack"); + textAlign : center; +} \ No newline at end of file diff --git a/clients/flash/air-client/src/Main-app.xml b/clients/flash/air-client/src/Main-app.xml index 754c74d44431928c74444a8f4d6506033d378b70..80fb0991dc347faf5e1e92bcb74a6b2f03fb8867 100755 --- a/clients/flash/air-client/src/Main-app.xml +++ b/clients/flash/air-client/src/Main-app.xml @@ -385,5 +385,6 @@ <extensions> <extensionID>com.freshplanet.AirCapabilities</extensionID> <extensionID>com.juankpro.ane.LocalNotification</extensionID> + <extensionID>org.bigbluebutton.BBBRtmpPlayer</extensionID> </extensions> </application> diff --git a/clients/flash/air-client/src/css/hdpi.css b/clients/flash/air-client/src/css/hdpi.css index 1797d518cf9ad024cdb232fc0d6cf99f2f777929..a6e870b095549b9bcea2ba0619344a143d07645f 100755 --- a/clients/flash/air-client/src/css/hdpi.css +++ b/clients/flash/air-client/src/css/hdpi.css @@ -58,8 +58,8 @@ padding: 10.50; gap: 6.00; leftIndent: 33.00; - fontSize: 23.00; - nameFontSize: 23.00; + fontSize: 24.00; + nameFontSize: 24.00; timeFontSize: 21.00; } @@ -131,8 +131,8 @@ } .sendButton { - diameter: 48.00; - fontSize: 22.00; + diameter: 42.00; + fontSize: 22.50; } .titleGroup { @@ -221,4 +221,8 @@ .audioButtonStyle { iconSize: 66.00; } + + .startingScreensharingLabel { + fontSize: 42.00; + } } diff --git a/clients/flash/air-client/src/css/ldpi.css b/clients/flash/air-client/src/css/ldpi.css index 3cd66acbcaab29e76c9438f5cbd5fd7a0cc80c58..99aeba7e3725cf7c44905673d6abf0051b82bd31 100755 --- a/clients/flash/air-client/src/css/ldpi.css +++ b/clients/flash/air-client/src/css/ldpi.css @@ -221,4 +221,8 @@ .audioButtonStyle { iconSize: 33.000; } + + .startingScreensharingLabel { + fontSize: 21.000; + } } diff --git a/clients/flash/air-client/src/css/mdpi.css b/clients/flash/air-client/src/css/mdpi.css index 191b02de0cf02c9734faa99f97d2801cd6e85b85..5d8bc2ed42a6bed142953bb12ac548854cf4ed04 100755 --- a/clients/flash/air-client/src/css/mdpi.css +++ b/clients/flash/air-client/src/css/mdpi.css @@ -132,7 +132,7 @@ .sendButton { diameter: 28.0; - fontSize: 15; + fontSize: 15.0; } .titleGroup { @@ -221,4 +221,8 @@ .audioButtonStyle { iconSize: 44.0; } + + .startingScreensharingLabel { + fontSize: 28.0; + } } diff --git a/clients/flash/air-client/src/css/xhdpi.css b/clients/flash/air-client/src/css/xhdpi.css index b238f81348d487c3dfd1db060b76ec57a5ec74a0..8effe499fab7b975630a244addb0085a66dded0a 100755 --- a/clients/flash/air-client/src/css/xhdpi.css +++ b/clients/flash/air-client/src/css/xhdpi.css @@ -209,4 +209,8 @@ .audioButtonStyle { iconSize : 88; } + + .startingScreensharingLabel { + fontSize : 56; + } } diff --git a/clients/flash/air-client/src/css/xxhdpi.css b/clients/flash/air-client/src/css/xxhdpi.css index 8e6e62c31b2c22f11bb2462bd155f6a45871600d..c75820f5c8babd9ecdfc9a1125a2d6ab91d4d6ce 100755 --- a/clients/flash/air-client/src/css/xxhdpi.css +++ b/clients/flash/air-client/src/css/xxhdpi.css @@ -132,7 +132,7 @@ .sendButton { diameter: 84.0; - fontSize: 45; + fontSize: 45.0; } .titleGroup { @@ -221,4 +221,8 @@ .audioButtonStyle { iconSize: 132.0; } + + .startingScreensharingLabel { + fontSize: 84.0; + } } diff --git a/clients/flash/air-client/src/css/xxxhdpi.css b/clients/flash/air-client/src/css/xxxhdpi.css index 146b8b5e1c5b937cb5bd2dea240207c37b839393..b95b4f244f51205e0255de7ceae4b095bd67e2e6 100755 --- a/clients/flash/air-client/src/css/xxxhdpi.css +++ b/clients/flash/air-client/src/css/xxxhdpi.css @@ -221,4 +221,8 @@ .audioButtonStyle { iconSize: 176; } + + .startingScreensharingLabel { + fontSize: 112; + } } diff --git a/clients/flash/air-client/src/org/bigbluebutton/air/common/views/IOSVideoView.as b/clients/flash/air-client/src/org/bigbluebutton/air/common/views/IOSVideoView.as new file mode 100755 index 0000000000000000000000000000000000000000..ba9f0e96c9d704ac8069b752e9554e70e7e6e33a --- /dev/null +++ b/clients/flash/air-client/src/org/bigbluebutton/air/common/views/IOSVideoView.as @@ -0,0 +1,121 @@ +package org.bigbluebutton.air.common.views { + import mx.formatters.DateFormatter; + + import spark.components.Image; + + import org.bigbluebutton.BBBRtmpPlayer; + import org.bigbluebutton.BBBRtmpPlayerEvent; + import org.bigbluebutton.air.util.ConnUtil; + + public class IOSVideoView extends VideoBaseView { + + protected var player:BBBRtmpPlayer; + protected var dateFormat:DateFormatter = new DateFormatter("Y-MM-DD J:NN:SS:QQ"); + + private var _connectionId : String; + + private function get image():Image { + return videoComp as Image; + } + + public function startStream(uri:String, streamName:String, imgWidth:Number, imgHeight:Number, meetingId:String, authToken:String, externalUserId:String):void { + + if (player) { + close(); + } + + videoComp = new Image(); + if (numChildren == 0) { + addChild(videoComp); + } + + this.originalVideoWidth = imgWidth; + this.originalVideoHeight = imgHeight; + + _connectionId = ConnUtil.generateConnId(); + + var url:String = uri + "/" + streamName + " live=1 conn=S:" + meetingId + " conn=S:" + externalUserId + " conn=S:" + authToken + " conn=S:" + _connectionId; + + player = new BBBRtmpPlayer(url); + + player.addEventListener(BBBRtmpPlayerEvent.CONNECTED, onConnected); + player.addEventListener(BBBRtmpPlayerEvent.CONNECTING, onConnecting); + player.addEventListener(BBBRtmpPlayerEvent.CONNECTION_FAILED, onConnectionFailed); + player.addEventListener(BBBRtmpPlayerEvent.DISCONNECTED, onDisconnected); + + player.play(); + } + + private function onConnected(e:BBBRtmpPlayerEvent):void { + trace(dateFormat.format(new Date()) + " EVENT: " + e.type + " MESSAGE: " + e.getMessage()); + image.source = player.getBmpData(); + } + + private function onConnecting(e:BBBRtmpPlayerEvent):void { + trace(dateFormat.format(new Date()) + " EVENT: " + e.type + " MESSAGE: " + e.getMessage()); + } + + private function onConnectionFailed(e:BBBRtmpPlayerEvent):void { + close(); + } + + private function onDisconnected(e:BBBRtmpPlayerEvent):void { + close(); + } + + public function close():void { + if (player) { + player.removeEventListener(BBBRtmpPlayerEvent.CONNECTED, onConnected); + player.removeEventListener(BBBRtmpPlayerEvent.CONNECTING, onConnecting); + player.removeEventListener(BBBRtmpPlayerEvent.CONNECTION_FAILED, onConnectionFailed); + player.removeEventListener(BBBRtmpPlayerEvent.DISCONNECTED, onDisconnected); + } + if (numChildren > 0 && getChildAt(0) == image) { + removeChild(image); + } + videoComp = null; + player = null; + } + + override protected function updateDisplayList(w:Number, h:Number):void { + super.updateDisplayList(w, h); + + if (player) { + resizeForPortrait(); + } + } + + public function rotateVideo(rotation:Number):void { + if (image && stage.contains(image)) { + removeChild(image); + } + videoComp = new Image(); + switch (rotation) { + case 0: + resizeForPortrait(); + image.x = width / 2 - image.width / 2; + image.y = height / 2 - image.height / 2; // + topMenuBarHeight; + break; + case -90: + resizeForLandscape(); + image.x = (width / 2) - (image.height / 2); + image.y = (height / 2) + (image.width / 2); // + topMenuBarHeight; + break; + case 90: + resizeForLandscape(); + image.x = (width / 2) + (image.height / 2); + image.y = (height / 2) - (image.width / 2); // + topMenuBarHeight; + break; + case 180: + resizeForPortrait(); + image.x = width / 2 + image.width / 2; + image.y = (height / 2) + (image.height / 2); // + topMenuBarHeight + break; + default: + break; + } + image.rotation = rotation; + addChild(image); + } + } +} diff --git a/clients/flash/air-client/src/org/bigbluebutton/air/common/views/VideoBaseView.as b/clients/flash/air-client/src/org/bigbluebutton/air/common/views/VideoBaseView.as new file mode 100644 index 0000000000000000000000000000000000000000..7ecd55b8d9c3f3af47207e51e265c82d8e182f25 --- /dev/null +++ b/clients/flash/air-client/src/org/bigbluebutton/air/common/views/VideoBaseView.as @@ -0,0 +1,65 @@ +package org.bigbluebutton.air.common.views { + import flash.display.DisplayObject; + + import mx.core.UIComponent; + + public class VideoBaseView extends UIComponent { + protected var videoComp:DisplayObject; + + protected var originalVideoWidth:Number; + + protected var originalVideoHeight:Number; + + public function resizeForLandscape():void { + if (height < width) { + videoComp.height = width; + videoComp.width = ((originalVideoWidth * videoComp.height) / originalVideoHeight); + if (width < videoComp.width) { + videoComp.width = height; + videoComp.height = (videoComp.width / originalVideoWidth) * originalVideoHeight; + } + } else { + videoComp.width = height; + videoComp.height = (videoComp.width / originalVideoWidth) * originalVideoHeight; + if (height < videoComp.height) { + videoComp.height = width; + videoComp.width = ((originalVideoWidth * videoComp.height) / originalVideoHeight); + } + } + } + + public function resizeForPortrait():void { + // if we have device where screen width less than screen height e.g. phone + if (width < height) { + // make the video width full width of the screen + videoComp.width = width; + // calculate height based on a video width, it order to keep the same aspect ratio + videoComp.height = (videoComp.width / originalVideoWidth) * originalVideoHeight; + // if calculated height appeared to be bigger than screen height, recalculuate the video size based on width + if (height < videoComp.height) { + // make the video height full height of the screen + videoComp.height = height; + // calculate width based on a video height, it order to keep the same aspect ratio + videoComp.width = ((originalVideoWidth * videoComp.height) / originalVideoHeight); + } + } // if we have device where screen height less than screen width e.g. tablet + else { + // make the video height full height of the screen + videoComp.height = height; + // calculate width based on a video height, it order to keep the same aspect ratio + videoComp.width = ((originalVideoWidth * videoComp.height) / originalVideoHeight); + // if calculated width appeared to be bigger than screen width, recalculuate the video size based on height + if (width < videoComp.width) { + // make the video width full width of the screen + videoComp.width = width; + // calculate height based on a video width, it order to keep the same aspect ratio + videoComp.height = (videoComp.width / originalVideoWidth) * originalVideoHeight; + } + } + + videoComp.x = width - videoComp.width; + videoComp.y = height - videoComp.height; + } + + } +} diff --git a/clients/flash/air-client/src/org/bigbluebutton/air/common/views/VideoView.as b/clients/flash/air-client/src/org/bigbluebutton/air/common/views/VideoView.as index 50d3b096c35d7d8ae81856feed15b73e70713a8a..aefdc6b30f237b62827138f825030a5cae9f8882 100755 --- a/clients/flash/air-client/src/org/bigbluebutton/air/common/views/VideoView.as +++ b/clients/flash/air-client/src/org/bigbluebutton/air/common/views/VideoView.as @@ -6,13 +6,10 @@ package org.bigbluebutton.air.common.views { import flash.net.NetConnection; import flash.net.NetStream; import flash.system.Capabilities; - import mx.core.UIComponent; - public class VideoView extends UIComponent { + public class VideoView extends VideoBaseView { protected var ns:NetStream; - protected var _video:Video; - protected var connection:NetConnection; public var userId:String; @@ -21,13 +18,13 @@ package org.bigbluebutton.air.common.views { public var streamName:String; - protected var originalVideoWidth:Number; - - protected var originalVideoHeight:Number; + private function get video():Video { + return videoComp as Video; + } public function VideoView():void { - _video = new Video(); - addChild(_video); + videoComp = new Video(); + addChild(videoComp); } public function startStream(connection:NetConnection, name:String, streamName:String, userId:String, oWidth:Number, oHeight:Number):void { @@ -51,8 +48,8 @@ package org.bigbluebutton.air.common.views { ns.bufferTime = 0; ns.receiveVideo(true); ns.receiveAudio(false); - _video.smoothing = true; - _video.attachNetStream(ns); + video.smoothing = true; + video.attachNetStream(ns); ns.play(streamName); } @@ -87,7 +84,7 @@ package org.bigbluebutton.air.common.views { public function close():void { if (ns) { - _video.attachCamera(null); + video.attachCamera(null); ns.removeEventListener(NetStatusEvent.NET_STATUS, onNetStatus); ns.removeEventListener(AsyncErrorEvent.ASYNC_ERROR, onAsyncError); ns.close(); @@ -103,89 +100,38 @@ package org.bigbluebutton.air.common.views { } } - public function resizeForPortrait():void { - // if we have device where screen width less than screen height e.g. phone - if (width < height) { - // make the video width full width of the screen - _video.width = width; - // calculate height based on a video width, it order to keep the same aspect ratio - _video.height = (_video.width / originalVideoWidth) * originalVideoHeight; - // if calculated height appeared to be bigger than screen height, recalculuate the video size based on width - if (height < _video.height) { - // make the video height full height of the screen - _video.height = height; - // calculate width based on a video height, it order to keep the same aspect ratio - _video.width = ((originalVideoWidth * _video.height) / originalVideoHeight); - } - } // if we have device where screen height less than screen width e.g. tablet - else { - // make the video height full height of the screen - _video.height = height; - // calculate width based on a video height, it order to keep the same aspect ratio - _video.width = ((originalVideoWidth * _video.height) / originalVideoHeight); - // if calculated width appeared to be bigger than screen width, recalculuate the video size based on height - if (width < _video.width) { - // make the video width full width of the screen - _video.width = width; - // calculate height based on a video width, it order to keep the same aspect ratio - _video.height = (_video.width / originalVideoWidth) * originalVideoHeight; - } - } - - _video.x = width - _video.width; - _video.y = height - _video.height; - } - - public function resizeForLandscape():void { - if (height < width) { - _video.height = width; - _video.width = ((originalVideoWidth * _video.height) / originalVideoHeight); - if (width < _video.width) { - _video.width = height; - _video.height = (_video.width / originalVideoWidth) * originalVideoHeight; - } - } else { - _video.width = height; - _video.height = (_video.width / originalVideoWidth) * originalVideoHeight; - if (height < _video.height) { - _video.height = width; - _video.width = ((originalVideoWidth * _video.height) / originalVideoHeight); - } - } - } - public function rotateVideo(rotation:Number):void { - if (_video && stage.contains(_video)) { - stage.removeChild(_video); + if (video && stage.contains(video)) { + stage.removeChild(video); } - _video = new Video(); - _video.attachNetStream(ns); + videoComp = new Video(); + video.attachNetStream(ns); switch (rotation) { case 0: resizeForPortrait(); - _video.x = width / 2 - _video.width / 2; - _video.y = height / 2 - _video.height / 2; // + topMenuBarHeight; + video.x = width / 2 - video.width / 2; + video.y = height / 2 - video.height / 2; // + topMenuBarHeight; break; case -90: resizeForLandscape(); - _video.x = (width / 2) - (_video.height / 2); - _video.y = (height / 2) + (_video.width / 2); // + topMenuBarHeight; + video.x = (width / 2) - (video.height / 2); + video.y = (height / 2) + (video.width / 2); // + topMenuBarHeight; break; case 90: resizeForLandscape(); - _video.x = (width / 2) + (_video.height / 2); - _video.y = (height / 2) - (_video.width / 2); // + topMenuBarHeight; + video.x = (width / 2) + (video.height / 2); + video.y = (height / 2) - (video.width / 2); // + topMenuBarHeight; break; case 180: resizeForPortrait(); - _video.x = width / 2 + _video.width / 2; - _video.y = (height / 2) + (_video.height / 2); // + topMenuBarHeight + video.x = width / 2 + video.width / 2; + video.y = (height / 2) + (video.height / 2); // + topMenuBarHeight break; default: break; } - _video.rotation = rotation; - this.stage.addChild(_video); + video.rotation = rotation; + this.stage.addChild(video); } } } diff --git a/clients/flash/air-client/src/org/bigbluebutton/air/main/models/IUISession.as b/clients/flash/air-client/src/org/bigbluebutton/air/main/models/IUISession.as index 7e28f65aca8d641713d3cd729ab238903075b64e..e811a152b21ed812bfb740403df85d4ec766b4a0 100755 --- a/clients/flash/air-client/src/org/bigbluebutton/air/main/models/IUISession.as +++ b/clients/flash/air-client/src/org/bigbluebutton/air/main/models/IUISession.as @@ -12,8 +12,8 @@ package org.bigbluebutton.air.main.models { function get pageTransitionStartSignal():ISignal; function get currentPage():String; function get lastPage():String; - function popPage(animation:int = TransitionAnimationEnum.APPEAR):void; - function pushPage(value:String, details:Object = null, animation:int = TransitionAnimationEnum.APPEAR):void; + function popPage(animation:int = 0):void; + function pushPage(value:String, details:Object = null, animation:int = 0):void; function get currentPageDetails():Object; } } diff --git a/clients/flash/air-client/src/org/bigbluebutton/air/main/services/BigBlueButtonConnection.as b/clients/flash/air-client/src/org/bigbluebutton/air/main/services/BigBlueButtonConnection.as index 795ebabb88287523daddae3fac94852790d46ba9..e2f0009888e357d7d07a6c4f0ac52bf15017cb16 100755 --- a/clients/flash/air-client/src/org/bigbluebutton/air/main/services/BigBlueButtonConnection.as +++ b/clients/flash/air-client/src/org/bigbluebutton/air/main/services/BigBlueButtonConnection.as @@ -1,107 +1,110 @@ -package org.bigbluebutton.air.main.services { - - import flash.net.NetConnection; - import flash.net.Responder; - import mx.utils.ObjectUtil; - import org.bigbluebutton.air.common.services.DefaultConnectionCallback; - import org.bigbluebutton.air.common.services.IBaseConnection; - import org.bigbluebutton.air.main.models.IConferenceParameters; - import org.osflash.signals.ISignal; - import org.osflash.signals.Signal; - - public class BigBlueButtonConnection extends DefaultConnectionCallback implements IBigBlueButtonConnection { - public static const NAME:String = "BigBlueButtonConnection"; - - protected var _connectionSuccessSignal:ISignal = new Signal(); - - protected var _connectionFailureSignal:ISignal = new Signal(); - - [Inject] - public var baseConnection:IBaseConnection; - - private var _applicationURI:String; - - private var _conferenceParameters:IConferenceParameters; - - private var _tried_tunneling:Boolean = false; - - [PostConstruct] - public function init():void { - baseConnection.init(this); - baseConnection.connectionSuccessSignal.add(onConnectionSuccess); - baseConnection.connectionFailureSignal.add(onConnectionFailure); - } - - private function onConnectionFailure(reason:String):void { - connectionFailureSignal.dispatch(reason); - } - - private function onConnectionSuccess():void { - connectionSuccessSignal.dispatch(); - } - - public function get connectionFailureSignal():ISignal { - return _connectionFailureSignal; - } - - public function get connectionSuccessSignal():ISignal { - return _connectionSuccessSignal; - } - - public function set uri(uri:String):void { - _applicationURI = uri; - } - - public function get uri():String { - return _applicationURI; - } - - public function get connection():NetConnection { - return baseConnection.connection; - } - - /** - * Connect to the server. - * uri: The uri to the conference application. - * username: Fullname of the participant. - * role: MODERATOR/VIEWER - * conference: The conference room - * mode: LIVE/PLAYBACK - Live:when used to collaborate, Playback:when being used to playback a recorded conference. - * room: Need the room number when playing back a recorded conference. When LIVE, the room is taken from the URI. - */ - public function connect(params:IConferenceParameters, tunnel:Boolean = false):void { - _conferenceParameters = params; - _tried_tunneling = tunnel; - var uri:String = _applicationURI + "/" + _conferenceParameters.room; - - var username:String = _conferenceParameters.username; - var role:String = _conferenceParameters.role; - var intMeetingId:String = _conferenceParameters.room; - var voiceConf:String = _conferenceParameters.voicebridge; - var recorded:Boolean = _conferenceParameters.record; - var extUserId:String = _conferenceParameters.externUserID; - var intUserId:String = _conferenceParameters.internalUserID; - var muteOnStart:Boolean = _conferenceParameters.muteOnStart; - var guest:Boolean = _conferenceParameters.guest; - var authToken:String = _conferenceParameters.authToken; - - var connectParams:Array = [username, role, intMeetingId, voiceConf, recorded, extUserId, intUserId, muteOnStart, guest, authToken]; - - trace("BBB Apps connect: " + connectParams); - baseConnection.connect.apply(null, new Array(uri).concat(connectParams)); - } - - public function disconnect(onUserCommand:Boolean):void { - baseConnection.disconnect(onUserCommand); - } - - /**** NEED TO REMOVE THIS BEFORE CONVERSION IS FINISHED ******/ - public function sendMessage(service:String, onSuccess:Function, onFailure:Function, message:Object = null):void { - //baseConnection.sendMessage(service, onSuccess, onFailure, message); - } - - public function sendMessage2x(onSuccess:Function, onFailure:Function, message:Object):void { - baseConnection.sendMessage2x(onSuccess, onFailure, message); - } - } -} +package org.bigbluebutton.air.main.services { + + import flash.net.NetConnection; + + import org.bigbluebutton.air.common.services.DefaultConnectionCallback; + import org.bigbluebutton.air.common.services.IBaseConnection; + import org.bigbluebutton.air.main.models.IConferenceParameters; + import org.bigbluebutton.air.util.ConnUtil; + import org.osflash.signals.ISignal; + import org.osflash.signals.Signal; + + public class BigBlueButtonConnection extends DefaultConnectionCallback implements IBigBlueButtonConnection { + public static const NAME:String = "BigBlueButtonConnection"; + + protected var _connectionSuccessSignal:ISignal = new Signal(); + + protected var _connectionFailureSignal:ISignal = new Signal(); + + [Inject] + public var baseConnection:IBaseConnection; + + private var _applicationURI:String; + + private var _conferenceParameters:IConferenceParameters; + + private var _connectionId : String; + + private var _tried_tunneling:Boolean = false; + + [PostConstruct] + public function init():void { + baseConnection.init(this); + baseConnection.connectionSuccessSignal.add(onConnectionSuccess); + baseConnection.connectionFailureSignal.add(onConnectionFailure); + } + + private function onConnectionFailure(reason:String):void { + connectionFailureSignal.dispatch(reason); + } + + private function onConnectionSuccess():void { + connectionSuccessSignal.dispatch(); + } + + public function get connectionFailureSignal():ISignal { + return _connectionFailureSignal; + } + + public function get connectionSuccessSignal():ISignal { + return _connectionSuccessSignal; + } + + public function set uri(uri:String):void { + _applicationURI = uri; + } + + public function get uri():String { + return _applicationURI; + } + + public function get connection():NetConnection { + return baseConnection.connection; + } + + /** + * Connect to the server. + * uri: The uri to the conference application. + * username: Fullname of the participant. + * role: MODERATOR/VIEWER + * conference: The conference room + * mode: LIVE/PLAYBACK - Live:when used to collaborate, Playback:when being used to playback a recorded conference. + * room: Need the room number when playing back a recorded conference. When LIVE, the room is taken from the URI. + */ + public function connect(params:IConferenceParameters, tunnel:Boolean = false):void { + _conferenceParameters = params; + _tried_tunneling = tunnel; + _connectionId = ConnUtil.generateConnId(); + var uri:String = _applicationURI + "/" + _conferenceParameters.room; + + var username:String = _conferenceParameters.username; + var role:String = _conferenceParameters.role; + var intMeetingId:String = _conferenceParameters.room; + var voiceConf:String = _conferenceParameters.voicebridge; + var recorded:Boolean = _conferenceParameters.record; + var extUserId:String = _conferenceParameters.externUserID; + var intUserId:String = _conferenceParameters.internalUserID; + var muteOnStart:Boolean = _conferenceParameters.muteOnStart; + var guest:Boolean = _conferenceParameters.guest; + var authToken:String = _conferenceParameters.authToken; + + var connectParams:Array = [username, role, intMeetingId, voiceConf, recorded, extUserId, intUserId, muteOnStart, guest, authToken, _connectionId]; + + trace("BBB Apps connect: " + connectParams); + baseConnection.connect.apply(null, new Array(uri).concat(connectParams)); + } + + public function disconnect(onUserCommand:Boolean):void { + baseConnection.disconnect(onUserCommand); + } + + /**** NEED TO REMOVE THIS BEFORE CONVERSION IS FINISHED ******/ + public function sendMessage(service:String, onSuccess:Function, onFailure:Function, message:Object = null):void { + //baseConnection.sendMessage(service, onSuccess, onFailure, message); + } + + public function sendMessage2x(onSuccess:Function, onFailure:Function, message:Object):void { + baseConnection.sendMessage2x(onSuccess, onFailure, message); + } + } +} diff --git a/clients/flash/air-client/src/org/bigbluebutton/air/main/views/LoadingScreenMediator.as b/clients/flash/air-client/src/org/bigbluebutton/air/main/views/LoadingScreenMediator.as index cf2de0bb5b068a3d957c2f111257d5f7d50ea611..23e01333e9c32dcd8a0b145bf7b011b662677a27 100755 --- a/clients/flash/air-client/src/org/bigbluebutton/air/main/views/LoadingScreenMediator.as +++ b/clients/flash/air-client/src/org/bigbluebutton/air/main/views/LoadingScreenMediator.as @@ -40,7 +40,9 @@ package org.bigbluebutton.air.main.views { // If we are in the Flash Builder debugger the InvokeEvent will never be fired if (Capabilities.isDebugger) { //var url:String = "bigbluebutton://test-install.blindsidenetworks.com/bigbluebutton/api/join?fullName=AIR&meetingID=Demo+Meeting&password=mp&redirect=false&checksum=3fdf56e9915c1031c3ea012b4ec8823cedd7c272"; - var url:String = "bigbluebutton://test-install.blindsidenetworks.com/bigbluebutton/api/join?fullName=User+2021828&meetingID=Demo+Meeting&password=ap&redirect=true&checksum=8751963df96437c7d435eac8124e4fb3ec147115"; + //var url:String = "bigbluebutton://test-install.blindsidenetworks.com/bigbluebutton/api/join?fullName=User+2021828&meetingID=Demo+Meeting&password=ap&redirect=true&checksum=8751963df96437c7d435eac8124e4fb3ec147115"; + //var url:String = "bigbluebuttons://dev21.bigbluebutton.org/bigbluebutton/api/join?meetingID=Demo+Meeting&fullName=AAA&password=mp&checksum=5516fb6f7d7330d0c02fd94c8e1fb683957e2da4a1c7013f66004b68148d4478"; + var url:String = "bigbluebutton://206.47.241.19/bigbluebutton/api/join?meetingID=Demo+Meeting&fullName=AAA&password=mp&checksum=94cc8981bf215b13101fa4d3307d3aca8512578b23b91b7be8042f65abd4a007"; joinRoom(url); } } diff --git a/clients/flash/air-client/src/org/bigbluebutton/air/screenshare/views/IOSScreenshareView.as b/clients/flash/air-client/src/org/bigbluebutton/air/screenshare/views/IOSScreenshareView.as new file mode 100755 index 0000000000000000000000000000000000000000..608d6aa28ee96f3d5589c7dd65e0248f6eefa8b1 --- /dev/null +++ b/clients/flash/air-client/src/org/bigbluebutton/air/screenshare/views/IOSScreenshareView.as @@ -0,0 +1,223 @@ +package org.bigbluebutton.air.screenshare.views { + import flash.display.DisplayObject; + import flash.events.TimerEvent; + import flash.utils.Timer; + + import mx.core.UIComponent; + import mx.formatters.DateFormatter; + + import spark.components.Image; + import spark.components.Label; + import spark.components.ProgressBar; + + import org.bigbluebutton.BBBRtmpPlayer; + import org.bigbluebutton.BBBRtmpPlayerEvent; + import org.bigbluebutton.air.util.ConnUtil; + + // FIXME : Work in progress class, needs behave like Android screensahring display + public class IOSScreenshareView extends UIComponent { + protected var player:BBBRtmpPlayer; + + protected var videoComp:DisplayObject; + + protected var originalVideoWidth:Number; + + protected var originalVideoHeight:Number; + + private var _waitingBar:ProgressBar; + + private var _waitingTimer:Timer; + + private var _connectionId:String; + + private var _startingLabel:Label; + + private const WAITING_SECONDS:int = 15; + + protected var dateFormat:DateFormatter = new DateFormatter("Y-MM-DD J:NN:SS:QQ"); + + private function waitingTimerProgressHandler(e:TimerEvent):void { + trace("PROGRESS " + _waitingTimer.currentCount); + _waitingBar.currentProgress = _waitingTimer.currentCount; + } + + public function resizeForProgressBar():void { + // if we have device where screen width less than screen height e.g. phone + if (width < height) { + // make the video width full width of the screen + _waitingBar.width = width; + // calculate height based on a video width, it order to keep the same aspect ratio + _waitingBar.height = (_waitingBar.width / originalVideoWidth) * originalVideoHeight; + // if calculated height appeared to be bigger than screen height, recalculuate the video size based on width + if (height < _waitingBar.height) { + // make the video height full height of the screen + _waitingBar.height = height; + // calculate width based on a video height, it order to keep the same aspect ratio + _waitingBar.width = ((originalVideoWidth * _waitingBar.height) / originalVideoHeight); + } + } // if we have device where screen height less than screen width e.g. tablet + else { + // make the video height full height of the screen + _waitingBar.height = height; + // calculate width based on a video height, it order to keep the same aspect ratio + _waitingBar.width = ((originalVideoWidth * _waitingBar.height) / originalVideoHeight); + // if calculated width appeared to be bigger than screen width, recalculuate the video size based on height + if (width < _waitingBar.width) { + // make the video width full width of the screen + _waitingBar.width = width; + // calculate height based on a video width, it order to keep the same aspect ratio + _waitingBar.height = (_waitingBar.width / originalVideoWidth) * originalVideoHeight; + } + } + + _startingLabel.x = _waitingBar.x = width - _waitingBar.width; + _waitingBar.y = height - _waitingBar.height; + + _startingLabel.y = _waitingBar.y + (_waitingBar.height * 0.5); + _startingLabel.width = _waitingBar.width; + _startingLabel.height = _waitingBar.height; + } + + public function resizeForPortrait():void { + // if we have device where screen width less than screen height e.g. phone + if (width < height) { + // make the video width full width of the screen + videoComp.width = width; + // calculate height based on a video width, it order to keep the same aspect ratio + videoComp.height = (videoComp.width / originalVideoWidth) * originalVideoHeight; + // if calculated height appeared to be bigger than screen height, recalculuate the video size based on width + if (height < videoComp.height) { + // make the video height full height of the screen + videoComp.height = height; + // calculate width based on a video height, it order to keep the same aspect ratio + videoComp.width = ((originalVideoWidth * videoComp.height) / originalVideoHeight); + } + } // if we have device where screen height less than screen width e.g. tablet + else { + // make the video height full height of the screen + videoComp.height = height; + // calculate width based on a video height, it order to keep the same aspect ratio + videoComp.width = ((originalVideoWidth * videoComp.height) / originalVideoHeight); + // if calculated width appeared to be bigger than screen width, recalculuate the video size based on height + if (width < videoComp.width) { + // make the video width full width of the screen + videoComp.width = width; + // calculate height based on a video width, it order to keep the same aspect ratio + videoComp.height = (videoComp.width / originalVideoWidth) * originalVideoHeight; + } + } + + videoComp.x = width - videoComp.width; + videoComp.y = height - videoComp.height; + } + + private function get image():Image { + return videoComp as Image; + } + + public function startStream(uri:String, streamName:String, imgWidth:Number, imgHeight:Number, meetingId:String, authToken:String, externalUserId:String):void { + _waitingBar = new ProgressBar(); + + _waitingBar.width = imgWidth; + _waitingBar.height = imgWidth; + + _waitingBar.currentProgress = 0; + _waitingBar.totalProgress = WAITING_SECONDS; + _waitingBar.percentWidth = 80; + _waitingBar.percentHeight = 100; + _waitingBar.bottom = 20; + _waitingBar.styleName = "micLevelProgressBar"; + + _startingLabel = new Label(); + + _startingLabel.horizontalCenter = 0; + _startingLabel.text = "Starting screen sharing"; + _startingLabel.styleName = "startingScreensharingLabel"; + + addChild(_waitingBar); + addChild(_startingLabel); + + _waitingTimer = new Timer(1000, WAITING_SECONDS); + _waitingTimer.addEventListener(TimerEvent.TIMER, waitingTimerProgressHandler); + + if (player) { + close(); + } + + videoComp = new Image(); + addChild(videoComp); + + this.originalVideoWidth = imgWidth; + this.originalVideoHeight = imgHeight; + + _connectionId = ConnUtil.generateConnId(); + + var url:String = uri + "/" + streamName + " live=1 conn=S:" + meetingId + " conn=S:" + externalUserId + " conn=S:" + authToken + " conn=S:" + _connectionId; + + player = new BBBRtmpPlayer(url); + + player.addEventListener(BBBRtmpPlayerEvent.CONNECTED, onConnected); + player.addEventListener(BBBRtmpPlayerEvent.CONNECTING, onConnecting); + player.addEventListener(BBBRtmpPlayerEvent.CONNECTION_FAILED, onConnectionFailed); + player.addEventListener(BBBRtmpPlayerEvent.DISCONNECTED, onDisconnected); + + player.play(); + } + + private function onConnected(e:BBBRtmpPlayerEvent):void { + trace(dateFormat.format(new Date()) + " EVENT: " + e.type + " MESSAGE: " + e.getMessage()); + if (_waitingBar && _waitingBar.parent == this) { + _waitingTimer.removeEventListener(TimerEvent.TIMER, waitingTimerProgressHandler); + _waitingBar.currentProgress = WAITING_SECONDS; + removeChild(_waitingBar); + } + if (_startingLabel && _startingLabel.parent == this) { + removeChild(_startingLabel); + } + if (image) { + image.source = player.getBmpData(); + } + } + + private function onConnecting(e:BBBRtmpPlayerEvent):void { + trace(dateFormat.format(new Date()) + " EVENT: " + e.type + " MESSAGE: " + e.getMessage()); + _waitingTimer.start(); + } + + private function onConnectionFailed(e:BBBRtmpPlayerEvent):void { + trace("EVENT: " + e.type + " MESSAGE: " + e.getMessage()); + close(); + } + + private function onDisconnected(e:BBBRtmpPlayerEvent):void { + trace("EVENT: " + e.type + " MESSAGE: " + e.getMessage()); + close(); + } + + public function close():void { + if (player) { + player.addEventListener(BBBRtmpPlayerEvent.CONNECTED, onConnected); + player.addEventListener(BBBRtmpPlayerEvent.CONNECTING, onConnecting); + player.removeEventListener(BBBRtmpPlayerEvent.CONNECTION_FAILED, onConnectionFailed); + player.removeEventListener(BBBRtmpPlayerEvent.DISCONNECTED, onDisconnected); + if (image && image.parent == this) { + removeChild(image); + } + videoComp = null; + player = null; + } + } + + override protected function updateDisplayList(w:Number, h:Number):void { + super.updateDisplayList(w, h); + + if (_waitingBar) { + resizeForProgressBar(); + } + + if (player) { + resizeForPortrait(); + } + } + } +} diff --git a/clients/flash/air-client/src/org/bigbluebutton/air/screenshare/views/ScreenshareDock.as b/clients/flash/air-client/src/org/bigbluebutton/air/screenshare/views/ScreenshareDock.as index ff3947fd60968bec9aae14997c6764d57bd9054c..df2652556c52b47691cba3fd29f697ea88d0233e 100755 --- a/clients/flash/air-client/src/org/bigbluebutton/air/screenshare/views/ScreenshareDock.as +++ b/clients/flash/air-client/src/org/bigbluebutton/air/screenshare/views/ScreenshareDock.as @@ -10,6 +10,7 @@ package org.bigbluebutton.air.screenshare.views { import flash.media.VideoStatus; import flash.net.NetConnection; import flash.net.NetStream; + import flash.system.Capabilities; import spark.components.Group; @@ -31,7 +32,16 @@ package org.bigbluebutton.air.screenshare.views { private var _topRect:RectCoverView; private var _bottomRect:RectCoverView; - public function ScreenshareDock():void { } + private var _iosScrenshareView:IOSScreenshareView; + + public function ScreenshareDock():void { + if (Capabilities.version.indexOf("IOS") >= 0) { + _iosScrenshareView = new IOSScreenshareView(); + _iosScrenshareView.percentWidth = 100; + _iosScrenshareView.percentHeight = 100; + addElement(_iosScrenshareView); + } + } private function onStageVideoState(event:StageVideoAvailabilityEvent):void { var available:Boolean = (event.availability == StageVideoAvailability.AVAILABLE); @@ -167,21 +177,25 @@ package org.bigbluebutton.air.screenshare.views { } - public function viewStream(conn:NetConnection, streamId:String, width:int, height:int):void { + public function startStream(conn:NetConnection, streamId:String, width:int, height:int, meetingId:String, authToken:String, externalUserId:String):void { //trace("************ ScreenshareView: viewing of screenshare streamId=" + streamId + " w=" + width + " h=" + height); _conn = conn; _streamId = streamId; _origVidWidth = width; _origVidHeight = height; - - if (stage == null) { - //trace("************ ScreenshareView: STAGE IS NULL!!!!!!!!"); + + if (Capabilities.version.indexOf("IOS") >= 0) { + _iosScrenshareView.startStream(conn.uri, streamId, width, height, meetingId, authToken, externalUserId); } else { - stage.addEventListener(StageVideoAvailabilityEvent.STAGE_VIDEO_AVAILABILITY, onStageVideoState); + if (stage == null) { + //trace("************ ScreenshareView: STAGE IS NULL!!!!!!!!"); + } else { + stage.addEventListener(StageVideoAvailabilityEvent.STAGE_VIDEO_AVAILABILITY, onStageVideoState); + } } } - public function streamStopped(session:String, reason:String):void { + public function stopStream(session:String, reason:String):void { if (_screenshareRunningListener != null) { _screenshareRunningListener(_usingStageVideo, false); } @@ -287,6 +301,10 @@ package org.bigbluebutton.air.screenshare.views { _sv.removeEventListener(StageVideoEvent.RENDER_STATE, stageVideoStateChange); _sv = null; } + + if (Capabilities.version.indexOf("IOS") >= 0) { + _iosScrenshareView.close(); + } _ns = null; diff --git a/clients/flash/air-client/src/org/bigbluebutton/air/screenshare/views/ScreenshareViewMediator.as b/clients/flash/air-client/src/org/bigbluebutton/air/screenshare/views/ScreenshareViewMediator.as index 6b37470f67ebc92a98fc29567e6b75a93fb0ebfc..c4c7df5cefcbdb9a4fd51692247d5aab06776cab 100755 --- a/clients/flash/air-client/src/org/bigbluebutton/air/screenshare/views/ScreenshareViewMediator.as +++ b/clients/flash/air-client/src/org/bigbluebutton/air/screenshare/views/ScreenshareViewMediator.as @@ -1,5 +1,6 @@ package org.bigbluebutton.air.screenshare.views { + import org.bigbluebutton.air.main.models.IConferenceParameters; import org.bigbluebutton.air.screenshare.model.IScreenshareModel; import org.bigbluebutton.air.screenshare.services.IScreenshareConnection; @@ -16,6 +17,9 @@ package org.bigbluebutton.air.screenshare.views { [Inject] public var model:IScreenshareModel; + [Inject] + public var conferenceParameters:IConferenceParameters; + override public function initialize():void { //trace("************ ScreenshareViewMediator:: INIT **************"); model.screenshareStreamStartedSignal.add(viewStream); @@ -26,11 +30,11 @@ package org.bigbluebutton.air.screenshare.views { } public function viewStream(streamId:String, width:int, height:int):void { - view.viewStream(conn.connection, streamId, width, height); + view.startStream(conn.connection, streamId, width, height, conferenceParameters.meetingID, conferenceParameters.authToken, conferenceParameters.externUserID); } public function streamStopped(session:String, reason:String):void { - view.streamStopped(session, reason); + view.stopStream(session, reason); } override public function destroy():void { diff --git a/clients/flash/air-client/src/org/bigbluebutton/air/user/services/UsersMessageSender.as b/clients/flash/air-client/src/org/bigbluebutton/air/user/services/UsersMessageSender.as index 20f059643ffa6a26045f853d0e6dcc7b766e5b76..bb34b34c40d6b17e15d3cd749e9b0713cc4ce5a6 100755 --- a/clients/flash/air-client/src/org/bigbluebutton/air/user/services/UsersMessageSender.as +++ b/clients/flash/air-client/src/org/bigbluebutton/air/user/services/UsersMessageSender.as @@ -19,7 +19,7 @@ package org.bigbluebutton.air.user.services { public function joinMeeting():void { var message:Object = { header: {name: "UserJoinMeetingReqMsg", meetingId: conferenceParameters.meetingID, userId: conferenceParameters.internalUserID}, - body: {userId: conferenceParameters.internalUserID, authToken: conferenceParameters.authToken} + body: {userId: conferenceParameters.internalUserID, authToken: conferenceParameters.authToken, clientType: "FLASH"} }; userSession.mainConnection.sendMessage2x(defaultSuccessResponse, defaultFailureResponse, message); diff --git a/clients/flash/air-client/src/org/bigbluebutton/air/util/ConnUtil.as b/clients/flash/air-client/src/org/bigbluebutton/air/util/ConnUtil.as index 125930a9af246b027b1376628f5316c600847ecd..ceeddfb9edcb97efadd396f76b6eecd230a69c51 100755 --- a/clients/flash/air-client/src/org/bigbluebutton/air/util/ConnUtil.as +++ b/clients/flash/air-client/src/org/bigbluebutton/air/util/ConnUtil.as @@ -14,5 +14,20 @@ package org.bigbluebutton.air.util var result:Array = pattern.exec(appURL); return result; } + + private static function generateRandomString(strlen:Number):String{ + var chars:String = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + var num_chars:Number = chars.length - 1; + var randomChar:String = ""; + + for (var i:Number = 0; i < strlen; i++){ + randomChar += chars.charAt(Math.floor(Math.random() * num_chars)); + } + return randomChar; + } + + public static function generateConnId():String { + return generateRandomString(15); + } } } \ No newline at end of file diff --git a/clients/flash/air-client/src/org/bigbluebutton/air/video/services/VideoConnection.as b/clients/flash/air-client/src/org/bigbluebutton/air/video/services/VideoConnection.as index b8b8dc714cbba765edfb09f614133618567cc37e..50cb850696f029fe597ed500491c3fb4de58edfe 100755 --- a/clients/flash/air-client/src/org/bigbluebutton/air/video/services/VideoConnection.as +++ b/clients/flash/air-client/src/org/bigbluebutton/air/video/services/VideoConnection.as @@ -16,6 +16,7 @@ package org.bigbluebutton.air.video.services { import org.bigbluebutton.air.main.models.IUserSession; import org.bigbluebutton.air.main.models.LockSettings2x; import org.bigbluebutton.air.user.models.UserRole; + import org.bigbluebutton.air.util.ConnUtil; import org.bigbluebutton.air.video.commands.ShareCameraSignal; import org.bigbluebutton.air.video.commands.StopShareCameraSignal; import org.bigbluebutton.air.video.models.VideoProfile; @@ -64,6 +65,8 @@ package org.bigbluebutton.air.video.services { protected var _selectedCameraRotation:int; + private var _connectionId : String; + [PostConstruct] public function init():void { baseConnection.init(this); @@ -130,7 +133,8 @@ package org.bigbluebutton.air.video.services { public function connect():void { trace("Video connect"); - baseConnection.connect(uri, conferenceParameters.meetingID, userSession.userId, conferenceParameters.authToken); + _connectionId = ConnUtil.generateConnId(); + baseConnection.connect(uri, conferenceParameters.meetingID, userSession.userId, conferenceParameters.authToken, _connectionId); } public function disconnect(onUserCommand:Boolean):void { diff --git a/clients/flash/air-client/src/org/bigbluebutton/air/video/views/WebcamDock.as b/clients/flash/air-client/src/org/bigbluebutton/air/video/views/WebcamDock.as index ad7a0c3352f9a5f2a17201c587722da1267fd1ee..9d34d9f507769eb89e06e8f4df45c617371117e4 100755 --- a/clients/flash/air-client/src/org/bigbluebutton/air/video/views/WebcamDock.as +++ b/clients/flash/air-client/src/org/bigbluebutton/air/video/views/WebcamDock.as @@ -1,29 +1,48 @@ package org.bigbluebutton.air.video.views { import flash.net.NetConnection; + import flash.system.Capabilities; import spark.components.Group; - import org.bigbluebutton.air.common.views.VideoView; + import org.bigbluebutton.air.common.views.IOSVideoView; + import org.bigbluebutton.air.common.views.VideoView; public class WebcamDock extends Group { private var _video:VideoView; + private var _iosVideoView:IOSVideoView; public function WebcamDock() { super(); - _video = new VideoView(); - _video.percentHeight = 100; - _video.percentWidth = 100; - addElement(_video); + if (Capabilities.version.indexOf("IOS") >= 0) { + _iosVideoView = new IOSVideoView(); + _iosVideoView.percentWidth = 100; + _iosVideoView.percentHeight = 100; + addElement(_iosVideoView); + } else { + _video = new VideoView(); + _video.percentHeight = 100; + _video.percentWidth = 100; + addElement(_video); + } } - public function startStream(connection:NetConnection, name:String, streamName:String, userId:String, oWidth:Number, oHeight:Number):void { - _video.startStream(connection, name, streamName, userId, oWidth, oHeight); + public function startStream(connection:NetConnection, name:String, streamName:String, userId:String, oWidth:Number, oHeight:Number, meetingId:String, authToken:String, externalUserId:String):void { + + if (Capabilities.version.indexOf("IOS") >= 0) { + _iosVideoView.startStream(connection.uri, streamName, oWidth, oHeight, meetingId, authToken, externalUserId); + } else { + _video.startStream(connection, name, streamName, userId, oWidth, oHeight); + } } public function closeStream():void { - _video.close(); + if (Capabilities.version.indexOf("IOS") >= 0) { + _iosVideoView.close(); + } else { + _video.close(); + } } } } diff --git a/clients/flash/air-client/src/org/bigbluebutton/air/video/views/WebcamDockMediator.as b/clients/flash/air-client/src/org/bigbluebutton/air/video/views/WebcamDockMediator.as index f1cc8bcee3c60d4d46273a6ea9225a961e8b74ba..b8a956b99efb3d17fb4ce93da2cc9cea24e71697 100755 --- a/clients/flash/air-client/src/org/bigbluebutton/air/video/views/WebcamDockMediator.as +++ b/clients/flash/air-client/src/org/bigbluebutton/air/video/views/WebcamDockMediator.as @@ -1,11 +1,12 @@ package org.bigbluebutton.air.video.views { + import org.bigbluebutton.air.main.models.IConferenceParameters; import org.bigbluebutton.air.main.models.IMeetingData; import org.bigbluebutton.air.main.models.IUserSession; import org.bigbluebutton.air.video.models.VideoProfile; import org.bigbluebutton.air.video.models.WebcamChangeEnum; import org.bigbluebutton.air.video.models.WebcamStreamInfo; - import robotlegs.bender.bundles.mvcs.Mediator; + import robotlegs.bender.bundles.mvcs.Mediator; public class WebcamDockMediator extends Mediator { @@ -18,6 +19,9 @@ package org.bigbluebutton.air.video.views { [Inject] public var userSession:IUserSession; + [Inject] + public var conferenceParameters:IConferenceParameters; + override public function initialize():void { meetingData.webcams.webcamChangeSignal.add(onWebcamChangeSignal); @@ -65,7 +69,7 @@ package org.bigbluebutton.air.video.views { private function startWebcam(webcam:WebcamStreamInfo):void { var videoProfile:VideoProfile = userSession.videoProfileManager.getVideoProfileByStreamName(webcam.streamId); if (videoProfile) { - view.startStream(userSession.videoConnection.connection, webcam.name, webcam.streamId, webcam.userId, videoProfile.width, videoProfile.height); + view.startStream(userSession.videoConnection.connection, webcam.name, webcam.streamId, webcam.userId, videoProfile.width, videoProfile.height, conferenceParameters.meetingID, conferenceParameters.authToken, conferenceParameters.externUserID); meetingData.webcams.viewedWebcamStream = webcam.streamId; } } diff --git a/clients/flash/air-client/src/org/bigbluebutton/air/voice/services/VoiceConnection.as b/clients/flash/air-client/src/org/bigbluebutton/air/voice/services/VoiceConnection.as index b6f4d5dae3576c3315bc98399a5776471ad6fdb3..711842d5cecbc0d486e738d1b8dc32a4bf71fa37 100755 --- a/clients/flash/air-client/src/org/bigbluebutton/air/voice/services/VoiceConnection.as +++ b/clients/flash/air-client/src/org/bigbluebutton/air/voice/services/VoiceConnection.as @@ -13,11 +13,11 @@ package org.bigbluebutton.air.voice.services { import org.bigbluebutton.air.main.models.IUserSession; import org.bigbluebutton.air.main.models.LockSettings2x; import org.bigbluebutton.air.user.models.UserRole; + import org.bigbluebutton.air.util.ConnUtil; import org.bigbluebutton.air.voice.commands.MicrophoneMuteSignal; - import org.bigbluebutton.air.voice.commands.ShareMicrophoneSignal; import org.bigbluebutton.air.voice.models.VoiceUser; import org.osflash.signals.ISignal; - import org.osflash.signals.Signal; + import org.osflash.signals.Signal; public class VoiceConnection extends DefaultConnectionCallback implements IVoiceConnection { public const LOG:String = "VoiceConnection::"; @@ -50,6 +50,8 @@ package org.bigbluebutton.air.voice.services { protected var _conferenceParameters:IConferenceParameters; + private var _connectionId : String; + public function VoiceConnection() { } @@ -116,8 +118,9 @@ package org.bigbluebutton.air.voice.services { // we don't use scope in the voice communication (many hours lost on it) _conferenceParameters = confParams; _username = encodeURIComponent(confParams.internalUserID + "-bbbID-" + confParams.username); + _connectionId = ConnUtil.generateConnId(); trace("Voice app connect"); - baseConnection.connect(_applicationURI, confParams.meetingID, confParams.externUserID, _username, confParams.authToken); + baseConnection.connect(_applicationURI, confParams.meetingID, confParams.externUserID, _username, confParams.authToken, _connectionId); } public function disconnect(onUserCommand:Boolean):void { diff --git a/labs/bbb-webrtc-sfu/Dockerfile b/labs/bbb-webrtc-sfu/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..ebba290ee6887c5df9ec7f5424a8fe6deb40fb77 --- /dev/null +++ b/labs/bbb-webrtc-sfu/Dockerfile @@ -0,0 +1,17 @@ +FROM node:8 + +ADD . /source +RUN cp /source/config/default.example.yml /source/config/production.yml + +ENV NODE_ENV production + +RUN cd /source \ + && mv docker-entrypoint.sh /usr/local/bin/ \ + && npm install \ + && npm cache clear --force + +WORKDIR /source + +EXPOSE 3008 + +CMD [ "docker-entrypoint.sh" ] diff --git a/labs/bbb-webrtc-sfu/config/custom-environment-variables.yml b/labs/bbb-webrtc-sfu/config/custom-environment-variables.yml new file mode 100644 index 0000000000000000000000000000000000000000..074de09626d59cbb357795d1c39eb81d35f51044 --- /dev/null +++ b/labs/bbb-webrtc-sfu/config/custom-environment-variables.yml @@ -0,0 +1,7 @@ +kurentoUrl: KURENTO_URL +kurentoIp: KURENTO_IP +redisHost: REDIS_HOST +freeswitch: + ip: FREESWITCH_IP +log: + level: LOG_LEVEL diff --git a/labs/bbb-webrtc-sfu/config/default.example.yml b/labs/bbb-webrtc-sfu/config/default.example.yml index 565d12ea97d7aca479ddb84eeb91d9125cba45f6..7c63f131cbd443fbc9421c78f950b63283848264 100644 --- a/labs/bbb-webrtc-sfu/config/default.example.yml +++ b/labs/bbb-webrtc-sfu/config/default.example.yml @@ -18,9 +18,14 @@ to-akka: "to-akka-apps-redis-channel" from-akka: "from-akka-apps-redis-channel" common-message-version: "2.x" webcam-force-h264: true +webcam-preferred-h264-profile: "42e01f" +# Target bitrate (kbps) for webcams. Value 0 leaves it unconstrained. +webcam-target-bitrate: 300 screenshare-force-h264: true screenshare-preferred-h264-profile: "42e01f" screenshareKeyframeInterval: 2 +# Target bitrate (kbps) for screenshare. Value 0 leaves it unconstrained. +screenshare-target-bitrate: 0 recordScreenSharing: true recordWebcams: false @@ -32,7 +37,7 @@ recordingFormat: 'mkv' redisExpireTime: 1209600 # 14 days as per the akka keys freeswitch: - ip: '' + ip: 'FREESWITCH_IP' port: '5066' log: diff --git a/labs/bbb-webrtc-sfu/docker-entrypoint.sh b/labs/bbb-webrtc-sfu/docker-entrypoint.sh new file mode 100755 index 0000000000000000000000000000000000000000..d424e511a15e8814d6dbb9fad53b62c07729b2d6 --- /dev/null +++ b/labs/bbb-webrtc-sfu/docker-entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/bash -e + +CONTAINER_IP=$(hostname -I | awk '{print $1}') + +sed -i "s|^\(localIpAddress\):.*|\1: \"$CONTAINER_IP\"|g" config/production.yml + +if [ ! -z "$KURENTO_NAME" ]; then + export KURENTO_IP=$(getent hosts $KURENTO_NAME | awk '{ print $1 }') +fi + +npm start diff --git a/labs/bbb-webrtc-sfu/lib/audio/AudioManager.js b/labs/bbb-webrtc-sfu/lib/audio/AudioManager.js index c72815e3f33f5d72863306488c21d4b3ec44de17..8997af06d0b77288cdf28bcbcc369eb8a8b90680 100755 --- a/labs/bbb-webrtc-sfu/lib/audio/AudioManager.js +++ b/labs/bbb-webrtc-sfu/lib/audio/AudioManager.js @@ -48,7 +48,7 @@ module.exports = class AudioManager extends BaseManager { } } - _onMessage(message) { + async _onMessage(message) { Logger.debug(this._logPrefix, 'Received message [' + message.id + '] from connection', message.connectionId); let session; @@ -62,24 +62,27 @@ module.exports = class AudioManager extends BaseManager { switch (message.id) { case 'start': + try { + if (!session) { + session = new Audio(this._bbbGW, connectionId, voiceBridge); + } - if (!session) { - session = new Audio(this._bbbGW, connectionId, voiceBridge); - } + this._meetings[message.internalMeetingId] = sessionId; + this._sessions[sessionId] = session; - this._meetings[message.internalMeetingId] = sessionId; - this._sessions[sessionId] = session; + const { sdpOffer, caleeName, userId, userName } = message; - // starts audio session by sending sessionID, websocket and sdpoffer - session.start(sessionId, connectionId, message.sdpOffer, message.caleeName, message.userId, message.userName, (error, sdpAnswer) => { + // starts audio session by sending sessionID, websocket and sdpoffer + const sdpAnswer = await session.start(sessionId, connectionId, sdpOffer, caleeName, userId, userName); Logger.info(this._logPrefix, "Started presenter ", sessionId, " for connection", connectionId); - Logger.debug(this._logPrefix, "SDP answer was", sdpAnswer); - if (error) { - const errorMessage = this._handleError(this._logPrefix, connectionId, callerName, C.RECV_ROLE, error); - return this._bbbGW.publish(JSON.stringify({ - ...errorMessage + + session.once(C.MEDIA_SERVER_OFFLINE, async (event) => { + const errorMessage = this._handleError(this._logPrefix, connectionId, caleeName, C.RECV_ROLE, errors.MEDIA_SERVER_OFFLINE); + errorMessage.id = 'webRTCAudioError'; + this._bbbGW.publish(JSON.stringify({ + ...errorMessage, }), C.FROM_AUDIO); - } + }); this._bbbGW.publish(JSON.stringify({ connectionId: connectionId, @@ -90,7 +93,13 @@ module.exports = class AudioManager extends BaseManager { }), C.FROM_AUDIO); Logger.info(this._logPrefix, "Sending startResponse to user", sessionId, "for connection", session._id); - }); + } catch (err) { + const errorMessage = this._handleError(this._logPrefix, connectionId, message.caleeName, C.RECV_ROLE, err); + errorMessage.id = 'webRTCAudioError'; + return this._bbbGW.publish(JSON.stringify({ + ...errorMessage + }), C.FROM_AUDIO); + } break; case 'stop': diff --git a/labs/bbb-webrtc-sfu/lib/audio/audio.js b/labs/bbb-webrtc-sfu/lib/audio/audio.js index 8e6667e65adcd386940cdf5c6961b3e421b3f8be..3f877245b6b1f468fcf84ff046c283da97d8fd64 100644 --- a/labs/bbb-webrtc-sfu/lib/audio/audio.js +++ b/labs/bbb-webrtc-sfu/lib/audio/audio.js @@ -155,53 +155,56 @@ module.exports = class Audio extends BaseProvider { } } - async start (sessionId, connectionId, sdpOffer, caleeName, userId, userName, callback) { - Logger.info(LOG_PREFIX, "Starting audio instance for", this.id); - let sdpAnswer; + start (sessionId, connectionId, sdpOffer, caleeName, userId, userName) { + return new Promise(async (resolve, reject) => { + Logger.info(LOG_PREFIX, "Starting audio instance for", this.id); + let sdpAnswer; - // Storing the user data to be used by the pub calls - let user = {userId: userId, userName: userName}; - this.addUser(connectionId, user); + // Storing the user data to be used by the pub calls + let user = {userId: userId, userName: userName}; + this.addUser(connectionId, user); - try { - if (!this.sourceAudioStarted) { - this.userId = await this.mcs.join(this.voiceBridge, 'SFU', {}); - Logger.info(LOG_PREFIX, "MCS join for", this.id, "returned", this.userId); + try { + if (!this.sourceAudioStarted) { + this.userId = await this.mcs.join(this.voiceBridge, 'SFU', {}); + Logger.info(LOG_PREFIX, "MCS join for", this.id, "returned", this.userId); - const ret = await this.mcs.publish(this.userId, + const ret = await this.mcs.publish(this.userId, this.voiceBridge, 'RtpEndpoint', {descriptor: sdpOffer, adapter: 'Freeswitch', name: caleeName}); - this.sourceAudio = ret.sessionId; - this.mcs.on('MediaEvent' + this.sourceAudio, this.mediaState.bind(this)); - this.sourceAudioStarted = true; + this.sourceAudio = ret.sessionId; + this.mcs.on('MediaEvent' + this.sourceAudio, this.mediaState.bind(this)); + this.mcs.on('ServerState' + this.sourceAudio, this.serverState.bind(this)); + this.sourceAudioStarted = true; - Logger.info(LOG_PREFIX, "MCS publish for user", this.userId, "returned", this.sourceAudio); - } + Logger.info(LOG_PREFIX, "MCS publish for user", this.userId, "returned", this.sourceAudio); + } - const retSubscribe = await this.mcs.subscribe(this.userId, + const retSubscribe = await this.mcs.subscribe(this.userId, this.sourceAudio, 'WebRtcEndpoint', {descriptor: sdpOffer, adapter: 'Kurento'}); - this.audioEndpoints[connectionId] = retSubscribe.sessionId; + this.audioEndpoints[connectionId] = retSubscribe.sessionId; - sdpAnswer = retSubscribe.answer; - this.flushCandidatesQueue(connectionId); + sdpAnswer = retSubscribe.answer; + this.flushCandidatesQueue(connectionId); - this.mcs.on('MediaEvent' + retSubscribe.sessionId, (event) => { - this.mediaStateWebRtc(event, connectionId) - }); + this.mcs.on('MediaEvent' + retSubscribe.sessionId, (event) => { + this.mediaStateWebRtc(event, connectionId) + }); - Logger.info(LOG_PREFIX, "MCS subscribe for user", this.userId, "returned", retSubscribe.sessionId); + Logger.info(LOG_PREFIX, "MCS subscribe for user", this.userId, "returned", retSubscribe.sessionId); - return callback(null, sdpAnswer); - } - catch (err) { - return callback(this._handleError(LOG_PREFIX, err, "recv", userId)); - } - }; + return resolve(sdpAnswer); + } + catch (err) { + return reject(this._handleError(LOG_PREFIX, err, "recv", userId)); + } + }); + } async stopListener(id) { const listener = this.audioEndpoints[id]; @@ -255,7 +258,7 @@ module.exports = class Audio extends BaseProvider { return Promise.resolve(); } catch (err) { - reject(this._handleError(LOG_PREFIX, err, "recv", this.userId)); + throw (this._handleError(LOG_PREFIX, err, "recv", this.userId)); } }; @@ -310,4 +313,17 @@ module.exports = class Audio extends BaseProvider { }), C.FROM_AUDIO); this.removeUser(connectionId); }; + + serverState (event) { + const { eventTag: { code } } = { ...event }; + switch (code) { + case C.MEDIA_SERVER_OFFLINE: + Logger.error(LOG_PREFIX, "Provider received MEDIA_SERVER_OFFLINE event"); + this.emit(C.MEDIA_SERVER_OFFLINE, event); + break; + + default: + Logger.warn(LOG_PREFIX, "Unknown server state", event); + } + } }; diff --git a/labs/bbb-webrtc-sfu/lib/bbb/pubsub/bbb-gw.js b/labs/bbb-webrtc-sfu/lib/bbb/pubsub/bbb-gw.js index 0d57aa7c47b4a38ac2444f6493d9147698596cae..f55ba1153d91ced626e16620200bcb63c17a2a58 100644 --- a/labs/bbb-webrtc-sfu/lib/bbb/pubsub/bbb-gw.js +++ b/labs/bbb-webrtc-sfu/lib/bbb/pubsub/bbb-gw.js @@ -157,9 +157,4 @@ module.exports = class BigBlueButtonGW extends EventEmitter { setEventEmitter (emitter) { this.emitter = emitter; } - - _onServerResponse(data) { - // Here this is the 'ws' instance - this.sendMessage(data); - } } diff --git a/labs/bbb-webrtc-sfu/lib/connection-manager/WebsocketConnectionManager.js b/labs/bbb-webrtc-sfu/lib/connection-manager/WebsocketConnectionManager.js index 8b1727351088dada9d62c1efd02229a5a44e6ef6..dafc0040732956e8f9f2e280e0762a9a4db49e7a 100644 --- a/labs/bbb-webrtc-sfu/lib/connection-manager/WebsocketConnectionManager.js +++ b/labs/bbb-webrtc-sfu/lib/connection-manager/WebsocketConnectionManager.js @@ -7,25 +7,6 @@ const Logger = require('../utils/Logger'); // ID counter let connectionIDCounter = 0; -ws.prototype.sendMessage = function(json) { - - let websocket = this; - - if (this._closeCode === 1000) { - Logger.error("[WebsocketConnectionManager] Websocket closed, not sending"); - this._errorCallback("[WebsocketConnectionManager] Error: not opened"); - } - - return this.send(JSON.stringify(json), function(error) { - if(error) { - Logger.error('[WebsocketConnectionManager] Websocket error "' + error + '" on message "' + json.id + '"'); - - websocket._errorCallback(error); - } - }); - -}; - module.exports = class WebsocketConnectionManager { constructor (server, path) { this.wss = new ws.Server({ @@ -48,7 +29,7 @@ module.exports = class WebsocketConnectionManager { const connectionId = data? data.connectionId : null; const ws = this.webSockets[connectionId]; if (ws) { - ws.sendMessage(data); + this.sendMessage(ws, data); } } @@ -77,7 +58,7 @@ module.exports = class WebsocketConnectionManager { message = JSON.parse(data); if (message.id === 'ping') { - ws.sendMessage({id: 'pong'}); + this.sendMessage(ws, {id: 'pong'}); return; } @@ -134,4 +115,20 @@ module.exports = class WebsocketConnectionManager { delete this.webSockets[ws.id]; } + + sendMessage (ws, json) { + + if (ws._closeCode === 1000) { + Logger.error("[WebsocketConnectionManager] Websocket closed, not sending"); + this._onError(ws, "[WebsocketConnectionManager] Error: not opened"); + } + + return ws.send(JSON.stringify(json), (error) => { + if(error) { + Logger.error('[WebsocketConnectionManager] Websocket error "' + error + '" on message "' + json.id + '"'); + + this._onError(ws, error); + } + }); + } } diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/adapters/freeswitch/AudioHandler.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/adapters/freeswitch/AudioHandler.js index e456e87412fec5b869a00e8bb742632a396221cb..e5028f2a4b0bd0b2519d8a7af356f09b5c2a5fd7 100644 --- a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/adapters/freeswitch/AudioHandler.js +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/adapters/freeswitch/AudioHandler.js @@ -1,7 +1,7 @@ 'use strict'; - const Logger = require('../../../../utils/Logger'); const config = require('config'); +const KURENTO_IP = config.get('kurentoIp'); var kmh = function(sdp) { this.endpointSdp = sdp; @@ -77,6 +77,7 @@ kmh.prototype.AudioHandler.prototype = { if(this.endpointSdp === null) { Logger.info("[mcs-audio-handler] Processing SDP for Kurento RTP endpoint", this.rtp); this.endpointSdp = await this.Kurento.processOffer(this.rtp, this.remote_sdp); + this.endpointSdp = this.endpointSdp.replace(/(IP4\s[0-9.]*)/g, 'IP4 ' + KURENTO_IP); } this.sdp = this.endpointSdp; this.timeout = setTimeout(function () { diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/adapters/freeswitch/freeswitch.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/adapters/freeswitch/freeswitch.js index 9a8028bcbcc19b339fbec693d8f16de3453adb69..10f8e5c254f82842aaf81658583e1e5d3dc8332b 100644 --- a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/adapters/freeswitch/freeswitch.js +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/adapters/freeswitch/freeswitch.js @@ -24,6 +24,10 @@ module.exports = class Freeswitch extends EventEmitter { this._sessions = {}; this._rtpConverters = {}; this._Kurento = new Kurento(config.get('kurentoUrl')); + this._Kurento.on(C.ERROR.MEDIA_SERVER_OFFLINE, () => { + this.emit(C.ERROR.MEDIA_SERVER_OFFLINE); + }); + instance = this; } diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SdpSession.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SdpSession.js index a1eac49832889e4b81062107c9d48264a8b88617..7b1345fdb7ccf5e13e95513d6a84f1c70db8b325 100644 --- a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SdpSession.js +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SdpSession.js @@ -60,13 +60,19 @@ module.exports = class SdpSession extends MediaSession { return reject(this._handleError(C.ERROR.MEDIA_NO_AVAILABLE_CODEC)); } + const { targetBitrate } = this._options; + + if (answer && targetBitrate && targetBitrate !== '0') { + this._answer.addBandwidth('video', targetBitrate); + } + if (this._type !== 'WebRtcEndpoint') { this._offer.replaceServerIpv4(kurentoIp); - return resolve(answer); + return resolve(this._answer? this._answer._plainSdp : null); } await this._MediaServer.gatherCandidates(this._mediaElement); - resolve(answer); + resolve(this._answer._plainSdp); } catch (err) { return reject(this._handleError(err)); diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/SdpWrapper.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/SdpWrapper.js index 6f35e3b7c7d64b083b752d6a62c8652da012d0b6..a19912bdba9d2002543a2f01d5e57d050c1c6169 100644 --- a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/SdpWrapper.js +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/SdpWrapper.js @@ -54,6 +54,20 @@ module.exports = class SdpWrapper { return this._mediaCapabilities.hasAvailableAudioCodec; } + addBandwidth (type, bw) { + // Bandwidth format + // { type: 'TIAS or AS', limit: 2048000 } + for(var ml of this._jsonSdp.media) { + if(ml.type === type ) { + ml['bandwidth'] = []; + ml.bandwidth.push({ type: 'TIAS', limit: (bw >>> 0) * 1000 }); + ml.bandwidth.push({ type: 'AS', limit: bw }); + } + } + + this._plainSdp = transform.write(this._jsonSdp); + } + /** * Given a SDP, test if there is an audio description in it * @return {boolean} true if there is more than one video description, else false diff --git a/labs/bbb-webrtc-sfu/lib/screenshare/screenshare.js b/labs/bbb-webrtc-sfu/lib/screenshare/screenshare.js index f2eaa8cc957533ac649516f195c1612846a281ce..aeab9fc3afc1e2860d09a8697cdbcc0f2b12952c 100644 --- a/labs/bbb-webrtc-sfu/lib/screenshare/screenshare.js +++ b/labs/bbb-webrtc-sfu/lib/screenshare/screenshare.js @@ -21,6 +21,7 @@ const kurentoIp = config.get('kurentoIp'); const localIpAddress = config.get('localIpAddress'); const FORCE_H264 = config.get('screenshare-force-h264'); const PREFERRED_H264_PROFILE = config.get('screenshare-preferred-h264-profile'); +const SCREENSHARE_TARGET_BITRATE = config.get('screenshare-target-bitrate'); const SHOULD_RECORD = config.get('recordScreenSharing'); const KEYFRAME_INTERVAL = config.get('screenshareKeyframeInterval'); const LOG_PREFIX = "[screenshare]"; @@ -274,7 +275,7 @@ module.exports = class Screenshare extends BaseProvider { _startPresenter (sdpOffer) { return new Promise(async (resolve, reject) => { try { - const retSource = await this.mcs.publish(this.mcsUserId, this._meetingId, 'WebRtcEndpoint', {descriptor: sdpOffer}); + const retSource = await this.mcs.publish(this.mcsUserId, this._meetingId, 'WebRtcEndpoint', {descriptor: sdpOffer, targetBitrate: SCREENSHARE_TARGET_BITRATE }); this._presenterEndpoint = retSource.sessionId; sharedScreens[this._voiceBridge] = this._presenterEndpoint; diff --git a/labs/bbb-webrtc-sfu/lib/video/video.js b/labs/bbb-webrtc-sfu/lib/video/video.js index 9250d86d90774f12d12590a6f0526ffe9b28171e..41e2f7ccbb34eecdaba18e1a2f2208345e24dd61 100644 --- a/labs/bbb-webrtc-sfu/lib/video/video.js +++ b/labs/bbb-webrtc-sfu/lib/video/video.js @@ -7,8 +7,10 @@ const C = require('../bbb/messages/Constants'); const Logger = require('../utils/Logger'); const Messaging = require('../bbb/messages/Messaging'); const h264_sdp = require('../h264-sdp'); +const PREFERRED_H264_PROFILE = config.get('webcam-preferred-h264-profile'); const BaseProvider = require('../base/BaseProvider'); const FORCE_H264 = config.get('webcam-force-h264'); +const WEBCAM_TARGET_BITRATE = config.get('webcam-target-bitrate'); const SHOULD_RECORD = config.get('recordWebcams'); const LOG_PREFIX = "[video]"; @@ -230,7 +232,7 @@ module.exports = class Video extends BaseProvider { // Force H264 if (FORCE_H264) { - sdpOffer = h264_sdp.transform(sdpOffer); + sdpOffer = h264_sdp.transform(sdpOffer, PREFERRED_H264_PROFILE); } // Start the recording process @@ -259,7 +261,7 @@ module.exports = class Video extends BaseProvider { return new Promise(async (resolve, reject) => { try { if (this.shared) { - let { answer, sessionId } = await this.mcs.publish(this.userId, this.meetingId, type, { descriptor }); + let { answer, sessionId } = await this.mcs.publish(this.userId, this.meetingId, type, { descriptor, targetBitrate: WEBCAM_TARGET_BITRATE }); this.mediaId = sessionId; sharedWebcams[this.id] = this.mediaId; return resolve(answer); diff --git a/labs/bbb-webrtc-sfu/package-lock.json b/labs/bbb-webrtc-sfu/package-lock.json index 590ed3dd4556f3d7a6513cc917e19214583c3ddc..667d138dcc4c024982bf66cb8ff9ac482ea1586c 100644 --- a/labs/bbb-webrtc-sfu/package-lock.json +++ b/labs/bbb-webrtc-sfu/package-lock.json @@ -142,32 +142,45 @@ "inherits": "2.0.3", "kurento-client-core": "git+https://github.com/mconf/kurento-client-core-js.git#3bfcff9cb21430a4f451239100b4c306b9705757", "kurento-client-elements": "git+https://github.com/mconf/kurento-client-elements-js.git#8db04d5a31c299c9c6bacdfbc8fb358ad1c80fbb", - "kurento-client-filters": "github:Kurento/kurento-client-filters-js#51308da53e432a2db9559dcdb308d87951417bf0", + "kurento-client-filters": "github:Kurento/kurento-client-filters-js#67d43d4fca03c6002015448973c3e6d82e14cb3c", "kurento-jsonrpc": "github:Kurento/kurento-jsonrpc-js#1cdb36884ca8f096d21c335f28129a5449214e7b", "minimist": "1.2.0", "promise": "7.1.1", "promisecallback": "0.0.4", "reconnect-ws": "github:KurentoForks/reconnect-ws#f287385d75861654528c352e60221f95c9209f8a" - } - }, - "kurento-client-core": { - "version": "git+https://github.com/mconf/kurento-client-core-js.git#3bfcff9cb21430a4f451239100b4c306b9705757" - }, - "kurento-client-elements": { - "version": "git+https://github.com/mconf/kurento-client-elements-js.git#8db04d5a31c299c9c6bacdfbc8fb358ad1c80fbb" - }, - "kurento-client-filters": { - "version": "github:Kurento/kurento-client-filters-js#51308da53e432a2db9559dcdb308d87951417bf0" - }, - "kurento-jsonrpc": { - "version": "github:Kurento/kurento-jsonrpc-js#1cdb36884ca8f096d21c335f28129a5449214e7b", - "requires": { - "bufferutil": "1.2.1", - "inherits": "2.0.3", - "utf-8-validate": "1.2.2", - "ws": "1.1.5" }, "dependencies": { + "kurento-client-core": { + "version": "git+https://github.com/mconf/kurento-client-core-js.git#3bfcff9cb21430a4f451239100b4c306b9705757" + }, + "kurento-client-elements": { + "version": "git+https://github.com/mconf/kurento-client-elements-js.git#8db04d5a31c299c9c6bacdfbc8fb358ad1c80fbb" + }, + "kurento-client-filters": { + "version": "github:Kurento/kurento-client-filters-js#67d43d4fca03c6002015448973c3e6d82e14cb3c" + }, + "kurento-jsonrpc": { + "version": "github:Kurento/kurento-jsonrpc-js#1cdb36884ca8f096d21c335f28129a5449214e7b", + "requires": { + "bufferutil": "1.2.1", + "inherits": "2.0.3", + "utf-8-validate": "1.2.2", + "ws": "1.1.5" + } + }, + "reconnect-core": { + "version": "github:KurentoForks/reconnect-core#921d43e91578abb2fb2613f585c010c1939cf734", + "requires": { + "backoff": "2.3.0" + } + }, + "reconnect-ws": { + "version": "github:KurentoForks/reconnect-ws#f287385d75861654528c352e60221f95c9209f8a", + "requires": { + "reconnect-core": "github:KurentoForks/reconnect-core#921d43e91578abb2fb2613f585c010c1939cf734", + "websocket-stream": "0.5.1" + } + }, "ws": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz", @@ -256,19 +269,6 @@ "nanoid": "1.0.1" } }, - "reconnect-core": { - "version": "github:KurentoForks/reconnect-core#921d43e91578abb2fb2613f585c010c1939cf734", - "requires": { - "backoff": "2.3.0" - } - }, - "reconnect-ws": { - "version": "github:KurentoForks/reconnect-ws#f287385d75861654528c352e60221f95c9209f8a", - "requires": { - "reconnect-core": "github:KurentoForks/reconnect-core#921d43e91578abb2fb2613f585c010c1939cf734", - "websocket-stream": "0.5.1" - } - }, "redis": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz", diff --git a/labs/docker/Makefile b/labs/docker/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..63d7fb33b4acdddd21284585f94c651199b5ecf5 --- /dev/null +++ b/labs/docker/Makefile @@ -0,0 +1,57 @@ +SHELL=/bin/bash + +# in order to build images for bigbluebutton/bigbluebutton-docker, run: +# IMAGE_ACCOUNT=bigbluebutton IMAGE_REPO=bigbluebutton-docker make release + +# build options +BUILD_REVISION=`git rev-parse --short HEAD` +BUILD_DIR_BASE=`git rev-parse --git-dir`/.. +BUILD_VERSION?= +BUILD_IMAGE=0 +IMAGE_ACCOUNT?= +IMAGE_REPO?= +IMAGE_TAG=latest +TAG_REVISION?=0 + +all: release + +image: + -cd $(DIR) && docker build -t $(IMAGE_NAME):$(IMAGE_TAG) $(BUILD_ARGS) . + if [ "$(BUILD_IMAGE)" == "0" ]; then \ + if [ "$(IMAGE_ACCOUNT)" != "" ]; then \ + if [ "$(IMAGE_REPO)" != "" ]; then \ + docker tag $(IMAGE_NAME):$(IMAGE_TAG) $(IMAGE_ACCOUNT)/$(IMAGE_REPO):$(IMAGE_NAME); \ + if [ "$(TAG_REVISION)" == "1" ]; then \ + docker tag $(IMAGE_NAME):$(IMAGE_TAG) $(IMAGE_ACCOUNT)/$(IMAGE_REPO):$(IMAGE_NAME)-$(BUILD_REVISION); \ + fi \ + fi \ + else \ + if [ "$(IMAGE_REPO)" != "" ]; then \ + docker tag $(IMAGE_NAME):$(IMAGE_TAG) $(IMAGE_REPO):$(IMAGE_NAME); \ + if [ "$(TAG_REVISION)" == "1" ]; then \ + docker tag $(IMAGE_NAME):$(IMAGE_TAG) $(IMAGE_REPO):$(IMAGE_NAME)-$(BUILD_REVISION); \ + fi \ + else \ + if [ "$(TAG_REVISION)" == "1" ]; then \ + docker tag $(IMAGE_NAME):$(IMAGE_TAG) $(IMAGE_NAME):$(BUILD_REVISION); \ + fi \ + fi \ + fi \ + fi + +release: + make image DIR=$(BUILD_DIR_BASE)/labs/docker/sbt IMAGE_NAME=sbt IMAGE_TAG=0.13.8 BUILD_IMAGE=1 + make image DIR=$(BUILD_DIR_BASE)/bbb-common-message IMAGE_NAME=bbb-common-message BUILD_ARGS="--build-arg COMMON_VERSION=0.0.1-SNAPSHOT" BUILD_IMAGE=1 + make image DIR=$(BUILD_DIR_BASE)/bbb-common-web IMAGE_NAME=bbb-common-web BUILD_ARGS="--build-arg COMMON_VERSION=0.0.1-SNAPSHOT" BUILD_IMAGE=1 + make image DIR=$(BUILD_DIR_BASE)/bbb-fsesl-client IMAGE_NAME=bbb-fsesl-client BUILD_ARGS="--build-arg COMMON_VERSION=0.0.1-SNAPSHOT" BUILD_IMAGE=1 + make image DIR=$(BUILD_DIR_BASE)/akka-bbb-apps IMAGE_NAME=bbb-apps-akka BUILD_ARGS="--build-arg COMMON_VERSION=0.0.1-SNAPSHOT" + make image DIR=$(BUILD_DIR_BASE)/akka-bbb-fsesl IMAGE_NAME=bbb-fsesl-akka BUILD_ARGS="--build-arg COMMON_VERSION=0.0.1-SNAPSHOT" + make image DIR=$(BUILD_DIR_BASE)/bigbluebutton-web IMAGE_NAME=bbb-web BUILD_ARGS="--build-arg COMMON_VERSION=0.0.1-SNAPSHOT" + make image DIR=$(BUILD_DIR_BASE)/bigbluebutton-html5 IMAGE_NAME=bbb-html5 + make image DIR=$(BUILD_DIR_BASE)/labs/bbb-webrtc-sfu IMAGE_NAME=bbb-webrtc-sfu + make image DIR=$(BUILD_DIR_BASE)/bbb-webhooks IMAGE_NAME=bbb-webhooks + make image DIR=$(BUILD_DIR_BASE)/labs/docker/kurento IMAGE_NAME=bbb-kurento + make image DIR=$(BUILD_DIR_BASE)/labs/docker/freeswitch IMAGE_NAME=bbb-freeswitch + make image DIR=$(BUILD_DIR_BASE)/labs/docker/nginx IMAGE_NAME=bbb-nginx + make image DIR=$(BUILD_DIR_BASE)/labs/docker/coturn IMAGE_NAME=bbb-coturn + make image DIR=$(BUILD_DIR_BASE)/bbb-lti IMAGE_NAME=bbb-lti diff --git a/labs/docker/README.md b/labs/docker/README.md new file mode 100644 index 0000000000000000000000000000000000000000..654d2201a6633e6810e84b566893f62e4d4db29b --- /dev/null +++ b/labs/docker/README.md @@ -0,0 +1,274 @@ +IMPORTANT: this is a work in progress! + +# Purpose + +The purpose of this repo is to get BigBlueButton working in a multi-container Docker configuration over a single port, then to deploy and scale it using Kubernetes + +# Launching BBB via Docker + +## Prerequisites + +#### Ensure you have the latest version of Docker-CE by following the install steps + +Ubuntu: https://docs.docker.com/install/linux/docker-ce/ubuntu/ + +Fedora: https://docs.docker.com/install/linux/docker-ce/fedora/ + +#### Make sure to also do the post install steps + +https://docs.docker.com/install/linux/linux-postinstall/ + +#### Install docker-compose + +Ubuntu: +``` +sudo apt-get install docker-compose +``` + +Fedora: +``` +sudo dnf install docker-compose +``` + +## Build all docker images + +#### Build all docker images with one command +``` +cd labs/docker/ +make release +``` + +#### Verify that you have all the necessary images +``` +docker images +``` + +You should see: +* sbt +* bbb-common-message +* bbb-common-web +* bbb-fsesl-client +* bbb-apps-akka +* bbb-fsesl-akka +* bbb-web +* bbb-html5 +* bbb-webrtc-sfu +* bbb-webhooks +* bbb-kurento +* bbb-freeswitch +* bbb-nginx +* bbb-coturn +* bbb-lti + + +In the event that any of the above images are missing, you'll need to build them individually + +## Build images individually + +sbt is needed to build the Scala components +``` +cd labs/docker/sbt/ +docker build -t 'sbt:0.13.8' . +``` + +Build libraries +``` +cd bbb-common-message/ +docker build -t 'bbb-common-message' --build-arg COMMON_VERSION=0.0.1-SNAPSHOT . + +cd bbb-common-web/ +docker build -t 'bbb-common-web' --build-arg COMMON_VERSION=0.0.1-SNAPSHOT . + +cd bbb-fsesl-client/ +docker build -t 'bbb-fsesl-client' --build-arg COMMON_VERSION=0.0.1-SNAPSHOT . +``` + +Build akka components +``` +cd akka-bbb-apps/ +docker build -t bbb-apps-akka --build-arg COMMON_VERSION=0.0.1-SNAPSHOT . + +# Not needed since we're setting up HTML5 only +cd akka-bbb-transcode/ +docker build -t bbb-transcode --build-arg COMMON_VERSION=0.0.1-SNAPSHOT . + +cd akka-bbb-fsesl/ +docker build -t bbb-fsesl-akka --build-arg COMMON_VERSION=0.0.1-SNAPSHOT . +``` + +Build bbb-web +``` +cd bigbluebutton-web/ +docker build -t bbb-web --build-arg COMMON_VERSION=0.0.1-SNAPSHOT . +``` + +Build bbb-html5 +``` +cd bigbluebutton-html5/ +docker build -t bbb-html5 . +``` + +Build bbb-webrtc-sfu +``` +cd labs/bbb-webrtc-sfu/ +docker build -t bbb-webrtc-sfu . +``` + +Build bbb-webhooks +``` +cd bbb-webhooks/ +docker build -t bbb-webhooks . +``` + +Build Kurento Media Server +``` +cd labs/docker/kurento/ +docker build -t bbb-kurento . +``` + +Build FreeSWITCH +``` +cd labs/docker/freeswitch/ +docker build -t bbb-freeswitch . +``` + +Build nginx +``` +cd labs/docker/nginx/ +docker build -t bbb-nginx . +``` + +Build coturn +``` +cd labs/docker/coturn +docker build -t bbb-coturn . +``` + +(Optional) Build bbb-lti +``` +cd bbb-lti/ +docker build -t bbb-lti . +``` + +## Setup + +#### Export your configuration as environment variables +NOTE: replace the example SERVER_DOMAIN's value with your own FQDN +``` +export SERVER_DOMAIN=docker.bigbluebutton.org +export EXTERNAL_IP=$(dig +short $SERVER_DOMAIN | grep '^[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*$' | head -n 1) +export SHARED_SECRET=`openssl rand -hex 16` +export COTURN_REST_SECRET=`openssl rand -hex 16` +export SECRET_KEY_BASE=`openssl rand -hex 64` +export SCREENSHARE_EXTENSION_KEY=akgoaoikmbmhcopjgakkcepdgdgkjfbc +export SCREENSHARE_EXTENSION_LINK=https://chrome.google.com/webstore/detail/bigbluebutton-screenshare/akgoaoikmbmhcopjgakkcepdgdgkjfbc +export TAG_PREFIX= +export TAG_SUFFIX= +``` + +#### Create a volume for the SSL certs +``` +docker volume create docker_ssl-conf +``` + +#### Generate SSL certs +``` +docker run --rm -p 80:80 -v docker_ssl-conf:/etc/letsencrypt -it certbot/certbot certonly --non-interactive --register-unsafely-without-email --agree-tos --expand --domain $SERVER_DOMAIN --standalone + +# certificate path: docker_ssl-conf/live/$SERVER_DOMAIN/fullchain.pem +# key path: docker_ssl-conf/live/$SERVER_DOMAIN/privkey.pem +``` +NOTE: If running on AWS, you won't be able to use the default Public DNS for your SERVER_DOMAIN as Let's Encrypt doesn't allow generating SSL certs from any *.amazonaws.com domain. Alternatively, you can create a PTR record that goes from a non-AWS FQDN to the AWS FQDN. + +#### Create a volume for the static files (optional) +``` +docker volume create docker_static +cd bigbluebutton-config/web/ +docker run -d --rm --name nginx -v docker_static:/var/www/bigbluebutton-default nginx tail -f /dev/null +docker cp . nginx:/var/www/bigbluebutton-default +docker exec -it nginx chown -R www-data:www-data /var/www/bigbluebutton-default +docker stop nginx +``` + +#### Ensure the following ports are open +* TCP/UDP 3478 +* TCP 80 +* TCP 443 + +## Run + +#### Launch everything with docker compose +``` +cd labs/docker/ +docker-compose up +``` + +#### Access your server via greenlight and create meetings + +https://<your_fqdn_here>/b + +#### To shut down and exit gracefully +``` +CTRL+C +``` + + +# Setting up a Kubernetes Cluster + +## Prerequisites + +#### Install kubeadm, kubelet, and kubectl + +https://kubernetes.io/docs/setup/independent/install-kubeadm/ + +#### Disable swap by commenting out the "swap" line in /etc/fstab, then do a reboot +``` +sudo vi /etc/fstab +sudo systemctl reboot +``` + +#### Verify swap is disabled +``` +sudo free -h +``` + +#### Install Minikube + +https://kubernetes.io/docs/tasks/tools/install-minikube/ + +#### Install VirtualBox Manager + +Ubuntu: +``` +sudo apt-get install virtualbox +``` + +Fedora: +``` +sudo dnf install virtualbox +``` + +## Setup + +#### The following kernel modules are required to avoid preflight errors and warnings during cluster setup +* ip_vs +* ip_vs_rr +* ip_vs_wrr +* ip_vs_sh + +#### Check if kernel modules are already loaded +``` +lsmod | grep ip_vs +``` + +#### Add the kernel modules (if not already loaded) +``` +sudo modprobe ip_vs +sudo modprobe ip_vs_rr +sudo modprobe ip_vs_wrr +sudo modprobe ip_vs_sh +``` + +#### Create a single master cluster with kubeadm + +https://kubernetes.io/docs/setup/independent/create-cluster-kubeadm/ diff --git a/labs/docker/coturn/Dockerfile b/labs/docker/coturn/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..aa4912ae026922b53caff6ecbef5e988f5606b1b --- /dev/null +++ b/labs/docker/coturn/Dockerfile @@ -0,0 +1,14 @@ +FROM ubuntu:16.04 + +RUN apt-get update && apt-get install -y coturn wget + +ENV DOCKERIZE_VERSION v0.6.1 +RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \ + && tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \ + && rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz + +COPY ./turnserver.conf.tmpl /etc/turnserver.conf.tmpl + +CMD [ "dockerize", \ + "-template", "/etc/turnserver.conf.tmpl:/etc/turnserver.conf", \ + "turnserver", "--syslog" ] diff --git a/labs/docker/coturn/turnserver.conf.tmpl b/labs/docker/coturn/turnserver.conf.tmpl new file mode 100644 index 0000000000000000000000000000000000000000..c9810fb8229d2ab440e91ffea1f56cfd73dc0fee --- /dev/null +++ b/labs/docker/coturn/turnserver.conf.tmpl @@ -0,0 +1,14 @@ +listening-port={{ .Env.PORT }} +min-port=49152 +max-port=65535 +fingerprint +lt-cred-mech +realm={{ .Env.SERVER_DOMAIN }} +external-ip={{ .Env.EXTERNAL_IP }} + +{{ if isTrue .Env.ENABLE_REST_API }} +use-auth-secret +static-auth-secret={{ .Env.SECRET }} +{{ else }} +user={{ .Env.LT_USER }}:{{ .Env.LT_SECRET }} +{{ end }} diff --git a/labs/docker/docker-compose.yml b/labs/docker/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..08d4ecb6644d5a3561dfcd465481a320e3cea6ab --- /dev/null +++ b/labs/docker/docker-compose.yml @@ -0,0 +1,183 @@ +version: '2' + +services: + mongo: + image: mongo:3.4 + restart: unless-stopped + + redis: + image: redis + restart: unless-stopped + + bbb-html5: + image: ${TAG_PREFIX}bbb-html5${TAG_SUFFIX} + restart: unless-stopped + depends_on: + - mongo + - redis + environment: + MONGO_URL: mongodb://mongo/bbbhtml5 + METEOR_SETTINGS_MODIFIER: ".public.kurento.wsUrl = \"wss://${SERVER_DOMAIN}/bbb-webrtc-sfu\" | .public.kurento.enableVideo = true | .public.kurento.enableScreensharing = true | .public.kurento.chromeDefaultExtensionKey = \"${SCREENSHARE_EXTENSION_KEY}\" | .public.kurento.chromeDefaultExtensionLink = \"${SCREENSHARE_EXTENSION_LINK}\" | .public.kurento.enableVideoStats = true | .public.kurento.enableListenOnly = true" + REDIS_HOST: redis + ROOT_URL: http://127.0.0.1/html5client + labels: + - "traefik.backend=bbb-html5" + - "traefik.frontend.rule=PathPrefix: /html5client,/_timesync" + + bbb-webhooks: + image: ${TAG_PREFIX}bbb-webhooks${TAG_SUFFIX} + restart: unless-stopped + depends_on: + - redis + environment: + REDIS_HOST: redis + SHARED_SECRET: ${SHARED_SECRET} + BEARER_AUTH: 1 + SERVER_DOMAIN: ${SERVER_DOMAIN} + labels: + - "traefik.backend=bbb-webhooks" + - "traefik.frontend.rule=PathPrefix: /bigbluebutton/api/hooks" + + bbb-freeswitch: + image: ${TAG_PREFIX}bbb-freeswitch${TAG_SUFFIX} + restart: unless-stopped + depends_on: + - coturn + volumes: + - media-audio:/var/freeswitch/meetings + + bbb-webrtc-sfu: + image: ${TAG_PREFIX}bbb-webrtc-sfu${TAG_SUFFIX} + restart: unless-stopped + depends_on: + - redis + - kurento + - bbb-freeswitch + environment: + KURENTO_NAME: kurento + KURENTO_URL: ws://kurento:8888/kurento + REDIS_HOST: redis + FREESWITCH_IP: bbb-freeswitch + LOG_LEVEL: debug + labels: + - "traefik.backend=bbb-webrtc-sfu" + - "traefik.frontend.rule=PathPrefix: /bbb-webrtc-sfu" + + coturn: + image: ${TAG_PREFIX}bbb-coturn${TAG_SUFFIX} + restart: unless-stopped + environment: + TURN_DOMAIN: ${SERVER_DOMAIN} + SECRET: ${COTURN_REST_SECRET} + EXTERNAL_IP: ${EXTERNAL_IP} + ENABLE_REST_API: 1 + PORT: 3478 + ports: + - 3478:3478/udp + - 3478:3478/tcp + + kurento: + image: ${TAG_PREFIX}bbb-kurento${TAG_SUFFIX} + restart: unless-stopped + volumes: + - media-video:/var/kurento/recordings + - media-screenshare:/var/kurento/screenshare + environment: + KMS_STUN_IP: ${EXTERNAL_IP} + KMS_STUN_PORT: 3478 + + bbb-apps-akka: + image: ${TAG_PREFIX}bbb-apps-akka${TAG_SUFFIX} + restart: unless-stopped + depends_on: + - redis + environment: + JAVA_OPTS: -Dredis.host=redis + + bbb-fsesl-akka: + image: ${TAG_PREFIX}bbb-fsesl-akka${TAG_SUFFIX} + restart: unless-stopped + depends_on: + - bbb-freeswitch + - redis + command: ["wait-for-it.sh", "bbb-freeswitch:8021", "--timeout=60", "--strict", "--", "/usr/share/bbb-fsesl-akka/bin/bbb-fsesl-akka"] + environment: + JAVA_OPTS: -Dredis.host=redis -Dfreeswitch.esl.host=bbb-freeswitch + + bbb-web: + image: ${TAG_PREFIX}bbb-web${TAG_SUFFIX} + restart: unless-stopped + depends_on: + - redis + volumes: + - bigbluebutton:/var/bigbluebutton + environment: + SERVER_DOMAIN: ${SERVER_DOMAIN} + SHARED_SECRET: ${SHARED_SECRET} + TURN_DOMAIN: ${SERVER_DOMAIN} + TURN_SECRET: ${COTURN_REST_SECRET} + labels: + - "traefik.backend=bbb-web" + - "traefik.frontend.rule=PathPrefix: /bigbluebutton" + + bbb-greenlight: + image: bigbluebutton/greenlight:v2 + restart: unless-stopped + volumes: + - greenlight_db:/usr/src/app/db/production + - greenlight_logs:/usr/src/app/log + environment: + BIGBLUEBUTTON_ENDPOINT: https://${SERVER_DOMAIN}/bigbluebutton/ + BIGBLUEBUTTON_SECRET: ${SHARED_SECRET} + SECRET_KEY_BASE: ${SECRET_KEY_BASE} + ALLOW_GREENLIGHT_ACCOUNTS: "true" + labels: + - "traefik.backend=bbb-greenlight" + - "traefik.frontend.rule=PathPrefix: /b" + + # when we're able to setup traefik properly for wss, nginx is no longer needed + nginx: + image: ${TAG_PREFIX}bbb-nginx${TAG_SUFFIX} + restart: unless-stopped + depends_on: + - bbb-freeswitch + environment: + SERVER_DOMAIN: ${SERVER_DOMAIN} + labels: + - "traefik.backend=bbb-freeswitch" + - "traefik.frontend.rule=PathPrefix: /ws" + + traefik: + image: traefik + restart: unless-stopped + ports: + - 80:80 + - 8080:8080 + - 443:443 + command: traefik + - --docker + - --logLevel=INFO + - --acme + - --acme.httpchallenge + - --acme.httpchallenge.entrypoint=http + - --acme.acmelogging + - --acme.storage=/etc/traefik/acme/acme.json + - --acme.email=felipe@mconf.com + - --acme.entrypoint=https + - --acme.domains=${SERVER_DOMAIN} + - --defaultentrypoints=http,https + - --entryPoints='Name:http Address::80 Redirect.EntryPoint:https' + - --entryPoints='Name:https Address::443 TLS' + volumes: + - traefik-acme:/etc/traefik/acme/ + - /var/run/docker.sock:/var/run/docker.sock + +volumes: + traefik-acme: + static: + bigbluebutton: + media-audio: + media-video: + media-screenshare: + greenlight_db: + greenlight_logs: diff --git a/labs/docker/freeswitch/Dockerfile b/labs/docker/freeswitch/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..86865a8fb847ec0af679b46ce674cd386697d596 --- /dev/null +++ b/labs/docker/freeswitch/Dockerfile @@ -0,0 +1,24 @@ +FROM ubuntu:16.04 + +ENV DEBIAN_FRONTEND noninteractive + +RUN apt-get update \ + && apt-get -y install wget libedit-dev xmlstarlet + +RUN echo "deb http://ubuntu.bigbluebutton.org/xenial-200-dev bigbluebutton-xenial main " | tee /etc/apt/sources.list.d/bigbluebutton.list \ + && wget http://ubuntu.bigbluebutton.org/repo/bigbluebutton.asc -O- | apt-key add - \ + && apt-get update \ + && apt-get -y install bbb-freeswitch-core \ + && find /opt/freeswitch/conf/sip_profiles/ -name "*ipv6*" -prune -exec rm -rf "{}" \; + +EXPOSE 7443 + +COPY docker-entrypoint.sh /usr/local/bin/ +COPY event_socket_conf.xml /opt/freeswitch/conf/autoload_configs/event_socket.conf.xml + +RUN wget -O /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v1.2.2/dumb-init_1.2.2_amd64 \ + && chmod +x /usr/local/bin/dumb-init + +ENTRYPOINT ["/usr/local/bin/dumb-init", "--"] + +CMD [ "docker-entrypoint.sh" ] diff --git a/labs/docker/freeswitch/docker-entrypoint.sh b/labs/docker/freeswitch/docker-entrypoint.sh new file mode 100755 index 0000000000000000000000000000000000000000..90e82ba379894259f46797fe6e166406a4ad0bfb --- /dev/null +++ b/labs/docker/freeswitch/docker-entrypoint.sh @@ -0,0 +1,14 @@ +#!/bin/bash -xe + +IP=$(hostname -I | cut -d' ' -f1) + +xmlstarlet edit --inplace --update '//X-PRE-PROCESS[@cmd="set" and starts-with(@data, "external_rtp_ip=")]/@data' --value "stun:coturn" /opt/freeswitch/conf/vars.xml +xmlstarlet edit --inplace --update '//X-PRE-PROCESS[@cmd="set" and starts-with(@data, "external_sip_ip=")]/@data' --value "stun:coturn" /opt/freeswitch/conf/vars.xml +xmlstarlet edit --inplace --update '//X-PRE-PROCESS[@cmd="set" and starts-with(@data, "local_ip_v4=")]/@data' --value "${IP}" /opt/freeswitch/conf/vars.xml +# add wss-binding element +xmlstarlet edit --inplace --subnode '//settings' --type elem --name 'newsubnode' --value '' /opt/freeswitch/conf/sip_profiles/external.xml +xmlstarlet edit --inplace --insert '//newsubnode' --type attr --name 'name' --value 'wss-binding' /opt/freeswitch/conf/sip_profiles/external.xml +xmlstarlet edit --inplace --insert '//newsubnode' --type attr --name 'value' --value ':7443' /opt/freeswitch/conf/sip_profiles/external.xml +xmlstarlet edit --inplace --rename '//newsubnode' --value 'param' /opt/freeswitch/conf/sip_profiles/external.xml + +/opt/freeswitch/bin/freeswitch diff --git a/labs/docker/freeswitch/event_socket_conf.xml b/labs/docker/freeswitch/event_socket_conf.xml new file mode 100644 index 0000000000000000000000000000000000000000..c10b16fb1ba4d1ecc42921913373ac049a9347b1 --- /dev/null +++ b/labs/docker/freeswitch/event_socket_conf.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<configuration name="event_socket.conf" description="Socket Client"> + <settings> + <param name="nat-map" value="false"/> + <param name="listen-ip" value="0.0.0.0"/> + <param name="listen-port" value="8021"/> + <param name="password" value="ClueCon"/> + <param name="apply-inbound-acl" value="localnet.auto"/> + </settings> +</configuration> diff --git a/labs/docker/k8s/bbb-apps-akka.yaml b/labs/docker/k8s/bbb-apps-akka.yaml new file mode 100644 index 0000000000000000000000000000000000000000..d21e1d44abe6651aac809d3f5b4040e2bc5237e4 --- /dev/null +++ b/labs/docker/k8s/bbb-apps-akka.yaml @@ -0,0 +1,45 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + labels: + app: bbb-apps-akka + version: latest + name: bbb-apps-akka + namespace: bigbluebutton +spec: + replicas: 1 + selector: + matchLabels: + app: bbb-apps-akka + version: latest + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app: bbb-apps-akka + version: latest + name: bbb-apps-akka + spec: + containers: + - env: + - name: JAVA_OPTS + value: -Dredis.host=redis + image: fcecagno/bigbluebutton:bbb-apps-akka + imagePullPolicy: Always + name: bbb-apps-akka + ports: + - containerPort: 8080 + protocol: TCP + restartPolicy: Always + terminationGracePeriodSeconds: 30 + nodeSelector: + role: 'bigbluebutton' + tolerations: + - key: role + operator: Equal + value: 'bigbluebutton' + effect: NoSchedule diff --git a/labs/docker/k8s/bbb-coturn.yaml b/labs/docker/k8s/bbb-coturn.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f68eb0cca329a7de723b63db8ce773c79ef93fea --- /dev/null +++ b/labs/docker/k8s/bbb-coturn.yaml @@ -0,0 +1,71 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + labels: + app: bbb-coturn + version: latest + name: bbb-coturn + namespace: bigbluebutton +spec: + replicas: 1 + selector: + matchLabels: + app: bbb-coturn + version: latest + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app: bbb-coturn + version: latest + name: bbb-coturn + spec: + containers: + - env: + - name: PORT + value: "3478" + - name: SERVER_DOMAIN + value: bigbluebutton.rocket.chat + - name: SECRET + value: "54321" + - name: EXTERNAL_IP + value: "35.185.19.180" + - name: ENABLE_REST_API + value: "1" + image: fcecagno/bigbluebutton:bbb-coturn + imagePullPolicy: Always + name: bbb-coturn + ports: + - containerPort: 3478 + protocol: TCP + - containerPort: 3478 + protocol: UDP + restartPolicy: Always + terminationGracePeriodSeconds: 30 + nodeSelector: + role: 'bigbluebutton' + tolerations: + - key: role + operator: Equal + value: 'bigbluebutton' + effect: NoSchedule + +--- +apiVersion: v1 +kind: Service +metadata: + name: bbb-coturn + namespace: bigbluebutton + annotations: + traefik.backend.circuitbreaker: "NetworkErrorRatio() > 0.5" +spec: + ports: + - name: http + targetPort: 3478 + port: 3478 + selector: + app: bbb-coturn diff --git a/labs/docker/k8s/bbb-freeswitch-nginx.yaml b/labs/docker/k8s/bbb-freeswitch-nginx.yaml new file mode 100644 index 0000000000000000000000000000000000000000..d550783745d6b122972de76f1b4ef0bf9da861f0 --- /dev/null +++ b/labs/docker/k8s/bbb-freeswitch-nginx.yaml @@ -0,0 +1,79 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + labels: + app: bbb-freeswitch-nginx + version: latest + name: bbb-freeswitch-nginx + namespace: bigbluebutton +spec: + replicas: 1 + selector: + matchLabels: + app: bbb-freeswitch-nginx + version: latest + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app: bbb-freeswitch-nginx + version: latest + name: bbb-freeswitch-nginx + spec: + containers: + - env: + - name: SERVER_DOMAIN + value: bigbluebutton.rocket.chat + image: fcecagno/bigbluebutton:bbb-nginx + imagePullPolicy: Always + name: bbb-freeswitch-nginx + ports: + - containerPort: 80 + protocol: TCP + restartPolicy: Always + terminationGracePeriodSeconds: 30 + nodeSelector: + role: 'bigbluebutton' + tolerations: + - key: role + operator: Equal + value: 'bigbluebutton' + effect: NoSchedule + +--- +apiVersion: v1 +kind: Service +metadata: + name: bbb-freeswitch-nginx + namespace: bigbluebutton + annotations: + traefik.backend.circuitbreaker: "NetworkErrorRatio() > 0.5" +spec: + ports: + - name: http + targetPort: 80 + port: 80 + selector: + app: bbb-freeswitch-nginx + +--- +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: bbb-freeswitch-nginx + namespace: bigbluebutton + annotations: + kubernetes.io/ingress.class: "traefik" +spec: + rules: + - host: bigbluebutton.rocket.chat + http: + paths: + - path: /ws + backend: + serviceName: bbb-freeswitch-nginx + servicePort: 80 diff --git a/labs/docker/k8s/bbb-freeswitch.yaml b/labs/docker/k8s/bbb-freeswitch.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6c78e391a0217f34ef3b9385374e06e9851b4282 --- /dev/null +++ b/labs/docker/k8s/bbb-freeswitch.yaml @@ -0,0 +1,58 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + labels: + app: bbb-freeswitch + version: latest + name: bbb-freeswitch + namespace: bigbluebutton +spec: + replicas: 1 + selector: + matchLabels: + app: bbb-freeswitch + version: latest + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app: bbb-freeswitch + version: latest + name: bbb-freeswitch + spec: + containers: + - image: fcecagno/bigbluebutton:bbb-freeswitch + imagePullPolicy: Always + name: bbb-freeswitch + ports: + - containerPort: 7443 + protocol: TCP + restartPolicy: Always + terminationGracePeriodSeconds: 30 + nodeSelector: + role: 'bigbluebutton' + tolerations: + - key: role + operator: Equal + value: 'bigbluebutton' + effect: NoSchedule + +--- +apiVersion: v1 +kind: Service +metadata: + name: bbb-freeswitch + namespace: bigbluebutton + annotations: + traefik.backend.circuitbreaker: "NetworkErrorRatio() > 0.5" +spec: + ports: + - name: http + targetPort: 7443 + port: 7443 + selector: + app: bbb-freeswitch diff --git a/labs/docker/k8s/bbb-fsesl-akka.yaml b/labs/docker/k8s/bbb-fsesl-akka.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b37ba7d94fee4c938cc732ce01e8c7cb93441712 --- /dev/null +++ b/labs/docker/k8s/bbb-fsesl-akka.yaml @@ -0,0 +1,43 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + labels: + app: bbb-fsesl-akka + version: latest + name: bbb-fsesl-akka + namespace: bigbluebutton +spec: + replicas: 1 + selector: + matchLabels: + app: bbb-fsesl-akka + version: latest + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app: bbb-fsesl-akka + version: latest + name: bbb-fsesl-akka + spec: + containers: + - env: + - name: JAVA_OPTS + value: -Dredis.host=redis -Dfreeswitch.esl.host=bbb-freeswitch + image: fcecagno/bigbluebutton:bbb-fsesl-akka + imagePullPolicy: Always + name: bbb-fsesl-akka + command: ["wait-for-it.sh", "bbb-freeswitch:8021", "--timeout=60", "--strict", "--", "/usr/share/bbb-fsesl-akka/bin/bbb-fsesl-akka"] + restartPolicy: Always + terminationGracePeriodSeconds: 30 + nodeSelector: + role: 'bigbluebutton' + tolerations: + - key: role + operator: Equal + value: 'bigbluebutton' + effect: NoSchedule diff --git a/labs/docker/k8s/bbb-html5.yaml b/labs/docker/k8s/bbb-html5.yaml new file mode 100644 index 0000000000000000000000000000000000000000..cf8fdafcf3b0c3977909121d5de4c1ba2db5f141 --- /dev/null +++ b/labs/docker/k8s/bbb-html5.yaml @@ -0,0 +1,91 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + labels: + app: bbb-html5 + version: latest + name: bbb-html5 + namespace: bigbluebutton +spec: + replicas: 1 + selector: + matchLabels: + app: bbb-html5 + version: latest + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app: bbb-html5 + version: latest + name: bbb-html5 + spec: + containers: + - env: + - name: MONGO_URL + value: mongodb://mongo:27017/ + - name: MONGO_DB + value: bbbhtml5 + - name: METEOR_SETTINGS_MODIFIER + value: ".public.kurento.wsUrl = \"wss://bigbluebutton.rocket.chat/bbb-webrtc-sfu\" | .public.kurento.enableVideo = true | .public.kurento.enableScreensharing = true | .public.kurento.enableVideoStats = true | .public.kurento.enableListenOnly = true" + - name: REDIS_HOST + value: redis + - name: ROOT_URL + value: http://127.0.0.1/html5client + image: fcecagno/bigbluebutton:bbb-html5 + imagePullPolicy: Always + name: bbb-html5 + ports: + - containerPort: 3000 + protocol: TCP + restartPolicy: Always + terminationGracePeriodSeconds: 30 + nodeSelector: + role: 'bigbluebutton' + tolerations: + - key: role + operator: Equal + value: 'bigbluebutton' + effect: NoSchedule + +--- +apiVersion: v1 +kind: Service +metadata: + name: bbb-html5 + namespace: bigbluebutton + annotations: + traefik.backend.circuitbreaker: "NetworkErrorRatio() > 0.5" +spec: + ports: + - name: http + targetPort: 3000 + port: 3000 + selector: + app: bbb-html5 + +--- +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: bbb-html5 + namespace: bigbluebutton + annotations: + kubernetes.io/ingress.class: "traefik" +spec: + rules: + - host: bigbluebutton.rocket.chat + http: + paths: + - path: /html5client + backend: + serviceName: bbb-html5 + servicePort: 3000 + - path: /_timesync + backend: + serviceName: bbb-html5 + servicePort: 3000 diff --git a/labs/docker/k8s/bbb-kurento.yaml b/labs/docker/k8s/bbb-kurento.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3c2a0ac6e9fb64b43f7ef32be6a580ecdd5f6607 --- /dev/null +++ b/labs/docker/k8s/bbb-kurento.yaml @@ -0,0 +1,69 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + labels: + app: bbb-kurento + version: latest + name: bbb-kurento + namespace: bigbluebutton +spec: + replicas: 1 + selector: + matchLabels: + app: bbb-kurento + version: latest + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app: bbb-kurento + version: latest + name: bbb-kurento + spec: + containers: + - env: + - name: KMS_STUN_IP + # TODO: how to get this IP? + value: "35.185.19.180" + - name: KMS_STUN_PORT + value: "3478" + image: fcecagno/bigbluebutton:bbb-kurento + imagePullPolicy: Always + name: bbb-kurento + ports: + - containerPort: 8888 + protocol: TCP + restartPolicy: Always + terminationGracePeriodSeconds: 30 + nodeSelector: + role: 'bigbluebutton' + tolerations: + - key: role + operator: Equal + value: 'bigbluebutton' + effect: NoSchedule + +--- +apiVersion: v1 +kind: Service +metadata: + name: bbb-kurento + namespace: bigbluebutton + annotations: + traefik.backend.circuitbreaker: "NetworkErrorRatio() > 0.5" +spec: + ports: + - name: http + targetPort: 8888 + port: 8888 + selector: + app: bbb-kurento + + + # volumes: + # - media-video:/var/kurento/recordings + # - media-screenshare:/var/kurento/screenshare diff --git a/labs/docker/k8s/bbb-web.yaml b/labs/docker/k8s/bbb-web.yaml new file mode 100644 index 0000000000000000000000000000000000000000..784ae337eb475c6f17da7844160ad063166cb394 --- /dev/null +++ b/labs/docker/k8s/bbb-web.yaml @@ -0,0 +1,83 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + labels: + app: bbb-web + version: latest + name: bbb-web + namespace: bigbluebutton +spec: + replicas: 1 + selector: + matchLabels: + app: bbb-web + version: latest + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app: bbb-web + version: latest + name: bbb-web + spec: + containers: + - env: + - name: SERVER_DOMAIN + value: bigbluebutton.rocket.chat + - name: SHARED_SECRET + value: "12345" + - name: TURN_SECRET + value: "54321" + image: fcecagno/bigbluebutton:bbb-web + imagePullPolicy: Always + name: bbb-web + ports: + - containerPort: 8080 + protocol: TCP + restartPolicy: Always + terminationGracePeriodSeconds: 30 + nodeSelector: + role: 'bigbluebutton' + tolerations: + - key: role + operator: Equal + value: 'bigbluebutton' + effect: NoSchedule + +--- +apiVersion: v1 +kind: Service +metadata: + name: bbb-web + namespace: bigbluebutton + annotations: + traefik.backend.circuitbreaker: "NetworkErrorRatio() > 0.5" +spec: + ports: + - name: http + targetPort: 8080 + port: 8080 + selector: + app: bbb-web + +--- +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: bbb-web + namespace: bigbluebutton + annotations: + kubernetes.io/ingress.class: "traefik" +spec: + rules: + - host: bigbluebutton.rocket.chat + http: + paths: + - path: /bigbluebutton + backend: + serviceName: bbb-web + servicePort: 8080 diff --git a/labs/docker/k8s/bbb-webhooks.yaml b/labs/docker/k8s/bbb-webhooks.yaml new file mode 100644 index 0000000000000000000000000000000000000000..9239ce01e43844f471ce19b3c8657df49b0d3111 --- /dev/null +++ b/labs/docker/k8s/bbb-webhooks.yaml @@ -0,0 +1,81 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + labels: + app: bbb-webhooks + version: latest + name: bbb-webhooks + namespace: bigbluebutton +spec: + replicas: 1 + selector: + matchLabels: + app: bbb-webhooks + version: latest + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app: bbb-webhooks + version: latest + name: bbb-webhooks + spec: + containers: + - env: + - name: REDIS_HOST + value: redis + - name: SHARED_SECRET + value: "12345" + image: fcecagno/bigbluebutton:bbb-webhooks + imagePullPolicy: Always + name: bbb-webhooks + ports: + - containerPort: 3005 + protocol: TCP + restartPolicy: Always + terminationGracePeriodSeconds: 30 + nodeSelector: + role: 'bigbluebutton' + tolerations: + - key: role + operator: Equal + value: 'bigbluebutton' + effect: NoSchedule + +--- +apiVersion: v1 +kind: Service +metadata: + name: bbb-webhooks + namespace: bigbluebutton + annotations: + traefik.backend.circuitbreaker: "NetworkErrorRatio() > 0.5" +spec: + ports: + - name: http + targetPort: 3005 + port: 3005 + selector: + app: bbb-webhooks + +--- +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: bbb-webhooks + namespace: bigbluebutton + annotations: + kubernetes.io/ingress.class: "traefik" +spec: + rules: + - host: bigbluebutton.rocket.chat + http: + paths: + - path: /bigbluebutton/api/hooks + backend: + serviceName: bbb-webhooks + servicePort: 3005 diff --git a/labs/docker/k8s/bbb-webrtc-sfu.yaml b/labs/docker/k8s/bbb-webrtc-sfu.yaml new file mode 100644 index 0000000000000000000000000000000000000000..323622f023a8e7137a6eb53e0abafb1fb39481e3 --- /dev/null +++ b/labs/docker/k8s/bbb-webrtc-sfu.yaml @@ -0,0 +1,87 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + labels: + app: bbb-webrtc-sfu + version: latest + name: bbb-webrtc-sfu + namespace: bigbluebutton +spec: + replicas: 1 + selector: + matchLabels: + app: bbb-webrtc-sfu + version: latest + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app: bbb-webrtc-sfu + version: latest + name: bbb-webrtc-sfu + spec: + containers: + - env: + - name: KURENTO_NAME + value: kurento + - name: KURENTO_URL + value: ws://kurento:8888/kurento + - name: REDIS_HOST + value: redis + - name: FREESWITCH_IP + value: bbb-freeswitch + - name: LOG_LEVEL + value: debug + image: fcecagno/bigbluebutton:bbb-webrtc-sfu + imagePullPolicy: Always + name: bbb-webrtc-sfu + ports: + - containerPort: 3008 + protocol: TCP + restartPolicy: Always + terminationGracePeriodSeconds: 30 + nodeSelector: + role: 'bigbluebutton' + tolerations: + - key: role + operator: Equal + value: 'bigbluebutton' + effect: NoSchedule + +--- +apiVersion: v1 +kind: Service +metadata: + name: bbb-webrtc-sfu + namespace: bigbluebutton + annotations: + traefik.backend.circuitbreaker: "NetworkErrorRatio() > 0.5" +spec: + ports: + - name: http + targetPort: 3008 + port: 3008 + selector: + app: bbb-webrtc-sfu + +--- +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: bbb-webrtc-sfu + namespace: bigbluebutton + annotations: + kubernetes.io/ingress.class: "traefik" +spec: + rules: + - host: bigbluebutton.rocket.chat + http: + paths: + - path: /bbb-webrtc-sfu + backend: + serviceName: bbb-webrtc-sfu + servicePort: 3008 diff --git a/labs/docker/k8s/bigbluebutton-ns.yaml b/labs/docker/k8s/bigbluebutton-ns.yaml new file mode 100644 index 0000000000000000000000000000000000000000..d66b7579ed4aae4a45759e7f1ecb3fa3a7d4df8b --- /dev/null +++ b/labs/docker/k8s/bigbluebutton-ns.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: bigbluebutton diff --git a/labs/docker/k8s/mongo.yaml b/labs/docker/k8s/mongo.yaml new file mode 100644 index 0000000000000000000000000000000000000000..5a09cdd532b9a840e16bb612914a7cd8f2650933 --- /dev/null +++ b/labs/docker/k8s/mongo.yaml @@ -0,0 +1,55 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + labels: + app: mongo + version: latest + name: mongo + namespace: bigbluebutton +spec: + replicas: 1 + selector: + matchLabels: + app: mongo + version: latest + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app: mongo + version: latest + name: mongo + spec: + containers: + - image: mongo:3.6 + imagePullPolicy: Always + name: mongo + ports: + - containerPort: 27017 + protocol: TCP + restartPolicy: Always + terminationGracePeriodSeconds: 30 + nodeSelector: + role: 'bigbluebutton' + tolerations: + - key: role + operator: Equal + value: 'bigbluebutton' + effect: NoSchedule + +--- +apiVersion: v1 +kind: Service +metadata: + name: mongo + namespace: bigbluebutton +spec: + ports: + - targetPort: 27017 + port: 27017 + selector: + app: mongo diff --git a/labs/docker/k8s/redis.yaml b/labs/docker/k8s/redis.yaml new file mode 100644 index 0000000000000000000000000000000000000000..fe12f4eb193aed1043b39c905d0539f7af0709c2 --- /dev/null +++ b/labs/docker/k8s/redis.yaml @@ -0,0 +1,55 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + labels: + app: redis + version: latest + name: redis + namespace: bigbluebutton +spec: + replicas: 1 + selector: + matchLabels: + app: redis + version: latest + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app: redis + version: latest + name: redis + spec: + containers: + - image: redis:latest + imagePullPolicy: Always + name: redis + ports: + - containerPort: 6379 + protocol: TCP + restartPolicy: Always + terminationGracePeriodSeconds: 30 + nodeSelector: + role: 'bigbluebutton' + tolerations: + - key: role + operator: Equal + value: 'bigbluebutton' + effect: NoSchedule + +--- +apiVersion: v1 +kind: Service +metadata: + name: redis + namespace: bigbluebutton +spec: + ports: + - targetPort: 6379 + port: 6379 + selector: + app: redis diff --git a/labs/docker/kurento/Dockerfile b/labs/docker/kurento/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..97dbf889259eeca09b76fdb364bd280d9b317c37 --- /dev/null +++ b/labs/docker/kurento/Dockerfile @@ -0,0 +1,28 @@ +FROM ubuntu:16.04 + +ENV DEBIAN_FRONTEND noninteractive + +RUN apt-get update \ + && apt-get -y dist-upgrade \ + && apt-get install -y software-properties-common curl wget apt-transport-https + +RUN wget https://ubuntu.bigbluebutton.org/repo/bigbluebutton.asc -O- | apt-key add - \ + && add-apt-repository "deb https://ubuntu.bigbluebutton.org/xenial-200-dev bigbluebutton-xenial main" \ + && apt-get update \ + && apt-get -y install kurento-media-server bzip2 jq \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +RUN apt-get update \ + && apt-get install -y --download-only openh264-gst-plugins-bad-1.5 + +COPY ./docker-entrypoint.sh /usr/local/bin/ +COPY ./healthchecker.sh /healthchecker.sh + +HEALTHCHECK --start-period=15s --interval=30s --timeout=3s --retries=1 CMD /healthchecker.sh + +ENV GST_DEBUG=Kurento*:5 +ENV PORT=8888 + +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["/usr/bin/kurento-media-server"] diff --git a/labs/docker/kurento/docker-entrypoint.sh b/labs/docker/kurento/docker-entrypoint.sh new file mode 100755 index 0000000000000000000000000000000000000000..4d1d3a444ebd238994df8c48195f5027b68f6d4e --- /dev/null +++ b/labs/docker/kurento/docker-entrypoint.sh @@ -0,0 +1,24 @@ +#!/bin/bash -e + +apt-get install -y openh264-gst-plugins-bad-1.5 + +rm -f /etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini +touch /etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini + +if [ -n "$KMS_TURN_URL" ]; then + echo "turnURL=$KMS_TURN_URL" >> /etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini +fi + +if [ -n "$KMS_STUN_IP" -a -n "$KMS_STUN_PORT" ]; then + # Generate WebRtcEndpoint configuration + echo "stunServerAddress=$KMS_STUN_IP" >> /etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini + echo "stunServerPort=$KMS_STUN_PORT" >> /etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini +fi + +KMS_CONFIG=$(cat /etc/kurento/kurento.conf.json | sed '/^[ ]*\/\//d' | jq ".mediaServer.net.websocket.port = $PORT") +echo $KMS_CONFIG > /etc/kurento/kurento.conf.json + +# Remove ipv6 local loop until ipv6 is supported +cat /etc/hosts | sed '/::1/d' | tee /etc/hosts > /dev/null + +exec "$@" diff --git a/labs/docker/kurento/healthchecker.sh b/labs/docker/kurento/healthchecker.sh new file mode 100755 index 0000000000000000000000000000000000000000..c7f6ce82faf2c3667e2ce65030963ab02bff83c9 --- /dev/null +++ b/labs/docker/kurento/healthchecker.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +[[ "$(curl -w '%{http_code}' -N -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Host: 127.0.0.1:8888" -H "Origin: 127.0.0.1" http://127.0.0.1:8888/kurento)" == 500 ]] && exit 0 || exit 1 diff --git a/labs/docker/nginx/Dockerfile b/labs/docker/nginx/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..56fc11c4192341c49542f0584f8c8273db0e870a --- /dev/null +++ b/labs/docker/nginx/Dockerfile @@ -0,0 +1,14 @@ +FROM nginx + +RUN apt-get update && apt-get install -y wget + +ENV DOCKERIZE_VERSION v0.6.1 +RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \ + && tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \ + && rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz + +COPY ./nginx.conf.tmpl /etc/nginx/nginx.conf.tmpl + +CMD [ "dockerize", \ + "-template", "/etc/nginx/nginx.conf.tmpl:/etc/nginx/nginx.conf", \ + "nginx", "-g", "daemon off;" ] diff --git a/labs/docker/nginx/nginx.conf.tmpl b/labs/docker/nginx/nginx.conf.tmpl new file mode 100644 index 0000000000000000000000000000000000000000..0210fc399b1ed6e9d5bb3a30173982f30961a688 --- /dev/null +++ b/labs/docker/nginx/nginx.conf.tmpl @@ -0,0 +1,66 @@ +user www-data; +worker_processes auto; +pid /run/nginx.pid; + +events { + worker_connections 768; +} + +http { + ## + # Basic Settings + ## + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + ## + # SSL Settings + ## + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE + ssl_prefer_server_ciphers on; + + ## + # Logging Settings + ## + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + ## + # Gzip Settings + ## + + gzip on; + gzip_disable "msie6"; + + ## + # Virtual Host Configs + ## + + server { + listen 80; + listen [::]:80; + server_name {{ .Env.SERVER_DOMAIN }}; + + access_log /var/log/nginx/bigbluebutton.access.log; + + location /ws { + proxy_pass https://bbb-freeswitch:7443; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_read_timeout 6h; + proxy_send_timeout 6h; + client_body_timeout 6h; + send_timeout 6h; + } + } +} diff --git a/labs/docker/sbt/Dockerfile b/labs/docker/sbt/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..99e285c61de4892f93bc2a40fa267d28da9e737b --- /dev/null +++ b/labs/docker/sbt/Dockerfile @@ -0,0 +1,14 @@ +FROM openjdk:8 + +ARG SBT_VERSION=0.13.8 + +RUN curl -L -o sbt-$SBT_VERSION.deb https://dl.bintray.com/sbt/debian/sbt-$SBT_VERSION.deb \ + && dpkg -i sbt-$SBT_VERSION.deb \ + && rm sbt-$SBT_VERSION.deb \ + && apt-get update \ + && apt-get install sbt \ + && sbt sbtVersion + +RUN echo 'resolvers += "Artima Maven Repository" at "http://repo.artima.com/releases"' | tee -a ~/.sbt/0.13/global.sbt + +WORKDIR /root diff --git a/record-and-playback/presentation/scripts/process/presentation.rb b/record-and-playback/presentation/scripts/process/presentation.rb index 43894728edd578a5b51f8496dd3548372dfcb5b8..d7d594eb839720174d67b132c752fec34a297419 100755 --- a/record-and-playback/presentation/scripts/process/presentation.rb +++ b/record-and-playback/presentation/scripts/process/presentation.rb @@ -228,13 +228,13 @@ if not FileTest.directory?(target_dir) end processed_audio_file = BigBlueButton::AudioProcessor.get_processed_audio_file("#{temp_dir}/#{meeting_id}", "#{target_dir}/audio") - BigBlueButton.process_webcam_videos(target_dir, temp_dir, meeting_id, webcam_width, webcam_height, presentation_props['audio_offset'], processed_audio_file) + BigBlueButton.process_webcam_videos(target_dir, temp_dir, meeting_id, webcam_width, webcam_height, presentation_props['audio_offset'], processed_audio_file, presentation_props['video_formats']) end if !Dir["#{raw_archive_dir}/deskshare/*"].empty? and presentation_props['include_deskshare'] deskshare_width = presentation_props['deskshare_output_width'] deskshare_height = presentation_props['deskshare_output_height'] - BigBlueButton.process_deskshare_videos(target_dir, temp_dir, meeting_id, deskshare_width, deskshare_height) + BigBlueButton.process_deskshare_videos(target_dir, temp_dir, meeting_id, deskshare_width, deskshare_height, presentation_props['video_formats']) end process_done = File.new("#{recording_dir}/status/processed/#{meeting_id}-presentation.done", "w")