Browse Source

Merge branch 'release-0.23'

pull/175/head v0.23.4
Simon Eisenmann 11 years ago
parent
commit
40a7711e07
  1. 1
      .hound.yml
  2. 1
      .javascript_ignore
  3. 14
      README.md
  4. 43
      build/r.js
  5. 16
      debian/changelog
  6. 9
      html/main.html
  7. 10
      server.conf.in
  8. 0
      src/app/spreed-webrtc-server/channelling.go
  9. 93
      src/app/spreed-webrtc-server/channelling_api.go
  10. 20
      src/app/spreed-webrtc-server/channelling_api_test.go
  11. 6
      src/app/spreed-webrtc-server/client.go
  12. 18
      src/app/spreed-webrtc-server/config.go
  13. 12
      src/app/spreed-webrtc-server/connection.go
  14. 22
      src/app/spreed-webrtc-server/hub.go
  15. 4
      src/app/spreed-webrtc-server/main.go
  16. 61
      src/app/spreed-webrtc-server/room_manager.go
  17. 25
      src/app/spreed-webrtc-server/room_manager_test.go
  18. 18
      src/app/spreed-webrtc-server/roomworker.go
  19. 222
      src/app/spreed-webrtc-server/session.go
  20. 34
      src/app/spreed-webrtc-server/session_manager.go
  21. 4
      src/app/spreed-webrtc-server/user.go
  22. 2
      src/app/spreed-webrtc-server/ws.go
  23. 61
      src/i18n/messages-de.po
  24. 59
      src/i18n/messages-ja.po
  25. 59
      src/i18n/messages-ko.po
  26. 59
      src/i18n/messages-zh-cn.po
  27. 59
      src/i18n/messages-zh-tw.po
  28. 61
      src/i18n/messages.pot
  29. 13
      src/styles/components/_buddylist.scss
  30. 2
      static/css/main.min.css
  31. 38
      static/js/controllers/mediastreamcontroller.js
  32. 3
      static/js/directives/audiovideo.js
  33. 6
      static/js/directives/directives.js
  34. 46
      static/js/directives/menu.js
  35. 16
      static/js/directives/roombar.js
  36. 1
      static/js/directives/welcome.js
  37. 135
      static/js/libs/lodash.min.js
  38. 2112
      static/js/libs/require/require.js
  39. 59
      static/js/mediastream/api.js
  40. 7
      static/js/mediastream/webrtc.js
  41. 7
      static/js/services/appdata.js
  42. 6
      static/js/services/buddylist.js
  43. 14
      static/js/services/rooms.js
  44. 10
      static/partials/menu.html
  45. 10
      static/partials/page/welcome.html
  46. 2
      static/translation/messages-de.json
  47. 2
      static/translation/messages-ja.json
  48. 2
      static/translation/messages-ko.json
  49. 2
      static/translation/messages-zh-cn.json
  50. 2
      static/translation/messages-zh-tw.json

1
.hound.yml

@ -1,3 +1,4 @@
java_script: java_script:
enabled: true enabled: true
config_file: .jshint config_file: .jshint
ignore_file: .javascript_ignore

1
.javascript_ignore

@ -0,0 +1 @@
static/js/libs/**.js

14
README.md

@ -108,20 +108,6 @@ The latest version of Spreed WebRTC can be found on GitHub:
and CSS reload directly. and CSS reload directly.
## Branding
Insert logo in `static/img`. Edit `src/styles/global/_branding.scss` to link
to desired custom logo. It is also possible to insert the raw svg data.
## Skins
Insert skins in `src/styles/global/skins` and edit the `@import "skins/light";`
line in `src/styles/global/_variables.scss`. Available skins are light and
dark. It is recommended to create a new skin file if you wish to customize
colors.
## Running for production ## Running for production
Spreed WebRTC should be run through a SSL frontend proxy with Spreed WebRTC should be run through a SSL frontend proxy with

43
build/r.js

@ -1,5 +1,5 @@
/** /**
* @license r.js 2.1.14 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved. * @license r.js 2.1.15 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved.
* Available via the MIT or new BSD license. * Available via the MIT or new BSD license.
* see: http://github.com/jrburke/requirejs for details * see: http://github.com/jrburke/requirejs for details
*/ */
@ -20,7 +20,7 @@ var requirejs, require, define, xpcUtil;
(function (console, args, readFileFunc) { (function (console, args, readFileFunc) {
var fileName, env, fs, vm, path, exec, rhinoContext, dir, nodeRequire, var fileName, env, fs, vm, path, exec, rhinoContext, dir, nodeRequire,
nodeDefine, exists, reqMain, loadedOptimizedLib, existsForNode, Cc, Ci, nodeDefine, exists, reqMain, loadedOptimizedLib, existsForNode, Cc, Ci,
version = '2.1.14', version = '2.1.15',
jsSuffixRegExp = /\.js$/, jsSuffixRegExp = /\.js$/,
commandOption = '', commandOption = '',
useLibLoaded = {}, useLibLoaded = {},
@ -238,7 +238,7 @@ var requirejs, require, define, xpcUtil;
} }
/** vim: et:ts=4:sw=4:sts=4 /** vim: et:ts=4:sw=4:sts=4
* @license RequireJS 2.1.14 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved. * @license RequireJS 2.1.15 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved.
* Available via the MIT or new BSD license. * Available via the MIT or new BSD license.
* see: http://github.com/jrburke/requirejs for details * see: http://github.com/jrburke/requirejs for details
*/ */
@ -251,7 +251,7 @@ var requirejs, require, define, xpcUtil;
(function (global) { (function (global) {
var req, s, head, baseElement, dataMain, src, var req, s, head, baseElement, dataMain, src,
interactiveScript, currentlyAddingScript, mainScript, subPath, interactiveScript, currentlyAddingScript, mainScript, subPath,
version = '2.1.14', version = '2.1.15',
commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg, commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg,
cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g, cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,
jsSuffixRegExp = /\.js$/, jsSuffixRegExp = /\.js$/,
@ -22949,7 +22949,7 @@ define('parse', ['./esprimaAdapter', 'lang'], function (esprima, lang) {
//Like traverse, but skips if branches that would not be processed //Like traverse, but skips if branches that would not be processed
//after has application that results in tests of true or false boolean //after has application that results in tests of true or false boolean
//literal values. //literal values.
var key, child, result, i, params, param, var key, child, result, i, params, param, tempObject,
hasHas = options && options.has; hasHas = options && options.has;
fnExpScope = fnExpScope || emptyScope; fnExpScope = fnExpScope || emptyScope;
@ -22979,23 +22979,23 @@ define('parse', ['./esprimaAdapter', 'lang'], function (esprima, lang) {
//Build up a "scope" object that informs nested recurse calls if //Build up a "scope" object that informs nested recurse calls if
//the define call references an identifier that is likely a UMD //the define call references an identifier that is likely a UMD
//wrapped function expresion argument. //wrapped function expression argument.
if (object.type === 'ExpressionStatement' && object.expression && if (object.type === 'ExpressionStatement' && object.expression &&
object.expression.type === 'CallExpression' && object.expression.callee && object.expression.type === 'CallExpression' && object.expression.callee &&
object.expression.callee.type === 'FunctionExpression') { object.expression.callee.type === 'FunctionExpression') {
object = object.expression.callee; tempObject = object.expression.callee;
if (object.params && object.params.length) { if (tempObject.params && tempObject.params.length) {
params = object.params; params = tempObject.params;
fnExpScope = mixin({}, fnExpScope, true); fnExpScope = mixin({}, fnExpScope, true);
for (i = 0; i < params.length; i++) { for (i = 0; i < params.length; i++) {
param = params[i]; param = params[i];
if (param.type === 'Identifier') { if (param.type === 'Identifier') {
fnExpScope[param.name] = true; fnExpScope[param.name] = true;
}
} }
} }
} }
}
for (key in object) { for (key in object) {
if (object.hasOwnProperty(key)) { if (object.hasOwnProperty(key)) {
@ -23014,12 +23014,12 @@ define('parse', ['./esprimaAdapter', 'lang'], function (esprima, lang) {
//wrapping. //wrapping.
if (typeof result === 'string') { if (typeof result === 'string') {
if (hasProp(fnExpScope, result)) { if (hasProp(fnExpScope, result)) {
//Just a plain return, parsing can continue past this //result still in scope, keep jumping out indicating the
//point. //identifier still in use.
return; return result;
} }
return result; return;
} }
} }
}; };
@ -26539,7 +26539,8 @@ define('build', function (require) {
"excludeShallow": true, "excludeShallow": true,
"insertRequire": true, "insertRequire": true,
"stubModules": true, "stubModules": true,
"deps": true "deps": true,
"mainConfigFile": true
}; };
for (i = 0; i < ary.length; i++) { for (i = 0; i < ary.length; i++) {

16
debian/changelog vendored

@ -1,3 +1,19 @@
spreed-webrtc-server (0.23.4) precise; urgency=low
* Cleanup of README.
* Fixed a problem where videos were not sized correctly.
* Lodash was updated to 3.0.0.
* Server now has an option to require authentication to join rooms.
* Screen sharing, youtube and presentation modules can now be disabled in server.conf.
* Fixed position of buddy list loader animation.
* Fixed loaded of anonymous user data when a plugin uses the authorization api.
* Refactored session cleanup to fix ghost sessions.
* Reorganization to allow better app support.
* Updated require.js and r.js to 2.1.5.
* Fixed room reset when default room is disabled.
-- Simon Eisenmann <simon@struktur.de> Mon, 02 Feb 2015 17:18:53 +0100
spreed-webrtc-server (0.23.3) precise; urgency=low spreed-webrtc-server (0.23.3) precise; urgency=low
* Improved room bar room change and leave buttons. * Improved room bar room change and leave buttons.

9
html/main.html

@ -22,14 +22,7 @@
<div class="navbar-collapse collapse" collapse="isCollapsed"> <div class="navbar-collapse collapse" collapse="isCollapsed">
<ul class="nav navbar-nav navbar-right right"> <ul class="nav navbar-nav navbar-right right">
<li class="ng-cloak"> <li class="ng-cloak">
<button title="{{_('Share a YouTube video')}}" class="btn aenablebtn btn-youtubevideo" ng-show="status=='connected' || status=='conference' || layout.youtubevideo" ng-model="layout.youtubevideo" btn-checkbox><i class="fa fa-youtube"></i></button> <menu></menu>
<button title="{{_('Share a file as presentation')}}" class="btn aenablebtn btn-presentation" ng-show="status=='connected' || status=='conference' || layout.presentation" ng-model="layout.presentation" btn-checkbox><i class="fa fa-folder-open-o"></i></button>
<button title="{{_('Share your screen')}}" class="btn aenablebtn btn-screenshare" ng-disabled="!supported.screensharing" ng-show="status=='connected' || status=='conference' || layout.screenshare" ng-model="layout.screenshare" btn-checkbox><i class="fa fa-desktop"></i></button>
<button title="{{_('Chat')}}" class="btn btn-chat" ng-class="{messagesunseen: chatMessagesUnseen>0}" ng-model="layout.chat" btn-checkbox btn-checkbox-true="true" btn-checkbox-false="false"><i class="fa fa-comments-o"></i><span class="badge" ng-show="chatMessagesUnseen" ng-bind="chatMessagesUnseen"></span></button>
<button ng-show="myid && myuserid" title="{{_('Contacts')}}" class="btn btn-contacts" ng-click="openContactsManager()"><i class="fa fa-sitemap"></i></button>
<button title="{{_('Mute microphone')}}" class="btn btn-mutemicrophone amutebtn" ng-model="microphoneMute" btn-checkbox btn-checkbox-true="true" btn-checkbox-false="false"><i class="fa"></i></button>
<button title="{{_('Turn camera off')}}" class="btn btn-mutecamera amutebtn" ng-model="cameraMute" btn-checkbox btn-checkbox-true="true" btn-checkbox-false="false"><i class="fa"></i></button>
<button title="{{_('Settings')}}" class="btn btn-settings" ng-model="layout.settings" btn-checkbox btn-checkbox-true="true" btn-checkbox-false="false"><i class="fa fa-cog"></i></button>
</li> </li>
</ul> </ul>
</div> </div>

10
server.conf.in

@ -78,6 +78,9 @@ encryptionSecret = tne-default-encryption-block-key
; all users will join this room if enabled. If it is disabled then a room join ; all users will join this room if enabled. If it is disabled then a room join
; form will be shown instead. ; form will be shown instead.
;defaultRoomEnabled = true ;defaultRoomEnabled = true
; Whether a user account is required to join a room. This only has an effect
; if user accounts are enabled. Optional, defaults to false.
;authorizeRoomJoin = false
; Whether a user account is required to create a room. This only has an effect ; Whether a user account is required to create a room. This only has an effect
; if user accounts are enabled. Optional, defaults to false. ; if user accounts are enabled. Optional, defaults to false.
;authorizeRoomCreation = false ;authorizeRoomCreation = false
@ -115,6 +118,13 @@ serverRealm = local
; to test your CSP before putting it into production. ; to test your CSP before putting it into production.
;contentSecurityPolicyReportOnly = ;contentSecurityPolicyReportOnly =
[modules]
; Modules provide optional functionality. Modules are enabled by default and
; can be disabled by setting false to their corresponding configuration.
;screensharing = true
;youtube = true
;presentation = true
[log] [log]
;logfile = /var/log/spreed-webrtc-server.log ;logfile = /var/log/spreed-webrtc-server.log

0
src/app/spreed-webrtc-server/channeling.go → src/app/spreed-webrtc-server/channelling.go

93
src/app/spreed-webrtc-server/channelling_api.go

@ -23,7 +23,6 @@ package main
import ( import (
"log" "log"
"strings"
"time" "time"
) )
@ -34,7 +33,6 @@ const (
type ChannellingAPI interface { type ChannellingAPI interface {
OnConnect(Client, *Session) OnConnect(Client, *Session)
OnIncoming(ResponseSender, *Session, *DataIncoming) OnIncoming(ResponseSender, *Session, *DataIncoming)
OnDisconnect(*Session)
} }
type channellingAPI struct { type channellingAPI struct {
@ -46,11 +44,9 @@ type channellingAPI struct {
ContactManager ContactManager
TurnDataCreator TurnDataCreator
Unicaster Unicaster
Broadcaster
buddyImages ImageCache
} }
func NewChannellingAPI(config *Config, roomStatus RoomStatusManager, sessionEncoder SessionEncoder, sessionManager SessionManager, statsCounter StatsCounter, contactManager ContactManager, turnDataCreator TurnDataCreator, unicaster Unicaster, broadcaster Broadcaster, buddyImages ImageCache) ChannellingAPI { func NewChannellingAPI(config *Config, roomStatus RoomStatusManager, sessionEncoder SessionEncoder, sessionManager SessionManager, statsCounter StatsCounter, contactManager ContactManager, turnDataCreator TurnDataCreator, unicaster Unicaster) ChannellingAPI {
return &channellingAPI{ return &channellingAPI{
config, config,
roomStatus, roomStatus,
@ -60,8 +56,6 @@ func NewChannellingAPI(config *Config, roomStatus RoomStatusManager, sessionEnco
contactManager, contactManager,
turnDataCreator, turnDataCreator,
unicaster, unicaster,
broadcaster,
buddyImages,
} }
} }
@ -77,40 +71,31 @@ func (api *channellingAPI) OnIncoming(c ResponseSender, session *Session, msg *D
case "Hello": case "Hello":
//log.Println("Hello", msg.Hello, c.Index()) //log.Println("Hello", msg.Hello, c.Index())
// TODO(longsleep): Filter room id and user agent. // TODO(longsleep): Filter room id and user agent.
api.UpdateSession(session, &SessionUpdate{Types: []string{"Ua"}, Ua: msg.Hello.Ua}) session.Update(&SessionUpdate{Types: []string{"Ua"}, Ua: msg.Hello.Ua})
if session.Hello && session.Roomid != msg.Hello.Id {
api.LeaveRoom(session)
api.Broadcast(session, session.DataSessionLeft("soft"))
}
room, err := session.JoinRoom(msg.Hello.Id, msg.Hello.Credentials, c)
// NOTE(lcooper): Iid filtered for compatibility's sake. // NOTE(lcooper): Iid filtered for compatibility's sake.
// Evaluate sending unconditionally when supported by all clients. // Evaluate sending unconditionally when supported by all clients.
if room, err := api.JoinRoom(msg.Hello.Id, msg.Hello.Credentials, session, c); err == nil { if msg.Iid != "" {
session.Hello = true if err == nil {
session.Roomid = msg.Hello.Id
if msg.Iid != "" {
c.Reply(msg.Iid, &DataWelcome{ c.Reply(msg.Iid, &DataWelcome{
Type: "Welcome", Type: "Welcome",
Room: room, Room: room,
Users: api.RoomUsers(session), Users: api.RoomUsers(session),
}) })
} } else {
api.Broadcast(session, session.DataSessionJoined())
} else {
session.Hello = false
if msg.Iid != "" {
c.Reply(msg.Iid, err) c.Reply(msg.Iid, err)
} }
} }
case "Offer": case "Offer":
// TODO(longsleep): Validate offer // TODO(longsleep): Validate offer
api.Unicast(session, msg.Offer.To, msg.Offer) session.Unicast(msg.Offer.To, msg.Offer)
case "Candidate": case "Candidate":
// TODO(longsleep): Validate candidate // TODO(longsleep): Validate candidate
api.Unicast(session, msg.Candidate.To, msg.Candidate) session.Unicast(msg.Candidate.To, msg.Candidate)
case "Answer": case "Answer":
// TODO(longsleep): Validate Answer // TODO(longsleep): Validate Answer
api.Unicast(session, msg.Answer.To, msg.Answer) session.Unicast(msg.Answer.To, msg.Answer)
case "Users": case "Users":
if session.Hello { if session.Hello {
sessions := &DataSessions{Type: "Users", Users: api.RoomUsers(session)} sessions := &DataSessions{Type: "Users", Users: api.RoomUsers(session)}
@ -125,27 +110,27 @@ func (api *channellingAPI) OnIncoming(c ResponseSender, session *Session, msg *D
if err := api.Authenticate(session, st, ""); err == nil { if err := api.Authenticate(session, st, ""); err == nil {
log.Println("Authentication success", session.Userid) log.Println("Authentication success", session.Userid)
api.SendSelf(c, session) api.SendSelf(c, session)
api.BroadcastSessionStatus(session) session.BroadcastStatus()
} else { } else {
log.Println("Authentication failed", err, st.Userid, st.Nonce) log.Println("Authentication failed", err, st.Userid, st.Nonce)
} }
case "Bye": case "Bye":
api.Unicast(session, msg.Bye.To, msg.Bye) session.Unicast(msg.Bye.To, msg.Bye)
case "Status": case "Status":
//log.Println("Status", msg.Status) //log.Println("Status", msg.Status)
api.UpdateSession(session, &SessionUpdate{Types: []string{"Status"}, Status: msg.Status.Status}) session.Update(&SessionUpdate{Types: []string{"Status"}, Status: msg.Status.Status})
api.BroadcastSessionStatus(session) session.BroadcastStatus()
case "Chat": case "Chat":
// TODO(longsleep): Limit sent chat messages per incoming connection. // TODO(longsleep): Limit sent chat messages per incoming connection.
if !msg.Chat.Chat.NoEcho { if !msg.Chat.Chat.NoEcho {
api.Unicast(session, session.Id, msg.Chat) session.Unicast(session.Id, msg.Chat)
} }
msg.Chat.Chat.Time = time.Now().Format(time.RFC3339) msg.Chat.Chat.Time = time.Now().Format(time.RFC3339)
if msg.Chat.To == "" { if msg.Chat.To == "" {
// TODO(longsleep): Check if chat broadcast is allowed. // TODO(longsleep): Check if chat broadcast is allowed.
if session.Hello { if session.Hello {
api.CountBroadcastChat() api.CountBroadcastChat()
api.Broadcast(session, msg.Chat) session.Broadcast(msg.Chat)
} }
} else { } else {
if msg.Chat.Chat.Status != nil && msg.Chat.Chat.Status.ContactRequest != nil { if msg.Chat.Chat.Status != nil && msg.Chat.Chat.Status.ContactRequest != nil {
@ -159,10 +144,10 @@ func (api *channellingAPI) OnIncoming(c ResponseSender, session *Session, msg *D
api.CountUnicastChat() api.CountUnicastChat()
} }
api.Unicast(session, msg.Chat.To, msg.Chat) session.Unicast(msg.Chat.To, msg.Chat)
if msg.Chat.Chat.Mid != "" { if msg.Chat.Chat.Mid != "" {
// Send out delivery confirmation status chat message. // Send out delivery confirmation status chat message.
api.Unicast(session, session.Id, &DataChat{To: msg.Chat.To, Type: "Chat", Chat: &DataChatMessage{Mid: msg.Chat.Chat.Mid, Status: &DataChatStatus{State: "sent"}}}) session.Unicast(session.Id, &DataChat{To: msg.Chat.To, Type: "Chat", Chat: &DataChatMessage{Mid: msg.Chat.Chat.Mid, Status: &DataChatStatus{State: "sent"}}})
} }
} }
case "Conference": case "Conference":
@ -173,7 +158,7 @@ func (api *channellingAPI) OnIncoming(c ResponseSender, session *Session, msg *D
// Send conference update to anyone. // Send conference update to anyone.
for _, id := range msg.Conference.Conference { for _, id := range msg.Conference.Conference {
if id != session.Id { if id != session.Id {
api.Unicast(session, id, msg.Conference) session.Unicast(id, msg.Conference)
} }
} }
} }
@ -211,7 +196,7 @@ func (api *channellingAPI) OnIncoming(c ResponseSender, session *Session, msg *D
} }
case "Room": case "Room":
if room, err := api.UpdateRoom(session, msg.Room); err == nil { if room, err := api.UpdateRoom(session, msg.Room); err == nil {
api.Broadcast(session, room) session.Broadcast(room)
c.Reply(msg.Iid, room) c.Reply(msg.Iid, room)
} else { } else {
c.Reply(msg.Iid, err) c.Reply(msg.Iid, err)
@ -221,23 +206,6 @@ func (api *channellingAPI) OnIncoming(c ResponseSender, session *Session, msg *D
} }
} }
func (api *channellingAPI) OnDisconnect(session *Session) {
dsl := session.DataSessionLeft("hard")
if session.Hello {
api.LeaveRoom(session)
api.Broadcast(session, dsl)
}
session.RunForAllSubscribers(func(session *Session) {
log.Println("Notifying subscriber that we are gone", session.Id, session.Id)
api.Unicast(session, session.Id, dsl)
})
api.Unicaster.OnDisconnect(session)
api.buddyImages.Delete(session.Id)
}
func (api *channellingAPI) SendSelf(c Responder, session *Session) { func (api *channellingAPI) SendSelf(c Responder, session *Session) {
token, err := api.EncodeSessionToken(session) token, err := api.EncodeSessionToken(session)
if err != nil { if err != nil {
@ -259,26 +227,3 @@ func (api *channellingAPI) SendSelf(c Responder, session *Session) {
} }
c.Reply("", self) c.Reply("", self)
} }
func (api *channellingAPI) UpdateSession(session *Session, s *SessionUpdate) uint64 {
if s.Status != nil {
status, ok := s.Status.(map[string]interface{})
if ok && status["buddyPicture"] != nil {
pic := status["buddyPicture"].(string)
if strings.HasPrefix(pic, "data:") {
imageId := api.buddyImages.Update(session.Id, pic[5:])
if imageId != "" {
status["buddyPicture"] = "img:" + imageId
}
}
}
}
return session.Update(s)
}
func (api *channellingAPI) BroadcastSessionStatus(session *Session) {
if session.Hello {
api.Broadcast(session, session.DataSessionStatus())
}
}

20
src/app/spreed-webrtc-server/channelling_api_test.go

@ -57,17 +57,17 @@ func (fake *fakeRoomManager) RoomUsers(session *Session) []*DataSession {
return fake.roomUsers return fake.roomUsers
} }
func (fake *fakeRoomManager) JoinRoom(id string, _ *DataRoomCredentials, session *Session, _ Sender) (*DataRoom, error) { func (fake *fakeRoomManager) JoinRoom(id string, _ *DataRoomCredentials, session *Session, sessionAuthenticated bool, _ Sender) (*DataRoom, error) {
fake.joinedID = id fake.joinedID = id
return &DataRoom{Name: id}, fake.joinError return &DataRoom{Name: id}, fake.joinError
} }
func (fake *fakeRoomManager) LeaveRoom(session *Session) { func (fake *fakeRoomManager) LeaveRoom(roomID, sessionID string) {
fake.leftID = session.Roomid fake.leftID = roomID
} }
func (fake *fakeRoomManager) Broadcast(_ *Session, msg interface{}) { func (fake *fakeRoomManager) Broadcast(_, _ string, outgoing *DataOutgoing) {
fake.broadcasts = append(fake.broadcasts, msg) fake.broadcasts = append(fake.broadcasts, outgoing.Data)
} }
func (fake *fakeRoomManager) UpdateRoom(_ *Session, _ *DataRoom) (*DataRoom, error) { func (fake *fakeRoomManager) UpdateRoom(_ *Session, _ *DataRoom) (*DataRoom, error) {
@ -98,8 +98,14 @@ func assertErrorReply(t *testing.T, client *fakeClient, iid, code string) {
} }
func NewTestChannellingAPI() (ChannellingAPI, *fakeClient, *Session, *fakeRoomManager) { func NewTestChannellingAPI() (ChannellingAPI, *fakeClient, *Session, *fakeRoomManager) {
client, roomManager, session := &fakeClient{}, &fakeRoomManager{}, &Session{} client, roomManager := &fakeClient{}, &fakeRoomManager{}
return NewChannellingAPI(nil, roomManager, nil, nil, nil, nil, nil, nil, roomManager, nil), client, session, roomManager session := &Session{
attestations: sessionNonces,
Broadcaster: roomManager,
RoomStatusManager: roomManager,
}
session.attestation = &SessionAttestation{s: session}
return NewChannellingAPI(nil, roomManager, nil, nil, nil, nil, nil, nil), client, session, roomManager
} }
func Test_ChannellingAPI_OnIncoming_HelloMessage_JoinsTheSelectedRoom(t *testing.T) { func Test_ChannellingAPI_OnIncoming_HelloMessage_JoinsTheSelectedRoom(t *testing.T) {

6
src/app/spreed-webrtc-server/client.go

@ -42,7 +42,7 @@ type Client interface {
ResponseSender ResponseSender
Session() *Session Session() *Session
Index() uint64 Index() uint64
Close(bool) Close()
} }
type client struct { type client struct {
@ -70,10 +70,6 @@ func (client *client) OnText(b Buffer) {
} }
} }
func (client *client) OnDisconnect() {
client.ChannellingAPI.OnDisconnect(client.session)
}
func (client *client) Reply(iid string, m interface{}) { func (client *client) Reply(iid string, m interface{}) {
outgoing := &DataOutgoing{From: client.session.Id, Iid: iid, Data: m} outgoing := &DataOutgoing{From: client.session.Id, Iid: iid, Data: m}
if b, err := client.EncodeOutgoing(outgoing); err == nil { if b, err := client.EncodeOutgoing(outgoing); err == nil {

18
src/app/spreed-webrtc-server/config.go

@ -23,11 +23,11 @@ package main
import ( import (
"fmt" "fmt"
"github.com/strukturag/phoenix"
"log"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"github.com/strukturag/phoenix"
) )
type Config struct { type Config struct {
@ -46,6 +46,8 @@ type Config struct {
DefaultRoomEnabled bool // Flag if default room ("") is enabled DefaultRoomEnabled bool // Flag if default room ("") is enabled
Plugin string // Plugin to load Plugin string // Plugin to load
AuthorizeRoomCreation bool // Whether a user account is required to create rooms AuthorizeRoomCreation bool // Whether a user account is required to create rooms
AuthorizeRoomJoin bool // Whether a user account is required to join rooms
Modules []string // List of enabled modules
globalRoomID string // Id of the global room (not exported to Javascript) globalRoomID string // Id of the global room (not exported to Javascript)
contentSecurityPolicy string // HTML content security policy contentSecurityPolicy string // HTML content security policy
contentSecurityPolicyReportOnly string // HTML content security policy in report only mode contentSecurityPolicyReportOnly string // HTML content security policy in report only mode
@ -85,6 +87,16 @@ func NewConfig(container phoenix.Container, tokens bool) *Config {
turnURIs := strings.Split(turnURIsString, " ") turnURIs := strings.Split(turnURIsString, " ")
trimAndRemoveDuplicates(&turnURIs) trimAndRemoveDuplicates(&turnURIs)
// Get enabled modules.
allModules := []string{"screensharing", "youtube", "presentation"}
modules := allModules[:0]
for _, module := range allModules {
if container.GetBoolDefault("modules", module, true) {
modules = append(modules, module)
}
}
log.Println("Enabled modules:", modules)
return &Config{ return &Config{
Title: container.GetStringDefault("app", "title", "Spreed WebRTC"), Title: container.GetStringDefault("app", "title", "Spreed WebRTC"),
ver: ver, ver: ver,
@ -101,6 +113,8 @@ func NewConfig(container phoenix.Container, tokens bool) *Config {
DefaultRoomEnabled: container.GetBoolDefault("app", "defaultRoomEnabled", true), DefaultRoomEnabled: container.GetBoolDefault("app", "defaultRoomEnabled", true),
Plugin: container.GetStringDefault("app", "plugin", ""), Plugin: container.GetStringDefault("app", "plugin", ""),
AuthorizeRoomCreation: container.GetBoolDefault("app", "authorizeRoomCreation", false), AuthorizeRoomCreation: container.GetBoolDefault("app", "authorizeRoomCreation", false),
AuthorizeRoomJoin: container.GetBoolDefault("app", "authorizeRoomJoin", false),
Modules: modules,
globalRoomID: container.GetStringDefault("app", "globalRoom", ""), globalRoomID: container.GetStringDefault("app", "globalRoom", ""),
contentSecurityPolicy: container.GetStringDefault("app", "contentSecurityPolicy", ""), contentSecurityPolicy: container.GetStringDefault("app", "contentSecurityPolicy", ""),
contentSecurityPolicyReportOnly: container.GetStringDefault("app", "contentSecurityPolicyReportOnly", ""), contentSecurityPolicyReportOnly: container.GetStringDefault("app", "contentSecurityPolicyReportOnly", ""),

12
src/app/spreed-webrtc-server/connection.go

@ -56,7 +56,7 @@ const (
type Connection interface { type Connection interface {
Index() uint64 Index() uint64
Send(Buffer) Send(Buffer)
Close(runCallbacks bool) Close()
readPump() readPump()
writePump() writePump()
} }
@ -65,7 +65,6 @@ type ConnectionHandler interface {
NewBuffer() Buffer NewBuffer() Buffer
OnConnect(Connection) OnConnect(Connection)
OnText(Buffer) OnText(Buffer)
OnDisconnect()
} }
type connection struct { type connection struct {
@ -98,15 +97,12 @@ func (c *connection) Index() uint64 {
return c.Idx return c.Idx
} }
func (c *connection) Close(runCallbacks bool) { func (c *connection) Close() {
c.mutex.Lock() c.mutex.Lock()
if c.isClosed { if c.isClosed {
c.mutex.Unlock() c.mutex.Unlock()
return return
} }
if runCallbacks {
c.handler.OnDisconnect()
}
c.ws.Close() c.ws.Close()
c.isClosed = true c.isClosed = true
for { for {
@ -170,7 +166,7 @@ func (c *connection) readPump() {
} }
} }
c.Close(true) c.Close()
} }
// Write message to outbound queue. // Write message to outbound queue.
@ -269,7 +265,7 @@ func (c *connection) writePump() {
cleanup: cleanup:
//fmt.Println("writePump done") //fmt.Println("writePump done")
timer.Stop() timer.Stop()
c.Close(true) c.Close()
} }
// Write ping message. // Write ping message.

22
src/app/spreed-webrtc-server/hub.go

@ -48,8 +48,8 @@ type SessionStore interface {
type Unicaster interface { type Unicaster interface {
SessionStore SessionStore
OnConnect(Client, *Session) OnConnect(Client, *Session)
Unicast(session *Session, to string, m interface{}) Unicast(to string, outgoing *DataOutgoing)
OnDisconnect(*Session) OnDisconnect(sessionID string)
} }
type ContactManager interface { type ContactManager interface {
@ -158,7 +158,7 @@ func (h *hub) OnConnect(client Client, session *Session) {
// Register connection or replace existing one. // Register connection or replace existing one.
if ec, ok := h.clients[session.Id]; ok { if ec, ok := h.clients[session.Id]; ok {
ec.Close(false) ec.Close()
//log.Printf("Register (%d) from %s: %s (existing)\n", c.Idx, c.Id) //log.Printf("Register (%d) from %s: %s (existing)\n", c.Idx, c.Id)
} }
h.clients[session.Id] = client h.clients[session.Id] = client
@ -167,26 +167,20 @@ func (h *hub) OnConnect(client Client, session *Session) {
//log.Printf("Register (%d) from %s: %s\n", c.Idx, c.Id) //log.Printf("Register (%d) from %s: %s\n", c.Idx, c.Id)
} }
func (h *hub) OnDisconnect(session *Session) { func (h *hub) OnDisconnect(sessionID string) {
h.mutex.Lock() h.mutex.Lock()
delete(h.clients, session.Id) delete(h.clients, sessionID)
h.mutex.Unlock() h.mutex.Unlock()
} }
func (h *hub) GetClient(id string) (client Client, ok bool) { func (h *hub) GetClient(sessionID string) (client Client, ok bool) {
h.mutex.RLock() h.mutex.RLock()
client, ok = h.clients[id] client, ok = h.clients[sessionID]
h.mutex.RUnlock() h.mutex.RUnlock()
return return
} }
func (h *hub) Unicast(session *Session, to string, m interface{}) { func (h *hub) Unicast(to string, outgoing *DataOutgoing) {
outgoing := &DataOutgoing{
From: session.Id,
To: to,
A: session.Attestation(),
Data: m,
}
if message, err := h.EncodeOutgoing(outgoing); err == nil { if message, err := h.EncodeOutgoing(outgoing); err == nil {
client, ok := h.GetClient(to) client, ok := h.GetClient(to)
if !ok { if !ok {

4
src/app/spreed-webrtc-server/main.go

@ -340,9 +340,9 @@ func runner(runtime phoenix.Runtime) error {
roomManager := NewRoomManager(config, codec) roomManager := NewRoomManager(config, codec)
hub := NewHub(config, sessionSecret, encryptionSecret, turnSecret, codec) hub := NewHub(config, sessionSecret, encryptionSecret, turnSecret, codec)
tickets := NewTickets(sessionSecret, encryptionSecret, computedRealm) tickets := NewTickets(sessionSecret, encryptionSecret, computedRealm)
sessionManager := NewSessionManager(config, tickets, sessionSecret) sessionManager := NewSessionManager(config, tickets, hub, roomManager, roomManager, buddyImages, sessionSecret)
statsManager := NewStatsManager(hub, roomManager, sessionManager) statsManager := NewStatsManager(hub, roomManager, sessionManager)
channellingAPI := NewChannellingAPI(config, roomManager, tickets, sessionManager, statsManager, hub, hub, hub, roomManager, buddyImages) channellingAPI := NewChannellingAPI(config, roomManager, tickets, sessionManager, statsManager, hub, hub, hub)
r.HandleFunc("/", httputils.MakeGzipHandler(mainHandler)) r.HandleFunc("/", httputils.MakeGzipHandler(mainHandler))
r.Handle("/static/img/buddy/{flags}/{imageid}/{idx:.*}", http.StripPrefix(config.B, makeImageHandler(buddyImages, time.Duration(24)*time.Hour))) r.Handle("/static/img/buddy/{flags}/{imageid}/{idx:.*}", http.StripPrefix(config.B, makeImageHandler(buddyImages, time.Duration(24)*time.Hour)))
r.Handle("/static/{path:.*}", http.StripPrefix(config.B, httputils.FileStaticServer(http.Dir(rootFolder)))) r.Handle("/static/{path:.*}", http.StripPrefix(config.B, httputils.FileStaticServer(http.Dir(rootFolder))))

61
src/app/spreed-webrtc-server/room_manager.go

@ -28,13 +28,13 @@ import (
type RoomStatusManager interface { type RoomStatusManager interface {
RoomUsers(*Session) []*DataSession RoomUsers(*Session) []*DataSession
JoinRoom(string, *DataRoomCredentials, *Session, Sender) (*DataRoom, error) JoinRoom(roomID string, credentials *DataRoomCredentials, session *Session, sessionAuthenticated bool, sender Sender) (*DataRoom, error)
LeaveRoom(*Session) LeaveRoom(roomID, sessionID string)
UpdateRoom(*Session, *DataRoom) (*DataRoom, error) UpdateRoom(*Session, *DataRoom) (*DataRoom, error)
} }
type Broadcaster interface { type Broadcaster interface {
Broadcast(*Session, interface{}) Broadcast(sessionID, roomID string, outgoing *DataOutgoing)
} }
type RoomStats interface { type RoomStats interface {
@ -71,12 +71,12 @@ func (rooms *roomManager) RoomUsers(session *Session) []*DataSession {
return []*DataSession{} return []*DataSession{}
} }
func (rooms *roomManager) JoinRoom(id string, credentials *DataRoomCredentials, session *Session, sender Sender) (*DataRoom, error) { func (rooms *roomManager) JoinRoom(roomID string, credentials *DataRoomCredentials, session *Session, sessionAuthenticated bool, sender Sender) (*DataRoom, error) {
if id == "" && !rooms.DefaultRoomEnabled { if roomID == "" && !rooms.DefaultRoomEnabled {
return nil, NewDataError("default_room_disabled", "The default room is not enabled") return nil, NewDataError("default_room_disabled", "The default room is not enabled")
} }
roomWorker, err := rooms.GetOrCreate(id, credentials, session) roomWorker, err := rooms.GetOrCreate(roomID, credentials, sessionAuthenticated)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -84,9 +84,9 @@ func (rooms *roomManager) JoinRoom(id string, credentials *DataRoomCredentials,
return roomWorker.Join(credentials, session, sender) return roomWorker.Join(credentials, session, sender)
} }
func (rooms *roomManager) LeaveRoom(session *Session) { func (rooms *roomManager) LeaveRoom(roomID, sessionID string) {
if room, ok := rooms.Get(session.Roomid); ok { if room, ok := rooms.Get(roomID); ok {
room.Leave(session) room.Leave(sessionID)
} }
} }
@ -104,29 +104,22 @@ func (rooms *roomManager) UpdateRoom(session *Session, room *DataRoom) (*DataRoo
return room, nil return room, nil
} }
func (rooms *roomManager) Broadcast(session *Session, m interface{}) { func (rooms *roomManager) Broadcast(sessionID, roomID string, outgoing *DataOutgoing) {
outgoing := &DataOutgoing{
From: session.Id,
A: session.Attestation(),
Data: m,
}
message, err := rooms.EncodeOutgoing(outgoing) message, err := rooms.EncodeOutgoing(outgoing)
if err != nil { if err != nil {
return return
} }
id := session.Roomid if roomID != "" && roomID == rooms.globalRoomID {
if id != "" && id == rooms.globalRoomID {
rooms.RLock() rooms.RLock()
for _, room := range rooms.roomTable { for _, room := range rooms.roomTable {
room.Broadcast(session, message) room.Broadcast(sessionID, message)
} }
rooms.RUnlock() rooms.RUnlock()
} else if room, ok := rooms.Get(id); ok { } else if room, ok := rooms.Get(roomID); ok {
room.Broadcast(session, message) room.Broadcast(sessionID, message)
} else { } else {
log.Printf("No room named %s found for broadcast message %#v", id, m) log.Printf("No room named %s found for broadcast %#v", roomID, outgoing)
} }
message.Decref() message.Decref()
} }
@ -145,33 +138,37 @@ func (rooms *roomManager) RoomInfo(includeSessions bool) (count int, sessionInfo
return return
} }
func (rooms *roomManager) Get(id string) (room RoomWorker, ok bool) { func (rooms *roomManager) Get(roomID string) (room RoomWorker, ok bool) {
rooms.RLock() rooms.RLock()
room, ok = rooms.roomTable[id] room, ok = rooms.roomTable[roomID]
rooms.RUnlock() rooms.RUnlock()
return return
} }
func (rooms *roomManager) GetOrCreate(id string, credentials *DataRoomCredentials, session *Session) (RoomWorker, error) { func (rooms *roomManager) GetOrCreate(roomID string, credentials *DataRoomCredentials, sessionAuthenticated bool) (RoomWorker, error) {
if room, ok := rooms.Get(id); ok { if rooms.AuthorizeRoomJoin && rooms.UsersEnabled && !sessionAuthenticated {
return nil, NewDataError("room_join_requires_account", "Room join requires a user account")
}
if room, ok := rooms.Get(roomID); ok {
return room, nil return room, nil
} }
rooms.Lock() rooms.Lock()
// Need to re-check, another thread might have created the room // Need to re-check, another thread might have created the room
// while we waited for the lock. // while we waited for the lock.
if room, ok := rooms.roomTable[id]; ok { if room, ok := rooms.roomTable[roomID]; ok {
rooms.Unlock() rooms.Unlock()
return room, nil return room, nil
} }
if rooms.UsersEnabled && rooms.AuthorizeRoomCreation && !session.Authenticated() { if rooms.UsersEnabled && rooms.AuthorizeRoomCreation && !sessionAuthenticated {
rooms.Unlock() rooms.Unlock()
return nil, NewDataError("room_join_requires_account", "Room creation requires a user account") return nil, NewDataError("room_join_requires_account", "Room creation requires a user account")
} }
room := NewRoomWorker(rooms, id, credentials) room := NewRoomWorker(rooms, roomID, credentials)
rooms.roomTable[id] = room rooms.roomTable[roomID] = room
rooms.Unlock() rooms.Unlock()
go func() { go func() {
// Start room, this blocks until room expired. // Start room, this blocks until room expired.
@ -179,8 +176,8 @@ func (rooms *roomManager) GetOrCreate(id string, credentials *DataRoomCredential
// Cleanup room when we are done. // Cleanup room when we are done.
rooms.Lock() rooms.Lock()
defer rooms.Unlock() defer rooms.Unlock()
delete(rooms.roomTable, id) delete(rooms.roomTable, roomID)
log.Printf("Cleaned up room '%s'\n", id) log.Printf("Cleaned up room '%s'\n", roomID)
}() }()
return room, nil return room, nil

25
src/app/spreed-webrtc-server/room_manager_test.go

@ -36,21 +36,40 @@ func Test_RoomManager_JoinRoom_ReturnsAnErrorForUnauthenticatedSessionsWhenCreat
config.AuthorizeRoomCreation = true config.AuthorizeRoomCreation = true
unauthenticatedSession := &Session{} unauthenticatedSession := &Session{}
_, err := roomManager.JoinRoom("foo", nil, unauthenticatedSession, nil) _, err := roomManager.JoinRoom("foo", nil, unauthenticatedSession, false, nil)
assertDataError(t, err, "room_join_requires_account") assertDataError(t, err, "room_join_requires_account")
authenticatedSession := &Session{userid: "9870457"} authenticatedSession := &Session{userid: "9870457"}
_, err = roomManager.JoinRoom("foo", nil, authenticatedSession, nil) _, err = roomManager.JoinRoom("foo", nil, authenticatedSession, true, nil)
if err != nil { if err != nil {
t.Fatalf("Unexpected error %v joining room while authenticated", err) t.Fatalf("Unexpected error %v joining room while authenticated", err)
} }
_, err = roomManager.JoinRoom("foo", nil, unauthenticatedSession, nil) _, err = roomManager.JoinRoom("foo", nil, unauthenticatedSession, false, nil)
if err != nil { if err != nil {
t.Fatalf("Unexpected error %v joining room while unauthenticated", err) t.Fatalf("Unexpected error %v joining room while unauthenticated", err)
} }
} }
func Test_RoomManager_JoinRoom_ReturnsAnErrorForUnauthenticatedSessionsWhenJoinRequiresAnAccount(t *testing.T) {
roomManager, config := NewTestRoomManager()
config.UsersEnabled = true
config.AuthorizeRoomJoin = true
unauthenticatedSession := &Session{}
_, err := roomManager.JoinRoom("foo", nil, unauthenticatedSession, false, nil)
assertDataError(t, err, "room_join_requires_account")
authenticatedSession := &Session{userid: "9870457"}
_, err = roomManager.JoinRoom("foo", nil, authenticatedSession, true, nil)
if err != nil {
t.Fatalf("Unexpected error %v joining room while authenticated", err)
}
_, err = roomManager.JoinRoom("foo", nil, unauthenticatedSession, false, nil)
assertDataError(t, err, "room_join_requires_account")
}
func Test_RoomManager_UpdateRoom_ReturnsAnErrorIfNoRoomHasBeenJoined(t *testing.T) { func Test_RoomManager_UpdateRoom_ReturnsAnErrorIfNoRoomHasBeenJoined(t *testing.T) {
roomManager, _ := NewTestRoomManager() roomManager, _ := NewTestRoomManager()
_, err := roomManager.UpdateRoom(&Session{}, nil) _, err := roomManager.UpdateRoom(&Session{}, nil)

18
src/app/spreed-webrtc-server/roomworker.go

@ -39,9 +39,9 @@ type RoomWorker interface {
Users() []*roomUser Users() []*roomUser
Update(*DataRoom) error Update(*DataRoom) error
GetUsers() []*DataSession GetUsers() []*DataSession
Broadcast(*Session, Buffer) Broadcast(sessionID string, buf Buffer)
Join(*DataRoomCredentials, *Session, Sender) (*DataRoom, error) Join(*DataRoomCredentials, *Session, Sender) (*DataRoom, error)
Leave(*Session) Leave(sessionID string)
} }
type roomWorker struct { type roomWorker struct {
@ -213,19 +213,19 @@ func (r *roomWorker) GetUsers() []*DataSession {
return <-out return <-out
} }
func (r *roomWorker) Broadcast(session *Session, message Buffer) { func (r *roomWorker) Broadcast(sessionID string, message Buffer) {
worker := func() { worker := func() {
r.mutex.RLock() r.mutex.RLock()
defer r.mutex.RUnlock()
for id, user := range r.users { for id, user := range r.users {
if id == session.Id { if id == sessionID {
// Skip broadcast to self. // Skip broadcast to self.
continue continue
} }
//fmt.Printf("%s\n", m.Message) //fmt.Printf("%s\n", m.Message)
user.Send(message) user.Send(message)
} }
r.mutex.RUnlock()
message.Decref() message.Decref()
} }
@ -273,13 +273,13 @@ func (r *roomWorker) Join(credentials *DataRoomCredentials, session *Session, se
return result.DataRoom, result.error return result.DataRoom, result.error
} }
func (r *roomWorker) Leave(session *Session) { func (r *roomWorker) Leave(sessionID string) {
worker := func() { worker := func() {
r.mutex.Lock() r.mutex.Lock()
defer r.mutex.Unlock() if _, ok := r.users[sessionID]; ok {
if _, ok := r.users[session.Id]; ok { delete(r.users, sessionID)
delete(r.users, session.Id)
} }
r.mutex.Unlock()
} }
r.Run(worker) r.Run(worker)
} }

222
src/app/spreed-webrtc-server/session.go

@ -25,6 +25,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/gorilla/securecookie" "github.com/gorilla/securecookie"
"strings"
"sync" "sync"
"time" "time"
) )
@ -32,6 +33,11 @@ import (
var sessionNonces *securecookie.SecureCookie var sessionNonces *securecookie.SecureCookie
type Session struct { type Session struct {
SessionManager
Unicaster
Broadcaster
RoomStatusManager
buddyImages ImageCache
Id string Id string
Sid string Sid string
Ua string Ua string
@ -49,24 +55,35 @@ type Session struct {
attestations *securecookie.SecureCookie attestations *securecookie.SecureCookie
subscriptions map[string]*Session subscriptions map[string]*Session
subscribers map[string]*Session subscribers map[string]*Session
disconnected bool
} }
func NewSession(attestations *securecookie.SecureCookie, id, sid string) *Session { func NewSession(manager SessionManager, unicaster Unicaster, broadcaster Broadcaster, rooms RoomStatusManager, buddyImages ImageCache, attestations *securecookie.SecureCookie, id, sid string) *Session {
session := &Session{ session := &Session{
Id: id, SessionManager: manager,
Sid: sid, Unicaster: unicaster,
Prio: 100, Broadcaster: broadcaster,
stamp: time.Now().Unix(), RoomStatusManager: rooms,
attestations: attestations, buddyImages: buddyImages,
subscriptions: make(map[string]*Session), Id: id,
subscribers: make(map[string]*Session), Sid: sid,
Prio: 100,
stamp: time.Now().Unix(),
attestations: attestations,
subscriptions: make(map[string]*Session),
subscribers: make(map[string]*Session),
} }
session.NewAttestation() session.NewAttestation()
return session return session
} }
func (s *Session) authenticated() (authenticated bool) {
authenticated = s.userid != ""
return
}
func (s *Session) Subscribe(session *Session) { func (s *Session) Subscribe(session *Session) {
s.mutex.Lock() s.mutex.Lock()
@ -103,37 +120,144 @@ func (s *Session) RemoveSubscriber(id string) {
s.mutex.Unlock() s.mutex.Unlock()
} }
func (s *Session) RunForAllSubscribers(f func(session *Session)) { func (s *Session) JoinRoom(roomID string, credentials *DataRoomCredentials, sender Sender) (*DataRoom, error) {
s.mutex.Lock() s.mutex.Lock()
for _, session := range s.subscribers { defer s.mutex.Unlock()
s.mutex.Unlock()
f(session) if s.Hello && s.Roomid != roomID {
s.mutex.Lock() s.RoomStatusManager.LeaveRoom(s.Roomid, s.Id)
s.Broadcaster.Broadcast(s.Id, s.Roomid, &DataOutgoing{
From: s.Id,
A: s.attestation.Token(),
Data: &DataSession{
Type: "Left",
Id: s.Id,
Status: "soft",
},
})
} }
s.mutex.Unlock()
room, err := s.RoomStatusManager.JoinRoom(roomID, credentials, s, s.authenticated(), sender)
if err == nil {
s.Hello = true
s.Roomid = roomID
s.Broadcaster.Broadcast(s.Id, s.Roomid, &DataOutgoing{
From: s.Id,
A: s.attestation.Token(),
Data: &DataSession{
Type: "Joined",
Id: s.Id,
Userid: s.userid,
Ua: s.Ua,
Prio: s.Prio,
},
})
} else {
s.Hello = false
}
return room, err
}
func (s *Session) Broadcast(m interface{}) {
s.mutex.RLock()
if s.Hello {
s.Broadcaster.Broadcast(s.Id, s.Roomid, &DataOutgoing{
From: s.Id,
A: s.attestation.Token(),
Data: m,
})
}
s.mutex.RUnlock()
} }
func (s *Session) Close() { func (s *Session) BroadcastStatus() {
s.mutex.RLock()
if s.Hello {
s.Broadcaster.Broadcast(s.Id, s.Roomid, &DataOutgoing{
From: s.Id,
A: s.attestation.Token(),
Data: &DataSession{
Type: "Status",
Id: s.Id,
Userid: s.userid,
Status: s.Status,
Rev: s.UpdateRev,
Prio: s.Prio,
},
})
}
s.mutex.RUnlock()
}
func (s *Session) Unicast(to string, m interface{}) {
s.mutex.RLock()
outgoing := &DataOutgoing{
From: s.Id,
To: to,
A: s.attestation.Token(),
Data: m,
}
s.mutex.RUnlock()
s.Unicaster.Unicast(to, outgoing)
}
func (s *Session) Close() {
s.mutex.Lock() s.mutex.Lock()
// Remove foreign references.
outgoing := &DataOutgoing{
From: s.Id,
A: s.attestation.Token(),
Data: &DataSession{
Type: "Left",
Id: s.Id,
Status: "hard",
},
}
if s.Hello {
// NOTE(lcooper): If we don't check for Hello here, we could deadlock
// when implicitly creating a room while a user is reconnecting.
s.Broadcaster.Broadcast(s.Id, s.Roomid, outgoing)
s.RoomStatusManager.LeaveRoom(s.Roomid, s.Id)
}
for _, session := range s.subscribers {
s.Unicaster.Unicast(session.Id, outgoing)
}
for _, session := range s.subscriptions { for _, session := range s.subscriptions {
session.RemoveSubscriber(s.Id) session.RemoveSubscriber(s.Id)
} }
// Remove session cross references.
s.Unicaster.OnDisconnect(s.Id)
s.SessionManager.DestroySession(s.Id, s.userid)
s.buddyImages.Delete(s.Id)
s.subscriptions = make(map[string]*Session) s.subscriptions = make(map[string]*Session)
s.subscribers = make(map[string]*Session) s.subscribers = make(map[string]*Session)
s.mutex.Unlock() s.disconnected = true
s.mutex.Unlock()
} }
func (s *Session) Update(update *SessionUpdate) uint64 { func (s *Session) Update(update *SessionUpdate) uint64 {
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
if update.Status != nil {
status, ok := update.Status.(map[string]interface{})
if ok && status["buddyPicture"] != nil {
pic := status["buddyPicture"].(string)
if strings.HasPrefix(pic, "data:") {
imageId := s.buddyImages.Update(s.Id, pic[5:])
if imageId != "" {
status["buddyPicture"] = "img:" + imageId
}
}
}
}
for _, key := range update.Types { for _, key := range update.Types {
//fmt.Println("type update", key) //fmt.Println("type update", key)
@ -173,13 +297,6 @@ func (s *Session) Authorize(realm string, st *SessionToken) (string, error) {
} }
func (s *Session) Authenticated() (authenticated bool) {
s.mutex.Lock()
authenticated = s.userid != ""
s.mutex.Unlock()
return
}
func (s *Session) Authenticate(realm string, st *SessionToken, userid string) error { func (s *Session) Authenticate(realm string, st *SessionToken, userid string) error {
s.mutex.Lock() s.mutex.Lock()
@ -252,50 +369,6 @@ func (s *Session) SetUseridFake(userid string) {
} }
func (s *Session) DataSessionLeft(state string) *DataSession {
s.mutex.RLock()
defer s.mutex.RUnlock()
return &DataSession{
Type: "Left",
Id: s.Id,
Status: state,
}
}
func (s *Session) DataSessionJoined() *DataSession {
s.mutex.RLock()
defer s.mutex.RUnlock()
return &DataSession{
Type: "Joined",
Id: s.Id,
Userid: s.userid,
Ua: s.Ua,
Prio: s.Prio,
}
}
func (s *Session) DataSessionStatus() *DataSession {
s.mutex.RLock()
defer s.mutex.RUnlock()
return &DataSession{
Type: "Status",
Id: s.Id,
Userid: s.userid,
Status: s.Status,
Rev: s.UpdateRev,
Prio: s.Prio,
}
}
func (s *Session) NewAttestation() { func (s *Session) NewAttestation() {
s.attestation = &SessionAttestation{ s.attestation = &SessionAttestation{
s: s, s: s,
@ -303,13 +376,6 @@ func (s *Session) NewAttestation() {
s.attestation.Update() s.attestation.Update()
} }
func (s *Session) Attestation() (attestation string) {
s.mutex.RLock()
attestation = s.attestation.Token()
s.mutex.RUnlock()
return
}
func (s *Session) UpdateAttestation() { func (s *Session) UpdateAttestation() {
s.mutex.Lock() s.mutex.Lock()
s.attestation.Update() s.attestation.Update()

34
src/app/spreed-webrtc-server/session_manager.go

@ -37,14 +37,18 @@ type SessionManager interface {
UserStats UserStats
RetrieveUsersWith(func(*http.Request) (string, error)) RetrieveUsersWith(func(*http.Request) (string, error))
CreateSession(*http.Request) *Session CreateSession(*http.Request) *Session
DestroySession(*Session) DestroySession(sessionID, userID string)
Authenticate(*Session, *SessionToken, string) error Authenticate(*Session, *SessionToken, string) error
GetUserSessions(session *Session, id string) []*DataSession GetUserSessions(session *Session, id string) []*DataSession
} }
type sessionManager struct { type sessionManager struct {
Tickets
sync.RWMutex sync.RWMutex
Tickets
Unicaster
Broadcaster
RoomStatusManager
buddyImages ImageCache
config *Config config *Config
userTable map[string]*User userTable map[string]*User
fakesessionTable map[string]*Session fakesessionTable map[string]*Session
@ -52,10 +56,14 @@ type sessionManager struct {
attestations *securecookie.SecureCookie attestations *securecookie.SecureCookie
} }
func NewSessionManager(config *Config, tickets Tickets, sessionSecret []byte) SessionManager { func NewSessionManager(config *Config, tickets Tickets, unicaster Unicaster, broadcaster Broadcaster, rooms RoomStatusManager, buddyImages ImageCache, sessionSecret []byte) SessionManager {
sessionManager := &sessionManager{ sessionManager := &sessionManager{
tickets,
sync.RWMutex{}, sync.RWMutex{},
tickets,
unicaster,
broadcaster,
rooms,
buddyImages,
config, config,
make(map[string]*User), make(map[string]*User),
make(map[string]*Session), make(map[string]*Session),
@ -103,7 +111,7 @@ func (sessionManager *sessionManager) CreateSession(request *http.Request) *Sess
} }
} }
session := NewSession(sessionManager.attestations, st.Id, st.Sid) session := NewSession(sessionManager, sessionManager.Unicaster, sessionManager.Broadcaster, sessionManager.RoomStatusManager, sessionManager.buddyImages, sessionManager.attestations, st.Id, st.Sid)
if userid != "" { if userid != "" {
// XXX(lcooper): Should errors be handled here? // XXX(lcooper): Should errors be handled here?
@ -113,15 +121,15 @@ func (sessionManager *sessionManager) CreateSession(request *http.Request) *Sess
return session return session
} }
func (sessionManager *sessionManager) DestroySession(session *Session) { func (sessionManager *sessionManager) DestroySession(sessionID, userID string) {
session.Close() if userID == "" {
return
}
sessionManager.Lock() sessionManager.Lock()
if suserid := session.Userid(); suserid != "" { user, ok := sessionManager.userTable[userID]
user, ok := sessionManager.userTable[suserid] if ok && user.RemoveSession(sessionID) {
if ok && user.RemoveSession(session) { delete(sessionManager.userTable, userID)
delete(sessionManager.userTable, suserid)
}
} }
sessionManager.Unlock() sessionManager.Unlock()
} }
@ -158,7 +166,7 @@ func (sessionManager *sessionManager) GetUserSessions(session *Session, userid s
session, ok := sessionManager.fakesessionTable[userid] session, ok := sessionManager.fakesessionTable[userid]
if !ok { if !ok {
st := sessionManager.FakeSessionToken(userid) st := sessionManager.FakeSessionToken(userid)
session = NewSession(sessionManager.attestations, st.Id, st.Sid) session = NewSession(sessionManager, sessionManager.Unicaster, sessionManager.Broadcaster, sessionManager.RoomStatusManager, sessionManager.buddyImages, sessionManager.attestations, st.Id, st.Sid)
session.SetUseridFake(st.Userid) session.SetUseridFake(st.Userid)
sessionManager.fakesessionTable[userid] = session sessionManager.fakesessionTable[userid] = session
} }

4
src/app/spreed-webrtc-server/user.go

@ -57,10 +57,10 @@ func (u *User) AddSession(s *Session) bool {
} }
// Return true if no session left. // Return true if no session left.
func (u *User) RemoveSession(s *Session) bool { func (u *User) RemoveSession(sessionID string) bool {
last := false last := false
u.mutex.Lock() u.mutex.Lock()
delete(u.sessionTable, s.Id) delete(u.sessionTable, sessionID)
if len(u.sessionTable) == 0 { if len(u.sessionTable) == 0 {
log.Println("Last session unregistered for user", u.Id) log.Println("Last session unregistered for user", u.Id)
last = true last = true

2
src/app/spreed-webrtc-server/ws.go

@ -69,7 +69,7 @@ func makeWSHandler(connectionCounter ConnectionCounter, sessionManager SessionMa
// Create a new connection instance. // Create a new connection instance.
session := sessionManager.CreateSession(r) session := sessionManager.CreateSession(r)
defer sessionManager.DestroySession(session) defer session.Close()
client := NewClient(codec, channellingAPI, session) client := NewClient(codec, channellingAPI, session)
conn := NewConnection(connectionCounter.CountConnection(), ws, client) conn := NewConnection(connectionCounter.CountConnection(), ws, client)

61
src/i18n/messages-de.po

@ -8,8 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Spreed WebRTC 1.0\n" "Project-Id-Version: Spreed WebRTC 1.0\n"
"Report-Msgid-Bugs-To: simon@struktur.de\n" "Report-Msgid-Bugs-To: simon@struktur.de\n"
"POT-Creation-Date: 2014-12-08 16:48+0100\n" "POT-Creation-Date: 2015-01-28 15:06+0100\n"
"PO-Revision-Date: 2014-12-08 16:48+0100\n" "PO-Revision-Date: 2015-01-28 15:07+0100\n"
"Last-Translator: Simon Eisenmann <simon@struktur.de>\n" "Last-Translator: Simon Eisenmann <simon@struktur.de>\n"
"Language-Team: struktur AG <opensource@struktur.de>\n" "Language-Team: struktur AG <opensource@struktur.de>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n"
@ -18,30 +18,6 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 0.9.6\n" "Generated-By: Babel 0.9.6\n"
msgid "Share a YouTube video"
msgstr "Ein YouTube Video teilen"
msgid "Share a file as presentation"
msgstr "Datei als Präsentation teilen."
msgid "Share your screen"
msgstr "Bildschirm freigeben"
msgid "Chat"
msgstr "Chat"
msgid "Contacts"
msgstr "Kontakte"
msgid "Mute microphone"
msgstr "Mikrofon abschalten"
msgid "Turn camera off"
msgstr "Kamera abschalten"
msgid "Settings"
msgstr "Einstellungen"
msgid "Your audio level" msgid "Your audio level"
msgstr "Ihr Audio-Pegel" msgstr "Ihr Audio-Pegel"
@ -197,6 +173,30 @@ msgstr "Nochmal versuchen"
msgid "Download failed." msgid "Download failed."
msgstr "Fehler beim Download." msgstr "Fehler beim Download."
msgid "Share a YouTube video"
msgstr "Ein YouTube Video teilen"
msgid "Share a file as presentation"
msgstr "Datei als Präsentation teilen."
msgid "Share your screen"
msgstr "Bildschirm freigeben"
msgid "Chat"
msgstr "Chat"
msgid "Contacts"
msgstr "Kontakte"
msgid "Mute microphone"
msgstr "Mikrofon abschalten"
msgid "Turn camera off"
msgstr "Kamera abschalten"
msgid "Settings"
msgstr "Einstellungen"
msgid "Loading presentation ..." msgid "Loading presentation ..."
msgstr "Präsentation wird geladen..." msgstr "Präsentation wird geladen..."
@ -231,12 +231,12 @@ msgstr "Raum wechseln"
msgid "Room" msgid "Room"
msgstr "Raum" msgstr "Raum"
msgid "Main"
msgstr "Standard"
msgid "Leave room" msgid "Leave room"
msgstr "Raum verlassen" msgstr "Raum verlassen"
msgid "Main"
msgstr "Standard"
msgid "Current room" msgid "Current room"
msgstr "Raum" msgstr "Raum"
@ -500,6 +500,9 @@ msgstr ""
msgid "Room history" msgid "Room history"
msgstr "Raum-Verlauf" msgstr "Raum-Verlauf"
msgid "Please sign in."
msgstr "Bitte melden Sie sich an."
msgid "Videos are shared with everyone in this call." msgid "Videos are shared with everyone in this call."
msgstr "Das Video wird bei allen Gesprächsteilnehmern angezeigt." msgstr "Das Video wird bei allen Gesprächsteilnehmern angezeigt."

59
src/i18n/messages-ja.po

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Spreed WebRTC 1.0\n" "Project-Id-Version: Spreed WebRTC 1.0\n"
"Report-Msgid-Bugs-To: simon@struktur.de\n" "Report-Msgid-Bugs-To: simon@struktur.de\n"
"POT-Creation-Date: 2014-12-08 16:48+0100\n" "POT-Creation-Date: 2015-01-28 15:06+0100\n"
"PO-Revision-Date: 2014-04-23 22:25+0100\n" "PO-Revision-Date: 2014-04-23 22:25+0100\n"
"Last-Translator: Curt Frisemo <curt.frisemo@spreed.com>\n" "Last-Translator: Curt Frisemo <curt.frisemo@spreed.com>\n"
"Language-Team: Curt Frisemo <curt.frisemo@spreed.com>\n" "Language-Team: Curt Frisemo <curt.frisemo@spreed.com>\n"
@ -18,30 +18,6 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 0.9.6\n" "Generated-By: Babel 0.9.6\n"
msgid "Share a YouTube video"
msgstr ""
msgid "Share a file as presentation"
msgstr ""
msgid "Share your screen"
msgstr "画面を共有する."
msgid "Chat"
msgstr "チャット"
msgid "Contacts"
msgstr ""
msgid "Mute microphone"
msgstr "消音"
msgid "Turn camera off"
msgstr "カメラをオフにする"
msgid "Settings"
msgstr "設定"
msgid "Your audio level" msgid "Your audio level"
msgstr "あなたの音量" msgstr "あなたの音量"
@ -199,6 +175,30 @@ msgstr "リトライ"
msgid "Download failed." msgid "Download failed."
msgstr "ダウンロード失敗." msgstr "ダウンロード失敗."
msgid "Share a YouTube video"
msgstr ""
msgid "Share a file as presentation"
msgstr ""
msgid "Share your screen"
msgstr "画面を共有する."
msgid "Chat"
msgstr "チャット"
msgid "Contacts"
msgstr ""
msgid "Mute microphone"
msgstr "消音"
msgid "Turn camera off"
msgstr "カメラをオフにする"
msgid "Settings"
msgstr "設定"
msgid "Loading presentation ..." msgid "Loading presentation ..."
msgstr "" msgstr ""
@ -231,12 +231,12 @@ msgstr "ルームチェンジ"
msgid "Room" msgid "Room"
msgstr "ルーム" msgstr "ルーム"
msgid "Main"
msgstr "メイン"
msgid "Leave room" msgid "Leave room"
msgstr "ルームを出る" msgstr "ルームを出る"
msgid "Main"
msgstr "メイン"
msgid "Current room" msgid "Current room"
msgstr "現在のルーム" msgstr "現在のルーム"
@ -492,6 +492,9 @@ msgstr ""
msgid "Room history" msgid "Room history"
msgstr "" msgstr ""
msgid "Please sign in."
msgstr ""
msgid "Videos are shared with everyone in this call." msgid "Videos are shared with everyone in this call."
msgstr "" msgstr ""

59
src/i18n/messages-ko.po

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Spreed WebRTC 1.0\n" "Project-Id-Version: Spreed WebRTC 1.0\n"
"Report-Msgid-Bugs-To: simon@struktur.de\n" "Report-Msgid-Bugs-To: simon@struktur.de\n"
"POT-Creation-Date: 2014-12-08 16:48+0100\n" "POT-Creation-Date: 2015-01-28 15:06+0100\n"
"PO-Revision-Date: 2014-04-13 20:30+0900\n" "PO-Revision-Date: 2014-04-13 20:30+0900\n"
"Last-Translator: Curt Frisemo <curt.frisemo@spreed.com>\n" "Last-Translator: Curt Frisemo <curt.frisemo@spreed.com>\n"
"Language-Team: Curt Frisemo <curt.frisemo@spreed.com>\n" "Language-Team: Curt Frisemo <curt.frisemo@spreed.com>\n"
@ -18,30 +18,6 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 0.9.6\n" "Generated-By: Babel 0.9.6\n"
msgid "Share a YouTube video"
msgstr ""
msgid "Share a file as presentation"
msgstr ""
msgid "Share your screen"
msgstr "화면 공유하기"
msgid "Chat"
msgstr "대화"
msgid "Contacts"
msgstr ""
msgid "Mute microphone"
msgstr "음성제거"
msgid "Turn camera off"
msgstr "카메라꺼짐"
msgid "Settings"
msgstr "설정"
msgid "Your audio level" msgid "Your audio level"
msgstr "음성크기" msgstr "음성크기"
@ -199,6 +175,30 @@ msgstr "재시도"
msgid "Download failed." msgid "Download failed."
msgstr "다운로드실패" msgstr "다운로드실패"
msgid "Share a YouTube video"
msgstr ""
msgid "Share a file as presentation"
msgstr ""
msgid "Share your screen"
msgstr "화면 공유하기"
msgid "Chat"
msgstr "대화"
msgid "Contacts"
msgstr ""
msgid "Mute microphone"
msgstr "음성제거"
msgid "Turn camera off"
msgstr "카메라꺼짐"
msgid "Settings"
msgstr "설정"
msgid "Loading presentation ..." msgid "Loading presentation ..."
msgstr "" msgstr ""
@ -231,12 +231,12 @@ msgstr "방 변경"
msgid "Room" msgid "Room"
msgstr "방" msgstr "방"
msgid "Main"
msgstr "메인"
msgid "Leave room" msgid "Leave room"
msgstr "방 이동" msgstr "방 이동"
msgid "Main"
msgstr "메인"
msgid "Current room" msgid "Current room"
msgstr "현재 방" msgstr "현재 방"
@ -492,6 +492,9 @@ msgstr ""
msgid "Room history" msgid "Room history"
msgstr "" msgstr ""
msgid "Please sign in."
msgstr ""
msgid "Videos are shared with everyone in this call." msgid "Videos are shared with everyone in this call."
msgstr "" msgstr ""

59
src/i18n/messages-zh-cn.po

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Spreed WebRTC 1.0\n" "Project-Id-Version: Spreed WebRTC 1.0\n"
"Report-Msgid-Bugs-To: simon@struktur.de\n" "Report-Msgid-Bugs-To: simon@struktur.de\n"
"POT-Creation-Date: 2014-12-08 16:48+0100\n" "POT-Creation-Date: 2015-01-28 15:06+0100\n"
"PO-Revision-Date: 2014-05-21 09:54+0800\n" "PO-Revision-Date: 2014-05-21 09:54+0800\n"
"Last-Translator: Michael P.\n" "Last-Translator: Michael P.\n"
"Language-Team: Curt Frisemo <curt.frisemo@spreed.com>\n" "Language-Team: Curt Frisemo <curt.frisemo@spreed.com>\n"
@ -18,30 +18,6 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 0.9.6\n" "Generated-By: Babel 0.9.6\n"
msgid "Share a YouTube video"
msgstr ""
msgid "Share a file as presentation"
msgstr ""
msgid "Share your screen"
msgstr "共享您的屏幕"
msgid "Chat"
msgstr "聊天"
msgid "Contacts"
msgstr ""
msgid "Mute microphone"
msgstr "关闭麦克风"
msgid "Turn camera off"
msgstr "关闭摄像头"
msgid "Settings"
msgstr "系统设置"
msgid "Your audio level" msgid "Your audio level"
msgstr "您的通话音量" msgstr "您的通话音量"
@ -199,6 +175,30 @@ msgstr "重试"
msgid "Download failed." msgid "Download failed."
msgstr "下载失败" msgstr "下载失败"
msgid "Share a YouTube video"
msgstr ""
msgid "Share a file as presentation"
msgstr ""
msgid "Share your screen"
msgstr "共享您的屏幕"
msgid "Chat"
msgstr "聊天"
msgid "Contacts"
msgstr ""
msgid "Mute microphone"
msgstr "关闭麦克风"
msgid "Turn camera off"
msgstr "关闭摄像头"
msgid "Settings"
msgstr "系统设置"
msgid "Loading presentation ..." msgid "Loading presentation ..."
msgstr "" msgstr ""
@ -231,12 +231,12 @@ msgstr "更换房间"
msgid "Room" msgid "Room"
msgstr "房间" msgstr "房间"
msgid "Main"
msgstr "主房间"
msgid "Leave room" msgid "Leave room"
msgstr "离开房间" msgstr "离开房间"
msgid "Main"
msgstr "主房间"
msgid "Current room" msgid "Current room"
msgstr "當前房间" msgstr "當前房间"
@ -492,6 +492,9 @@ msgstr ""
msgid "Room history" msgid "Room history"
msgstr "" msgstr ""
msgid "Please sign in."
msgstr ""
msgid "Videos are shared with everyone in this call." msgid "Videos are shared with everyone in this call."
msgstr "" msgstr ""

59
src/i18n/messages-zh-tw.po

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Spreed WebRTC 1.0\n" "Project-Id-Version: Spreed WebRTC 1.0\n"
"Report-Msgid-Bugs-To: simon@struktur.de\n" "Report-Msgid-Bugs-To: simon@struktur.de\n"
"POT-Creation-Date: 2014-12-08 16:48+0100\n" "POT-Creation-Date: 2015-01-28 15:06+0100\n"
"PO-Revision-Date: 2014-05-21 09:55+0800\n" "PO-Revision-Date: 2014-05-21 09:55+0800\n"
"Last-Translator: Michael P.\n" "Last-Translator: Michael P.\n"
"Language-Team: Curt Frisemo <curt.frisemo@spreed.com>\n" "Language-Team: Curt Frisemo <curt.frisemo@spreed.com>\n"
@ -18,30 +18,6 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 0.9.6\n" "Generated-By: Babel 0.9.6\n"
msgid "Share a YouTube video"
msgstr ""
msgid "Share a file as presentation"
msgstr ""
msgid "Share your screen"
msgstr "共享您的屏幕"
msgid "Chat"
msgstr "聊天"
msgid "Contacts"
msgstr ""
msgid "Mute microphone"
msgstr "關閉麥克風"
msgid "Turn camera off"
msgstr "關閉攝像頭"
msgid "Settings"
msgstr "系統設置"
msgid "Your audio level" msgid "Your audio level"
msgstr "您的通話音量" msgstr "您的通話音量"
@ -199,6 +175,30 @@ msgstr "重試"
msgid "Download failed." msgid "Download failed."
msgstr "下載失敗" msgstr "下載失敗"
msgid "Share a YouTube video"
msgstr ""
msgid "Share a file as presentation"
msgstr ""
msgid "Share your screen"
msgstr "共享您的屏幕"
msgid "Chat"
msgstr "聊天"
msgid "Contacts"
msgstr ""
msgid "Mute microphone"
msgstr "關閉麥克風"
msgid "Turn camera off"
msgstr "關閉攝像頭"
msgid "Settings"
msgstr "系統設置"
msgid "Loading presentation ..." msgid "Loading presentation ..."
msgstr "" msgstr ""
@ -231,12 +231,12 @@ msgstr "更換房間"
msgid "Room" msgid "Room"
msgstr "房間" msgstr "房間"
msgid "Main"
msgstr "住房間"
msgid "Leave room" msgid "Leave room"
msgstr "離開房間" msgstr "離開房間"
msgid "Main"
msgstr "住房間"
msgid "Current room" msgid "Current room"
msgstr "當前房間" msgstr "當前房間"
@ -492,6 +492,9 @@ msgstr ""
msgid "Room history" msgid "Room history"
msgstr "" msgstr ""
msgid "Please sign in."
msgstr ""
msgid "Videos are shared with everyone in this call." msgid "Videos are shared with everyone in this call."
msgstr "" msgstr ""

61
src/i18n/messages.pot

@ -1,15 +1,15 @@
# Translations template for Spreed WebRTC. # Translations template for Spreed WebRTC.
# Copyright (C) 2014 struktur AG # Copyright (C) 2015 struktur AG
# This file is distributed under the same license as the Spreed WebRTC # This file is distributed under the same license as the Spreed WebRTC
# project. # project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2014. # FIRST AUTHOR <EMAIL@ADDRESS>, 2015.
# #
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Spreed WebRTC 1.0\n" "Project-Id-Version: Spreed WebRTC 1.0\n"
"Report-Msgid-Bugs-To: simon@struktur.de\n" "Report-Msgid-Bugs-To: simon@struktur.de\n"
"POT-Creation-Date: 2014-12-08 16:48+0100\n" "POT-Creation-Date: 2015-01-28 15:06+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,30 +18,6 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 0.9.6\n" "Generated-By: Babel 0.9.6\n"
msgid "Share a YouTube video"
msgstr ""
msgid "Share a file as presentation"
msgstr ""
msgid "Share your screen"
msgstr ""
msgid "Chat"
msgstr ""
msgid "Contacts"
msgstr ""
msgid "Mute microphone"
msgstr ""
msgid "Turn camera off"
msgstr ""
msgid "Settings"
msgstr ""
msgid "Your audio level" msgid "Your audio level"
msgstr "" msgstr ""
@ -195,6 +171,30 @@ msgstr ""
msgid "Download failed." msgid "Download failed."
msgstr "" msgstr ""
msgid "Share a YouTube video"
msgstr ""
msgid "Share a file as presentation"
msgstr ""
msgid "Share your screen"
msgstr ""
msgid "Chat"
msgstr ""
msgid "Contacts"
msgstr ""
msgid "Mute microphone"
msgstr ""
msgid "Turn camera off"
msgstr ""
msgid "Settings"
msgstr ""
msgid "Loading presentation ..." msgid "Loading presentation ..."
msgstr "" msgstr ""
@ -227,10 +227,10 @@ msgstr ""
msgid "Room" msgid "Room"
msgstr "" msgstr ""
msgid "Main" msgid "Leave room"
msgstr "" msgstr ""
msgid "Leave room" msgid "Main"
msgstr "" msgstr ""
msgid "Current room" msgid "Current room"
@ -486,6 +486,9 @@ msgstr ""
msgid "Room history" msgid "Room history"
msgstr "" msgstr ""
msgid "Please sign in."
msgstr ""
msgid "Videos are shared with everyone in this call." msgid "Videos are shared with everyone in this call."
msgstr "" msgstr ""

13
src/styles/components/_buddylist.scss

@ -94,7 +94,6 @@
position: relative; position: relative;
user-select: none; user-select: none;
} }
.buddylistloading,
.buddylistempty { .buddylistempty {
bottom: 0; bottom: 0;
color: $font-color-accent; color: $font-color-accent;
@ -109,6 +108,18 @@
text-align: center; text-align: center;
top: 0; top: 0;
} }
.buddylistloading {
bottom: 0;
color: $font-color-accent;
display: none;
font-size: 1.4em;
height: 2em;
margin: auto;
padding:.4em;
position: absolute;
right: 0;
text-align: center;
}
} }
.buddy { .buddy {

2
static/css/main.min.css vendored

File diff suppressed because one or more lines are too long

38
static/js/controllers/mediastreamcontroller.js

@ -40,6 +40,13 @@ define(['jquery', 'underscore', 'angular', 'bigscreen', 'moment', 'sjcl', 'moder
return translation._("Close this window and disconnect?"); return translation._("Close this window and disconnect?");
}); });
$($window).on("unload", function() {
mediaStream.webrtc.doHangup("unload");
if (mediaStream.api.connector) {
mediaStream.api.connector.disabled = true;
}
});
// Enable app full screen listener. // Enable app full screen listener.
$("#bar .logo").on("doubletap dblclick", _.debounce(function() { $("#bar .logo").on("doubletap dblclick", _.debounce(function() {
if (BigScreen.enabled) { if (BigScreen.enabled) {
@ -401,10 +408,28 @@ define(['jquery', 'underscore', 'angular', 'bigscreen', 'moment', 'sjcl', 'moder
// Unmark authorization process. // Unmark authorization process.
if (data.Userid) { if (data.Userid) {
appData.authorizing(false); appData.authorizing(false, data.Userid);
} else if (!appData.authorizing()) { } else {
// Trigger user data load when not in authorizing phase. if (!appData.authorizing()) {
$scope.loadUserSettings(); // Trigger user data load when not in authorizing phase.
$scope.loadUserSettings();
} else {
// Wait until authorizing is over and try it then.
var handler = (function() {
return function(event, authorizing, userid) {
if (!authorizing) {
// Turn of handler if we are no longer authorizing.
appData.e.off("authorizing", handler);
handler = null;
if (!userid) {
// Trigger user data load when without user after authorizing phase.
$scope.loadUserSettings();
}
}
}
})();
appData.e.on("authorizing", handler);
}
} }
// Select room if settings have an alternative default room. // Select room if settings have an alternative default room.
@ -573,6 +598,11 @@ define(['jquery', 'underscore', 'angular', 'bigscreen', 'moment', 'sjcl', 'moder
} }
}); });
// Start heartbeat timer.
$window.setInterval(function() {
mediaStream.api.heartbeat(5000, 11500)
}, 1000);
$scope.$on("active", function(event, currentcall) { $scope.$on("active", function(event, currentcall) {
console.info("Video state active (assuming connected)", currentcall.id); console.info("Video state active (assuming connected)", currentcall.id);

3
static/js/directives/audiovideo.js

@ -108,9 +108,8 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/
// Add created scope. // Add created scope.
if (stream === dummy) { if (stream === dummy) {
subscope.dummy = true; subscope.dummy = true;
} else {
streams[id] = subscope;
} }
streams[id] = subscope;
// Render template. // Render template.
peerTemplate(subscope, function(clonedElement, scope) { peerTemplate(subscope, function(clonedElement, scope) {

6
static/js/directives/directives.js

@ -47,7 +47,8 @@ define([
'directives/youtubevideo', 'directives/youtubevideo',
'directives/bfi', 'directives/bfi',
'directives/title', 'directives/title',
'directives/welcome'], function(_, onEnter, onEscape, statusMessage, buddyList, buddyPictureCapture, buddyPictureUpload, settings, chat, audioVideo, usability, audioLevel, fileInfo, screenshare, roomBar, socialShare, page, contactRequest, defaultDialog, pdfcanvas, odfcanvas, presentation, youtubevideo, bfi, title, welcome) { 'directives/welcome',
'directives/menu'], function(_, onEnter, onEscape, statusMessage, buddyList, buddyPictureCapture, buddyPictureUpload, settings, chat, audioVideo, usability, audioLevel, fileInfo, screenshare, roomBar, socialShare, page, contactRequest, defaultDialog, pdfcanvas, odfcanvas, presentation, youtubevideo, bfi, title, welcome, menu) {
var directives = { var directives = {
onEnter: onEnter, onEnter: onEnter,
@ -74,7 +75,8 @@ define([
youtubevideo: youtubevideo, youtubevideo: youtubevideo,
bfi: bfi, bfi: bfi,
title: title, title: title,
welcome: welcome welcome: welcome,
menu: menu
}; };
var initialize = function(angModule) { var initialize = function(angModule) {

46
static/js/directives/menu.js

@ -0,0 +1,46 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2015 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define(['text!partials/menu.html'], function(template) {
// menu
return ["mediaStream", function(mediaStream) {
var link = function($scope, $element) {
$scope.modules = mediaStream.config.Modules || [];
$scope.withModule = function(m) {
return $scope.modules.indexOf(m) !== -1;
};
};
return {
restrict: 'E',
replace: true,
template: template,
link: link
}
}];
});

16
static/js/directives/roombar.js

@ -23,7 +23,7 @@
define(['underscore', 'angular', 'text!partials/roombar.html'], function(_, angular, template) { define(['underscore', 'angular', 'text!partials/roombar.html'], function(_, angular, template) {
// roomBar // roomBar
return ["$window", "rooms", "$timeout", function($window, rooms, $timeout) { return ["$window", "rooms", "$timeout", "safeApply", function($window, rooms, $timeout, safeApply) {
var link = function($scope, $element) { var link = function($scope, $element) {
@ -51,13 +51,17 @@ define(['underscore', 'angular', 'text!partials/roombar.html'], function(_, angu
}; };
$scope.$on("room.updated", function(ev, room) { $scope.$on("room.updated", function(ev, room) {
$scope.currentRoomName = $scope.newRoomName = room.Name; safeApply($scope, function(scope) {
if ($scope.currentRoomName && !$scope.peer) { scope.currentRoomName = scope.newRoomName = room.Name;
$scope.layout.roombar = true; if (scope.currentRoomName && !scope.peer) {
} scope.layout.roombar = true;
}
});
}); });
$scope.$on("room.left", clearRoomName); $scope.$on("room.left", function() {
safeApply($scope, clearRoomName);
});
$scope.$watch("newRoomName", function(name) { $scope.$watch("newRoomName", function(name) {
if (name === $scope.currentRoomName) { if (name === $scope.currentRoomName) {

1
static/js/directives/welcome.js

@ -32,6 +32,7 @@ define([], function() {
$scope.randomRoom = rooms.randomRoom; $scope.randomRoom = rooms.randomRoom;
$scope.canCreateRooms = rooms.canCreateRooms; $scope.canCreateRooms = rooms.canCreateRooms;
$scope.canJoinRooms = rooms.canJoinRooms;
$scope.joinRoomByName = function(name) { $scope.joinRoomByName = function(name) {
if ($scope.welcome.$invalid) { if ($scope.welcome.$invalid) {
return; return;

135
static/js/libs/lodash.min.js vendored

@ -1,56 +1,85 @@
/** /**
* @license * @license
* Lo-Dash 2.4.1 (Custom Build) lodash.com/license | Underscore.js 1.5.2 underscorejs.org/LICENSE * lodash 3.0.0 (Custom Build) lodash.com/license | Underscore.js 1.7.0 underscorejs.org/LICENSE
* Build: `lodash modern -o ./dist/lodash.js` * Build: `lodash modern -o ./lodash.js`
*/ */
;(function(){function n(n,t,e){e=(e||0)-1;for(var r=n?n.length:0;++e<r;)if(n[e]===t)return e;return-1}function t(t,e){var r=typeof e;if(t=t.l,"boolean"==r||null==e)return t[e]?0:-1;"number"!=r&&"string"!=r&&(r="object");var u="number"==r?e:m+e;return t=(t=t[r])&&t[u],"object"==r?t&&-1<n(t,e)?0:-1:t?0:-1}function e(n){var t=this.l,e=typeof n;if("boolean"==e||null==n)t[n]=true;else{"number"!=e&&"string"!=e&&(e="object");var r="number"==e?n:m+n,t=t[e]||(t[e]={});"object"==e?(t[r]||(t[r]=[])).push(n):t[r]=true ;(function(){function n(n,t){if(n!==t){var r=n===n,e=t===t;if(n>t||!r||typeof n=="undefined"&&e)return 1;if(n<t||!e||typeof t=="undefined"&&r)return-1}return 0}function t(n,t,r){if(t!==t)return p(n,r);r=(r||0)-1;for(var e=n.length;++r<e;)if(n[r]===t)return r;return-1}function r(n,t){var r=n.length;for(n.sort(t);r--;)n[r]=n[r].c;return n}function e(n){return typeof n=="string"?n:null==n?"":n+""}function u(n){return n.charCodeAt(0)}function o(n,t){for(var r=-1,e=n.length;++r<e&&-1<t.indexOf(n.charAt(r)););return r
}}function r(n){return n.charCodeAt(0)}function u(n,t){for(var e=n.m,r=t.m,u=-1,o=e.length;++u<o;){var i=e[u],a=r[u];if(i!==a){if(i>a||typeof i=="undefined")return 1;if(i<a||typeof a=="undefined")return-1}}return n.n-t.n}function o(n){var t=-1,r=n.length,u=n[0],o=n[r/2|0],i=n[r-1];if(u&&typeof u=="object"&&o&&typeof o=="object"&&i&&typeof i=="object")return false;for(u=f(),u["false"]=u["null"]=u["true"]=u.undefined=false,o=f(),o.k=n,o.l=u,o.push=e;++t<r;)o.push(n[t]);return o}function i(n){return"\\"+U[n] }function i(n,t){for(var r=n.length;r--&&-1<t.indexOf(n.charAt(r)););return r}function a(t,r){return n(t.a,r.a)||t.b-r.b}function f(t,r){for(var e=-1,u=t.a,o=r.a,i=u.length;++e<i;){var a=n(u[e],o[e]);if(a)return a}return t.b-r.b}function c(n){return Wt[n]}function l(n){return Nt[n]}function s(n){return"\\"+Lt[n]}function p(n,t,r){var e=n.length;for(t=r?t||e:(t||0)-1;r?t--:++t<e;){var u=n[t];if(u!==u)return t}return-1}function h(n){return n&&typeof n=="object"||false}function g(n){return 160>=n&&9<=n&&13>=n||32==n||160==n||5760==n||6158==n||8192<=n&&(8202>=n||8232==n||8233==n||8239==n||8287==n||12288==n||65279==n)
}function a(){return h.pop()||[]}function f(){return g.pop()||{k:null,l:null,m:null,"false":false,n:0,"null":false,number:null,object:null,push:null,string:null,"true":false,undefined:false,o:null}}function l(n){n.length=0,h.length<_&&h.push(n)}function c(n){var t=n.l;t&&c(t),n.k=n.l=n.m=n.object=n.number=n.string=n.o=null,g.length<_&&g.push(n)}function p(n,t,e){t||(t=0),typeof e=="undefined"&&(e=n?n.length:0);var r=-1;e=e-t||0;for(var u=Array(0>e?0:e);++r<e;)u[r]=n[t+r];return u}function s(e){function h(n,t,e){if(!n||!V[typeof n])return n; }function v(n,t){for(var r=-1,e=n.length,u=-1,o=[];++r<e;)n[r]===t&&(n[r]=B,o[++u]=r);return o}function d(n){for(var t=-1,r=n.length;++t<r&&g(n.charCodeAt(t)););return t}function y(n){for(var t=n.length;t--&&g(n.charCodeAt(t)););return t}function _(n){return Ft[n]}function m(g){function Wt(n){if(h(n)&&!To(n)){if(n instanceof Nt)return n;if(Nu.call(n,"__wrapped__"))return new Nt(n.__wrapped__,n.__chain__,zt(n.__actions__))}return new Nt(n)}function Nt(n,t,r){this.__actions__=r||[],this.__chain__=!!t,this.__wrapped__=n
t=t&&typeof e=="undefined"?t:tt(t,e,3);for(var r=-1,u=V[typeof n]&&Fe(n),o=u?u.length:0;++r<o&&(e=u[r],false!==t(n[e],e,n)););return n}function g(n,t,e){var r;if(!n||!V[typeof n])return n;t=t&&typeof e=="undefined"?t:tt(t,e,3);for(r in n)if(false===t(n[r],r,n))break;return n}function _(n,t,e){var r,u=n,o=u;if(!u)return o;for(var i=arguments,a=0,f=typeof e=="number"?2:i.length;++a<f;)if((u=i[a])&&V[typeof u])for(var l=-1,c=V[typeof u]&&Fe(u),p=c?c.length:0;++l<p;)r=c[l],"undefined"==typeof o[r]&&(o[r]=u[r]); }function Ft(n){this.actions=null,this.dir=1,this.dropCount=0,this.filtered=false,this.iteratees=null,this.takeCount=lo,this.views=null,this.wrapped=n}function Ut(){this.__data__={}}function Lt(n){var t=n?n.length:0;for(this.data={hash:no(null),set:new Yu};t--;)this.push(n[t])}function Bt(n,t){var r=n.data;return(typeof t=="string"||Ge(t)?r.set.has(t):r.hash[t])?0:-1}function zt(n,t){var r=-1,e=n.length;for(t||(t=mu(e));++r<e;)t[r]=n[r];return t}function Mt(n,t){for(var r=-1,e=n.length;++r<e&&false!==t(n[r],r,n););return n
return o}function U(n,t,e){var r,u=n,o=u;if(!u)return o;var i=arguments,a=0,f=typeof e=="number"?2:i.length;if(3<f&&"function"==typeof i[f-2])var l=tt(i[--f-1],i[f--],2);else 2<f&&"function"==typeof i[f-1]&&(l=i[--f]);for(;++a<f;)if((u=i[a])&&V[typeof u])for(var c=-1,p=V[typeof u]&&Fe(u),s=p?p.length:0;++c<s;)r=p[c],o[r]=l?l(o[r],u[r]):u[r];return o}function H(n){var t,e=[];if(!n||!V[typeof n])return e;for(t in n)me.call(n,t)&&e.push(t);return e}function J(n){return n&&typeof n=="object"&&!Te(n)&&me.call(n,"__wrapped__")?n:new Q(n) }function qt(n,t){for(var r=-1,e=n.length;++r<e;)if(!t(n[r],r,n))return false;return true}function Pt(n,t){for(var r=-1,e=n.length,u=-1,o=[];++r<e;){var i=n[r];t(i,r,n)&&(o[++u]=i)}return o}function Kt(n,t){for(var r=-1,e=n.length,u=mu(e);++r<e;)u[r]=t(n[r],r,n);return u}function Vt(n){for(var t=-1,r=n.length,e=co;++t<r;){var u=n[t];u>e&&(e=u)}return e}function Yt(n,t,r,e){var u=-1,o=n.length;for(e&&o&&(r=n[++u]);++u<o;)r=t(r,n[u],u,n);return r}function Zt(n,t,r,e){var u=n.length;for(e&&u&&(r=n[--u]);u--;)r=t(r,n[u],u,n);
}function Q(n,t){this.__chain__=!!t,this.__wrapped__=n}function X(n){function t(){if(r){var n=p(r);be.apply(n,arguments)}if(this instanceof t){var o=nt(e.prototype),n=e.apply(o,n||arguments);return wt(n)?n:o}return e.apply(u,n||arguments)}var e=n[0],r=n[2],u=n[4];return $e(t,n),t}function Z(n,t,e,r,u){if(e){var o=e(n);if(typeof o!="undefined")return o}if(!wt(n))return n;var i=ce.call(n);if(!K[i])return n;var f=Ae[i];switch(i){case T:case F:return new f(+n);case W:case P:return new f(n);case z:return o=f(n.source,C.exec(n)),o.lastIndex=n.lastIndex,o return r}function Gt(n,t){for(var r=-1,e=n.length;++r<e;)if(t(n[r],r,n))return true;return false}function Jt(n,t){return typeof n=="undefined"?t:n}function Xt(n,t,r,e){return typeof n!="undefined"&&Nu.call(e,r)?n:t}function Ht(n,t,r){var e=Fo(t);if(!r)return nr(t,n,e);for(var u=-1,o=e.length;++u<o;){var i=e[u],a=n[i],f=r(a,t[i],i,n,t);(f===f?f===a:a!==a)&&(typeof a!="undefined"||i in n)||(n[i]=f)}return n}function Qt(n,t){for(var r=-1,e=n.length,u=ue(e),o=t.length,i=mu(o);++r<o;){var a=t[r];u?(a=parseFloat(a),i[r]=re(a,e)?n[a]:b):i[r]=n[a]
}if(i=Te(n),t){var c=!r;r||(r=a()),u||(u=a());for(var s=r.length;s--;)if(r[s]==n)return u[s];o=i?f(n.length):{}}else o=i?p(n):U({},n);return i&&(me.call(n,"index")&&(o.index=n.index),me.call(n,"input")&&(o.input=n.input)),t?(r.push(n),u.push(o),(i?St:h)(n,function(n,i){o[i]=Z(n,t,e,r,u)}),c&&(l(r),l(u)),o):o}function nt(n){return wt(n)?ke(n):{}}function tt(n,t,e){if(typeof n!="function")return Ut;if(typeof t=="undefined"||!("prototype"in n))return n;var r=n.__bindData__;if(typeof r=="undefined"&&(De.funcNames&&(r=!n.name),r=r||!De.funcDecomp,!r)){var u=ge.call(n); }return i}function nr(n,t,r){r||(r=t,t={});for(var e=-1,u=r.length;++e<u;){var o=r[e];t[o]=n[o]}return t}function tr(n,t,r){var u=typeof n;if("function"==u){if(u=typeof t!="undefined"){var u=Wt.support,o=!(u.funcNames?n.name:u.funcDecomp);if(!o){var i=Su.call(n);u.funcNames||(o=!_t.test(i)),o||(o=kt.test(i)||Je(n),bo(n,o))}u=o}n=u?Wr(n,t,r):n}else n=null==n?gu:"object"==u?wr(n,!r):Ar(r?e(n):n);return n}function rr(n,t,r,e,u,o,i){var a;if(r&&(a=u?r(n,e,u):r(n)),typeof a!="undefined")return a;if(!Ge(n))return n;
De.funcNames||(r=!O.test(u)),r||(r=E.test(u),$e(n,r))}if(false===r||true!==r&&1&r[1])return n;switch(e){case 1:return function(e){return n.call(t,e)};case 2:return function(e,r){return n.call(t,e,r)};case 3:return function(e,r,u){return n.call(t,e,r,u)};case 4:return function(e,r,u,o){return n.call(t,e,r,u,o)}}return Mt(n,t)}function et(n){function t(){var n=f?i:this;if(u){var h=p(u);be.apply(h,arguments)}return(o||c)&&(h||(h=p(arguments)),o&&be.apply(h,o),c&&h.length<a)?(r|=16,et([e,s?r:-4&r,h,null,i,a])):(h||(h=arguments),l&&(e=n[v]),this instanceof t?(n=nt(e.prototype),h=e.apply(n,h),wt(h)?h:n):e.apply(n,h)) if(e=To(n)){if(a=Qr(n),!t)return zt(n,a)}else{var f=Uu.call(n),c=f==K;if(f!=Y&&f!=z&&(!c||u))return Tt[f]?te(n,f,t):u?n:{};if(a=ne(c?{}:n),!t)return nr(n,a,Fo(n))}for(o||(o=[]),i||(i=[]),u=o.length;u--;)if(o[u]==n)return i[u];return o.push(n),i.push(a),(e?Mt:gr)(n,function(e,u){a[u]=rr(e,t,r,u,n,o,i)}),a}function er(n,t,r,e){if(!Ze(n))throw new Iu($);return Zu(function(){n.apply(b,Er(r,e))},t)}function ur(n,r){var e=n?n.length:0,u=[];if(!e)return u;var o=-1,i=Hr(),a=i==t,f=a&&200<=r.length&&wo(r),c=r.length;
}var e=n[0],r=n[1],u=n[2],o=n[3],i=n[4],a=n[5],f=1&r,l=2&r,c=4&r,s=8&r,v=e;return $e(t,n),t}function rt(e,r){var u=-1,i=st(),a=e?e.length:0,f=a>=b&&i===n,l=[];if(f){var p=o(r);p?(i=t,r=p):f=false}for(;++u<a;)p=e[u],0>i(r,p)&&l.push(p);return f&&c(r),l}function ut(n,t,e,r){r=(r||0)-1;for(var u=n?n.length:0,o=[];++r<u;){var i=n[r];if(i&&typeof i=="object"&&typeof i.length=="number"&&(Te(i)||yt(i))){t||(i=ut(i,t,e));var a=-1,f=i.length,l=o.length;for(o.length+=f;++a<f;)o[l++]=i[a]}else e||o.push(i)}return o f&&(i=Bt,a=false,r=f);n:for(;++o<e;)if(f=n[o],a&&f===f){for(var l=c;l--;)if(r[l]===f)continue n;u.push(f)}else 0>i(r,f)&&u.push(f);return u}function or(n,t){var r=n?n.length:0;if(!ue(r))return gr(n,t);for(var e=-1,u=se(n);++e<r&&false!==t(u[e],e,u););return n}function ir(n,t){var r=n?n.length:0;if(!ue(r))return vr(n,t);for(var e=se(n);r--&&false!==t(e[r],r,e););return n}function ar(n,t){var r=true;return or(n,function(n,e,u){return r=!!t(n,e,u)}),r}function fr(n,t){var r=[];return or(n,function(n,e,u){t(n,e,u)&&r.push(n)
}function ot(n,t,e,r,u,o){if(e){var i=e(n,t);if(typeof i!="undefined")return!!i}if(n===t)return 0!==n||1/n==1/t;if(n===n&&!(n&&V[typeof n]||t&&V[typeof t]))return false;if(null==n||null==t)return n===t;var f=ce.call(n),c=ce.call(t);if(f==D&&(f=q),c==D&&(c=q),f!=c)return false;switch(f){case T:case F:return+n==+t;case W:return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case z:case P:return n==oe(t)}if(c=f==$,!c){var p=me.call(n,"__wrapped__"),s=me.call(t,"__wrapped__");if(p||s)return ot(p?n.__wrapped__:n,s?t.__wrapped__:t,e,r,u,o); }),r}function cr(n,t,r,e){var u;return r(n,function(n,r,o){return t(n,r,o)?(u=e?r:n,false):void 0}),u}function lr(n,t,r,e){e=(e||0)-1;for(var u=n.length,o=-1,i=[];++e<u;){var a=n[e];if(h(a)&&ue(a.length)&&(To(a)||Ke(a))){t&&(a=lr(a,t,r));var f=-1,c=a.length;for(i.length+=c;++f<c;)i[++o]=a[f]}else r||(i[++o]=a)}return i}function sr(n,t,r){var e=-1,u=se(n);r=r(n);for(var o=r.length;++e<o;){var i=r[e];if(false===t(u[i],i,u))break}return n}function pr(n,t,r){var e=se(n);r=r(n);for(var u=r.length;u--;){var o=r[u];
if(f!=q)return false;if(f=n.constructor,p=t.constructor,f!=p&&!(dt(f)&&f instanceof f&&dt(p)&&p instanceof p)&&"constructor"in n&&"constructor"in t)return false}for(f=!u,u||(u=a()),o||(o=a()),p=u.length;p--;)if(u[p]==n)return o[p]==t;var v=0,i=true;if(u.push(n),o.push(t),c){if(p=n.length,v=t.length,(i=v==p)||r)for(;v--;)if(c=p,s=t[v],r)for(;c--&&!(i=ot(n[c],s,e,r,u,o)););else if(!(i=ot(n[v],s,e,r,u,o)))break}else g(t,function(t,a,f){return me.call(f,a)?(v++,i=me.call(n,a)&&ot(n[a],t,e,r,u,o)):void 0}),i&&!r&&g(n,function(n,t,e){return me.call(e,t)?i=-1<--v:void 0 if(false===t(e[o],o,e))break}return n}function hr(n,t){sr(n,t,eu)}function gr(n,t){return sr(n,t,Fo)}function vr(n,t){return pr(n,t,Fo)}function dr(n,t){for(var r=-1,e=t.length,u=-1,o=[];++r<e;){var i=t[r];Ze(n[i])&&(o[++u]=i)}return o}function yr(n,t,r){var e=-1,u=typeof t=="function",o=n?n.length:0,i=ue(o)?mu(o):[];return or(n,function(n){var o=u?t:null!=n&&n[t];i[++e]=o?o.apply(n,r):b}),i}function _r(n,t,r,e,u,o){if(n===t)return 0!==n||1/n==1/t;var i=typeof n,a=typeof t;if("function"!=i&&"object"!=i&&"function"!=a&&"object"!=a||null==n||null==t)n=n!==n&&t!==t;
});return u.pop(),o.pop(),f&&(l(u),l(o)),i}function it(n,t,e,r,u){(Te(t)?St:h)(t,function(t,o){var i,a,f=t,l=n[o];if(t&&((a=Te(t))||Pe(t))){for(f=r.length;f--;)if(i=r[f]==t){l=u[f];break}if(!i){var c;e&&(f=e(l,t),c=typeof f!="undefined")&&(l=f),c||(l=a?Te(l)?l:[]:Pe(l)?l:{}),r.push(t),u.push(l),c||it(l,t,e,r,u)}}else e&&(f=e(l,t),typeof f=="undefined"&&(f=t)),typeof f!="undefined"&&(l=f);n[o]=l})}function at(n,t){return n+he(Re()*(t-n+1))}function ft(e,r,u){var i=-1,f=st(),p=e?e.length:0,s=[],v=!r&&p>=b&&f===n,h=u||v?a():s; else n:{var i=_r,a=To(n),f=To(t),c=D,l=D;a||(c=Uu.call(n),c==z?c=Y:c!=Y&&(a=nu(n))),f||(l=Uu.call(t),l==z?l=Y:l!=Y&&nu(t));var s=c==Y,f=l==Y,l=c==l;if(!l||a||s)if(c=s&&Nu.call(n,"__wrapped__"),f=f&&Nu.call(t,"__wrapped__"),c||f)n=i(c?n.value():n,f?t.value():t,r,e,u,o);else if(l){for(u||(u=[]),o||(o=[]),c=u.length;c--;)if(u[c]==n){n=o[c]==t;break n}u.push(n),o.push(t),n=(a?Yr:Gr)(n,t,i,r,e,u,o),u.pop(),o.pop()}else n=false;else n=Zr(n,t,c)}return n}function mr(n,t,r,e,u){var o=t.length;if(null==n)return!o;
for(v&&(h=o(h),f=t);++i<p;){var g=e[i],y=u?u(g,i,e):g;(r?!i||h[h.length-1]!==y:0>f(h,y))&&((u||v)&&h.push(y),s.push(g))}return v?(l(h.k),c(h)):u&&l(h),s}function lt(n){return function(t,e,r){var u={};e=J.createCallback(e,r,3),r=-1;var o=t?t.length:0;if(typeof o=="number")for(;++r<o;){var i=t[r];n(u,i,e(i,r,t),t)}else h(t,function(t,r,o){n(u,t,e(t,r,o),o)});return u}}function ct(n,t,e,r,u,o){var i=1&t,a=4&t,f=16&t,l=32&t;if(!(2&t||dt(n)))throw new ie;f&&!e.length&&(t&=-17,f=e=false),l&&!r.length&&(t&=-33,l=r=false); for(var i=-1,a=!u;++i<o;)if(a&&e[i]?r[i]!==n[t[i]]:!Nu.call(n,t[i]))return false;for(i=-1;++i<o;){var f=t[i];if(a&&e[i])f=Nu.call(n,f);else{var c=n[f],l=r[i],f=u?u(c,l,f):b;typeof f=="undefined"&&(f=_r(l,c,u,true))}if(!f)return false}return true}function br(n,t){var r=[];return or(n,function(n,e,u){r.push(t(n,e,u))}),r}function wr(n,t){var r=Fo(n),e=r.length;if(1==e){var u=r[0],o=n[u];if(oe(o))return function(n){return null!=n&&o===n[u]&&Nu.call(n,u)}}t&&(n=rr(n,true));for(var i=mu(e),a=mu(e);e--;)o=n[r[e]],i[e]=o,a[e]=oe(o);
var c=n&&n.__bindData__;return c&&true!==c?(c=p(c),c[2]&&(c[2]=p(c[2])),c[3]&&(c[3]=p(c[3])),!i||1&c[1]||(c[4]=u),!i&&1&c[1]&&(t|=8),!a||4&c[1]||(c[5]=o),f&&be.apply(c[2]||(c[2]=[]),e),l&&we.apply(c[3]||(c[3]=[]),r),c[1]|=t,ct.apply(null,c)):(1==t||17===t?X:et)([n,t,e,r,u,o])}function pt(n){return Be[n]}function st(){var t=(t=J.indexOf)===Wt?n:t;return t}function vt(n){return typeof n=="function"&&pe.test(n)}function ht(n){var t,e;return n&&ce.call(n)==q&&(t=n.constructor,!dt(t)||t instanceof t)?(g(n,function(n,t){e=t return function(n){return mr(n,r,i,a)}}function xr(n,t,r,e,u){var o=ue(t.length)&&(To(t)||nu(t));return(o?Mt:gr)(t,function(t,i,a){if(h(t)){e||(e=[]),u||(u=[]);n:{t=e;for(var f=u,c=t.length,l=a[i];c--;)if(t[c]==l){n[i]=f[c],i=void 0;break n}c=n[i],a=r?r(c,l,i,n,a):b;var s=typeof a=="undefined";s&&(a=l,ue(l.length)&&(To(l)||nu(l))?a=To(c)?c:c?zt(c):[]:(Wo(l)||Ke(l))&&(a=Ke(c)?tu(c):Wo(c)?c:{})),t.push(l),f.push(a),s?n[i]=xr(a,l,r,t,f):(a===a?a!==c:c===c)&&(n[i]=a),i=void 0}return i}f=n[i],a=r?r(f,t,i,n,a):b,(l=typeof a=="undefined")&&(a=t),!o&&typeof a=="undefined"||!l&&(a===a?a===f:f!==f)||(n[i]=a)
}),typeof e=="undefined"||me.call(n,e)):false}function gt(n){return We[n]}function yt(n){return n&&typeof n=="object"&&typeof n.length=="number"&&ce.call(n)==D||false}function mt(n,t,e){var r=Fe(n),u=r.length;for(t=tt(t,e,3);u--&&(e=r[u],false!==t(n[e],e,n)););return n}function bt(n){var t=[];return g(n,function(n,e){dt(n)&&t.push(e)}),t.sort()}function _t(n){for(var t=-1,e=Fe(n),r=e.length,u={};++t<r;){var o=e[t];u[n[o]]=o}return u}function dt(n){return typeof n=="function"}function wt(n){return!(!n||!V[typeof n]) }),n}function Ar(n){return function(t){return null==t?b:t[n]}}function jr(n,t){return n+qu(fo()*(t-n+1))}function kr(n,t,r,e,u){return u(n,function(n,u,o){r=e?(e=false,n):t(r,n,u,o)}),r}function Er(n,t,r){var e=-1,u=n.length;for(t=null==t?0:+t||0,0>t&&(t=-t>u?0:u+t),r=typeof r=="undefined"||r>u?u:+r||0,0>r&&(r+=u),u=t>r?0:r-t,r=mu(u);++e<u;)r[e]=n[e+t];return r}function Rr(n,t){var r;return or(n,function(n,e,u){return r=t(n,e,u),!r}),!!r}function Ir(n,r){var e=-1,u=Hr(),o=n.length,i=u==t,a=i&&200<=o,f=a&&wo(),c=[];
}function jt(n){return typeof n=="number"||n&&typeof n=="object"&&ce.call(n)==W||false}function kt(n){return typeof n=="string"||n&&typeof n=="object"&&ce.call(n)==P||false}function xt(n){for(var t=-1,e=Fe(n),r=e.length,u=Xt(r);++t<r;)u[t]=n[e[t]];return u}function Ct(n,t,e){var r=-1,u=st(),o=n?n.length:0,i=false;return e=(0>e?Ie(0,o+e):e)||0,Te(n)?i=-1<u(n,t,e):typeof o=="number"?i=-1<(kt(n)?n.indexOf(t,e):u(n,t,e)):h(n,function(n){return++r<e?void 0:!(i=n===t)}),i}function Ot(n,t,e){var r=true;t=J.createCallback(t,e,3),e=-1; f?(u=Bt,i=false):(a=false,f=r?[]:c);n:for(;++e<o;){var l=n[e],s=r?r(l,e,n):l;if(i&&l===l){for(var p=f.length;p--;)if(f[p]===s)continue n;r&&f.push(s),c.push(l)}else 0>u(f,s)&&((r||a)&&f.push(s),c.push(l))}return c}function Or(n,t){for(var r=-1,e=t.length,u=mu(e);++r<e;)u[r]=n[t[r]];return u}function Cr(n,t){var r=n;r instanceof Ft&&(r=r.value());for(var e=-1,u=t.length;++e<u;){var r=[r],o=t[e];Ku.apply(r,o.args),r=o.func.apply(o.thisArg,r)}return r}function Tr(n,t,r){var e=0,u=n?n.length:e;if(typeof t=="number"&&t===t&&u<=ho){for(;e<u;){var o=e+u>>>1,i=n[o];
var u=n?n.length:0;if(typeof u=="number")for(;++e<u&&(r=!!t(n[e],e,n)););else h(n,function(n,e,u){return r=!!t(n,e,u)});return r}function Nt(n,t,e){var r=[];t=J.createCallback(t,e,3),e=-1;var u=n?n.length:0;if(typeof u=="number")for(;++e<u;){var o=n[e];t(o,e,n)&&r.push(o)}else h(n,function(n,e,u){t(n,e,u)&&r.push(n)});return r}function It(n,t,e){t=J.createCallback(t,e,3),e=-1;var r=n?n.length:0;if(typeof r!="number"){var u;return h(n,function(n,e,r){return t(n,e,r)?(u=n,false):void 0}),u}for(;++e<r;){var o=n[e]; (r?i<=t:i<t)?e=o+1:u=o}return u}return Sr(n,t,gu,r)}function Sr(n,t,r,e){t=r(t);for(var u=0,o=n?n.length:0,i=t!==t,a=typeof t=="undefined";u<o;){var f=qu((u+o)/2),c=r(n[f]),l=c===c;(i?l||e:a?l&&(e||typeof c!="undefined"):e?c<=t:c<t)?u=f+1:o=f}return uo(o,po)}function Wr(n,t,r){if(typeof n!="function")return gu;if(typeof t=="undefined")return n;switch(r){case 1:return function(r){return n.call(t,r)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,o){return n.call(t,r,e,u,o)
if(t(o,e,n))return o}}function St(n,t,e){var r=-1,u=n?n.length:0;if(t=t&&typeof e=="undefined"?t:tt(t,e,3),typeof u=="number")for(;++r<u&&false!==t(n[r],r,n););else h(n,t);return n}function Et(n,t,e){var r=n?n.length:0;if(t=t&&typeof e=="undefined"?t:tt(t,e,3),typeof r=="number")for(;r--&&false!==t(n[r],r,n););else{var u=Fe(n),r=u.length;h(n,function(n,e,o){return e=u?u[--r]:--r,t(o[e],e,o)})}return n}function Rt(n,t,e){var r=-1,u=n?n.length:0;if(t=J.createCallback(t,e,3),typeof u=="number")for(var o=Xt(u);++r<u;)o[r]=t(n[r],r,n); };case 5:return function(r,e,u,o,i){return n.call(t,r,e,u,o,i)}}return function(){return n.apply(t,arguments)}}function Nr(n){return zu.call(n,0)}function Fr(n,t,r){for(var e=r.length,u=-1,o=eo(n.length-e,0),i=-1,a=t.length,f=mu(o+a);++i<a;)f[i]=t[i];for(;++u<e;)f[r[u]]=n[u];for(;o--;)f[i++]=n[u++];return f}function Ur(n,t,r){for(var e=-1,u=r.length,o=-1,i=eo(n.length-u,0),a=-1,f=t.length,c=mu(i+f);++o<i;)c[o]=n[o];for(i=o;++a<f;)c[i+a]=t[a];for(;++e<u;)c[i+r[e]]=n[o++];return c}function Lr(n,t){return function(r,e,u){var o=t?t():{};
else o=[],h(n,function(n,e,u){o[++r]=t(n,e,u)});return o}function At(n,t,e){var u=-1/0,o=u;if(typeof t!="function"&&e&&e[t]===n&&(t=null),null==t&&Te(n)){e=-1;for(var i=n.length;++e<i;){var a=n[e];a>o&&(o=a)}}else t=null==t&&kt(n)?r:J.createCallback(t,e,3),St(n,function(n,e,r){e=t(n,e,r),e>u&&(u=e,o=n)});return o}function Dt(n,t,e,r){if(!n)return e;var u=3>arguments.length;t=J.createCallback(t,r,4);var o=-1,i=n.length;if(typeof i=="number")for(u&&(e=n[++o]);++o<i;)e=t(e,n[o],o,n);else h(n,function(n,r,o){e=u?(u=false,n):t(e,n,r,o) if(e=Xr(e,u,3),To(r)){u=-1;for(var i=r.length;++u<i;){var a=r[u];n(o,a,e(a,u,r),r)}}else or(r,function(t,r,u){n(o,t,e(t,r,u),u)});return o}}function $r(n){return function(){var t=arguments.length,r=arguments[0];if(2>t||null==r)return r;if(3<t&&ee(arguments[1],arguments[2],arguments[3])&&(t=2),3<t&&"function"==typeof arguments[t-2])var e=Wr(arguments[--t-1],arguments[t--],5);else 2<t&&"function"==typeof arguments[t-1]&&(e=arguments[--t]);for(var u=0;++u<t;){var o=arguments[u];o&&n(r,o,e)}return r}
});return e}function $t(n,t,e,r){var u=3>arguments.length;return t=J.createCallback(t,r,4),Et(n,function(n,r,o){e=u?(u=false,n):t(e,n,r,o)}),e}function Tt(n){var t=-1,e=n?n.length:0,r=Xt(typeof e=="number"?e:0);return St(n,function(n){var e=at(0,++t);r[t]=r[e],r[e]=n}),r}function Ft(n,t,e){var r;t=J.createCallback(t,e,3),e=-1;var u=n?n.length:0;if(typeof u=="number")for(;++e<u&&!(r=t(n[e],e,n)););else h(n,function(n,e,u){return!(r=t(n,e,u))});return!!r}function Bt(n,t,e){var r=0,u=n?n.length:0;if(typeof t!="number"&&null!=t){var o=-1; }function Br(n,t){function r(){return(this instanceof r?e:n).apply(t,arguments)}var e=Dr(n);return r}function zr(n){return function(t){var r=-1;t=lu(ou(t));for(var e=t.length,u="";++r<e;)u=n(u,t[r],r);return u}}function Dr(n){return function(){var t=mo(n.prototype),r=n.apply(t,arguments);return Ge(r)?r:t}}function Mr(n,t){return function(r,e,o){o&&ee(r,e,o)&&(e=null);var i=Xr(),a=null==e;if(i===tr&&a||(a=false,e=i(e,o,3)),a){if(e=To(r),e||!Qe(r))return n(e?r:le(r));e=u}return Jr(r,e,t)}}function qr(n,t,r,e,u,o,i,a,f,c){function l(){for(var w=arguments.length,j=w,k=mu(w);j--;)k[j]=arguments[j];
for(t=J.createCallback(t,e,3);++o<u&&t(n[o],o,n);)r++}else if(r=t,null==r||e)return n?n[0]:v;return p(n,0,Se(Ie(0,r),u))}function Wt(t,e,r){if(typeof r=="number"){var u=t?t.length:0;r=0>r?Ie(0,u+r):r||0}else if(r)return r=zt(t,e),t[r]===e?r:-1;return n(t,e,r)}function qt(n,t,e){if(typeof t!="number"&&null!=t){var r=0,u=-1,o=n?n.length:0;for(t=J.createCallback(t,e,3);++u<o&&t(n[u],u,n);)r++}else r=null==t||e?1:Ie(0,t);return p(n,r)}function zt(n,t,e,r){var u=0,o=n?n.length:u;for(e=e?J.createCallback(e,r,1):Ut,t=e(t);u<o;)r=u+o>>>1,e(n[r])<t?u=r+1:o=r; if(e&&(k=Fr(k,e,u)),o&&(k=Ur(k,o,i)),g||y){var j=l.placeholder,E=v(k,j),w=w-E.length;if(w<c){var O=a?zt(a):null,w=eo(c-w,0),C=g?E:null,E=g?null:E,T=g?k:null,k=g?null:k;return t|=g?R:I,t&=~(g?I:R),d||(t&=~(x|A)),k=qr(n,t,r,T,C,k,E,O,f,w),k.placeholder=j,k}}if(j=p?r:this,h&&(n=j[m]),a)for(O=k.length,w=uo(a.length,O),C=zt(k);w--;)E=a[w],k[w]=re(E,O)?C[E]:b;return s&&f<k.length&&(k.length=f),(this instanceof l?_||Dr(n):n).apply(j,k)}var s=t&C,p=t&x,h=t&A,g=t&k,d=t&j,y=t&E,_=!h&&Dr(n),m=n;return l}function Pr(n,t,r){return n=n.length,t=+t,n<t&&to(t)?(t-=n,r=null==r?" ":e(r),fu(r,Du(t/r.length)).slice(0,t)):""
return u}function Pt(n,t,e,r){return typeof t!="boolean"&&null!=t&&(r=e,e=typeof t!="function"&&r&&r[t]===n?null:t,t=false),null!=e&&(e=J.createCallback(e,r,3)),ft(n,t,e)}function Kt(){for(var n=1<arguments.length?arguments:arguments[0],t=-1,e=n?At(Ve(n,"length")):0,r=Xt(0>e?0:e);++t<e;)r[t]=Ve(n,t);return r}function Lt(n,t){var e=-1,r=n?n.length:0,u={};for(t||!r||Te(n[0])||(t=[]);++e<r;){var o=n[e];t?u[o]=t[e]:o&&(u[o[0]]=o[1])}return u}function Mt(n,t){return 2<arguments.length?ct(n,17,p(arguments,2),null,t):ct(n,1,null,null,t) }function Kr(n,t,r,e){function u(){for(var t=-1,a=arguments.length,f=-1,c=e.length,l=mu(a+c);++f<c;)l[f]=e[f];for(;a--;)l[f++]=arguments[++t];return(this instanceof u?i:n).apply(o?r:this,l)}var o=t&x,i=Dr(n);return u}function Vr(n,t,r,e,u,o,i,a){var f=t&A;if(!f&&!Ze(n))throw new Iu($);var c=e?e.length:0;if(c||(t&=~(R|I),e=u=null),c-=u?u.length:0,t&I){var l=e,s=u;e=u=null}var p=!f&&xo(n);if(r=[n,t,r,e,u,l,s,o,i,a],p&&true!==p){e=r[1],t=p[1],a=e|t,o=C|O,u=x|A,i=o|u|j|E;var l=e&C&&!(t&C),s=e&O&&!(t&O),h=(s?r:p)[7],g=(l?r:p)[8];
}function Vt(n,t,e){function r(){c&&ve(c),i=c=p=v,(g||h!==t)&&(s=Ue(),a=n.apply(l,o),c||i||(o=l=null))}function u(){var e=t-(Ue()-f);0<e?c=_e(u,e):(i&&ve(i),e=p,i=c=p=v,e&&(s=Ue(),a=n.apply(l,o),c||i||(o=l=null)))}var o,i,a,f,l,c,p,s=0,h=false,g=true;if(!dt(n))throw new ie;if(t=Ie(0,t)||0,true===e)var y=true,g=false;else wt(e)&&(y=e.leading,h="maxWait"in e&&(Ie(t,e.maxWait)||0),g="trailing"in e?e.trailing:g);return function(){if(o=arguments,f=Ue(),l=this,p=g&&(c||!y),false===h)var e=y&&!c;else{i||y||(s=f);var v=h-(f-s),m=0>=v; o=a>=o&&a<=i&&(e<O||(s||l)&&h.length<=g),(!(e>=O&&t>u||e>u&&t>=O)||o)&&(t&x&&(r[2]=p[2],a|=e&x?0:j),(e=p[3])&&(u=r[3],r[3]=u?Fr(u,e,p[4]):zt(e),r[4]=u?v(r[3],B):zt(p[4])),(e=p[5])&&(u=r[5],r[5]=u?Ur(u,e,p[6]):zt(e),r[6]=u?v(r[5],B):zt(p[6])),(e=p[7])&&(r[7]=zt(e)),t&C&&(r[8]=null==r[8]?p[8]:uo(r[8],p[8])),null==r[9]&&(r[9]=p[9]),r[0]=p[0],r[1]=a),t=r[1],a=r[9]}return r[9]=null==a?f?0:n.length:eo(a-c,0)||0,(p?bo:Ao)(t==x?Br(r[0],r[2]):t!=R&&t!=(x|R)||r[4].length?qr.apply(null,r):Kr.apply(null,r),r)
m?(i&&(i=ve(i)),s=f,a=n.apply(l,o)):i||(i=_e(r,v))}return m&&c?c=ve(c):c||t===h||(c=_e(u,t)),e&&(m=true,a=n.apply(l,o)),!m||c||i||(o=l=null),a}}function Ut(n){return n}function Gt(n,t,e){var r=true,u=t&&bt(t);t&&(e||u.length)||(null==e&&(e=t),o=Q,t=n,n=J,u=bt(t)),false===e?r=false:wt(e)&&"chain"in e&&(r=e.chain);var o=n,i=dt(o);St(u,function(e){var u=n[e]=t[e];i&&(o.prototype[e]=function(){var t=this.__chain__,e=this.__wrapped__,i=[e];if(be.apply(i,arguments),i=u.apply(n,i),r||t){if(e===i&&wt(i))return this; }function Yr(n,t,r,e,u,o,i){var a=-1,f=n.length,c=t.length,l=true;if(f!=c&&(!u||c<=f))return false;for(;l&&++a<f;){var s=n[a],p=t[a],l=b;if(e&&(l=u?e(p,s,a):e(s,p,a)),typeof l=="undefined")if(u)for(var h=c;h--&&(p=t[h],!(l=s&&s===p||r(s,p,e,u,o,i))););else l=s&&s===p||r(s,p,e,u,o,i)}return!!l}function Zr(n,t,r){switch(r){case M:case q:return+n==+t;case P:return n.name==t.name&&n.message==t.message;case V:return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case Z:case G:return n==e(t)}return false}function Gr(n,t,r,e,u,o,i){var a=Fo(n),f=a.length,c=Fo(t).length;
i=new o(i),i.__chain__=t}return i})})}function Ht(){}function Jt(n){return function(t){return t[n]}}function Qt(){return this.__wrapped__}e=e?Y.defaults(G.Object(),e,Y.pick(G,A)):G;var Xt=e.Array,Yt=e.Boolean,Zt=e.Date,ne=e.Function,te=e.Math,ee=e.Number,re=e.Object,ue=e.RegExp,oe=e.String,ie=e.TypeError,ae=[],fe=re.prototype,le=e._,ce=fe.toString,pe=ue("^"+oe(ce).replace(/[.*+?^${}()|[\]\\]/g,"\\$&").replace(/toString| for [^\]]+/g,".*?")+"$"),se=te.ceil,ve=e.clearTimeout,he=te.floor,ge=ne.prototype.toString,ye=vt(ye=re.getPrototypeOf)&&ye,me=fe.hasOwnProperty,be=ae.push,_e=e.setTimeout,de=ae.splice,we=ae.unshift,je=function(){try{var n={},t=vt(t=re.defineProperty)&&t,e=t(n,n,n)&&t if(f!=c&&!u)return false;for(var l,c=-1;++c<f;){var s=a[c],p=Nu.call(t,s);if(p){var h=n[s],g=t[s],p=b;e&&(p=u?e(g,h,s):e(h,g,s)),typeof p=="undefined"&&(p=h&&h===g||r(h,g,e,u,o,i))}if(!p)return false;l||(l="constructor"==s)}return l||(r=n.constructor,e=t.constructor,!(r!=e&&"constructor"in n&&"constructor"in t)||typeof r=="function"&&r instanceof r&&typeof e=="function"&&e instanceof e)?true:false}function Jr(n,t,r){var e=r?lo:co,u=e,o=u;return or(n,function(n,i,a){i=t(n,i,a),((r?i<u:i>u)||i===e&&i===o)&&(u=i,o=n)
}catch(r){}return e}(),ke=vt(ke=re.create)&&ke,xe=vt(xe=Xt.isArray)&&xe,Ce=e.isFinite,Oe=e.isNaN,Ne=vt(Ne=re.keys)&&Ne,Ie=te.max,Se=te.min,Ee=e.parseInt,Re=te.random,Ae={};Ae[$]=Xt,Ae[T]=Yt,Ae[F]=Zt,Ae[B]=ne,Ae[q]=re,Ae[W]=ee,Ae[z]=ue,Ae[P]=oe,Q.prototype=J.prototype;var De=J.support={};De.funcDecomp=!vt(e.a)&&E.test(s),De.funcNames=typeof ne.name=="string",J.templateSettings={escape:/<%-([\s\S]+?)%>/g,evaluate:/<%([\s\S]+?)%>/g,interpolate:N,variable:"",imports:{_:J}},ke||(nt=function(){function n(){}return function(t){if(wt(t)){n.prototype=t; }),o}function Xr(n,t,r){var e=Wt.callback||pu,e=e===pu?tr:e;return r?e(n,t,r):e}function Hr(n,r,e){var u=Wt.indexOf||de,u=u===de?t:u;return n?u(n,r,e):u}function Qr(n){var t=n.length,r=new n.constructor(t);return t&&"string"==typeof n[0]&&Nu.call(n,"index")&&(r.index=n.index,r.input=n.input),r}function ne(n){return n=n.constructor,typeof n=="function"&&n instanceof n||(n=ku),new n}function te(n,t,r){var e=n.constructor;switch(t){case J:return Nr(n);case M:case q:return new e(+n);case X:case H:case Q:case nt:case tt:case rt:case et:case ut:case ot:return t=n.buffer,new e(r?Nr(t):t,n.byteOffset,n.length);
var r=new n;n.prototype=null}return r||e.Object()}}());var $e=je?function(n,t){M.value=t,je(n,"__bindData__",M)}:Ht,Te=xe||function(n){return n&&typeof n=="object"&&typeof n.length=="number"&&ce.call(n)==$||false},Fe=Ne?function(n){return wt(n)?Ne(n):[]}:H,Be={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},We=_t(Be),qe=ue("("+Fe(We).join("|")+")","g"),ze=ue("["+Fe(Be).join("")+"]","g"),Pe=ye?function(n){if(!n||ce.call(n)!=q)return false;var t=n.valueOf,e=vt(t)&&(e=ye(t))&&ye(e);return e?n==e||ye(n)==e:ht(n) case V:case G:return new e(n);case Z:var u=new e(n.source,yt.exec(n));u.lastIndex=n.lastIndex}return u}function re(n,t){return n=+n,t=null==t?vo:t,-1<n&&0==n%1&&n<t}function ee(n,t,r){if(!Ge(r))return false;var e=typeof t;return"number"==e?(e=r.length,e=ue(e)&&re(t,e)):e="string"==e&&t in n,e&&r[t]===n}function ue(n){return typeof n=="number"&&-1<n&&0==n%1&&n<=vo}function oe(n){return n===n&&(0===n?0<1/n:!Ge(n))}function ie(n,t){n=se(n);for(var r=-1,e=t.length,u={};++r<e;){var o=t[r];o in n&&(u[o]=n[o])
}:ht,Ke=lt(function(n,t,e){me.call(n,e)?n[e]++:n[e]=1}),Le=lt(function(n,t,e){(me.call(n,e)?n[e]:n[e]=[]).push(t)}),Me=lt(function(n,t,e){n[e]=t}),Ve=Rt,Ue=vt(Ue=Zt.now)&&Ue||function(){return(new Zt).getTime()},Ge=8==Ee(d+"08")?Ee:function(n,t){return Ee(kt(n)?n.replace(I,""):n,t||0)};return J.after=function(n,t){if(!dt(t))throw new ie;return function(){return 1>--n?t.apply(this,arguments):void 0}},J.assign=U,J.at=function(n){for(var t=arguments,e=-1,r=ut(t,true,false,1),t=t[2]&&t[2][t[1]]===n?1:r.length,u=Xt(t);++e<t;)u[e]=n[r[e]]; }return u}function ae(n,t){var r={};return hr(n,function(n,e,u){t(n,e,u)&&(r[e]=n)}),r}function fe(n){var t;if(!h(n)||Uu.call(n)!=Y||!(Nu.call(n,"constructor")||(t=n.constructor,typeof t!="function"||t instanceof t)))return false;var r;return hr(n,function(n,t){r=t}),typeof r=="undefined"||Nu.call(n,r)}function ce(n){for(var t=eu(n),r=t.length,e=r&&n.length,u=Wt.support,u=e&&ue(e)&&(To(n)||u.nonEnumArgs&&Ke(n)),o=-1,i=[];++o<r;){var a=t[o];(u&&re(a,e)||Nu.call(n,a))&&i.push(a)}return i}function le(n){return null==n?[]:ue(n.length)?Ge(n)?n:ku(n):uu(n)
return u},J.bind=Mt,J.bindAll=function(n){for(var t=1<arguments.length?ut(arguments,true,false,1):bt(n),e=-1,r=t.length;++e<r;){var u=t[e];n[u]=ct(n[u],1,null,null,n)}return n},J.bindKey=function(n,t){return 2<arguments.length?ct(t,19,p(arguments,2),null,n):ct(t,3,null,null,n)},J.chain=function(n){return n=new Q(n),n.__chain__=true,n},J.compact=function(n){for(var t=-1,e=n?n.length:0,r=[];++t<e;){var u=n[t];u&&r.push(u)}return r},J.compose=function(){for(var n=arguments,t=n.length;t--;)if(!dt(n[t]))throw new ie; }function se(n){return Ge(n)?n:ku(n)}function pe(n,t,r){return n&&n.length?((r?ee(n,t,r):null==t)&&(t=1),Er(n,0>t?0:t)):[]}function he(n,t,r){var e=n?n.length:0;return e?((r?ee(n,t,r):null==t)&&(t=1),t=e-(+t||0),Er(n,0,0>t?0:t)):[]}function ge(n,t,r){var e=-1,u=n?n.length:0;for(t=Xr(t,r,3);++e<u;)if(t(n[e],e,n))return e;return-1}function ve(n){return n?n[0]:b}function de(n,r,e){var u=n?n.length:0;if(!u)return-1;if(typeof e=="number")e=0>e?eo(u+e,0):e||0;else if(e)return e=Tr(n,r),n=n[e],(r===r?r===n:n!==n)?e:-1;
return function(){for(var t=arguments,e=n.length;e--;)t=[n[e].apply(this,t)];return t[0]}},J.constant=function(n){return function(){return n}},J.countBy=Ke,J.create=function(n,t){var e=nt(n);return t?U(e,t):e},J.createCallback=function(n,t,e){var r=typeof n;if(null==n||"function"==r)return tt(n,t,e);if("object"!=r)return Jt(n);var u=Fe(n),o=u[0],i=n[o];return 1!=u.length||i!==i||wt(i)?function(t){for(var e=u.length,r=false;e--&&(r=ot(t[u[e]],n[u[e]],null,true)););return r}:function(n){return n=n[o],i===n&&(0!==i||1/i==1/n) return t(n,r,e)}function ye(n){return pe(n,1)}function _e(n,r,e,u){if(!n||!n.length)return[];typeof r!="boolean"&&null!=r&&(u=e,e=ee(n,r,u)?null:r,r=false);var o=Xr();if((o!==tr||null!=e)&&(e=o(e,u,3)),r&&Hr()==t){r=e;var i;e=-1,u=n.length;for(var o=-1,a=[];++e<u;){var f=n[e],c=r?r(f,e,n):f;e&&i===c||(i=c,a[++o]=f)}n=a}else n=Ir(n,e);return n}function me(n){for(var t=-1,r=(n&&n.length&&Vt(Kt(n,Wu)))>>>0,e=mu(r);++t<r;)e[t]=Kt(n,Ar(t));return e}function be(n,t){var r=-1,e=n?n.length:0,u={};for(!e||t||To(n[0])||(t=[]);++r<e;){var o=n[r];
}},J.curry=function(n,t){return t=typeof t=="number"?t:+t||n.length,ct(n,4,null,null,null,t)},J.debounce=Vt,J.defaults=_,J.defer=function(n){if(!dt(n))throw new ie;var t=p(arguments,1);return _e(function(){n.apply(v,t)},1)},J.delay=function(n,t){if(!dt(n))throw new ie;var e=p(arguments,2);return _e(function(){n.apply(v,e)},t)},J.difference=function(n){return rt(n,ut(arguments,true,true,1))},J.filter=Nt,J.flatten=function(n,t,e,r){return typeof t!="boolean"&&null!=t&&(r=e,e=typeof t!="function"&&r&&r[t]===n?null:t,t=false),null!=e&&(n=Rt(n,e,r)),ut(n,t) t?u[o]=t[r]:o&&(u[o[0]]=o[1])}return u}function we(n){return n=Wt(n),n.__chain__=true,n}function xe(n,t,r){return t.call(r,n)}function Ae(n,t,r){var e=n?n.length:0;return ue(e)||(n=uu(n),e=n.length),e?(r=typeof r=="number"?0>r?eo(e+r,0):r||0:0,typeof n=="string"||!To(n)&&Qe(n)?r<e&&-1<n.indexOf(t,r):-1<Hr(n,t,r)):false}function je(n,t,r){var e=To(n)?qt:ar;return(typeof t!="function"||typeof r!="undefined")&&(t=Xr(t,r,3)),e(n,t)}function ke(n,t,r){var e=To(n)?Pt:fr;return t=Xr(t,r,3),e(n,t)}function Ee(n,t,r){return To(n)?(t=ge(n,t,r),-1<t?n[t]:b):(t=Xr(t,r,3),cr(n,t,or))
},J.forEach=St,J.forEachRight=Et,J.forIn=g,J.forInRight=function(n,t,e){var r=[];g(n,function(n,t){r.push(t,n)});var u=r.length;for(t=tt(t,e,3);u--&&false!==t(r[u--],r[u],n););return n},J.forOwn=h,J.forOwnRight=mt,J.functions=bt,J.groupBy=Le,J.indexBy=Me,J.initial=function(n,t,e){var r=0,u=n?n.length:0;if(typeof t!="number"&&null!=t){var o=u;for(t=J.createCallback(t,e,3);o--&&t(n[o],o,n);)r++}else r=null==t||e?1:t||r;return p(n,0,Se(Ie(0,u-r),u))},J.intersection=function(){for(var e=[],r=-1,u=arguments.length,i=a(),f=st(),p=f===n,s=a();++r<u;){var v=arguments[r]; }function Re(n,t,r){return typeof t=="function"&&typeof r=="undefined"&&To(n)?Mt(n,t):or(n,Wr(t,r,3))}function Ie(n,t,r){if(typeof t=="function"&&typeof r=="undefined"&&To(n))for(r=n.length;r--&&false!==t(n[r],r,n););else n=ir(n,Wr(t,r,3));return n}function Oe(n,t,r){var e=To(n)?Kt:br;return t=Xr(t,r,3),e(n,t)}function Ce(n,t,r,e){return(To(n)?Yt:kr)(n,Xr(t,e,4),r,3>arguments.length,or)}function Te(n,t,r,e){return(To(n)?Zt:kr)(n,Xr(t,e,4),r,3>arguments.length,ir)}function Se(n,t,r){return(r?ee(n,t,r):null==t)?(n=le(n),t=n.length,0<t?n[jr(0,t-1)]:b):(n=We(n),n.length=uo(0>t?0:+t||0,n.length),n)
(Te(v)||yt(v))&&(e.push(v),i.push(p&&v.length>=b&&o(r?e[r]:s)))}var p=e[0],h=-1,g=p?p.length:0,y=[];n:for(;++h<g;){var m=i[0],v=p[h];if(0>(m?t(m,v):f(s,v))){for(r=u,(m||s).push(v);--r;)if(m=i[r],0>(m?t(m,v):f(e[r],v)))continue n;y.push(v)}}for(;u--;)(m=i[u])&&c(m);return l(i),l(s),y},J.invert=_t,J.invoke=function(n,t){var e=p(arguments,2),r=-1,u=typeof t=="function",o=n?n.length:0,i=Xt(typeof o=="number"?o:0);return St(n,function(n){i[++r]=(u?t:n[t]).apply(n,e)}),i},J.keys=Fe,J.map=Rt,J.mapValues=function(n,t,e){var r={}; }function We(n){n=le(n);for(var t=-1,r=n.length,e=mu(r);++t<r;){var u=jr(0,t);t!=u&&(e[t]=e[u]),e[u]=n[t]}return e}function Ne(n,t,r){var e=To(n)?Gt:Rr;return(typeof t!="function"||typeof r!="undefined")&&(t=Xr(t,r,3)),e(n,t)}function Fe(n,t){var r;if(!Ze(t)){if(!Ze(n))throw new Iu($);var e=n;n=t,t=e}return function(){return 0<--n?r=t.apply(this,arguments):t=null,r}}function Ue(n,t){var r=x;if(2<arguments.length)var e=Er(arguments,2),u=v(e,Ue.placeholder),r=r|R;return Vr(n,r,t,e,u)}function Le(n,t){var r=x|A;
return t=J.createCallback(t,e,3),h(n,function(n,e,u){r[e]=t(n,e,u)}),r},J.max=At,J.memoize=function(n,t){function e(){var r=e.cache,u=t?t.apply(this,arguments):m+arguments[0];return me.call(r,u)?r[u]:r[u]=n.apply(this,arguments)}if(!dt(n))throw new ie;return e.cache={},e},J.merge=function(n){var t=arguments,e=2;if(!wt(n))return n;if("number"!=typeof t[2]&&(e=t.length),3<e&&"function"==typeof t[e-2])var r=tt(t[--e-1],t[e--],2);else 2<e&&"function"==typeof t[e-1]&&(r=t[--e]);for(var t=p(arguments,1,e),u=-1,o=a(),i=a();++u<e;)it(n,t[u],r,o,i); if(2<arguments.length)var e=Er(arguments,2),u=v(e,Le.placeholder),r=r|R;return Vr(t,r,n,e,u)}function $e(n,t,r){return r&&ee(n,t,r)&&(t=null),n=Vr(n,k,null,null,null,null,null,t),n.placeholder=$e.placeholder,n}function Be(n,t,r){return r&&ee(n,t,r)&&(t=null),n=Vr(n,E,null,null,null,null,null,t),n.placeholder=Be.placeholder,n}function ze(n,t,r){function e(){var r=t-(Co()-c);0>=r||r>t?(a&&Mu(a),r=p,a=s=p=b,r&&(h=Co(),f=n.apply(l,i),s||a||(i=l=null))):s=Zu(e,r)}function u(){s&&Mu(s),a=s=p=b,(v||g!==t)&&(h=Co(),f=n.apply(l,i),s||a||(i=l=null))
return l(o),l(i),n},J.min=function(n,t,e){var u=1/0,o=u;if(typeof t!="function"&&e&&e[t]===n&&(t=null),null==t&&Te(n)){e=-1;for(var i=n.length;++e<i;){var a=n[e];a<o&&(o=a)}}else t=null==t&&kt(n)?r:J.createCallback(t,e,3),St(n,function(n,e,r){e=t(n,e,r),e<u&&(u=e,o=n)});return o},J.omit=function(n,t,e){var r={};if(typeof t!="function"){var u=[];g(n,function(n,t){u.push(t)});for(var u=rt(u,ut(arguments,true,false,1)),o=-1,i=u.length;++o<i;){var a=u[o];r[a]=n[a]}}else t=J.createCallback(t,e,3),g(n,function(n,e,u){t(n,e,u)||(r[e]=n) }function o(){if(i=arguments,c=Co(),l=this,p=v&&(s||!d),false===g)var r=d&&!s;else{a||d||(h=c);var o=g-(c-h),y=0>=o||o>g;y?(a&&(a=Mu(a)),h=c,f=n.apply(l,i)):a||(a=Zu(u,o))}return y&&s?s=Mu(s):s||t===g||(s=Zu(e,t)),r&&(y=true,f=n.apply(l,i)),!y||s||a||(i=l=null),f}var i,a,f,c,l,s,p,h=0,g=false,v=true;if(!Ze(n))throw new Iu($);if(t=0>t?0:t,true===r)var d=true,v=false;else Ge(r)&&(d=r.leading,g="maxWait"in r&&eo(+r.maxWait||0,t),v="trailing"in r?r.trailing:v);return o.cancel=function(){s&&Mu(s),a&&Mu(a),a=s=p=b},o}function De(){var n=arguments,t=n.length-1;
});return r},J.once=function(n){var t,e;if(!dt(n))throw new ie;return function(){return t?e:(t=true,e=n.apply(this,arguments),n=null,e)}},J.pairs=function(n){for(var t=-1,e=Fe(n),r=e.length,u=Xt(r);++t<r;){var o=e[t];u[t]=[o,n[o]]}return u},J.partial=function(n){return ct(n,16,p(arguments,1))},J.partialRight=function(n){return ct(n,32,null,p(arguments,1))},J.pick=function(n,t,e){var r={};if(typeof t!="function")for(var u=-1,o=ut(arguments,true,false,1),i=wt(n)?o.length:0;++u<i;){var a=o[u];a in n&&(r[a]=n[a]) if(0>t)return function(){};if(!qt(n,Ze))throw new Iu($);return function(){for(var r=t,e=n[r].apply(this,arguments);r--;)e=n[r].call(this,e);return e}}function Me(n,t){function r(){var e=r.cache,u=t?t.apply(this,arguments):arguments[0];if(e.has(u))return e.get(u);var o=n.apply(this,arguments);return e.set(u,o),o}if(!Ze(n)||t&&!Ze(t))throw new Iu($);return r.cache=new Me.Cache,r}function qe(n){var t=Er(arguments,1),r=v(t,qe.placeholder);return Vr(n,R,null,t,r)}function Pe(n){var t=Er(arguments,1),r=v(t,Pe.placeholder);
}else t=J.createCallback(t,e,3),g(n,function(n,e,u){t(n,e,u)&&(r[e]=n)});return r},J.pluck=Ve,J.property=Jt,J.pull=function(n){for(var t=arguments,e=0,r=t.length,u=n?n.length:0;++e<r;)for(var o=-1,i=t[e];++o<u;)n[o]===i&&(de.call(n,o--,1),u--);return n},J.range=function(n,t,e){n=+n||0,e=typeof e=="number"?e:+e||1,null==t&&(t=n,n=0);var r=-1;t=Ie(0,se((t-n)/(e||1)));for(var u=Xt(t);++r<t;)u[r]=n,n+=e;return u},J.reject=function(n,t,e){return t=J.createCallback(t,e,3),Nt(n,function(n,e,r){return!t(n,e,r) return Vr(n,I,null,t,r)}function Ke(n){return ue(h(n)?n.length:b)&&Uu.call(n)==z||false}function Ve(n){return n&&1===n.nodeType&&h(n)&&-1<Uu.call(n).indexOf("Element")||false}function Ye(n){return h(n)&&typeof n.message=="string"&&Uu.call(n)==P||false}function Ze(n){return typeof n=="function"||false}function Ge(n){var t=typeof n;return"function"==t||n&&"object"==t||false}function Je(n){return null==n?false:Uu.call(n)==K?$u.test(Su.call(n)):h(n)&&bt.test(n)||false}function Xe(n){return typeof n=="number"||h(n)&&Uu.call(n)==V||false
})},J.remove=function(n,t,e){var r=-1,u=n?n.length:0,o=[];for(t=J.createCallback(t,e,3);++r<u;)e=n[r],t(e,r,n)&&(o.push(e),de.call(n,r--,1),u--);return o},J.rest=qt,J.shuffle=Tt,J.sortBy=function(n,t,e){var r=-1,o=Te(t),i=n?n.length:0,p=Xt(typeof i=="number"?i:0);for(o||(t=J.createCallback(t,e,3)),St(n,function(n,e,u){var i=p[++r]=f();o?i.m=Rt(t,function(t){return n[t]}):(i.m=a())[0]=t(n,e,u),i.n=r,i.o=n}),i=p.length,p.sort(u);i--;)n=p[i],p[i]=n.o,o||l(n.m),c(n);return p},J.tap=function(n,t){return t(n),n }function He(n){return h(n)&&Uu.call(n)==Z||false}function Qe(n){return typeof n=="string"||h(n)&&Uu.call(n)==G||false}function nu(n){return h(n)&&ue(n.length)&&Ct[Uu.call(n)]||false}function tu(n){return nr(n,eu(n))}function ru(n){return dr(n,eu(n))}function eu(n){if(null==n)return[];Ge(n)||(n=ku(n));for(var t=n.length,t=t&&ue(t)&&(To(n)||_o.nonEnumArgs&&Ke(n))&&t||0,r=n.constructor,e=-1,r=typeof r=="function"&&r.prototype==n,u=mu(t),o=0<t;++e<t;)u[e]=e+"";for(var i in n)o&&re(i,t)||"constructor"==i&&(r||!Nu.call(n,i))||u.push(i);
},J.throttle=function(n,t,e){var r=true,u=true;if(!dt(n))throw new ie;return false===e?r=false:wt(e)&&(r="leading"in e?e.leading:r,u="trailing"in e?e.trailing:u),L.leading=r,L.maxWait=t,L.trailing=u,Vt(n,t,L)},J.times=function(n,t,e){n=-1<(n=+n)?n:0;var r=-1,u=Xt(n);for(t=tt(t,e,1);++r<n;)u[r]=t(r);return u},J.toArray=function(n){return n&&typeof n.length=="number"?p(n):xt(n)},J.transform=function(n,t,e,r){var u=Te(n);if(null==e)if(u)e=[];else{var o=n&&n.constructor;e=nt(o&&o.prototype)}return t&&(t=J.createCallback(t,r,4),(u?St:h)(n,function(n,r,u){return t(e,n,r,u) return u}function uu(n){return Or(n,Fo(n))}function ou(n){return(n=e(n))&&n.replace(wt,c)}function iu(n){return(n=e(n))&&jt.test(n)?n.replace(At,"\\$&"):n}function au(n,t,r){return r&&ee(n,t,r)&&(t=0),ao(n,t)}function fu(n,t){var r="";if(n=e(n),t=+t,1>t||!n||!to(t))return r;do t%2&&(r+=n),t=qu(t/2),n+=n;while(t);return r}function cu(n,t,r){var u=n;return(n=e(n))?(r?ee(u,t,r):null==t)?n.slice(d(n),y(n)+1):(t=e(t),n.slice(o(n,t),i(n,t)+1)):n}function lu(n,t,r){return r&&ee(n,t,r)&&(t=null),n=e(n),n.match(t||Rt)||[]
})),e},J.union=function(){return ft(ut(arguments,true,true))},J.uniq=Pt,J.values=xt,J.where=Nt,J.without=function(n){return rt(n,p(arguments,1))},J.wrap=function(n,t){return ct(t,16,[n])},J.xor=function(){for(var n=-1,t=arguments.length;++n<t;){var e=arguments[n];if(Te(e)||yt(e))var r=r?ft(rt(r,e).concat(rt(e,r))):e}return r||[]},J.zip=Kt,J.zipObject=Lt,J.collect=Rt,J.drop=qt,J.each=St,J.eachRight=Et,J.extend=U,J.methods=bt,J.object=Lt,J.select=Nt,J.tail=qt,J.unique=Pt,J.unzip=Kt,Gt(J),J.clone=function(n,t,e,r){return typeof t!="boolean"&&null!=t&&(r=e,e=t,t=false),Z(n,t,typeof e=="function"&&tt(e,r,1)) }function su(n){try{return n()}catch(t){return Ye(t)?t:wu(t)}}function pu(n,t,r){return r&&ee(n,t,r)&&(t=null),tr(n,t)}function hu(n){return function(){return n}}function gu(n){return n}function vu(n){return wr(n,true)}function du(n,t,r){if(null==r){var e=Ge(t),u=e&&Fo(t);((u=u&&u.length&&dr(t,u))?u.length:e)||(u=false,r=t,t=n,n=this)}u||(u=dr(t,Fo(t)));var o=true,e=-1,i=Ze(n),a=u.length;false===r?o=false:Ge(r)&&"chain"in r&&(o=r.chain);for(;++e<a;){r=u[e];var f=t[r];n[r]=f,i&&(n.prototype[r]=function(t){return function(){var r=this.__chain__;
},J.cloneDeep=function(n,t,e){return Z(n,true,typeof t=="function"&&tt(t,e,1))},J.contains=Ct,J.escape=function(n){return null==n?"":oe(n).replace(ze,pt)},J.every=Ot,J.find=It,J.findIndex=function(n,t,e){var r=-1,u=n?n.length:0;for(t=J.createCallback(t,e,3);++r<u;)if(t(n[r],r,n))return r;return-1},J.findKey=function(n,t,e){var r;return t=J.createCallback(t,e,3),h(n,function(n,e,u){return t(n,e,u)?(r=e,false):void 0}),r},J.findLast=function(n,t,e){var r;return t=J.createCallback(t,e,3),Et(n,function(n,e,u){return t(n,e,u)?(r=n,false):void 0 if(o||r){var e=n(this.__wrapped__);return(e.__actions__=zt(this.__actions__)).push({func:t,args:arguments,thisArg:n}),e.__chain__=r,e}return r=[this.value()],Ku.apply(r,arguments),t.apply(n,r)}}(f))}return n}function yu(){}function _u(n){return Ar(n+"")}g=g?Dt.defaults($t.Object(),g,Dt.pick($t,Ot)):$t;var mu=g.Array,bu=g.Date,wu=g.Error,xu=g.Function,Au=g.Math,ju=g.Number,ku=g.Object,Eu=g.RegExp,Ru=g.String,Iu=g.TypeError,Ou=mu.prototype,Cu=ku.prototype,Tu=(Tu=g.window)&&Tu.document,Su=xu.prototype.toString,Wu=Ar("length"),Nu=Cu.hasOwnProperty,Fu=0,Uu=Cu.toString,Lu=g._,$u=Eu("^"+iu(Uu).replace(/toString|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$"),Bu=Je(Bu=g.ArrayBuffer)&&Bu,zu=Je(zu=Bu&&new Bu(0).slice)&&zu,Du=Au.ceil,Mu=g.clearTimeout,qu=Au.floor,Pu=Je(Pu=ku.getPrototypeOf)&&Pu,Ku=Ou.push,Vu=Cu.propertyIsEnumerable,Yu=Je(Yu=g.Set)&&Yu,Zu=g.setTimeout,Gu=Ou.splice,Ju=Je(Ju=g.Uint8Array)&&Ju,Xu=Je(Xu=g.WeakMap)&&Xu,Hu=function(){try{var n=Je(n=g.Float64Array)&&n,t=new n(new Bu(10),0,1)&&n
}),r},J.findLastIndex=function(n,t,e){var r=n?n.length:0;for(t=J.createCallback(t,e,3);r--;)if(t(n[r],r,n))return r;return-1},J.findLastKey=function(n,t,e){var r;return t=J.createCallback(t,e,3),mt(n,function(n,e,u){return t(n,e,u)?(r=e,false):void 0}),r},J.has=function(n,t){return n?me.call(n,t):false},J.identity=Ut,J.indexOf=Wt,J.isArguments=yt,J.isArray=Te,J.isBoolean=function(n){return true===n||false===n||n&&typeof n=="object"&&ce.call(n)==T||false},J.isDate=function(n){return n&&typeof n=="object"&&ce.call(n)==F||false }catch(r){}return t}(),Qu=Je(Qu=mu.isArray)&&Qu,no=Je(no=ku.create)&&no,to=g.isFinite,ro=Je(ro=ku.keys)&&ro,eo=Au.max,uo=Au.min,oo=Je(oo=bu.now)&&oo,io=Je(io=ju.isFinite)&&io,ao=g.parseInt,fo=Au.random,co=ju.NEGATIVE_INFINITY,lo=ju.POSITIVE_INFINITY,so=Au.pow(2,32)-1,po=so-1,ho=so>>>1,go=Hu?Hu.BYTES_PER_ELEMENT:0,vo=Au.pow(2,53)-1,yo=Xu&&new Xu,_o=Wt.support={};!function(n){_o.funcDecomp=!Je(g.WinRTError)&&kt.test(m),_o.funcNames=typeof xu.name=="string";try{_o.dom=11===Tu.createDocumentFragment().nodeType
},J.isElement=function(n){return n&&1===n.nodeType||false},J.isEmpty=function(n){var t=true;if(!n)return t;var e=ce.call(n),r=n.length;return e==$||e==P||e==D||e==q&&typeof r=="number"&&dt(n.splice)?!r:(h(n,function(){return t=false}),t)},J.isEqual=function(n,t,e,r){return ot(n,t,typeof e=="function"&&tt(e,r,2))},J.isFinite=function(n){return Ce(n)&&!Oe(parseFloat(n))},J.isFunction=dt,J.isNaN=function(n){return jt(n)&&n!=+n},J.isNull=function(n){return null===n},J.isNumber=jt,J.isObject=wt,J.isPlainObject=Pe,J.isRegExp=function(n){return n&&typeof n=="object"&&ce.call(n)==z||false }catch(t){_o.dom=false}try{_o.nonEnumArgs=!Vu.call(arguments,1)}catch(r){_o.nonEnumArgs=true}}(0,0),Wt.templateSettings={escape:ht,evaluate:gt,interpolate:vt,variable:"",imports:{_:Wt}};var mo=function(){function n(){}return function(t){if(Ge(t)){n.prototype=t;var r=new n;n.prototype=null}return r||g.Object()}}(),bo=yo?function(n,t){return yo.set(n,t),n}:gu;zu||(Nr=Bu&&Ju?function(n){var t=n.byteLength,r=Hu?qu(t/go):0,e=r*go,u=new Bu(t);if(r){var o=new Hu(u,0,r);o.set(new Hu(n,0,r))}return t!=e&&(o=new Ju(u,e),o.set(new Ju(n,e))),u
},J.isString=kt,J.isUndefined=function(n){return typeof n=="undefined"},J.lastIndexOf=function(n,t,e){var r=n?n.length:0;for(typeof e=="number"&&(r=(0>e?Ie(0,r+e):Se(e,r-1))+1);r--;)if(n[r]===t)return r;return-1},J.mixin=Gt,J.noConflict=function(){return e._=le,this},J.noop=Ht,J.now=Ue,J.parseInt=Ge,J.random=function(n,t,e){var r=null==n,u=null==t;return null==e&&(typeof n=="boolean"&&u?(e=n,n=1):u||typeof t!="boolean"||(e=t,u=true)),r&&u&&(t=1),n=+n||0,u?(t=n,n=0):t=+t||0,e||n%1||t%1?(e=Re(),Se(n+e*(t-n+parseFloat("1e-"+((e+"").length-1))),t)):at(n,t) }:hu(null));var wo=no&&Yu?function(n){return new Lt(n)}:hu(null),xo=yo?function(n){return yo.get(n)}:yu,Ao=function(){var n=0,t=0;return function(r,e){var u=Co(),o=N-(u-t);if(t=u,0<o){if(++n>=W)return r}else n=0;return bo(r,e)}}(),jo=Lr(function(n,t,r){Nu.call(n,r)?++n[r]:n[r]=1}),ko=Lr(function(n,t,r){Nu.call(n,r)?n[r].push(t):n[r]=[t]}),Eo=Lr(function(n,t,r){n[r]=t}),Ro=Mr(Vt),Io=Mr(function(n){for(var t=-1,r=n.length,e=lo;++t<r;){var u=n[t];u<e&&(e=u)}return e},true),Oo=Lr(function(n,t,r){n[r?0:1].push(t)
},J.reduce=Dt,J.reduceRight=$t,J.result=function(n,t){if(n){var e=n[t];return dt(e)?n[t]():e}},J.runInContext=s,J.size=function(n){var t=n?n.length:0;return typeof t=="number"?t:Fe(n).length},J.some=Ft,J.sortedIndex=zt,J.template=function(n,t,e){var r=J.templateSettings;n=oe(n||""),e=_({},e,r);var u,o=_({},e.imports,r.imports),r=Fe(o),o=xt(o),a=0,f=e.interpolate||S,l="__p+='",f=ue((e.escape||S).source+"|"+f.source+"|"+(f===N?x:S).source+"|"+(e.evaluate||S).source+"|$","g");n.replace(f,function(t,e,r,o,f,c){return r||(r=o),l+=n.slice(a,c).replace(R,i),e&&(l+="'+__e("+e+")+'"),f&&(u=true,l+="';"+f+";\n__p+='"),r&&(l+="'+((__t=("+r+"))==null?'':__t)+'"),a=c+t.length,t },function(){return[[],[]]}),Co=oo||function(){return(new bu).getTime()},To=Qu||function(n){return h(n)&&ue(n.length)&&Uu.call(n)==D||false};_o.dom||(Ve=function(n){return n&&1===n.nodeType&&h(n)&&!Wo(n)||false});var So=io||function(n){return typeof n=="number"&&to(n)};(Ze(/x/)||Ju&&!Ze(Ju))&&(Ze=function(n){return Uu.call(n)==K});var Wo=Pu?function(n){if(!n||Uu.call(n)!=Y)return false;var t=n.valueOf,r=Je(t)&&(r=Pu(t))&&Pu(r);return r?n==r||Pu(n)==r:fe(n)}:fe,No=$r(Ht),Fo=ro?function(n){if(n)var t=n.constructor,r=n.length;
}),l+="';",f=e=e.variable,f||(e="obj",l="with("+e+"){"+l+"}"),l=(u?l.replace(w,""):l).replace(j,"$1").replace(k,"$1;"),l="function("+e+"){"+(f?"":e+"||("+e+"={});")+"var __t,__p='',__e=_.escape"+(u?",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,'')}":";")+l+"return __p}";try{var c=ne(r,"return "+l).apply(v,o)}catch(p){throw p.source=l,p}return t?c(t):(c.source=l,c)},J.unescape=function(n){return null==n?"":oe(n).replace(qe,gt)},J.uniqueId=function(n){var t=++y;return oe(null==n?"":n)+t return typeof t=="function"&&t.prototype===n||typeof n!="function"&&r&&ue(r)?ce(n):Ge(n)?ro(n):[]}:ce,Uo=$r(xr),Lo=zr(function(n,t,r){return t=t.toLowerCase(),r?n+t.charAt(0).toUpperCase()+t.slice(1):t}),$o=zr(function(n,t,r){return n+(r?"-":"")+t.toLowerCase()});8!=ao(It+"08")&&(au=function(n,t,r){return(r?ee(n,t,r):null==t)?t=0:t&&(t=+t),n=cu(n),ao(n,t||(mt.test(n)?16:10))});var Bo=zr(function(n,t,r){return n+(r?"_":"")+t.toLowerCase()});return Nt.prototype=Wt.prototype,Ut.prototype["delete"]=function(n){return this.has(n)&&delete this.__data__[n]
},J.all=Ot,J.any=Ft,J.detect=It,J.findWhere=It,J.foldl=Dt,J.foldr=$t,J.include=Ct,J.inject=Dt,Gt(function(){var n={};return h(J,function(t,e){J.prototype[e]||(n[e]=t)}),n}(),false),J.first=Bt,J.last=function(n,t,e){var r=0,u=n?n.length:0;if(typeof t!="number"&&null!=t){var o=u;for(t=J.createCallback(t,e,3);o--&&t(n[o],o,n);)r++}else if(r=t,null==r||e)return n?n[u-1]:v;return p(n,Ie(0,u-r))},J.sample=function(n,t,e){return n&&typeof n.length!="number"&&(n=xt(n)),null==t||e?n?n[at(0,n.length-1)]:v:(n=Tt(n),n.length=Se(Ie(0,t),n.length),n) },Ut.prototype.get=function(n){return"__proto__"==n?b:this.__data__[n]},Ut.prototype.has=function(n){return"__proto__"!=n&&Nu.call(this.__data__,n)},Ut.prototype.set=function(n,t){return"__proto__"!=n&&(this.__data__[n]=t),this},Lt.prototype.push=function(n){var t=this.data;typeof n=="string"||Ge(n)?t.set.add(n):t.hash[n]=true},Me.Cache=Ut,Wt.after=function(n,t){if(!Ze(t)){if(!Ze(n))throw new Iu($);var r=n;n=t,t=r}return n=to(n=+n)?n:0,function(){return 1>--n?t.apply(this,arguments):void 0}},Wt.ary=function(n,t,r){return r&&ee(n,t,r)&&(t=null),t=n&&null==t?n.length:eo(+t||0,0),Vr(n,C,null,null,null,null,t)
},J.take=Bt,J.head=Bt,h(J,function(n,t){var e="sample"!==t;J.prototype[t]||(J.prototype[t]=function(t,r){var u=this.__chain__,o=n(this.__wrapped__,t,r);return u||null!=t&&(!r||e&&typeof t=="function")?new Q(o,u):o})}),J.VERSION="2.4.1",J.prototype.chain=function(){return this.__chain__=true,this},J.prototype.toString=function(){return oe(this.__wrapped__)},J.prototype.value=Qt,J.prototype.valueOf=Qt,St(["join","pop","shift"],function(n){var t=ae[n];J.prototype[n]=function(){var n=this.__chain__,e=t.apply(this.__wrapped__,arguments); },Wt.assign=No,Wt.at=function(n){return ue(n?n.length:0)&&(n=le(n)),Qt(n,lr(arguments,false,false,1))},Wt.before=Fe,Wt.bind=Ue,Wt.bindAll=function(n){for(var t=n,r=1<arguments.length?lr(arguments,false,false,1):ru(n),e=-1,u=r.length;++e<u;){var o=r[e];t[o]=Vr(t[o],x,t)}return t},Wt.bindKey=Le,Wt.callback=pu,Wt.chain=we,Wt.chunk=function(n,t,r){t=(r?ee(n,t,r):null==t)?1:eo(+t||1,1),r=0;for(var e=n?n.length:0,u=-1,o=mu(Du(e/t));r<e;)o[++u]=Er(n,r,r+=t);return o},Wt.compact=function(n){for(var t=-1,r=n?n.length:0,e=-1,u=[];++t<r;){var o=n[t];
return n?new Q(e,n):e}}),St(["push","reverse","sort","unshift"],function(n){var t=ae[n];J.prototype[n]=function(){return t.apply(this.__wrapped__,arguments),this}}),St(["concat","slice","splice"],function(n){var t=ae[n];J.prototype[n]=function(){return new Q(t.apply(this.__wrapped__,arguments),this.__chain__)}}),J}var v,h=[],g=[],y=0,m=+new Date+"",b=75,_=40,d=" \t\x0B\f\xa0\ufeff\n\r\u2028\u2029\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000",w=/\b__p\+='';/g,j=/\b(__p\+=)''\+/g,k=/(__e\(.*?\)|\b__t\))\+'';/g,x=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,C=/\w*$/,O=/^\s*function[ \n\r\t]+\w/,N=/<%=([\s\S]+?)%>/g,I=RegExp("^["+d+"]*0+(?=.$)"),S=/($^)/,E=/\bthis\b/,R=/['\n\r\t\u2028\u2029\\]/g,A="Array Boolean Date Function Math Number Object RegExp String _ attachEvent clearTimeout isFinite isNaN parseInt setTimeout".split(" "),D="[object Arguments]",$="[object Array]",T="[object Boolean]",F="[object Date]",B="[object Function]",W="[object Number]",q="[object Object]",z="[object RegExp]",P="[object String]",K={}; o&&(u[++e]=o)}return u},Wt.constant=hu,Wt.countBy=jo,Wt.create=function(n,t,r){var e=mo(n);return r&&ee(n,t,r)&&(t=null),t?nr(t,e,Fo(t)):e},Wt.curry=$e,Wt.curryRight=Be,Wt.debounce=ze,Wt.defaults=function(n){if(null==n)return n;var t=zt(arguments);return t.push(Jt),No.apply(b,t)},Wt.defer=function(n){return er(n,1,arguments,1)},Wt.delay=function(n,t){return er(n,t,arguments,2)},Wt.difference=function(){for(var n=-1,t=arguments.length;++n<t;){var r=arguments[n];if(To(r)||Ke(r))break}return ur(r,lr(arguments,false,true,++n))
K[B]=false,K[D]=K[$]=K[T]=K[F]=K[W]=K[q]=K[z]=K[P]=true;var L={leading:false,maxWait:0,trailing:false},M={configurable:false,enumerable:false,value:null,writable:false},V={"boolean":false,"function":true,object:true,number:false,string:false,undefined:false},U={"\\":"\\","'":"'","\n":"n","\r":"r","\t":"t","\u2028":"u2028","\u2029":"u2029"},G=V[typeof window]&&window||this,H=V[typeof exports]&&exports&&!exports.nodeType&&exports,J=V[typeof module]&&module&&!module.nodeType&&module,Q=J&&J.exports===H&&H,X=V[typeof global]&&global;!X||X.global!==X&&X.window!==X||(G=X); },Wt.drop=pe,Wt.dropRight=he,Wt.dropRightWhile=function(n,t,r){var e=n?n.length:0;if(!e)return[];for(t=Xr(t,r,3);e--&&t(n[e],e,n););return Er(n,0,e+1)},Wt.dropWhile=function(n,t,r){var e=n?n.length:0;if(!e)return[];var u=-1;for(t=Xr(t,r,3);++u<e&&t(n[u],u,n););return Er(n,u)},Wt.filter=ke,Wt.flatten=function(n,t,r){var e=n?n.length:0;return r&&ee(n,t,r)&&(t=false),e?lr(n,t):[]},Wt.flattenDeep=function(n){return n&&n.length?lr(n,true):[]},Wt.flow=function(){var n=arguments,t=n.length;if(!t)return function(){};
var Y=s();typeof define=="function"&&typeof define.amd=="object"&&define.amd?(G._=Y, define(function(){return Y})):H&&J?Q?(J.exports=Y)._=Y:H._=Y:G._=Y}).call(this); if(!qt(n,Ze))throw new Iu($);return function(){for(var r=0,e=n[r].apply(this,arguments);++r<t;)e=n[r].call(this,e);return e}},Wt.flowRight=De,Wt.forEach=Re,Wt.forEachRight=Ie,Wt.forIn=function(n,t,r){return(typeof t!="function"||typeof r!="undefined")&&(t=Wr(t,r,3)),sr(n,t,eu)},Wt.forInRight=function(n,t,r){return t=Wr(t,r,3),pr(n,t,eu)},Wt.forOwn=function(n,t,r){return(typeof t!="function"||typeof r!="undefined")&&(t=Wr(t,r,3)),gr(n,t)},Wt.forOwnRight=function(n,t,r){return t=Wr(t,r,3),pr(n,t,Fo)
},Wt.functions=ru,Wt.groupBy=ko,Wt.indexBy=Eo,Wt.initial=function(n){return he(n,1)},Wt.intersection=function(){for(var n=[],r=-1,e=arguments.length,u=[],o=Hr(),i=o==t;++r<e;){var a=arguments[r];(To(a)||Ke(a))&&(n.push(a),u.push(i&&120<=a.length&&wo(r&&a)))}var e=n.length,i=n[0],f=-1,c=i?i.length:0,l=[],s=u[0];n:for(;++f<c;)if(a=i[f],0>(s?Bt(s,a):o(l,a))){for(r=e;--r;){var p=u[r];if(0>(p?Bt(p,a):o(n[r],a)))continue n}s&&s.push(a),l.push(a)}return l},Wt.invert=function(n,t,r){r&&ee(n,t,r)&&(t=null),r=-1;
for(var e=Fo(n),u=e.length,o={};++r<u;){var i=e[r],a=n[i];t?Nu.call(o,a)?o[a].push(i):o[a]=[i]:o[a]=i}return o},Wt.invoke=function(n,t){return yr(n,t,Er(arguments,2))},Wt.keys=Fo,Wt.keysIn=eu,Wt.map=Oe,Wt.mapValues=function(n,t,r){var e={};return t=Xr(t,r,3),gr(n,function(n,r,u){e[r]=t(n,r,u)}),e},Wt.matches=vu,Wt.memoize=Me,Wt.merge=Uo,Wt.mixin=du,Wt.negate=function(n){if(!Ze(n))throw new Iu($);return function(){return!n.apply(this,arguments)}},Wt.omit=function(n,t,r){if(null==n)return{};if(typeof t!="function"){var e=Kt(lr(arguments,false,false,1),Ru);
return ie(n,ur(eu(n),e))}return t=Wr(t,r,3),ae(n,function(n,r,e){return!t(n,r,e)})},Wt.once=function(n){return Fe(n,2)},Wt.pairs=function(n){for(var t=-1,r=Fo(n),e=r.length,u=mu(e);++t<e;){var o=r[t];u[t]=[o,n[o]]}return u},Wt.partial=qe,Wt.partialRight=Pe,Wt.partition=Oo,Wt.pick=function(n,t,r){return null==n?{}:typeof t=="function"?ae(n,Wr(t,r,3)):ie(n,lr(arguments,false,false,1))},Wt.pluck=function(n,t){return Oe(n,_u(t))},Wt.property=_u,Wt.propertyOf=function(n){return function(t){return null==n?b:n[t]
}},Wt.pull=function(){var n=arguments[0];if(!n||!n.length)return n;for(var t=0,r=Hr(),e=arguments.length;++t<e;)for(var u=0,o=arguments[t];-1<(u=r(n,o,u));)Gu.call(n,u,1);return n},Wt.pullAt=function(t){var r=t||[],e=lr(arguments,false,false,1),u=e.length,o=Qt(r,e);for(e.sort(n);u--;){var i=parseFloat(e[u]);if(i!=a&&re(i)){var a=i;Gu.call(r,i,1)}}return o},Wt.range=function(n,t,r){r&&ee(n,t,r)&&(t=r=null),n=+n||0,r=null==r?1:+r||0,null==t?(t=n,n=0):t=+t||0;var e=-1;t=eo(Du((t-n)/(r||1)),0);for(var u=mu(t);++e<t;)u[e]=n,n+=r;
return u},Wt.rearg=function(n){var t=lr(arguments,false,false,1);return Vr(n,O,null,null,null,t)},Wt.reject=function(n,t,r){var e=To(n)?Pt:fr;return t=Xr(t,r,3),e(n,function(n,r,e){return!t(n,r,e)})},Wt.remove=function(n,t,r){var e=-1,u=n?n.length:0,o=[];for(t=Xr(t,r,3);++e<u;)r=n[e],t(r,e,n)&&(o.push(r),Gu.call(n,e--,1),u--);return o},Wt.rest=ye,Wt.shuffle=We,Wt.slice=function(n,t,r){var e=n?n.length:0;return e?(r&&typeof r!="number"&&ee(n,t,r)&&(t=0,r=e),Er(n,t,r)):[]},Wt.sortBy=function(n,t,e){var u=-1,o=n?n.length:0,i=ue(o)?mu(o):[];
return e&&ee(n,t,e)&&(t=null),t=Xr(t,e,3),or(n,function(n,r,e){i[++u]={a:t(n,r,e),b:u,c:n}}),r(i,a)},Wt.sortByAll=function(n){var t=arguments;3<t.length&&ee(t[1],t[2],t[3])&&(t=[n,t[1]]);var e=-1,u=n?n.length:0,o=lr(t,false,false,1),i=ue(u)?mu(u):[];return or(n,function(n){for(var t=o.length,r=mu(t);t--;)r[t]=null==n?b:n[o[t]];i[++e]={a:r,b:e,c:n}}),r(i,f)},Wt.take=function(n,t,r){return n&&n.length?((r?ee(n,t,r):null==t)&&(t=1),Er(n,0,0>t?0:t)):[]},Wt.takeRight=function(n,t,r){var e=n?n.length:0;return e?((r?ee(n,t,r):null==t)&&(t=1),t=e-(+t||0),Er(n,0>t?0:t)):[]
},Wt.takeRightWhile=function(n,t,r){var e=n?n.length:0;if(!e)return[];for(t=Xr(t,r,3);e--&&t(n[e],e,n););return Er(n,e+1)},Wt.takeWhile=function(n,t,r){var e=n?n.length:0;if(!e)return[];var u=-1;for(t=Xr(t,r,3);++u<e&&t(n[u],u,n););return Er(n,0,u)},Wt.tap=function(n,t,r){return t.call(r,n),n},Wt.throttle=function(n,t,r){var e=true,u=true;if(!Ze(n))throw new Iu($);return false===r?e=false:Ge(r)&&(e="leading"in r?!!r.leading:e,u="trailing"in r?!!r.trailing:u),St.leading=e,St.maxWait=+t,St.trailing=u,ze(n,t,St)
},Wt.thru=xe,Wt.times=function(n,t,r){if(n=+n,1>n||!to(n))return[];var e=-1,u=mu(uo(n,so));for(t=Wr(t,r,1);++e<n;)e<so?u[e]=t(e):t(e);return u},Wt.toArray=function(n){var t=n?n.length:0;return ue(t)?t?zt(n):[]:uu(n)},Wt.toPlainObject=tu,Wt.transform=function(n,t,r,e){var u=To(n)||nu(n);return t=Xr(t,e,4),null==r&&(u||Ge(n)?(e=n.constructor,r=u?To(n)?new e:[]:mo(typeof e=="function"&&e.prototype)):r={}),(u?Mt:gr)(n,function(n,e,u){return t(r,n,e,u)}),r},Wt.union=function(){return Ir(lr(arguments,false,true))
},Wt.uniq=_e,Wt.unzip=me,Wt.values=uu,Wt.valuesIn=function(n){return Or(n,eu(n))},Wt.where=function(n,t){return ke(n,vu(t))},Wt.without=function(n){return ur(n,Er(arguments,1))},Wt.wrap=function(n,t){return t=null==t?gu:t,Vr(t,R,null,[n],[])},Wt.xor=function(){for(var n=-1,t=arguments.length;++n<t;){var r=arguments[n];if(To(r)||Ke(r))var e=e?ur(e,r).concat(ur(r,e)):r}return e?Ir(e):[]},Wt.zip=function(){for(var n=arguments.length,t=mu(n);n--;)t[n]=arguments[n];return me(t)},Wt.zipObject=be,Wt.backflow=De,Wt.collect=Oe,Wt.compose=De,Wt.each=Re,Wt.eachRight=Ie,Wt.extend=No,Wt.iteratee=pu,Wt.methods=ru,Wt.object=be,Wt.select=ke,Wt.tail=ye,Wt.unique=_e,du(Wt,Wt),Wt.attempt=su,Wt.camelCase=Lo,Wt.capitalize=function(n){return(n=e(n))&&n.charAt(0).toUpperCase()+n.slice(1)
},Wt.clone=function(n,t,r,e){return typeof t!="boolean"&&null!=t&&(e=r,r=ee(n,t,e)?null:t,t=false),r=typeof r=="function"&&Wr(r,e,1),rr(n,t,r)},Wt.cloneDeep=function(n,t,r){return t=typeof t=="function"&&Wr(t,r,1),rr(n,true,t)},Wt.deburr=ou,Wt.endsWith=function(n,t,r){n=e(n),t+="";var u=n.length;return r=(typeof r=="undefined"?u:uo(0>r?0:+r||0,u))-t.length,0<=r&&n.indexOf(t,r)==r},Wt.escape=function(n){return(n=e(n))&&pt.test(n)?n.replace(lt,l):n},Wt.escapeRegExp=iu,Wt.every=je,Wt.find=Ee,Wt.findIndex=ge,Wt.findKey=function(n,t,r){return t=Xr(t,r,3),cr(n,t,gr,true)
},Wt.findLast=function(n,t,r){return t=Xr(t,r,3),cr(n,t,ir)},Wt.findLastIndex=function(n,t,r){var e=n?n.length:0;for(t=Xr(t,r,3);e--;)if(t(n[e],e,n))return e;return-1},Wt.findLastKey=function(n,t,r){return t=Xr(t,r,3),cr(n,t,vr,true)},Wt.findWhere=function(n,t){return Ee(n,vu(t))},Wt.first=ve,Wt.has=function(n,t){return n?Nu.call(n,t):false},Wt.identity=gu,Wt.includes=Ae,Wt.indexOf=de,Wt.isArguments=Ke,Wt.isArray=To,Wt.isBoolean=function(n){return true===n||false===n||h(n)&&Uu.call(n)==M||false},Wt.isDate=function(n){return h(n)&&Uu.call(n)==q||false
},Wt.isElement=Ve,Wt.isEmpty=function(n){if(null==n)return true;var t=n.length;return ue(t)&&(To(n)||Qe(n)||Ke(n)||h(n)&&Ze(n.splice))?!t:!Fo(n).length},Wt.isEqual=function(n,t,r,e){return r=typeof r=="function"&&Wr(r,e,3),!r&&oe(n)&&oe(t)?n===t:(e=r?r(n,t):b,typeof e=="undefined"?_r(n,t,r):!!e)},Wt.isError=Ye,Wt.isFinite=So,Wt.isFunction=Ze,Wt.isMatch=function(n,t,r,e){var u=Fo(t),o=u.length;if(r=typeof r=="function"&&Wr(r,e,3),!r&&1==o){var i=u[0];if(e=t[i],oe(e))return null!=n&&e===n[i]&&Nu.call(n,i)
}for(var i=mu(o),a=mu(o);o--;)e=i[o]=t[u[o]],a[o]=oe(e);return mr(n,u,i,a,r)},Wt.isNaN=function(n){return Xe(n)&&n!=+n},Wt.isNative=Je,Wt.isNull=function(n){return null===n},Wt.isNumber=Xe,Wt.isObject=Ge,Wt.isPlainObject=Wo,Wt.isRegExp=He,Wt.isString=Qe,Wt.isTypedArray=nu,Wt.isUndefined=function(n){return typeof n=="undefined"},Wt.kebabCase=$o,Wt.last=function(n){var t=n?n.length:0;return t?n[t-1]:b},Wt.lastIndexOf=function(n,t,r){var e=n?n.length:0;if(!e)return-1;var u=e;if(typeof r=="number")u=(0>r?eo(e+r,0):uo(r||0,e-1))+1;
else if(r)return u=Tr(n,t,true)-1,n=n[u],(t===t?t===n:n!==n)?u:-1;if(t!==t)return p(n,u,true);for(;u--;)if(n[u]===t)return u;return-1},Wt.max=Ro,Wt.min=Io,Wt.noConflict=function(){return g._=Lu,this},Wt.noop=yu,Wt.now=Co,Wt.pad=function(n,t,r){n=e(n),t=+t;var u=n.length;return u<t&&to(t)?(u=(t-u)/2,t=qu(u),u=Du(u),r=Pr("",u,r),r.slice(0,t)+n+r):n},Wt.padLeft=function(n,t,r){return(n=e(n))&&Pr(n,t,r)+n},Wt.padRight=function(n,t,r){return(n=e(n))&&n+Pr(n,t,r)},Wt.parseInt=au,Wt.random=function(n,t,r){r&&ee(n,t,r)&&(t=r=null);
var e=null==n,u=null==t;return null==r&&(u&&typeof n=="boolean"?(r=n,n=1):typeof t=="boolean"&&(r=t,u=true)),e&&u&&(t=1,u=false),n=+n||0,u?(t=n,n=0):t=+t||0,r||n%1||t%1?(r=fo(),uo(n+r*(t-n+parseFloat("1e-"+((r+"").length-1))),t)):jr(n,t)},Wt.reduce=Ce,Wt.reduceRight=Te,Wt.repeat=fu,Wt.result=function(n,t,r){return t=null==n?b:n[t],typeof t=="undefined"&&(t=r),Ze(t)?t.call(n):t},Wt.runInContext=m,Wt.size=function(n){var t=n?n.length:0;return ue(t)?t:Fo(n).length},Wt.snakeCase=Bo,Wt.some=Ne,Wt.sortedIndex=function(n,t,r,e){var u=Xr(r);
return u===tr&&null==r?Tr(n,t):Sr(n,t,u(r,e,1))},Wt.sortedLastIndex=function(n,t,r,e){var u=Xr(r);return u===tr&&null==r?Tr(n,t,true):Sr(n,t,u(r,e,1),true)},Wt.startsWith=function(n,t,r){return n=e(n),r=null==r?0:uo(0>r?0:+r||0,n.length),n.lastIndexOf(t,r)==r},Wt.template=function(n,t,r){var u=Wt.templateSettings;r&&ee(n,t,r)&&(t=r=null),n=e(n),t=Ht(Ht({},r||t),u,Xt),r=Ht(Ht({},t.imports),u.imports,Xt);var o,i,a=Fo(r),f=Or(r,a),c=0;r=t.interpolate||xt;var l="__p+='";r=Eu((t.escape||xt).source+"|"+r.source+"|"+(r===vt?dt:xt).source+"|"+(t.evaluate||xt).source+"|$","g");
var p="sourceURL"in t?"//# sourceURL="+t.sourceURL+"\n":"";if(n.replace(r,function(t,r,e,u,a,f){return e||(e=u),l+=n.slice(c,f).replace(Et,s),r&&(o=true,l+="'+__e("+r+")+'"),a&&(i=true,l+="';"+a+";\n__p+='"),e&&(l+="'+((__t=("+e+"))==null?'':__t)+'"),c=f+t.length,t}),l+="';",(t=t.variable)||(l="with(obj){"+l+"}"),l=(i?l.replace(it,""):l).replace(at,"$1").replace(ft,"$1;"),l="function("+(t||"obj")+"){"+(t?"":"obj||(obj={});")+"var __t,__p=''"+(o?",__e=_.escape":"")+(i?",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,'')}":";")+l+"return __p}",t=su(function(){return xu(a,p+"return "+l).apply(b,f)
}),t.source=l,Ye(t))throw t;return t},Wt.trim=cu,Wt.trimLeft=function(n,t,r){var u=n;return(n=e(n))?n.slice((r?ee(u,t,r):null==t)?d(n):o(n,e(t))):n},Wt.trimRight=function(n,t,r){var u=n;return(n=e(n))?(r?ee(u,t,r):null==t)?n.slice(0,y(n)+1):n.slice(0,i(n,e(t))+1):n},Wt.trunc=function(n,t,r){r&&ee(n,t,r)&&(t=null);var u=T;if(r=S,null!=t)if(Ge(t)){var o="separator"in t?t.separator:o,u="length"in t?+t.length||0:u;r="omission"in t?e(t.omission):r}else u=+t||0;if(n=e(n),u>=n.length)return n;if(u-=r.length,1>u)return r;
if(t=n.slice(0,u),null==o)return t+r;if(He(o)){if(n.slice(u).search(o)){var i,a=n.slice(0,u);for(o.global||(o=Eu(o.source,(yt.exec(o)||"")+"g")),o.lastIndex=0;n=o.exec(a);)i=n.index;t=t.slice(0,null==i?u:i)}}else n.indexOf(o,u)!=u&&(o=t.lastIndexOf(o),-1<o&&(t=t.slice(0,o)));return t+r},Wt.unescape=function(n){return(n=e(n))&&st.test(n)?n.replace(ct,_):n},Wt.uniqueId=function(n){var t=++Fu;return e(n)+t},Wt.words=lu,Wt.all=je,Wt.any=Ne,Wt.contains=Ae,Wt.detect=Ee,Wt.foldl=Ce,Wt.foldr=Te,Wt.head=ve,Wt.include=Ae,Wt.inject=Ce,du(Wt,function(){var n={};
return gr(Wt,function(t,r){Wt.prototype[r]||(n[r]=t)}),n}(),false),Wt.sample=Se,Wt.prototype.sample=function(n){return this.__chain__||null!=n?this.thru(function(t){return Se(t,n)}):Se(this.value())},Wt.VERSION=w,Mt("bind bindKey curry curryRight partial partialRight".split(" "),function(n){Wt[n].placeholder=Wt}),Mt(["filter","map","takeWhile"],function(n,t){var r=t==F;Ft.prototype[n]=function(n,e){var u=this.clone(),o=u.filtered,i=u.iteratees||(u.iteratees=[]);return u.filtered=o||r||t==L&&0>u.dir,i.push({iteratee:Xr(n,e,3),type:t}),u
}}),Mt(["drop","take"],function(n,t){var r=n+"Count",e=n+"While";Ft.prototype[n]=function(e){e=null==e?1:eo(+e||0,0);var u=this.clone();if(u.filtered){var o=u[r];u[r]=t?uo(o,e):o+e}else(u.views||(u.views=[])).push({size:e,type:n+(0>u.dir?"Right":"")});return u},Ft.prototype[n+"Right"]=function(t){return this.reverse()[n](t).reverse()},Ft.prototype[n+"RightWhile"]=function(n,t){return this.reverse()[e](n,t).reverse()}}),Mt(["first","last"],function(n,t){var r="take"+(t?"Right":"");Ft.prototype[n]=function(){return this[r](1).value()[0]
}}),Mt(["initial","rest"],function(n,t){var r="drop"+(t?"":"Right");Ft.prototype[n]=function(){return this[r](1)}}),Mt(["pluck","where"],function(n,t){var r=t?"filter":"map",e=t?vu:_u;Ft.prototype[n]=function(n){return this[r](e(n))}}),Ft.prototype.dropWhile=function(n,t){var r,e,u=0>this.dir;return n=Xr(n,t,3),this.filter(function(t,o,i){return r=r&&(u?o<e:o>e),e=o,r||(r=!n(t,o,i))})},Ft.prototype.reject=function(n,t){return n=Xr(n,t,3),this.filter(function(t,r,e){return!n(t,r,e)})},Ft.prototype.slice=function(n,t){n=null==n?0:+n||0;
var r=0>n?this.takeRight(-n):this.drop(n);return typeof t!="undefined"&&(t=+t||0,r=0>t?r.dropRight(-t):r.take(t-n)),r},gr(Ft.prototype,function(n,t){var r=/^(?:first|last)$/.test(t);Wt.prototype[t]=function(){function e(n){return n=[n],Ku.apply(n,o),Wt[t].apply(Wt,n)}var u=this.__wrapped__,o=arguments,i=this.__chain__,a=!!this.__actions__.length,f=u instanceof Ft,c=f&&!a;return r&&!i?c?n.call(u):Wt[t](this.value()):f||To(u)?(u=n.apply(c?u:new Ft(this),o),r||!a&&!u.actions||(u.actions||(u.actions=[])).push({func:xe,args:[e],thisArg:Wt}),new Nt(u,i)):this.thru(e)
}}),Mt("concat join pop push shift sort splice unshift".split(" "),function(n){var t=Ou[n],r=/^(?:push|sort|unshift)$/.test(n)?"tap":"thru",e=/^(?:join|pop|shift)$/.test(n);Wt.prototype[n]=function(){var n=arguments;return e&&!this.__chain__?t.apply(this.value(),n):this[r](function(r){return t.apply(r,n)})}}),Ft.prototype.clone=function(){var n=this.actions,t=this.iteratees,r=this.views,e=new Ft(this.wrapped);return e.actions=n?zt(n):null,e.dir=this.dir,e.dropCount=this.dropCount,e.filtered=this.filtered,e.iteratees=t?zt(t):null,e.takeCount=this.takeCount,e.views=r?zt(r):null,e
},Ft.prototype.reverse=function(){var n=this.filtered,t=n?new Ft(this):this.clone();return t.dir=-1*this.dir,t.filtered=n,t},Ft.prototype.value=function(){var n=this.wrapped.value();if(!To(n))return Cr(n,this.actions);var t,r=this.dir,e=0>r,u=n.length;t=u;for(var o=this.views,i=0,a=-1,f=o?o.length:0;++a<f;){var c=o[a],l=c.size;switch(c.type){case"drop":i+=l;break;case"dropRight":t-=l;break;case"take":t=uo(t,i+l);break;case"takeRight":i=eo(i,t-l)}}t={start:i,end:t},i=t.start,a=t.end,t=this.dropCount,o=uo(a-i,this.takeCount-t),i=e?a:i-1,f=(a=this.iteratees)?a.length:0,c=0,l=[];
n:for(;u--&&c<o;){for(var i=i+r,s=-1,p=n[i];++s<f;){var h=a[s],g=h.iteratee(p,i,n),h=h.type;if(h==U)p=g;else if(!g){if(h==F)continue n;break n}}t?t--:l[c++]=p}return e?l.reverse():l},Wt.prototype.chain=function(){return we(this)},Wt.prototype.reverse=function(){var n=this.__wrapped__;return n instanceof Ft?new Nt(n.reverse()):this.thru(function(n){return n.reverse()})},Wt.prototype.toString=function(){return this.value()+""},Wt.prototype.toJSON=Wt.prototype.valueOf=Wt.prototype.value=function(){return Cr(this.__wrapped__,this.__actions__)
},Wt.prototype.collect=Wt.prototype.map,Wt.prototype.head=Wt.prototype.first,Wt.prototype.select=Wt.prototype.filter,Wt.prototype.tail=Wt.prototype.rest,Wt}var b,w="3.0.0",x=1,A=2,j=4,k=8,E=16,R=32,I=64,O=128,C=256,T=30,S="...",W=150,N=16,F=0,U=1,L=2,$="Expected a function",B="__lodash_placeholder__",z="[object Arguments]",D="[object Array]",M="[object Boolean]",q="[object Date]",P="[object Error]",K="[object Function]",V="[object Number]",Y="[object Object]",Z="[object RegExp]",G="[object String]",J="[object ArrayBuffer]",X="[object Float32Array]",H="[object Float64Array]",Q="[object Int8Array]",nt="[object Int16Array]",tt="[object Int32Array]",rt="[object Uint8Array]",et="[object Uint8ClampedArray]",ut="[object Uint16Array]",ot="[object Uint32Array]",it=/\b__p\+='';/g,at=/\b(__p\+=)''\+/g,ft=/(__e\(.*?\)|\b__t\))\+'';/g,ct=/&(?:amp|lt|gt|quot|#39|#96);/g,lt=/[&<>"'`]/g,st=RegExp(ct.source),pt=RegExp(lt.source),ht=/<%-([\s\S]+?)%>/g,gt=/<%([\s\S]+?)%>/g,vt=/<%=([\s\S]+?)%>/g,dt=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,yt=/\w*$/,_t=/^\s*function[ \n\r\t]+\w/,mt=/^0[xX]/,bt=/^\[object .+?Constructor\]$/,wt=/[\xc0-\xd6\xd8-\xde\xdf-\xf6\xf8-\xff]/g,xt=/($^)/,At=/[.*+?^${}()|[\]\/\\]/g,jt=RegExp(At.source),kt=/\bthis\b/,Et=/['\n\r\u2028\u2029\\]/g,Rt=RegExp("[A-Z\\xc0-\\xd6\\xd8-\\xde]{2,}(?=[A-Z\\xc0-\\xd6\\xd8-\\xde][a-z\\xdf-\\xf6\\xf8-\\xff]+)|[A-Z\\xc0-\\xd6\\xd8-\\xde]?[a-z\\xdf-\\xf6\\xf8-\\xff]+|[A-Z\\xc0-\\xd6\\xd8-\\xde]+|[0-9]+","g"),It=" \t\x0b\f\xa0\ufeff\n\r\u2028\u2029\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000",Ot="Array ArrayBuffer Date Error Float32Array Float64Array Function Int8Array Int16Array Int32Array Math Number Object RegExp Set String _ clearTimeout document isFinite parseInt setTimeout TypeError Uint8Array Uint8ClampedArray Uint16Array Uint32Array WeakMap window WinRTError".split(" "),Ct={};
Ct[X]=Ct[H]=Ct[Q]=Ct[nt]=Ct[tt]=Ct[rt]=Ct[et]=Ct[ut]=Ct[ot]=true,Ct[z]=Ct[D]=Ct[J]=Ct[M]=Ct[q]=Ct[P]=Ct[K]=Ct["[object Map]"]=Ct[V]=Ct[Y]=Ct[Z]=Ct["[object Set]"]=Ct[G]=Ct["[object WeakMap]"]=false;var Tt={};Tt[z]=Tt[D]=Tt[J]=Tt[M]=Tt[q]=Tt[X]=Tt[H]=Tt[Q]=Tt[nt]=Tt[tt]=Tt[V]=Tt[Y]=Tt[Z]=Tt[G]=Tt[rt]=Tt[et]=Tt[ut]=Tt[ot]=true,Tt[P]=Tt[K]=Tt["[object Map]"]=Tt["[object Set]"]=Tt["[object WeakMap]"]=false;var St={leading:false,maxWait:0,trailing:false},Wt={"\xc0":"A","\xc1":"A","\xc2":"A","\xc3":"A","\xc4":"A","\xc5":"A","\xe0":"a","\xe1":"a","\xe2":"a","\xe3":"a","\xe4":"a","\xe5":"a","\xc7":"C","\xe7":"c","\xd0":"D","\xf0":"d","\xc8":"E","\xc9":"E","\xca":"E","\xcb":"E","\xe8":"e","\xe9":"e","\xea":"e","\xeb":"e","\xcc":"I","\xcd":"I","\xce":"I","\xcf":"I","\xec":"i","\xed":"i","\xee":"i","\xef":"i","\xd1":"N","\xf1":"n","\xd2":"O","\xd3":"O","\xd4":"O","\xd5":"O","\xd6":"O","\xd8":"O","\xf2":"o","\xf3":"o","\xf4":"o","\xf5":"o","\xf6":"o","\xf8":"o","\xd9":"U","\xda":"U","\xdb":"U","\xdc":"U","\xf9":"u","\xfa":"u","\xfb":"u","\xfc":"u","\xdd":"Y","\xfd":"y","\xff":"y","\xc6":"Ae","\xe6":"ae","\xde":"Th","\xfe":"th","\xdf":"ss"},Nt={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","`":"&#96;"},Ft={"&amp;":"&","&lt;":"<","&gt;":">","&quot;":'"',"&#39;":"'","&#96;":"`"},Ut={"function":true,object:true},Lt={"\\":"\\","'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},$t=Ut[typeof window]&&window!==(this&&this.window)?window:this,Bt=Ut[typeof exports]&&exports&&!exports.nodeType&&exports,Ut=Ut[typeof module]&&module&&!module.nodeType&&module,zt=Bt&&Ut&&typeof global=="object"&&global;
!zt||zt.global!==zt&&zt.window!==zt&&zt.self!==zt||($t=zt);var zt=Ut&&Ut.exports===Bt&&Bt,Dt=m();typeof define=="function"&&typeof define.amd=="object"&&define.amd?($t._=Dt, define(function(){return Dt})):Bt&&Ut?zt?(Ut.exports=Dt)._=Dt:Bt._=Dt:$t._=Dt}).call(this);

2112
static/js/libs/require/require.js

File diff suppressed because it is too large Load Diff

59
static/js/mediastream/api.js

@ -22,9 +22,6 @@
"use strict"; "use strict";
define(['jquery', 'underscore', 'ua-parser'], function($, _, uaparser) { define(['jquery', 'underscore', 'ua-parser'], function($, _, uaparser) {
var alive_check_timeout = 5000;
var alive_check_timeout_2 = 10000;
var Api = function(version, connector) { var Api = function(version, connector) {
this.version = version; this.version = version;
this.id = null; this.id = null;
@ -48,35 +45,39 @@ define(['jquery', 'underscore', 'ua-parser'], function($, _, uaparser) {
this.received(data); this.received(data);
}, this)); }, this));
// Trigger alive heartbeat when nothing is received for a while. // Heartbeat support.
this.heartbeat = window.setInterval(_.bind(function() { this.last_receive = null;
var last_receive = this.last_receive; this.last_receive_overdue = false;
if (this.connector.connected) {
if (last_receive !== null) { };
//console.log("api heartbeat", this.last_receive);
var now = new Date().getTime(); Api.prototype.heartbeat = function(timeout, timeout2) {
if (this.last_receive_overdue) {
if (now > last_receive + alive_check_timeout_2) { // Heartbeat emitter.
console.log("Reconnecting because alive timeout was reached."); var last_receive = this.last_receive;
this.last_receive_overdue = false; if (this.connector.connected) {
this.last_receive = null; if (last_receive !== null) {
this.connector.disconnect(true); //console.log("api heartbeat", this.last_receive);
} var now = new Date().getTime();
} else { if (this.last_receive_overdue) {
if (now > last_receive + alive_check_timeout) { if (now > last_receive + timeout2) {
//console.log("overdue 1"); console.log("Reconnecting because alive timeout was reached.");
this.last_receive_overdue = true; this.last_receive_overdue = false;
this.sendAlive(now); this.last_receive = null;
} this.connector.disconnect(true);
}
} else {
if (now > last_receive + timeout) {
//console.log("overdue 1");
this.last_receive_overdue = true;
this.sendAlive(now);
} }
} }
} else {
this.last_receive = null;
this.last_receive_overdue = false;
} }
}, this), 1000); } else {
this.last_receive = null; this.last_receive = null;
this.last_receive_overdue = false; this.last_receive_overdue = false;
}
}; };

7
static/js/mediastream/webrtc.js

@ -106,13 +106,6 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
this.api.e.bind("received.offer received.candidate received.answer received.bye received.conference", _.bind(this.processReceived, this)); this.api.e.bind("received.offer received.candidate received.answer received.bye received.conference", _.bind(this.processReceived, this));
$(window).on("unload", _.bind(function() {
this.doHangup("unload");
if (this.api.connector) {
this.api.connector.disabled = true;
}
}, this));
// Create default media (audio/video). // Create default media (audio/video).
this.usermedia = new UserMedia(); this.usermedia = new UserMedia();
this.usermedia.e.on("mediasuccess mediaerror", _.bind(function() { this.usermedia.e.on("mediasuccess mediaerror", _.bind(function() {

7
static/js/services/appdata.js

@ -40,8 +40,9 @@ define(["jquery"], function($) {
// - mainStatus(event, status) // - mainStatus(event, status)
// status (string) : Status id (connected, waiting, ...) // status (string) : Status id (connected, waiting, ...)
// //
// - authorizing(event, flag) // - authorizing(event, flag, userid)
// flag (bool) : True if authorizing phase, else false. // flag (bool) : True if authorizing phase, else false.
// userid (string) : User id if a user was authorized.
// //
// - userSettingsLoaded(event, loaded, user_settings) // - userSettingsLoaded(event, loaded, user_settings)
// loaded (bool) : True if something was loaded, else false. // loaded (bool) : True if something was loaded, else false.
@ -74,13 +75,13 @@ define(["jquery"], function($) {
service.data = d; service.data = d;
return d; return d;
}; };
service.authorizing = function(value) { service.authorizing = function(value, userid) {
// Boolean flag to indicate that an authentication is currently in progress. // Boolean flag to indicate that an authentication is currently in progress.
if (typeof(value) !== "undefined") { if (typeof(value) !== "undefined") {
var v = !!value; var v = !!value;
if (v !== service.flags.authorizing) { if (v !== service.flags.authorizing) {
service.flags.authorizing = v; service.flags.authorizing = v;
service.e.triggerHandler("authorizing", v); service.e.triggerHandler("authorizing", v, userid);
} }
} }
return service.flags.authorizing; return service.flags.authorizing;

6
static/js/services/buddylist.js

@ -383,6 +383,10 @@ define(['jquery', 'angular', 'underscore', 'modernizr', 'avltree', 'text!partial
Buddylist.prototype.setDisplay = function(id, scope, data, queueName) { Buddylist.prototype.setDisplay = function(id, scope, data, queueName) {
var status = data.Status; var status = data.Status;
if (!status) {
status = {}; // Make sure to show buddies which never set a status.
}
var display = scope.display; var display = scope.display;
// Set display.name. // Set display.name.
display.displayName = status.displayName; display.displayName = status.displayName;
@ -501,7 +505,7 @@ define(['jquery', 'angular', 'underscore', 'modernizr', 'avltree', 'text!partial
scope = newscope; scope = newscope;
} }
}, this)); }, this));
if (sessionData && sessionData.Status) { if (sessionData) {
this.setDisplay(id, scope, sessionData, "joined"); this.setDisplay(id, scope, sessionData, "joined");
} else if (!noApply) { } else if (!noApply) {
scope.$apply(); scope.$apply();

14
static/js/services/rooms.js

@ -34,13 +34,15 @@ define([
var helloedRoomName = null; var helloedRoomName = null;
var currentRoom = null; var currentRoom = null;
var randomRoom = null; var randomRoom = null;
var canCreateRooms = !mediaStream.config.AuthorizeRoomCreation; var canJoinRooms = !mediaStream.config.AuthorizeRoomJoin;
var canCreateRooms = canJoinRooms ? !mediaStream.config.AuthorizeRoomCreation : false;
var joinFailed = function(error) { var joinFailed = function(error) {
setCurrentRoom(null); setCurrentRoom(null);
switch(error.Code) { switch(error.Code) {
case "default_room_disabled": case "default_room_disabled":
priorRoomName = null;
rooms.randomRoom(); rooms.randomRoom();
break; break;
case "invalid_credentials": case "invalid_credentials":
@ -143,10 +145,11 @@ define([
appData.e.on("selfReceived", function(event, data) { appData.e.on("selfReceived", function(event, data) {
_.defer(joinRequestedRoom); _.defer(joinRequestedRoom);
if (mediaStream.config.AuthorizeRoomCreation && !$rootScope.myuserid) { canJoinRooms = (!mediaStream.config.AuthorizeRoomJoin || $rootScope.myuserid) ? true : false
canCreateRooms = false; if (canJoinRooms) {
canCreateRooms = (!mediaStream.config.AuthorizeRoomCreation || $rootScope.myuserid) ? true : false;
} else { } else {
canCreateRooms = true; canCreateRooms = false;
} }
}); });
@ -205,6 +208,9 @@ define([
canCreateRooms: function() { canCreateRooms: function() {
return canCreateRooms; return canCreateRooms;
}, },
canJoinRooms: function() {
return canJoinRooms;
},
joinByName: function(name, replace) { joinByName: function(name, replace) {
name = $window.encodeURIComponent(name); name = $window.encodeURIComponent(name);
name = name.replace(/^%40/, "@"); name = name.replace(/^%40/, "@");

10
static/partials/menu.html

@ -0,0 +1,10 @@
<span>
<button ng-if="withModule('youtube')" title="{{_('Share a YouTube video')}}" class="btn aenablebtn btn-youtubevideo" ng-show="status=='connected' || status=='conference' || layout.youtubevideo" ng-model="layout.youtubevideo" btn-checkbox><i class="fa fa-youtube"></i></button>
<button ng-if="withModule('presentation')" title="{{_('Share a file as presentation')}}" class="btn aenablebtn btn-presentation" ng-show="status=='connected' || status=='conference' || layout.presentation" ng-model="layout.presentation" btn-checkbox><i class="fa fa-folder-open-o"></i></button>
<button ng-if="withModule('screensharing')" title="{{_('Share your screen')}}" class="btn aenablebtn btn-screenshare" ng-disabled="!supported.screensharing" ng-show="status=='connected' || status=='conference' || layout.screenshare" ng-model="layout.screenshare" btn-checkbox><i class="fa fa-desktop"></i></button>
<button title="{{_('Chat')}}" class="btn btn-chat" ng-class="{messagesunseen: chatMessagesUnseen>0}" ng-model="layout.chat" btn-checkbox btn-checkbox-true="true" btn-checkbox-false="false"><i class="fa fa-comments-o"></i><span class="badge" ng-show="chatMessagesUnseen" ng-bind="chatMessagesUnseen"></span></button>
<button ng-show="myid && myuserid" title="{{_('Contacts')}}" class="btn btn-contacts" ng-click="openContactsManager()"><i class="fa fa-sitemap"></i></button>
<button title="{{_('Mute microphone')}}" class="btn btn-mutemicrophone amutebtn" ng-model="microphoneMute" btn-checkbox btn-checkbox-true="true" btn-checkbox-false="false"><i class="fa"></i></button>
<button title="{{_('Turn camera off')}}" class="btn btn-mutecamera amutebtn" ng-model="cameraMute" btn-checkbox btn-checkbox-true="true" btn-checkbox-false="false"><i class="fa"></i></button>
<button title="{{_('Settings')}}" class="btn btn-settings" ng-model="layout.settings" btn-checkbox btn-checkbox-true="true" btn-checkbox-false="false"><i class="fa fa-cog"></i></button>
</span>

10
static/partials/page/welcome.html

@ -1,6 +1,6 @@
<div welcome ng-form="welcome" class="welcome container-fluid" ng-show="!peer"> <div welcome ng-form="welcome" class="welcome container-fluid" ng-show="!peer" ng-controller="UsersettingsController as usersettings">
<div class="welcome-logo"></div> <div class="welcome-logo"></div>
<div class="welcome-container" ng-controller="UsersettingsController as usersettings"> <div ng-if="canJoinRooms()" class="welcome-container">
<h1>{{_("Enter a room name")}}</h1> <h1>{{_("Enter a room name")}}</h1>
<div class="form-group welcome-input" ng-class="{'has-error': welcome.$invalid, 'has-success': welcome.$valid && roomdata.name}"> <div class="form-group welcome-input" ng-class="{'has-error': welcome.$invalid, 'has-success': welcome.$valid && roomdata.name}">
<input type="text" class="form-control roomdata-link-input input-lg" ng-model="roomdataInput.name" ng-maxlength="30" on-enter="joinRoomByName(roomdata.name)" placeholder="{{roomdata.placeholder}}"> <input type="text" class="form-control roomdata-link-input input-lg" ng-model="roomdataInput.name" ng-maxlength="30" on-enter="joinRoomByName(roomdata.name)" placeholder="{{roomdata.placeholder}}">
@ -23,4 +23,10 @@
<a ng-repeat="room in roomsHistory" ng-click="joinRoomByName(room)"><span class="label label-default">{{room}}</span></a> <a ng-repeat="room in roomsHistory" ng-click="joinRoomByName(room)"><span class="label label-default">{{room}}</span></a>
</p> </p>
</div> </div>
<div ng-if="!canJoinRooms()" class="welcome-container">
<div class="welcome-login">
<p class="h4">{{_("Please sign in.")}}</p>
<button ng-if="!myuserid && usersettings.loginUserid" type="button" class="btn btn-primary" ng-click="usersettings.loginUserid()">{{_('Sign in')}}</button>
</div>
</div>
</div> </div>

2
static/translation/messages-de.json

File diff suppressed because one or more lines are too long

2
static/translation/messages-ja.json

File diff suppressed because one or more lines are too long

2
static/translation/messages-ko.json

File diff suppressed because one or more lines are too long

2
static/translation/messages-zh-cn.json

File diff suppressed because one or more lines are too long

2
static/translation/messages-zh-tw.json

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save