Browse Source

Merge branch 'release-0.23'

Conflicts:
	.jshint
pull/175/head v0.23.0
Simon Eisenmann 11 years ago
parent
commit
2bdbdc09ee
  1. 27
      .jshint
  2. 28
      debian/changelog
  3. 119
      doc/CHANNELING-API.txt
  4. 39
      doc/plugin-example.js
  5. 2
      doc/plugin-test-authorize.js
  6. 1
      html/head.html
  7. 4
      html/main.html
  8. 15
      server.conf.in
  9. 18
      src/app/spreed-webrtc-server/buffercache.go
  10. 38
      src/app/spreed-webrtc-server/channeling.go
  11. 284
      src/app/spreed-webrtc-server/channelling_api.go
  12. 240
      src/app/spreed-webrtc-server/channelling_api_test.go
  13. 87
      src/app/spreed-webrtc-server/client.go
  14. 43
      src/app/spreed-webrtc-server/common_test.go
  15. 121
      src/app/spreed-webrtc-server/config.go
  16. 164
      src/app/spreed-webrtc-server/connection.go
  17. 1
      src/app/spreed-webrtc-server/context.go
  18. 566
      src/app/spreed-webrtc-server/hub.go
  19. 69
      src/app/spreed-webrtc-server/incoming_codec.go
  20. 181
      src/app/spreed-webrtc-server/main.go
  21. 201
      src/app/spreed-webrtc-server/room_manager.go
  22. 79
      src/app/spreed-webrtc-server/room_manager_test.go
  23. 189
      src/app/spreed-webrtc-server/roomworker.go
  24. 124
      src/app/spreed-webrtc-server/roomworker_test.go
  25. 280
      src/app/spreed-webrtc-server/server.go
  26. 35
      src/app/spreed-webrtc-server/session.go
  27. 173
      src/app/spreed-webrtc-server/session_manager.go
  28. 13
      src/app/spreed-webrtc-server/sessions.go
  29. 8
      src/app/spreed-webrtc-server/stats.go
  30. 104
      src/app/spreed-webrtc-server/stats_manager.go
  31. 130
      src/app/spreed-webrtc-server/tickets.go
  32. 32
      src/app/spreed-webrtc-server/users.go
  33. 35
      src/app/spreed-webrtc-server/ws.go
  34. 1
      src/i18n/helpers/languages.py
  35. 127
      src/i18n/messages-de.po
  36. 111
      src/i18n/messages-ja.po
  37. 111
      src/i18n/messages-ko.po
  38. 111
      src/i18n/messages-zh-cn.po
  39. 111
      src/i18n/messages-zh-tw.po
  40. 93
      src/i18n/messages.pot
  41. 2
      src/styles/Makefile.am
  42. 49
      src/styles/_shame.scss
  43. 35
      src/styles/components/_audiovideo.scss
  44. 2
      src/styles/components/_buddylist.scss
  45. 5
      src/styles/components/_contactsmanager.scss
  46. 3
      src/styles/components/_social.scss
  47. 5
      src/styles/csp.scss
  48. 4
      src/styles/global/_overlaybar.scss
  49. 40
      src/styles/global/_pages.scss
  50. 3
      src/styles/global/_variables.scss
  51. 91
      src/styles/global/skins/_dark.scss
  52. 6
      src/styles/libs/_dialogs.scss
  53. 1
      src/styles/libs/_libs.scss
  54. 41
      src/styles/libs/angular/angular-csp.scss
  55. 0
      src/styles/libs/angular/angular.scss
  56. 44
      src/styles/libs/toastr/toastr.scss
  57. 1
      src/styles/main.scss
  58. 20
      static/css/csp.min.css
  59. 2
      static/css/main.min.css
  60. 59
      static/js/app.js
  61. 5
      static/js/base.js
  62. 14
      static/js/controllers/chatroomcontroller.js
  63. 25
      static/js/controllers/contactsmanagercontroller.js
  64. 2
      static/js/controllers/contactsmanagereditcontroller.js
  65. 6
      static/js/controllers/controllers.js
  66. 257
      static/js/controllers/mediastreamcontroller.js
  67. 90
      static/js/controllers/roomchangecontroller.js
  68. 2
      static/js/controllers/statusmessagecontroller.js
  69. 11
      static/js/controllers/usersettingscontroller.js
  70. 2
      static/js/directives/audiolevel.js
  71. 283
      static/js/directives/audiovideo.js
  72. 2
      static/js/directives/bfi.js
  73. 65
      static/js/directives/buddylist.js
  74. 14
      static/js/directives/buddypicturecapture.js
  75. 46
      static/js/directives/buddypictureupload.js
  76. 40
      static/js/directives/chat.js
  77. 2
      static/js/directives/contactrequest.js
  78. 2
      static/js/directives/defaultdialog.js
  79. 12
      static/js/directives/directives.js
  80. 4
      static/js/directives/fileinfo.js
  81. 2
      static/js/directives/odfcanvas.js
  82. 2
      static/js/directives/onenter.js
  83. 2
      static/js/directives/onescape.js
  84. 52
      static/js/directives/page.js
  85. 2
      static/js/directives/pdfcanvas.js
  86. 14
      static/js/directives/presentation.js
  87. 55
      static/js/directives/roombar.js
  88. 9
      static/js/directives/screenshare.js
  89. 160
      static/js/directives/settings.js
  90. 22
      static/js/directives/socialshare.js
  91. 2
      static/js/directives/statusmessage.js
  92. 50
      static/js/directives/title.js
  93. 40
      static/js/directives/usability.js
  94. 92
      static/js/directives/welcome.js
  95. 31
      static/js/directives/youtubevideo.js
  96. 2
      static/js/filters/buddyimagesrc.js
  97. 2
      static/js/filters/displayconference.js
  98. 2
      static/js/filters/displayname.js
  99. 2
      static/js/filters/displaynameforsession.js
  100. 2
      static/js/filters/displayuserid.js
  101. Some files were not shown because too many files have changed in this diff Show More

27
.jshint

@ -1,10 +1,27 @@ @@ -1,10 +1,27 @@
{
"smarttabs": true,
"onecase": true,
"asi": true,
"bitwise": false,
"browser": true,
"camelcase": false, // Disabled for now.
"curly": true,
"forin": true,
"trailing": true,
"asi": true,
"immed": true,
"latedef": true,
"maxlen": 1000,
"quotmark": false
"newcap": true,
"noarg": true,
"noempty": false, // Disabled for now.
"nonew": true,
"onecase": true,
"predef": [
"define",
"require",
"console"
],
"quotmark": false,
"smarttabs": true,
"trailing": true,
"undef": true,
"unused": false, // Disabled for now
"globalstrict": true
}

28
debian/changelog vendored

@ -1,3 +1,31 @@ @@ -1,3 +1,31 @@
spreed-webrtc-server (0.23.0) precise; urgency=low
* Added support for renegotation in web client (disabled).
* Rooms were refactored to be able to confirm joins.
* Added support to PIN lock rooms (server side).
* Updated javascript to follow now jshin rules.
* Updated plugin API to make the main App object available.
* Refactored server side configuration loading.
* Improved usability of image upload positioning and scaling.
* Stream lined third party javascript to reduce size.
* Javascript is now using 'strict' mode everywhere.
* Added suppport for Content Security Policy (CSP).
* Added Javascript source mappings where missing.
* Fixed bye handling in conferences to avoid endless dial tones.
* Added support for audio and/or video only connections when
the corresponding device is not there.
* Several icons were changed for usability reasons.
* Improved dialogs and texts for usability reasons.
* Room bar is now automatically visible when not in a call.
* Updated auto focus behavior of room select forms.
* Implemented a room history on welcome screen.
* Added a sign in button to the top bar.
* Changed order of settings form for usability reasons.
* Missed call toast now always is shown.
* Improved toast notification styles.
-- Simon Eisenmann <simon@struktur.de> Tue, 09 Dec 2014 15:45:52 +0100
spreed-webrtc-server (0.22.8) precise; urgency=low
* Removed opacity transition from chat pane to avoid compositing issues.

119
doc/CHANNELING-API.txt

@ -87,6 +87,16 @@ Sending vs receiving document data encapsulation @@ -87,6 +87,16 @@ Sending vs receiving document data encapsulation
A : Session attestation token. Only available for incoming data
created by other sessions (optional).
Error returns
Calls providing an Iid which fail will receive an Error document with the
following format:
{
"Type": "Error",
"Code": "value_identifying_error",
"Message": "A description of the error condition"
}
Special purpose documents for channling
@ -147,18 +157,115 @@ Special purpose documents for channling @@ -147,18 +157,115 @@ Special purpose documents for channling
Hello: {
Version: "1.0.0",
Ua: "Test client 1.0",
Id: ""
Id: "",
"Credentials": {...}
}
}
Hello document is to be send by the client after connection was
established.
Hello document is to be send by the client after connection was established.
If an Iid is provided, a Welcome document will be returned if joining the
room with the given Id succeeds. Otherwise an Error document with one of the
error codes listed below will be returned. Note that any previous room will
have been left regardless of whether the response is successful.
Keys under Hello:
Version : Channel protocol version (string).
Ua : User agent description (string).
Id : Room id. The default Room has the empty string Id ("") (string).
Version : Channel protocol version (string).
Ua : User agent description (string).
Id : Room id. The default Room has the empty string Id ("") (string).
Credentials : An optional RoomCredentials document containing room
authentication information. See the Room document for
information on how such credentials should be handled after
a Welcome is received for the requested room. Note that
providing credentials while joining an existing room which
does not require them is an error, such requests should be
retried without credentials. In contrast, joining a
non-existent room with credentials will create the room
using the given credentials. Note that an error with a code
of authorization_not_required or invalid_credentials shall
cause the client to discard any cached room credentials.
Error codes:
default_room_disabled : Joining the room "" is not allowed by this
server.
authorization_required : Joining the given room requires credentials.
authorization_not_required : No credentials should be provided for this
room.
invalid_credentials : The provided credentials are incorrect.
room_join_requires_account : Server configuration requires an
authenticated user account to join this room.
Welcome
{
"Type": "Welcome",
"Welcome": {
"Room": {...},
"Users": []
}
}
Welcome is sent in reply to a successful Hello, and contains all data
needed to set up the initial room connection.
Keys under Welcome:
Room: Contains the current state of the room, see the description of
the Room document for more details.
Users: Contains the user list for the room, see the description of
the Users document for more details.
RoomCredentials
{
"PIN": "my-super-sekrit-code"
}
RoomCredentials contains room authentication information, and is used as a
child document when joining or updating a room.
Keys under RoomCredentials:
PIN : A password string which may be used by clients to authenticate
themselves. Note that acceptable characters for this field may be
constrained by the server based upon its configuration.
Room
{
"Type": "Room",
"Name": "room-name-here"
"Credentials": {...}
}
Clients may send a Room document in order to update all room properties
to the values given in the document. The room name must be given and match
the currently joined room. Successful updates will receive an updated Room
document as a reply, or an Error document if the update fails.
Addtionally, the Room document is included in responses to initial joins
and broadcast when room properties are updated.
Keys under Room:
Name : The human readable ID of the room, currently must be globally
unique.
Credentials : Optional authentication information for the room, see the
documentation of the RoomCredentials document for more
details. This field shall only be present when sending or
receiving an update which alters room authentication data.
It should only be inferred that authentication is not
required if joining a room succeeds without credentials and
no updates containing credentials has been received.
Authentication may be disabled by sending a Room document
containing a RoomCredentials document with only empty
fields. Clients shall discard any cached authentication
information upon receiving such an update.
Error codes:
not_in_room : Clients may only update rooms which they have joined.
Peer connection documents

39
doc/plugin-example.js

@ -18,17 +18,24 @@ @@ -18,17 +18,24 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define(['angular'], function(angular) {
return {
// Angular modules - either overwrite existing modules or add new
// ones. All app dependencies are already loaded so modules defined
// here will be used instead of the default ones.
// To load add a new module to the angular app, append the name of
// the module to the modules array.
// Defining a module in a plugin is optional.
module: function(modules) {
/**
* Modules function for this plugin. Use it to add Angular modules.
* either overwrite existing modules or add new ones. All app dependencies
* are already loaded so modules defined here will be used instead of
* the default ones. Defining the module function on a plugin is optional.
*
* @param {array} modules - To add a new module to the angular app, append
* the name of the module to the modules array.
* @param {Object} launcher - The launcher object containg $q and $http.
*
*/
module: function(modules, launcher) {
// Create and add a new module.
// See http://docs.angularjs.org/guide/module for details on Angular.
@ -61,12 +68,18 @@ define(['angular'], function(angular) { @@ -61,12 +68,18 @@ define(['angular'], function(angular) {
},
// Initialize function for this module.
// Add your angular stuff here. The app is the base Angular module for
// the web application. The plugin initialize function is called after
// the app initialize function.
// Defining the initialize function is optional.
initialize: function(app) {
/**
* Initialize function for this plugin. Use this function to add your
* your Angular stuff to the app. The plugin initializse function is
* called after the app initialize function.
*
* @param {App} app - The base Angular module for the web application.
* @param {Object} launcher - The launcher object containg $q and $http.
* @returns {($q.promise|null)} - Return a $q.promise when you need the
* launcher to wait.
*
*/
initialize: function(app, launcher) {
console.log("Initializing plugin-example ...");

2
doc/plugin-test-authorize.js

@ -18,6 +18,8 @@ @@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define(['angular', 'sjcl'], function(angular, sjcl) {
return {

1
html/head.html

@ -5,6 +5,7 @@ @@ -5,6 +5,7 @@
<meta name="mobile-web-app-capable" content="yes">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<base href="<%.Cfg.B%>">
<%if.Csp%><link rel="stylesheet" type="text/css" href="<%.Cfg.S%>/css/csp.min.css"><%end%>
<link rel="stylesheet" type="text/css" href="<%.Cfg.S%>/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="<%.Cfg.S%>/css/font-awesome.min.css">
<link rel="stylesheet" type="text/css" href="<%.Cfg.S%>/css/main.min.css">

4
html/main.html

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
<%define "mainPage"%><!doctype html>
<html class="no-js">
<html class="no-js"<%if.Csp%> ng-csp<%end%>>
<head>
<%template "head" .%>
</head>
@ -26,7 +26,7 @@ @@ -26,7 +26,7 @@
<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-bookmark-o"></i></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>

15
server.conf.in

@ -78,6 +78,9 @@ encryptionSecret = tne-default-encryption-block-key @@ -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
; form will be shown instead.
;defaultRoomEnabled = true
; Whether a user account is required to create a room. This only has an effect
; if user accounts are enabled. Optional, defaults to false.
;authorizeRoomCreation = false
; Server token is a public random string which is used to enhance security of
; server generated security tokens. When the serverToken is changed all existing
; nonces become invalid. Use 32 or 64 characters (eg. 16 or 32 byte hex).
@ -98,6 +101,18 @@ serverRealm = local @@ -98,6 +101,18 @@ serverRealm = local
; a front end webserver. Check the doc folder for more info about plugins and
; examples.
;plugin = extra/static/myplugin.js
; Content-Security-Policy HTTP response header value.
; Spreed WebRTC requires inline styles, WebSocket connection to itself and
; data: URL for images.
; The currently recommended CSP is:
; default-src 'self';
; style-src 'self' 'unsafe-inline';
; img-src 'self' data:;
; connect-src 'self' wss://server:port/ws;
;contentSecurityPolicy =
; Content-Security-Policy-Report-Only HTTP response header value. Use this
; to test your CSP before putting it into production.
;contentSecurityPolicyReportOnly =
[log]
;logfile = /var/log/spreed-webrtc-server.log

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

@ -160,3 +160,21 @@ func (cache *bufferCache) New() Buffer { @@ -160,3 +160,21 @@ func (cache *bufferCache) New() Buffer {
func (cache *bufferCache) Wrap(data []byte) Buffer {
return &directBuffer{refcnt: 1, cache: cache, buf: bytes.NewBuffer(data)}
}
func readAll(dest Buffer, r io.Reader) error {
var err error
defer func() {
e := recover()
if e == nil {
return
}
if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge {
err = panicErr
} else {
panic(e)
}
}()
_, err = dest.ReadFrom(r)
return err
}

38
src/app/spreed-webrtc-server/channeling.go

@ -21,10 +21,41 @@ @@ -21,10 +21,41 @@
package main
type DataError struct {
Type string
Code string
Message string
}
func NewDataError(code, message string) error {
return &DataError{"Error", code, message}
}
func (err *DataError) Error() string {
return err.Message
}
type DataRoomCredentials struct {
PIN string
}
type DataHello struct {
Version string
Ua string
Id string
Version string
Ua string
Id string
Credentials *DataRoomCredentials
}
type DataWelcome struct {
Type string
Room *DataRoom
Users []*DataSession
}
type DataRoom struct {
Type string
Name string
Credentials *DataRoomCredentials
}
type DataOffer struct {
@ -159,6 +190,7 @@ type DataIncoming struct { @@ -159,6 +190,7 @@ type DataIncoming struct {
Alive *DataAlive
Authentication *DataAuthentication
Sessions *DataSessions
Room *DataRoom
Iid string `json:",omitempty"`
}

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

@ -0,0 +1,284 @@ @@ -0,0 +1,284 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 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/>.
*
*/
package main
import (
"log"
"strings"
"time"
)
const (
maxConferenceSize = 100
)
type ChannellingAPI interface {
OnConnect(Client, *Session)
OnIncoming(ResponseSender, *Session, *DataIncoming)
OnDisconnect(*Session)
}
type channellingAPI struct {
*Config
RoomStatusManager
SessionEncoder
SessionManager
StatsCounter
ContactManager
TurnDataCreator
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 {
return &channellingAPI{
config,
roomStatus,
sessionEncoder,
sessionManager,
statsCounter,
contactManager,
turnDataCreator,
unicaster,
broadcaster,
buddyImages,
}
}
func (api *channellingAPI) OnConnect(client Client, session *Session) {
api.Unicaster.OnConnect(client, session)
api.SendSelf(client, session)
}
func (api *channellingAPI) OnIncoming(c ResponseSender, session *Session, msg *DataIncoming) {
switch msg.Type {
case "Self":
api.SendSelf(c, session)
case "Hello":
//log.Println("Hello", msg.Hello, c.Index())
// TODO(longsleep): Filter room id and user agent.
api.UpdateSession(session, &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"))
}
// NOTE(lcooper): Iid filtered for compatibility's sake.
// Evaluate sending unconditionally when supported by all clients.
if room, err := api.JoinRoom(msg.Hello.Id, msg.Hello.Credentials, session, c); err == nil {
session.Hello = true
session.Roomid = msg.Hello.Id
if msg.Iid != "" {
c.Reply(msg.Iid, &DataWelcome{
Type: "Welcome",
Room: room,
Users: api.RoomUsers(session),
})
}
api.Broadcast(session, session.DataSessionJoined())
} else {
session.Hello = false
if msg.Iid != "" {
c.Reply(msg.Iid, err)
}
}
case "Offer":
// TODO(longsleep): Validate offer
api.Unicast(session, msg.Offer.To, msg.Offer)
case "Candidate":
// TODO(longsleep): Validate candidate
api.Unicast(session, msg.Candidate.To, msg.Candidate)
case "Answer":
// TODO(longsleep): Validate Answer
api.Unicast(session, msg.Answer.To, msg.Answer)
case "Users":
if session.Hello {
sessions := &DataSessions{Type: "Users", Users: api.RoomUsers(session)}
c.Reply(msg.Iid, sessions)
}
case "Authentication":
st := msg.Authentication.Authentication
if st == nil {
return
}
if err := api.Authenticate(session, st, ""); err == nil {
log.Println("Authentication success", session.Userid)
api.SendSelf(c, session)
api.BroadcastSessionStatus(session)
} else {
log.Println("Authentication failed", err, st.Userid, st.Nonce)
}
case "Bye":
api.Unicast(session, msg.Bye.To, msg.Bye)
case "Status":
//log.Println("Status", msg.Status)
api.UpdateSession(session, &SessionUpdate{Types: []string{"Status"}, Status: msg.Status.Status})
api.BroadcastSessionStatus(session)
case "Chat":
// TODO(longsleep): Limit sent chat messages per incoming connection.
if !msg.Chat.Chat.NoEcho {
api.Unicast(session, session.Id, msg.Chat)
}
msg.Chat.Chat.Time = time.Now().Format(time.RFC3339)
if msg.Chat.To == "" {
// TODO(longsleep): Check if chat broadcast is allowed.
if session.Hello {
api.CountBroadcastChat()
api.Broadcast(session, msg.Chat)
}
} else {
if msg.Chat.Chat.Status != nil && msg.Chat.Chat.Status.ContactRequest != nil {
if err := api.contactrequestHandler(session, msg.Chat.To, msg.Chat.Chat.Status.ContactRequest); err != nil {
log.Println("Ignoring invalid contact request.", err)
return
}
msg.Chat.Chat.Status.ContactRequest.Userid = session.Userid()
}
if msg.Chat.Chat.Status == nil {
api.CountUnicastChat()
}
api.Unicast(session, msg.Chat.To, msg.Chat)
if msg.Chat.Chat.Mid != "" {
// 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"}}})
}
}
case "Conference":
// Check conference maximum size.
if len(msg.Conference.Conference) > maxConferenceSize {
log.Println("Refusing to create conference above limit.", len(msg.Conference.Conference))
} else {
// Send conference update to anyone.
for _, id := range msg.Conference.Conference {
if id != session.Id {
api.Unicast(session, id, msg.Conference)
}
}
}
case "Alive":
c.Reply(msg.Iid, msg.Alive)
case "Sessions":
var users []*DataSession
switch msg.Sessions.Sessions.Type {
case "contact":
if userID, err := api.getContactID(session, msg.Sessions.Sessions.Token); err == nil {
users = api.GetUserSessions(session, userID)
} else {
log.Printf(err.Error())
}
case "session":
id, err := session.attestation.Decode(msg.Sessions.Sessions.Token)
if err != nil {
log.Printf("Failed to decode incoming attestation", err, msg.Sessions.Sessions.Token)
break
}
session, ok := api.GetSession(id)
if !ok {
log.Printf("Cannot retrieve session for id %s", id)
break
}
users = make([]*DataSession, 1, 1)
users[0] = session.Data()
default:
log.Printf("Unkown incoming sessions request type %s", msg.Sessions.Sessions.Type)
}
// TODO(lcooper): We ought to reply with a *DataError here if failed.
if users != nil {
c.Reply(msg.Iid, &DataSessions{Type: "Sessions", Users: users, Sessions: msg.Sessions.Sessions})
}
case "Room":
if room, err := api.UpdateRoom(session, msg.Room); err == nil {
api.Broadcast(session, room)
c.Reply(msg.Iid, room)
} else {
c.Reply(msg.Iid, err)
}
default:
log.Println("OnText unhandled message type", msg.Type)
}
}
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) {
token, err := api.EncodeSessionToken(session)
if err != nil {
log.Println("Error in OnRegister", err)
return
}
log.Println("Created new session token", len(token), token)
self := &DataSelf{
Type: "Self",
Id: session.Id,
Sid: session.Sid,
Userid: session.Userid(),
Suserid: api.EncodeSessionUserID(session),
Token: token,
Version: api.Version,
Turn: api.CreateTurnData(session),
Stun: api.StunURIs,
}
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())
}
}

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

@ -0,0 +1,240 @@ @@ -0,0 +1,240 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 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/>.
*
*/
package main
import (
"errors"
"testing"
)
type fakeClient struct {
replies map[string]interface{}
}
func (fake *fakeClient) Send(_ Buffer) {
}
func (fake *fakeClient) Reply(iid string, msg interface{}) {
if fake.replies == nil {
fake.replies = make(map[string]interface{})
}
fake.replies[iid] = msg
}
type fakeRoomManager struct {
joinedRoomID string
leftRoomID string
roomUsers []*DataSession
joinedID string
joinError error
leftID string
broadcasts []interface{}
updatedRoom *DataRoom
updateError error
}
func (fake *fakeRoomManager) RoomUsers(session *Session) []*DataSession {
return fake.roomUsers
}
func (fake *fakeRoomManager) JoinRoom(id string, _ *DataRoomCredentials, session *Session, _ Sender) (*DataRoom, error) {
fake.joinedID = id
return &DataRoom{Name: id}, fake.joinError
}
func (fake *fakeRoomManager) LeaveRoom(session *Session) {
fake.leftID = session.Roomid
}
func (fake *fakeRoomManager) Broadcast(_ *Session, msg interface{}) {
fake.broadcasts = append(fake.broadcasts, msg)
}
func (fake *fakeRoomManager) UpdateRoom(_ *Session, _ *DataRoom) (*DataRoom, error) {
return fake.updatedRoom, fake.updateError
}
func assertReply(t *testing.T, client *fakeClient, iid string) interface{} {
msg, ok := client.replies[iid]
if !ok {
t.Fatalf("No response received for Iid %v", iid)
}
return msg
}
func assertErrorReply(t *testing.T, client *fakeClient, iid, code string) {
err, ok := assertReply(t, client, iid).(*DataError)
if !ok {
t.Fatalf("Expected response message to be an Error")
}
if err.Type != "Error" {
t.Error("Message did not have the correct type")
}
if err.Code != code {
t.Errorf("Expected error code to be %v, but was %v", code, err.Code)
}
}
func NewTestChannellingAPI() (ChannellingAPI, *fakeClient, *Session, *fakeRoomManager) {
client, roomManager, session := &fakeClient{}, &fakeRoomManager{}, &Session{}
return NewChannellingAPI(nil, roomManager, nil, nil, nil, nil, nil, nil, roomManager, nil), client, session, roomManager
}
func Test_ChannellingAPI_OnIncoming_HelloMessage_JoinsTheSelectedRoom(t *testing.T) {
roomID, ua := "foobar", "unit tests"
api, client, session, roomManager := NewTestChannellingAPI()
api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Hello: &DataHello{Id: roomID, Ua: ua}})
if roomManager.joinedID != roomID {
t.Errorf("Expected to have joined room %v, but got %v", roomID, roomManager.joinedID)
}
if broadcastCount := len(roomManager.broadcasts); broadcastCount != 1 {
t.Fatalf("Expected 1 broadcast, but got %d", broadcastCount)
}
dataSession, ok := roomManager.broadcasts[0].(*DataSession)
if !ok {
t.Fatal("Expected a session data broadcast")
}
if dataSession.Ua != ua {
t.Errorf("Expected to have broadcasted a user agent of %v, but was %v", ua, dataSession.Ua)
}
}
func Test_ChannellingAPI_OnIncoming_HelloMessage_LeavesAnyPreviouslyJoinedRooms(t *testing.T) {
roomID := "foobar"
api, client, session, roomManager := NewTestChannellingAPI()
api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Hello: &DataHello{Id: roomID}})
api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Hello: &DataHello{Id: "baz"}})
if roomManager.leftID != roomID {
t.Errorf("Expected to have left room %v, but got %v", roomID, roomManager.leftID)
}
if broadcastCount := len(roomManager.broadcasts); broadcastCount != 3 {
t.Fatalf("Expected 3 broadcasts, but got %d", broadcastCount)
}
dataSession, ok := roomManager.broadcasts[1].(*DataSession)
if !ok {
t.Fatal("Expected a session data broadcast")
}
if status := "soft"; dataSession.Status != status {
t.Errorf("Expected to have broadcast a leave status of of %v, but was %v", status, dataSession.Status)
}
}
func Test_ChannellingAPI_OnIncoming_HelloMessage_DoesNotJoinIfNotPermitted(t *testing.T) {
api, client, session, roomManager := NewTestChannellingAPI()
roomManager.joinError = errors.New("Can't enter this room")
api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Hello: &DataHello{}})
if broadcastCount := len(roomManager.broadcasts); broadcastCount != 0 {
t.Fatalf("Expected no broadcasts, but got %d", broadcastCount)
}
}
func Test_ChannellingAPI_OnIncoming_HelloMessageWithAnIid_RespondsWithAWelcome(t *testing.T) {
iid, roomID := "foo", "a-room"
api, client, session, roomManager := NewTestChannellingAPI()
roomManager.roomUsers = []*DataSession{&DataSession{}}
api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Iid: iid, Hello: &DataHello{Id: roomID}})
msg, ok := client.replies[iid]
if !ok {
t.Fatalf("No response received for Iid %v", iid)
}
welcome, ok := msg.(*DataWelcome)
if !ok {
t.Fatalf("Expected response message %#v to be a Welcome", msg)
}
if welcome.Type != "Welcome" {
t.Error("Message did not have the correct type")
}
if welcome.Room == nil || welcome.Room.Name != roomID {
t.Errorf("Expected room with name %v, but got %#v", roomID, welcome.Room)
}
if len(welcome.Users) != len(roomManager.roomUsers) {
t.Errorf("Expected to get users %#v, but was %#v", roomManager.roomUsers, welcome.Users)
}
}
func Test_ChannellingAPI_OnIncoming_HelloMessageWithAnIid_RespondsWithAnErrorIfTheRoomCannotBeJoined(t *testing.T) {
iid := "foo"
api, client, session, roomManager := NewTestChannellingAPI()
roomManager.joinError = NewDataError("bad_join", "")
api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Iid: iid, Hello: &DataHello{}})
assertErrorReply(t, client, iid, "bad_join")
}
func Test_ChannellingAPI_OnIncoming_RoomMessage_RespondsWithAndBroadcastsTheUpdatedRoom(t *testing.T) {
iid, roomName := "123", "foo"
api, client, session, roomManager := NewTestChannellingAPI()
roomManager.updatedRoom = &DataRoom{Name: "FOO"}
api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Iid: "0", Hello: &DataHello{Id: roomName}})
api.OnIncoming(client, session, &DataIncoming{Type: "Room", Iid: iid, Room: &DataRoom{Name: roomName}})
room, ok := assertReply(t, client, iid).(*DataRoom)
if !ok {
t.Fatalf("Expected response message to be a Room")
}
if room.Name != roomManager.updatedRoom.Name {
t.Errorf("Expected updated room with name %v, but got %#v", roomManager.updatedRoom, room)
}
if broadcastCount := len(roomManager.broadcasts); broadcastCount != 2 {
t.Fatalf("Expected 1 broadcasts, but got %d", broadcastCount)
}
if _, ok := roomManager.broadcasts[1].(*DataRoom); !ok {
t.Fatal("Expected a room data broadcast")
}
}
func Test_ChannellingAPI_OnIncoming_RoomMessage_RespondsWithAnErrorIfUpdatingTheRoomFails(t *testing.T) {
iid, roomName := "123", "foo"
api, client, session, roomManager := NewTestChannellingAPI()
roomManager.updateError = NewDataError("a_room_error", "")
api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Iid: "0", Hello: &DataHello{Id: roomName}})
api.OnIncoming(client, session, &DataIncoming{Type: "Room", Iid: iid, Room: &DataRoom{Name: roomName}})
assertErrorReply(t, client, iid, "a_room_error")
}

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

@ -0,0 +1,87 @@ @@ -0,0 +1,87 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 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/>.
*
*/
package main
import (
"log"
)
type Sender interface {
Send(Buffer)
}
type ResponseSender interface {
Sender
Responder
}
type Responder interface {
Reply(iid string, m interface{})
}
type Client interface {
ResponseSender
Session() *Session
Index() uint64
Close(bool)
}
type client struct {
Codec
ChannellingAPI
Connection
session *Session
}
func NewClient(codec Codec, api ChannellingAPI, session *Session) *client {
return &client{codec, api, nil, session}
}
func (client *client) OnConnect(conn Connection) {
client.Connection = conn
client.ChannellingAPI.OnConnect(client, client.session)
}
func (client *client) OnText(b Buffer) {
if incoming, err := client.DecodeIncoming(b); err == nil {
client.OnIncoming(client, client.session, incoming)
} else {
log.Println("OnText error while decoding JSON", err)
log.Printf("JSON:\n%s\n", b)
}
}
func (client *client) OnDisconnect() {
client.ChannellingAPI.OnDisconnect(client.session)
}
func (client *client) Reply(iid string, m interface{}) {
outgoing := &DataOutgoing{From: client.session.Id, Iid: iid, Data: m}
if b, err := client.EncodeOutgoing(outgoing); err == nil {
client.Send(b)
b.Decref()
}
}
func (client *client) Session() *Session {
return client.session
}

43
src/app/spreed-webrtc-server/common_test.go

@ -0,0 +1,43 @@ @@ -0,0 +1,43 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 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/>.
*
*/
package main
import (
"testing"
)
func assertDataError(t *testing.T, err error, code string) {
if err == nil {
t.Error("Expected an error, but none was returned")
return
}
dataError, ok := err.(*DataError)
if !ok {
t.Errorf("Expected error %#v to be a *DataError", err)
return
}
if code != dataError.Code {
t.Errorf("Expected error code to be %v, but was %v", code, dataError.Code)
}
}

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

@ -24,47 +24,104 @@ package main @@ -24,47 +24,104 @@ package main
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/strukturag/phoenix"
)
type Config struct {
Title string // Title
ver string // Version (not exported to Javascript)
S string // Static URL prefix with version
B string // Base URL
Token string // Server token
StunURIs []string // STUN server URIs
TurnURIs []string // TURN server URIs
Tokens bool // True when we got a tokens file
Version string // Server version number
UsersEnabled bool // Flag if users are enabled
UsersAllowRegistration bool // Flag if users can register
UsersMode string // Users mode string
DefaultRoomEnabled bool // Flag if default room ("") is enabled
Plugin string // Plugin to load
globalRoomid string // Id of the global room (not exported to Javascript)
Title string // Title
ver string // Version (not exported to Javascript)
S string // Static URL prefix with version
B string // Base URL
Token string // Server token
StunURIs []string // STUN server URIs
TurnURIs []string // TURN server URIs
Tokens bool // True when we got a tokens file
Version string // Server version number
UsersEnabled bool // Flag if users are enabled
UsersAllowRegistration bool // Flag if users can register
UsersMode string // Users mode string
DefaultRoomEnabled bool // Flag if default room ("") is enabled
Plugin string // Plugin to load
AuthorizeRoomCreation bool // Whether a user account is required to create rooms
globalRoomID string // Id of the global room (not exported to Javascript)
contentSecurityPolicy string // HTML content security policy
contentSecurityPolicyReportOnly string // HTML content security policy in report only mode
}
func NewConfig(title, ver, runtimeVersion, basePath, serverToken string, stunURIs, turnURIs []string, tokens bool, globalRoomid string, defaultRoomEnabled, usersEnabled, usersAllowRegistration bool, usersMode, plugin string) *Config {
sv := fmt.Sprintf("static/ver=%s", ver)
func NewConfig(container phoenix.Container, tokens bool) *Config {
ver := container.GetStringDefault("app", "ver", "")
version := container.Version()
if version != "unreleased" {
ver = fmt.Sprintf("%s%s", ver, strings.Replace(version, ".", "", -1))
} else {
ts := fmt.Sprintf("%d", time.Now().Unix())
if ver == "" {
ver = ts
}
version = fmt.Sprintf("unreleased.%s", ts)
}
// Read base path from config and make sure it ends with a slash.
basePath := container.GetStringDefault("http", "basePath", "/")
if !strings.HasSuffix(basePath, "/") {
basePath = fmt.Sprintf("%s/", basePath)
}
if basePath != "/" {
container.Printf("Using '%s' base base path.", basePath)
}
//TODO(longsleep): When we have a database, generate this once from random source and store it.
serverToken := container.GetStringDefault("app", "serverToken", "i-did-not-change-the-public-token-boo")
stunURIsString := container.GetStringDefault("app", "stunURIs", "")
stunURIs := strings.Split(stunURIsString, " ")
trimAndRemoveDuplicates(&stunURIs)
turnURIsString := container.GetStringDefault("app", "turnURIs", "")
turnURIs := strings.Split(turnURIsString, " ")
trimAndRemoveDuplicates(&turnURIs)
return &Config{
Title: title,
ver: ver,
S: sv,
B: basePath,
Token: serverToken,
StunURIs: stunURIs,
TurnURIs: turnURIs,
Tokens: tokens,
Version: runtimeVersion,
UsersEnabled: usersEnabled,
UsersAllowRegistration: usersAllowRegistration,
UsersMode: usersMode,
DefaultRoomEnabled: defaultRoomEnabled,
Plugin: plugin,
globalRoomid: globalRoomid,
Title: container.GetStringDefault("app", "title", "Spreed WebRTC"),
ver: ver,
S: fmt.Sprintf("static/ver=%s", ver),
B: basePath,
Token: serverToken,
StunURIs: stunURIs,
TurnURIs: turnURIs,
Tokens: tokens,
Version: version,
UsersEnabled: container.GetBoolDefault("users", "enabled", false),
UsersAllowRegistration: container.GetBoolDefault("users", "allowRegistration", false),
UsersMode: container.GetStringDefault("users", "mode", ""),
DefaultRoomEnabled: container.GetBoolDefault("app", "defaultRoomEnabled", true),
Plugin: container.GetStringDefault("app", "plugin", ""),
AuthorizeRoomCreation: container.GetBoolDefault("app", "authorizeRoomCreation", false),
globalRoomID: container.GetStringDefault("app", "globalRoom", ""),
contentSecurityPolicy: container.GetStringDefault("app", "contentSecurityPolicy", ""),
contentSecurityPolicyReportOnly: container.GetStringDefault("app", "contentSecurityPolicyReportOnly", ""),
}
}
func (config *Config) Get(request *http.Request) (int, interface{}, http.Header) {
return 200, config, http.Header{"Content-Type": {"application/json; charset=utf-8"}}
}
// Helper function to clean up string arrays.
func trimAndRemoveDuplicates(data *[]string) {
found := make(map[string]bool)
j := 0
for i, x := range *data {
x = strings.TrimSpace(x)
if len(x) > 0 && !found[x] {
found[x] = true
(*data)[j] = (*data)[i]
j++
}
}
*data = (*data)[:j]
}

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

@ -22,14 +22,13 @@ @@ -22,14 +22,13 @@
package main
import (
"bytes"
"container/list"
"github.com/gorilla/websocket"
"io"
"log"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
)
const (
@ -54,110 +53,77 @@ const ( @@ -54,110 +53,77 @@ const (
maxRatePerSecond = 20
)
type Connection struct {
type Connection interface {
Index() uint64
Send(Buffer)
Close(runCallbacks bool)
readPump()
writePump()
}
type ConnectionHandler interface {
NewBuffer() Buffer
OnConnect(Connection)
OnText(Buffer)
OnDisconnect()
}
type connection struct {
// References.
h *Hub
ws *websocket.Conn
request *http.Request
handler ConnectionHandler
// Data handling.
condition *sync.Cond
queue list.List
mutex sync.Mutex
isClosed bool
isClosing bool
// Metadata.
Id string
Roomid string // Keep Roomid here for quick acess without locking c.Session.
Idx uint64
Session *Session
IsRegistered bool
Hello bool
Version string
}
func NewConnection(h *Hub, ws *websocket.Conn, request *http.Request) *Connection {
// Debugging
Idx uint64
}
c := &Connection{
h: h,
func NewConnection(index uint64, ws *websocket.Conn, handler ConnectionHandler) Connection {
c := &connection{
ws: ws,
request: request,
handler: handler,
Idx: index,
}
c.condition = sync.NewCond(&c.mutex)
return c
}
func (c *Connection) close() {
func (c *connection) Index() uint64 {
return c.Idx
}
if !c.isClosed {
c.ws.Close()
c.Session.Close()
c.mutex.Lock()
c.Session = nil
c.isClosed = true
for {
head := c.queue.Front()
if head == nil {
break
}
c.queue.Remove(head)
message := head.Value.(Buffer)
message.Decref()
}
c.condition.Signal()
func (c *connection) Close(runCallbacks bool) {
c.mutex.Lock()
if c.isClosed {
c.mutex.Unlock()
return
}
}
func (c *Connection) register() error {
s := c.h.CreateSession(c.request, nil)
c.h.registerHandler(c, s)
return nil
}
func (c *Connection) reregister(token string) error {
if st, err := c.h.DecodeSessionToken(token); err == nil {
s := c.h.CreateSession(c.request, st)
c.h.registerHandler(c, s)
} else {
log.Println("Error while decoding session token", err)
c.register()
if runCallbacks {
c.handler.OnDisconnect()
}
return nil
}
func (c *Connection) unregister() {
c.isClosing = true
c.h.unregisterHandler(c)
}
func (c *Connection) readAll(dest Buffer, r io.Reader) error {
var err error
defer func() {
e := recover()
if e == nil {
return
}
if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge {
err = panicErr
} else {
panic(e)
c.ws.Close()
c.isClosed = true
for {
head := c.queue.Front()
if head == nil {
break
}
}()
_, err = dest.ReadFrom(r)
return err
c.queue.Remove(head)
message := head.Value.(Buffer)
message.Decref()
}
c.condition.Signal()
c.mutex.Unlock()
}
// readPump pumps messages from the websocket connection to the hub.
func (c *Connection) readPump() {
func (c *connection) readPump() {
c.ws.SetReadLimit(maxMessageSize)
c.ws.SetReadDeadline(time.Now().Add(pongWait))
c.ws.SetPongHandler(func(string) error {
@ -165,6 +131,10 @@ func (c *Connection) readPump() { @@ -165,6 +131,10 @@ func (c *Connection) readPump() {
return nil
})
times := list.New()
// NOTE(lcooper): This more or less assumes that the write pump is started.
c.handler.OnConnect(c)
for {
//fmt.Println("readPump wait nextReader", c.Idx)
op, r, err := c.ws.NextReader()
@ -177,12 +147,6 @@ func (c *Connection) readPump() { @@ -177,12 +147,6 @@ func (c *Connection) readPump() {
}
switch op {
case websocket.TextMessage:
message := c.h.buffers.New()
err = c.readAll(message, r)
if err != nil {
message.Decref()
break
}
now := time.Now()
if times.Len() == maxRatePerSecond {
front := times.Front()
@ -194,18 +158,23 @@ func (c *Connection) readPump() { @@ -194,18 +158,23 @@ func (c *Connection) readPump() {
}
}
times.PushBack(now)
c.h.server.OnText(c, message)
message := c.handler.NewBuffer()
err = readAll(message, r)
if err != nil {
message.Decref()
break
}
c.handler.OnText(message)
message.Decref()
}
}
c.unregister()
c.ws.Close()
c.Close(true)
}
// Write message to outbound queue.
func (c *Connection) send(message Buffer) {
func (c *connection) Send(message Buffer) {
c.mutex.Lock()
defer c.mutex.Unlock()
if c.isClosed {
@ -223,8 +192,7 @@ func (c *Connection) send(message Buffer) { @@ -223,8 +192,7 @@ func (c *Connection) send(message Buffer) {
}
// writePump pumps messages from the queue to the websocket connection.
func (c *Connection) writePump() {
func (c *connection) writePump() {
var timer *time.Timer
ping := false
@ -301,16 +269,16 @@ func (c *Connection) writePump() { @@ -301,16 +269,16 @@ func (c *Connection) writePump() {
cleanup:
//fmt.Println("writePump done")
timer.Stop()
c.ws.Close()
c.Close(true)
}
// Write ping message.
func (c *Connection) ping() error {
func (c *connection) ping() error {
return c.write(websocket.PingMessage, []byte{})
}
// Write writes a message with the given opCode and payload.
func (c *Connection) write(opCode int, payload []byte) error {
func (c *connection) write(opCode int, payload []byte) error {
c.ws.SetWriteDeadline(time.Now().Add(writeWait))
return c.ws.WriteMessage(opCode, payload)
}

1
src/app/spreed-webrtc-server/context.go

@ -26,6 +26,7 @@ type Context struct { @@ -26,6 +26,7 @@ type Context struct {
Cfg *Config
Host string
Ssl bool
Csp bool
Languages []string
Room string `json:"-"`
Scheme string `json:"-"`

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

@ -22,21 +22,16 @@ @@ -22,21 +22,16 @@
package main
import (
"bytes"
"crypto/aes"
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/gorilla/securecookie"
"log"
"net/http"
"strings"
"sync"
"sync/atomic"
"time"
)
@ -46,85 +41,56 @@ const ( @@ -46,85 +41,56 @@ const (
maxUsersLength = 5000
)
// TODO(longsleep): Get rid of MessageRequest type.
type MessageRequest struct {
From string
To string
Message Buffer
Id string
type SessionStore interface {
GetSession(id string) (session *Session, ok bool)
}
type HubStat struct {
Rooms int `json:"rooms"`
Connections int `json:"connections"`
Sessions int `json:"sessions"`
Users int `json:"users"`
Count uint64 `json:"count"`
BroadcastChatMessages uint64 `json:"broadcastchatmessages"`
UnicastChatMessages uint64 `json:"unicastchatmessages"`
IdsInRoom map[string][]string `json:"idsinroom,omitempty"`
SessionsById map[string]*DataSession `json:"sessionsbyid,omitempty"`
UsersById map[string]*DataUser `json:"usersbyid,omitempty"`
ConnectionsByIdx map[string]string `json:"connectionsbyidx,omitempty"`
type Unicaster interface {
SessionStore
OnConnect(Client, *Session)
Unicast(session *Session, to string, m interface{})
OnDisconnect(*Session)
}
type Hub struct {
server *Server
connectionTable map[string]*Connection
sessionTable map[string]*Session
roomTable map[string]*RoomWorker
userTable map[string]*User
fakesessionTable map[string]*Session
version string
config *Config
sessionSecret []byte
encryptionSecret []byte
turnSecret []byte
tickets *securecookie.SecureCookie
attestations *securecookie.SecureCookie
count uint64
mutex sync.RWMutex
buffers BufferCache
broadcastChatMessages uint64
unicastChatMessages uint64
buddyImages ImageCache
realm string
tokenName string
useridRetriever func(*http.Request) (string, error)
contacts *securecookie.SecureCookie
type ContactManager interface {
contactrequestHandler(*Session, string, *DataContactRequest) error
getContactID(*Session, string) (string, error)
}
func NewHub(version string, config *Config, sessionSecret, encryptionSecret, turnSecret []byte, realm string) *Hub {
h := &Hub{
connectionTable: make(map[string]*Connection),
sessionTable: make(map[string]*Session),
roomTable: make(map[string]*RoomWorker),
userTable: make(map[string]*User),
fakesessionTable: make(map[string]*Session),
version: version,
config: config,
sessionSecret: sessionSecret,
encryptionSecret: encryptionSecret,
turnSecret: turnSecret,
realm: realm,
}
type TurnDataCreator interface {
CreateTurnData(*Session) *DataTurn
}
type ClientStats interface {
ClientInfo(details bool) (int, map[string]*DataSession, map[string]string)
}
type Hub interface {
ClientStats
Unicaster
TurnDataCreator
ContactManager
}
if len(h.sessionSecret) < 32 {
log.Printf("Weak sessionSecret (only %d bytes). It is recommended to use a key with 32 or 64 bytes.\n", len(h.sessionSecret))
type hub struct {
OutgoingEncoder
clients map[string]Client
config *Config
turnSecret []byte
mutex sync.RWMutex
contacts *securecookie.SecureCookie
}
func NewHub(config *Config, sessionSecret, encryptionSecret, turnSecret []byte, encoder OutgoingEncoder) Hub {
h := &hub{
OutgoingEncoder: encoder,
clients: make(map[string]Client),
config: config,
turnSecret: turnSecret,
}
h.tickets = securecookie.New(h.sessionSecret, h.encryptionSecret)
h.tickets.MaxAge(86400 * 30) // 30 days
h.tickets.HashFunc(sha256.New)
h.tickets.BlockFunc(aes.NewCipher)
h.attestations = securecookie.New(h.sessionSecret, nil)
h.attestations.MaxAge(300) // 5 minutes
h.tickets.HashFunc(sha256.New)
h.buffers = NewBufferCache(1024, bytes.MinRead)
h.buddyImages = NewImageCache()
h.tokenName = fmt.Sprintf("token@%s", h.realm)
h.contacts = securecookie.New(h.sessionSecret, h.encryptionSecret)
h.contacts = securecookie.New(sessionSecret, encryptionSecret)
h.contacts.MaxAge(0) // Forever
h.contacts.HashFunc(sha256.New)
h.contacts.BlockFunc(aes.NewCipher)
@ -132,48 +98,27 @@ func NewHub(version string, config *Config, sessionSecret, encryptionSecret, tur @@ -132,48 +98,27 @@ func NewHub(version string, config *Config, sessionSecret, encryptionSecret, tur
}
func (h *Hub) Stat(details bool) *HubStat {
func (h *hub) ClientInfo(details bool) (clientCount int, sessions map[string]*DataSession, connections map[string]string) {
h.mutex.RLock()
defer h.mutex.RUnlock()
stat := &HubStat{
Rooms: len(h.roomTable),
Connections: len(h.connectionTable),
Sessions: len(h.sessionTable),
Users: len(h.userTable),
Count: h.count,
BroadcastChatMessages: atomic.LoadUint64(&h.broadcastChatMessages),
UnicastChatMessages: atomic.LoadUint64(&h.unicastChatMessages),
}
clientCount = len(h.clients)
if details {
rooms := make(map[string][]string)
for roomid, room := range h.roomTable {
sessions := make([]string, 0, len(room.connections))
for id := range room.connections {
sessions = append(sessions, id)
}
rooms[roomid] = sessions
}
stat.IdsInRoom = rooms
sessions := make(map[string]*DataSession)
for sessionid, session := range h.sessionTable {
sessions[sessionid] = session.Data()
}
stat.SessionsById = sessions
users := make(map[string]*DataUser)
for userid, user := range h.userTable {
users[userid] = user.Data()
sessions = make(map[string]*DataSession)
for id, client := range h.clients {
sessions[id] = client.Session().Data()
}
stat.UsersById = users
connections := make(map[string]string)
for id, connection := range h.connectionTable {
connections[fmt.Sprintf("%d", connection.Idx)] = id
connections = make(map[string]string)
for id, client := range h.clients {
connections[fmt.Sprintf("%d", client.Index())] = id
}
stat.ConnectionsByIdx = connections
}
return stat
return
}
func (h *Hub) CreateTurnData(id string) *DataTurn {
func (h *hub) CreateTurnData(session *Session) *DataTurn {
// Create turn data credentials for shared secret auth with TURN
// server. See http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
@ -182,6 +127,7 @@ func (h *Hub) CreateTurnData(id string) *DataTurn { @@ -182,6 +127,7 @@ func (h *Hub) CreateTurnData(id string) *DataTurn {
if len(h.turnSecret) == 0 {
return &DataTurn{}
}
id := session.Id
bar := sha256.New()
bar.Write([]byte(id))
id = base64.StdEncoding.EncodeToString(bar.Sum(nil))
@ -194,389 +140,85 @@ func (h *Hub) CreateTurnData(id string) *DataTurn { @@ -194,389 +140,85 @@ func (h *Hub) CreateTurnData(id string) *DataTurn {
}
func (h *Hub) CreateSuserid(session *Session) (suserid string) {
userid := session.Userid()
if userid != "" {
m := hmac.New(sha256.New, h.encryptionSecret)
m.Write([]byte(userid))
suserid = base64.StdEncoding.EncodeToString(m.Sum(nil))
func (h *hub) GetSession(id string) (session *Session, ok bool) {
var client Client
client, ok = h.GetClient(id)
if ok {
session = client.Session()
}
return
}
func (h *Hub) CreateSession(request *http.Request, st *SessionToken) *Session {
var session *Session
var userid string
usersEnabled := h.config.UsersEnabled
if usersEnabled && h.useridRetriever != nil {
userid, _ = h.useridRetriever(request)
}
if st == nil {
sid := NewRandomString(32)
id, _ := h.tickets.Encode("id", sid)
session = NewSession(h, id, sid)
log.Println("Created new session id", id)
} else {
if userid == "" {
userid = st.Userid
}
if !usersEnabled {
userid = ""
}
session = NewSession(h, st.Id, st.Sid)
}
if userid != "" {
h.authenticateHandler(session, st, userid)
}
return session
}
func (h *Hub) CreateFakeSession(userid string) *Session {
h.mutex.Lock()
session, ok := h.fakesessionTable[userid]
if !ok {
sid := fmt.Sprintf("fake-%s", NewRandomString(27))
id, _ := h.tickets.Encode("id", sid)
log.Println("Created new fake session id", id)
session = NewSession(h, id, sid)
session.SetUseridFake(userid)
h.fakesessionTable[userid] = session
}
h.mutex.Unlock()
return session
}
func (h *Hub) ValidateSession(id, sid string) bool {
var decoded string
err := h.tickets.Decode("id", id, &decoded)
if err != nil {
log.Println("Session validation error", err, id, sid)
return false
}
if decoded != sid {
log.Println("Session validation failed", id, sid)
return false
}
return true
}
func (h *Hub) EncodeSessionToken(st *SessionToken) (string, error) {
return h.tickets.Encode(h.tokenName, st)
}
func (h *Hub) DecodeSessionToken(token string) (*SessionToken, error) {
st := &SessionToken{}
err := h.tickets.Decode(h.tokenName, token, st)
return st, err
}
func (h *Hub) GetRoom(id string) *RoomWorker {
h.mutex.RLock()
room, ok := h.roomTable[id]
if !ok {
h.mutex.RUnlock()
h.mutex.Lock()
// Need to re-check, another thread might have created the room
// while we waited for the lock.
room, ok = h.roomTable[id]
if !ok {
room = NewRoomWorker(h, id)
h.roomTable[id] = room
h.mutex.Unlock()
go func() {
// Start room, this blocks until room expired.
room.Start()
// Cleanup room when we are done.
h.mutex.Lock()
defer h.mutex.Unlock()
delete(h.roomTable, id)
log.Printf("Cleaned up room '%s'\n", id)
}()
} else {
h.mutex.Unlock()
}
} else {
h.mutex.RUnlock()
}
return room
}
func (h *Hub) GetGlobalConnections() []*Connection {
if h.config.globalRoomid == "" {
return make([]*Connection, 0)
}
h.mutex.RLock()
if room, ok := h.roomTable[h.config.globalRoomid]; ok {
h.mutex.RUnlock()
return room.GetConnections()
}
h.mutex.RUnlock()
return make([]*Connection, 0)
}
func (h *Hub) RunForAllRooms(f func(room *RoomWorker)) {
h.mutex.RLock()
for _, room := range h.roomTable {
f(room)
}
h.mutex.RUnlock()
}
func (h *Hub) isGlobalRoomid(id string) bool {
return id != "" && (id == h.config.globalRoomid)
}
func (h *Hub) isDefaultRoomid(id string) bool {
return id == ""
}
func (h *Hub) registerHandler(c *Connection, s *Session) {
// Apply session to connection.
c.Id = s.Id
c.Session = s
func (h *hub) OnConnect(client Client, session *Session) {
// Set flags.
h.mutex.Lock()
// Set flags.
h.count++
c.Idx = h.count
c.IsRegistered = true
log.Printf("Created client with id %s", session.Id)
// Register connection or replace existing one.
if ec, ok := h.connectionTable[c.Id]; ok {
ec.IsRegistered = false
ec.close()
if ec, ok := h.clients[session.Id]; ok {
ec.Close(false)
//log.Printf("Register (%d) from %s: %s (existing)\n", c.Idx, c.Id)
}
h.connectionTable[c.Id] = c
h.sessionTable[c.Id] = s
h.clients[session.Id] = client
//fmt.Println("registered", c.Id)
h.mutex.Unlock()
//log.Printf("Register (%d) from %s: %s\n", c.Idx, c.Id)
h.server.OnRegister(c)
}
func (h *Hub) unregisterHandler(c *Connection) {
func (h *hub) OnDisconnect(session *Session) {
h.mutex.Lock()
if !c.IsRegistered {
h.mutex.Unlock()
return
}
suserid := c.Session.Userid()
delete(h.connectionTable, c.Id)
delete(h.sessionTable, c.Id)
if suserid != "" {
user, ok := h.userTable[suserid]
if ok {
empty := user.RemoveSession(c.Session)
if empty {
delete(h.userTable, suserid)
}
}
}
delete(h.clients, session.Id)
h.mutex.Unlock()
h.buddyImages.Delete(c.Id)
//log.Printf("Unregister (%d) from %s: %s\n", c.Idx, c.RemoteAddr, c.Id)
h.server.OnUnregister(c)
c.close()
}
func (h *Hub) unicastHandler(m *MessageRequest) {
func (h *hub) GetClient(id string) (client Client, ok bool) {
h.mutex.RLock()
out, ok := h.connectionTable[m.To]
client, ok = h.clients[id]
h.mutex.RUnlock()
if !ok {
log.Println("Unicast To not found", m.To)
return
}
out.send(m.Message)
return
}
func (h *Hub) aliveHandler(c *Connection, alive *DataAlive, iid string) {
aliveJson := h.buffers.New()
encoder := json.NewEncoder(aliveJson)
err := encoder.Encode(&DataOutgoing{From: c.Id, Data: alive, Iid: iid})
if err != nil {
log.Println("Alive error while encoding JSON", err)
aliveJson.Decref()
return
func (h *hub) Unicast(session *Session, to string, m interface{}) {
outgoing := &DataOutgoing{
From: session.Id,
To: to,
A: session.Attestation(),
Data: m,
}
c.send(aliveJson)
aliveJson.Decref()
}
func (h *Hub) sessionsHandler(c *Connection, srq *DataSessionsRequest, iid string) {
var users []*DataSession
switch srq.Type {
case "contact":
contact := &Contact{}
err := h.contacts.Decode("contact", srq.Token, contact)
if err != nil {
log.Println("Failed to decode incoming contact token", err, srq.Token)
return
}
// Use the userid which is not ours from the contact data.
var userid string
suserid := c.Session.Userid()
if contact.A == suserid {
userid = contact.B
} else if contact.B == suserid {
userid = contact.A
}
if userid == "" {
log.Println("Ignoring foreign contact token", contact.A, contact.B)
return
}
// Find foreign user.
h.mutex.RLock()
user, ok := h.userTable[userid]
h.mutex.RUnlock()
if message, err := h.EncodeOutgoing(outgoing); err == nil {
client, ok := h.GetClient(to)
if !ok {
// No user. Create fake session.
users = make([]*DataSession, 1, 1)
users[0] = h.CreateFakeSession(userid).Data()
} else {
// Add sessions for forein user.
users = user.SubscribeSessions(c.Session)
}
case "session":
id, err := c.Session.attestation.Decode(srq.Token)
if err != nil {
log.Println("Failed to decode incoming attestation", err, srq.Token)
log.Println("Unicast To not found", to)
return
}
h.mutex.RLock()
session, ok := h.sessionTable[id]
h.mutex.RUnlock()
if !ok {
return
}
users = make([]*DataSession, 1, 1)
users[0] = session.Data()
default:
log.Println("Unkown incoming sessions request type", srq.Type)
client.Send(message)
message.Decref()
}
if users != nil {
sessions := &DataSessions{Type: "Sessions", Users: users, Sessions: srq}
sessionsJson := h.buffers.New()
encoder := json.NewEncoder(sessionsJson)
err := encoder.Encode(&DataOutgoing{From: c.Id, Data: sessions, Iid: iid})
if err != nil {
log.Println("Sessions error while encoding JSON", err)
sessionsJson.Decref()
return
}
c.send(sessionsJson)
sessionsJson.Decref()
}
}
func (h *Hub) sessionupdateHandler(s *SessionUpdate) uint64 {
//fmt.Println("Userupdate", u)
h.mutex.RLock()
session, ok := h.sessionTable[s.Id]
h.mutex.RUnlock()
var rev uint64
if ok {
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 := h.buddyImages.Update(s.Id, pic[5:])
if imageId != "" {
status["buddyPicture"] = "img:" + imageId
}
}
}
}
rev = session.Update(s)
} else {
log.Printf("Update data for unknown user %s\n", s.Id)
}
return rev
}
func (h *Hub) sessiontokenHandler(st *SessionToken) (string, error) {
h.mutex.RLock()
c, ok := h.connectionTable[st.Id]
h.mutex.RUnlock()
if !ok {
return "", errors.New("no such connection")
}
nonce, err := c.Session.Authorize(h.realm, st)
func (h *hub) getContactID(session *Session, token string) (userid string, err error) {
contact := &Contact{}
err = h.contacts.Decode("contact", token, contact)
if err != nil {
return "", err
err = fmt.Errorf("Failed to decode incoming contact token", err, token)
return
}
return nonce, nil
}
func (h *Hub) authenticateHandler(session *Session, st *SessionToken, userid string) error {
err := session.Authenticate(h.realm, st, userid)
if err == nil {
// Authentication success.
suserid := session.Userid()
h.mutex.Lock()
user, ok := h.userTable[suserid]
if !ok {
user = NewUser(suserid)
h.userTable[suserid] = user
}
h.mutex.Unlock()
user.AddSession(session)
// Use the userid which is not ours from the contact data.
suserid := session.Userid()
if contact.A == suserid {
userid = contact.B
} else if contact.B == suserid {
userid = contact.A
}
return err
if userid == "" {
err = fmt.Errorf("Ignoring foreign contact token", contact.A, contact.B)
}
return
}
func (h *Hub) contactrequestHandler(c *Connection, to string, cr *DataContactRequest) error {
func (h *hub) contactrequestHandler(session *Session, to string, cr *DataContactRequest) error {
var err error
@ -588,13 +230,11 @@ func (h *Hub) contactrequestHandler(c *Connection, to string, cr *DataContactReq @@ -588,13 +230,11 @@ func (h *Hub) contactrequestHandler(c *Connection, to string, cr *DataContactReq
if err != nil {
return err
}
suserid := c.Session.Userid()
suserid := session.Userid()
if suserid == "" {
return errors.New("no userid")
}
h.mutex.RLock()
session, ok := h.sessionTable[to]
h.mutex.RUnlock()
session, ok := h.GetSession(to)
if !ok {
return errors.New("unknown to session for confirm")
}
@ -616,13 +256,11 @@ func (h *Hub) contactrequestHandler(c *Connection, to string, cr *DataContactReq @@ -616,13 +256,11 @@ func (h *Hub) contactrequestHandler(c *Connection, to string, cr *DataContactReq
} else {
// New request.
// Create Token with flag and c.Session.Userid and the to Session.Userid.
suserid := c.Session.Userid()
suserid := session.Userid()
if suserid == "" {
return errors.New("no userid")
}
h.mutex.RLock()
session, ok := h.sessionTable[to]
h.mutex.RUnlock()
session, ok := h.GetSession(to)
if !ok {
return errors.New("unknown to session")
}

69
src/app/spreed-webrtc-server/incoming_codec.go

@ -0,0 +1,69 @@ @@ -0,0 +1,69 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 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/>.
*
*/
package main
import (
"bytes"
"encoding/json"
"log"
)
type IncomingDecoder interface {
DecodeIncoming(Buffer) (*DataIncoming, error)
}
type OutgoingEncoder interface {
EncodeOutgoing(*DataOutgoing) (Buffer, error)
}
type Codec interface {
NewBuffer() Buffer
IncomingDecoder
OutgoingEncoder
}
type incomingCodec struct {
buffers BufferCache
}
func NewCodec() Codec {
return &incomingCodec{NewBufferCache(1024, bytes.MinRead)}
}
func (codec incomingCodec) NewBuffer() Buffer {
return codec.buffers.New()
}
func (codec incomingCodec) DecodeIncoming(b Buffer) (*DataIncoming, error) {
incoming := &DataIncoming{}
return incoming, json.Unmarshal(b.Bytes(), incoming)
}
func (codec incomingCodec) EncodeOutgoing(outgoing *DataOutgoing) (Buffer, error) {
b := codec.NewBuffer()
if err := json.NewEncoder(b).Encode(outgoing); err != nil {
log.Println("Error while encoding JSON", err)
b.Decref()
return nil, err
}
return b, nil
}

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

@ -40,7 +40,6 @@ import ( @@ -40,7 +40,6 @@ import (
"path"
goruntime "runtime"
"strconv"
"strings"
"syscall"
"time"
)
@ -63,21 +62,6 @@ func getRequestLanguages(r *http.Request, supportedLanguages []string) []string @@ -63,21 +62,6 @@ func getRequestLanguages(r *http.Request, supportedLanguages []string) []string
}
// Helper function to clean up string arrays.
func trimAndRemoveDuplicates(data *[]string) {
found := make(map[string]bool)
j := 0
for i, x := range *data {
x = strings.TrimSpace(x)
if len(x) > 0 && !found[x] {
found[x] = true
(*data)[j] = (*data)[i]
j++
}
}
*data = (*data)[:j]
}
func mainHandler(w http.ResponseWriter, r *http.Request) {
handleRoomView("", w, r)
@ -91,12 +75,12 @@ func roomHandler(w http.ResponseWriter, r *http.Request) { @@ -91,12 +75,12 @@ func roomHandler(w http.ResponseWriter, r *http.Request) {
}
func makeImageHandler(hub *Hub, expires time.Duration) http.HandlerFunc {
func makeImageHandler(buddyImages ImageCache, expires time.Duration) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
image := hub.buddyImages.Get(vars["imageid"])
image := buddyImages.Get(vars["imageid"])
if image == nil {
http.Error(w, "Unknown image", http.StatusNotFound)
return
@ -125,6 +109,17 @@ func handleRoomView(room string, w http.ResponseWriter, r *http.Request) { @@ -125,6 +109,17 @@ func handleRoomView(room string, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Expires", "-1")
w.Header().Set("Cache-Control", "private, max-age=0")
csp := false
if config.contentSecurityPolicy != "" {
w.Header().Set("Content-Security-Policy", config.contentSecurityPolicy)
csp = true
}
if config.contentSecurityPolicyReportOnly != "" {
w.Header().Set("Content-Security-Policy-Report-Only", config.contentSecurityPolicyReportOnly)
csp = true
}
scheme := "http"
// Detect if the request was made with SSL.
@ -142,7 +137,7 @@ func handleRoomView(room string, w http.ResponseWriter, r *http.Request) { @@ -142,7 +137,7 @@ func handleRoomView(room string, w http.ResponseWriter, r *http.Request) {
}
// Prepare context to deliver to HTML..
context := &Context{Cfg: config, App: "main", Host: r.Host, Scheme: scheme, Ssl: ssl, Languages: langs, Room: room}
context := &Context{Cfg: config, App: "main", Host: r.Host, Scheme: scheme, Ssl: ssl, Csp: csp, Languages: langs, Room: room}
// Get URL parameters.
r.ParseForm()
@ -184,17 +179,6 @@ func runner(runtime phoenix.Runtime) error { @@ -184,17 +179,6 @@ func runner(runtime phoenix.Runtime) error {
return fmt.Errorf("Unable to find client. Path correct and compiled css?")
}
// Read base path from config and make sure it ends with a slash.
basePath, err := runtime.GetString("http", "basePath")
if err != nil {
basePath = "/"
} else {
if !strings.HasSuffix(basePath, "/") {
basePath = fmt.Sprintf("%s/", basePath)
}
log.Printf("Using '%s' base base path.", basePath)
}
statsEnabled, err := runtime.GetBool("http", "stats")
if err != nil {
statsEnabled = false
@ -223,6 +207,10 @@ func runner(runtime phoenix.Runtime) error { @@ -223,6 +207,10 @@ func runner(runtime phoenix.Runtime) error {
}
}
if len(sessionSecret) < 32 {
log.Printf("Weak sessionSecret (only %d bytes). It is recommended to use a key with 32 or 64 bytes.\n", len(sessionSecret))
}
var encryptionSecret []byte
encryptionSecretString, err := runtime.GetString("app", "encryptionSecret")
if err != nil {
@ -242,109 +230,33 @@ func runner(runtime phoenix.Runtime) error { @@ -242,109 +230,33 @@ func runner(runtime phoenix.Runtime) error {
}
}
tokenFile, err := runtime.GetString("app", "tokenFile")
if err == nil {
if !httputils.HasFilePath(path.Clean(tokenFile)) {
return fmt.Errorf("Unable to find token file at %s", tokenFile)
}
}
title, err := runtime.GetString("app", "title")
if err != nil {
title = "Spreed WebRTC"
}
ver, err := runtime.GetString("app", "ver")
if err != nil {
ver = ""
}
runtimeVersion := version
if version != "unreleased" {
ver1 := ver
if err != nil {
ver1 = ""
}
ver = fmt.Sprintf("%s%s", ver1, strings.Replace(version, ".", "", -1))
} else {
ts := fmt.Sprintf("%d", time.Now().Unix())
if err != nil {
ver = ts
}
runtimeVersion = fmt.Sprintf("unreleased.%s", ts)
}
turnURIsString, err := runtime.GetString("app", "turnURIs")
if err != nil {
turnURIsString = ""
}
turnURIs := strings.Split(turnURIsString, " ")
trimAndRemoveDuplicates(&turnURIs)
var turnSecret []byte
turnSecretString, err := runtime.GetString("app", "turnSecret")
if err == nil {
turnSecret = []byte(turnSecretString)
}
stunURIsString, err := runtime.GetString("app", "stunURIs")
if err != nil {
stunURIsString = ""
}
stunURIs := strings.Split(stunURIsString, " ")
trimAndRemoveDuplicates(&stunURIs)
globalRoomid, err := runtime.GetString("app", "globalRoom")
if err != nil {
// Global room is disabled.
globalRoomid = ""
}
plugin, err := runtime.GetString("app", "plugin")
if err != nil {
plugin = ""
}
defaultRoomEnabled := true
defaultRoomEnabledString, err := runtime.GetString("app", "defaultRoomEnabled")
if err == nil {
defaultRoomEnabled = defaultRoomEnabledString == "true"
}
usersEnabled := false
usersEnabledString, err := runtime.GetString("users", "enabled")
if err == nil {
usersEnabled = usersEnabledString == "true"
}
usersAllowRegistration := false
usersAllowRegistrationString, err := runtime.GetString("users", "allowRegistration")
if err == nil {
usersAllowRegistration = usersAllowRegistrationString == "true"
}
serverToken, err := runtime.GetString("app", "serverToken")
if err != nil {
//TODO(longsleep): When we have a database, generate this once from random source and store it.
serverToken = "i-did-not-change-the-public-token-boo"
}
serverRealm, err := runtime.GetString("app", "serverRealm")
if err != nil {
serverRealm = "local"
}
usersMode, _ := runtime.GetString("users", "mode")
// Create token provider.
tokenFile, err := runtime.GetString("app", "tokenFile")
if err == nil {
if !httputils.HasFilePath(path.Clean(tokenFile)) {
return fmt.Errorf("Unable to find token file at %s", tokenFile)
}
}
var tokenProvider TokenProvider
if tokenFile != "" {
log.Printf("Using token authorization from %s\n", tokenFile)
tokenProvider = TokenFileProvider(tokenFile)
}
// Create configuration data structure.
config = NewConfig(title, ver, runtimeVersion, basePath, serverToken, stunURIs, turnURIs, tokenProvider != nil, globalRoomid, defaultRoomEnabled, usersEnabled, usersAllowRegistration, usersMode, plugin)
// Load remaining configuration items.
config = NewConfig(runtime, tokenProvider != nil)
// Load templates.
tt := template.New("")
@ -369,10 +281,7 @@ func runner(runtime phoenix.Runtime) error { @@ -369,10 +281,7 @@ func runner(runtime phoenix.Runtime) error {
}
// Create realm string from config.
computedRealm := fmt.Sprintf("%s.%s", serverRealm, serverToken)
// Create our hub instance.
hub := NewHub(runtimeVersion, config, sessionSecret, encryptionSecret, turnSecret, computedRealm)
computedRealm := fmt.Sprintf("%s.%s", serverRealm, config.Token)
// Set number of go routines if it is 1
if goruntime.GOMAXPROCS(0) == 1 {
@ -405,7 +314,7 @@ func runner(runtime phoenix.Runtime) error { @@ -405,7 +314,7 @@ func runner(runtime phoenix.Runtime) error {
// Create router.
router := mux.NewRouter()
r := router.PathPrefix(basePath).Subrouter().StrictSlash(true)
r := router.PathPrefix(config.B).Subrouter().StrictSlash(true)
// HTTP listener support.
if _, err = runtime.GetString("http", "listen"); err == nil {
@ -426,12 +335,20 @@ func runner(runtime phoenix.Runtime) error { @@ -426,12 +335,20 @@ func runner(runtime phoenix.Runtime) error {
}
// Add handlers.
buddyImages := NewImageCache()
codec := NewCodec()
roomManager := NewRoomManager(config, codec)
hub := NewHub(config, sessionSecret, encryptionSecret, turnSecret, codec)
tickets := NewTickets(sessionSecret, encryptionSecret, computedRealm)
sessionManager := NewSessionManager(config, tickets, sessionSecret)
statsManager := NewStatsManager(hub, roomManager, sessionManager)
channellingAPI := NewChannellingAPI(config, roomManager, tickets, sessionManager, statsManager, hub, hub, hub, roomManager, buddyImages)
r.HandleFunc("/", httputils.MakeGzipHandler(mainHandler))
r.Handle("/static/img/buddy/{flags}/{imageid}/{idx:.*}", http.StripPrefix(basePath, makeImageHandler(hub, time.Duration(24)*time.Hour)))
r.Handle("/static/{path:.*}", http.StripPrefix(basePath, httputils.FileStaticServer(http.Dir(rootFolder))))
r.Handle("/robots.txt", http.StripPrefix(basePath, http.FileServer(http.Dir(path.Join(rootFolder, "static")))))
r.Handle("/favicon.ico", http.StripPrefix(basePath, http.FileServer(http.Dir(path.Join(rootFolder, "static", "img")))))
r.Handle("/ws", makeWsHubHandler(hub))
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("/robots.txt", http.StripPrefix(config.B, http.FileServer(http.Dir(path.Join(rootFolder, "static")))))
r.Handle("/favicon.ico", http.StripPrefix(config.B, http.FileServer(http.Dir(path.Join(rootFolder, "static", "img")))))
r.Handle("/ws", makeWSHandler(statsManager, sessionManager, codec, channellingAPI))
r.HandleFunc("/{room}", httputils.MakeGzipHandler(roomHandler))
// Add API end points.
@ -440,16 +357,16 @@ func runner(runtime phoenix.Runtime) error { @@ -440,16 +357,16 @@ func runner(runtime phoenix.Runtime) error {
api.AddResource(&Rooms{}, "/rooms")
api.AddResource(config, "/config")
api.AddResourceWithWrapper(&Tokens{tokenProvider}, httputils.MakeGzipHandler, "/tokens")
if usersEnabled {
if config.UsersEnabled {
// Create Users handler.
users := NewUsers(hub, usersMode, serverRealm, runtime)
api.AddResource(&Sessions{hub: hub, users: users}, "/sessions/{id}/")
if usersAllowRegistration {
users := NewUsers(hub, tickets, sessionManager, config.UsersMode, serverRealm, runtime)
api.AddResource(&Sessions{tickets, hub, users}, "/sessions/{id}/")
if config.UsersAllowRegistration {
api.AddResource(users, "/users")
}
}
if statsEnabled {
api.AddResourceWithWrapper(&Stats{hub: hub}, httputils.MakeGzipHandler, "/stats")
api.AddResourceWithWrapper(&Stats{statsManager}, httputils.MakeGzipHandler, "/stats")
log.Println("Stats are enabled!")
}
@ -457,7 +374,7 @@ func runner(runtime phoenix.Runtime) error { @@ -457,7 +374,7 @@ func runner(runtime phoenix.Runtime) error {
if extraFolder != "" {
extraFolderStatic := path.Join(extraFolder, "static")
if _, err = os.Stat(extraFolderStatic); err == nil {
r.Handle("/extra/static/{path:.*}", http.StripPrefix(fmt.Sprintf("%sextra", basePath), httputils.FileStaticServer(http.Dir(extraFolder))))
r.Handle("/extra/static/{path:.*}", http.StripPrefix(fmt.Sprintf("%sextra", config.B), httputils.FileStaticServer(http.Dir(extraFolder))))
log.Printf("Added URL handler /extra/static/... for static files in %s/...\n", extraFolderStatic)
}
}
@ -482,7 +399,7 @@ func boot() error { @@ -482,7 +399,7 @@ func boot() error {
return nil
}
return phoenix.NewServer("server", "").
return phoenix.NewServer("server", version).
Config(configPath).
Log(logPath).
CpuProfile(cpuprofile).

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

@ -0,0 +1,201 @@ @@ -0,0 +1,201 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 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/>.
*
*/
package main
import (
"log"
"sync"
)
type RoomStatusManager interface {
RoomUsers(*Session) []*DataSession
JoinRoom(string, *DataRoomCredentials, *Session, Sender) (*DataRoom, error)
LeaveRoom(*Session)
UpdateRoom(*Session, *DataRoom) (*DataRoom, error)
}
type Broadcaster interface {
Broadcast(*Session, interface{})
}
type RoomStats interface {
RoomInfo(includeSessions bool) (count int, sessionInfo map[string][]string)
}
type RoomManager interface {
RoomStatusManager
Broadcaster
RoomStats
}
type roomManager struct {
sync.RWMutex
*Config
OutgoingEncoder
roomTable map[string]RoomWorker
}
func NewRoomManager(config *Config, encoder OutgoingEncoder) RoomManager {
return &roomManager{
sync.RWMutex{},
config,
encoder,
make(map[string]RoomWorker),
}
}
func (rooms *roomManager) RoomUsers(session *Session) []*DataSession {
if room, ok := rooms.Get(session.Roomid); ok {
return room.GetUsers()
}
// TODO(lcooper): This should return an error.
return []*DataSession{}
}
func (rooms *roomManager) JoinRoom(id string, credentials *DataRoomCredentials, session *Session, sender Sender) (*DataRoom, error) {
if id == "" && !rooms.DefaultRoomEnabled {
return nil, NewDataError("default_room_disabled", "The default room is not enabled")
}
roomWorker, err := rooms.GetOrCreate(id, credentials, session)
if err != nil {
return nil, err
}
return roomWorker.Join(credentials, session, sender)
}
func (rooms *roomManager) LeaveRoom(session *Session) {
if room, ok := rooms.Get(session.Roomid); ok {
room.Leave(session)
}
}
func (rooms *roomManager) UpdateRoom(session *Session, room *DataRoom) (*DataRoom, error) {
if !session.Hello || session.Roomid != room.Name {
return nil, NewDataError("not_in_room", "Cannot update other rooms")
}
// XXX(lcooper): We'll process and send documents without this field
// correctly, however clients cannot not handle it currently.
room.Type = "Room"
if roomWorker, ok := rooms.Get(session.Roomid); ok {
return room, roomWorker.Update(room)
}
// TODO(lcooper): We should almost certainly return an error in this case.
return room, nil
}
func (rooms *roomManager) Broadcast(session *Session, m interface{}) {
outgoing := &DataOutgoing{
From: session.Id,
A: session.Attestation(),
Data: m,
}
message, err := rooms.EncodeOutgoing(outgoing)
if err != nil {
return
}
id := session.Roomid
if id != "" && id == rooms.globalRoomID {
rooms.RLock()
for _, room := range rooms.roomTable {
room.Broadcast(session, message)
}
rooms.RUnlock()
} else if room, ok := rooms.Get(id); ok {
room.Broadcast(session, message)
} else {
log.Printf("No room named %s found for broadcast message %#v", id, m)
}
message.Decref()
}
func (rooms *roomManager) RoomInfo(includeSessions bool) (count int, sessionInfo map[string][]string) {
rooms.RLock()
defer rooms.RUnlock()
count = len(rooms.roomTable)
if includeSessions {
sessionInfo := make(map[string][]string)
for roomid, room := range rooms.roomTable {
sessionInfo[roomid] = room.SessionIDs()
}
}
return
}
func (rooms *roomManager) Get(id string) (room RoomWorker, ok bool) {
rooms.RLock()
room, ok = rooms.roomTable[id]
rooms.RUnlock()
return
}
func (rooms *roomManager) GetOrCreate(id string, credentials *DataRoomCredentials, session *Session) (RoomWorker, error) {
if room, ok := rooms.Get(id); ok {
return room, nil
}
rooms.Lock()
// Need to re-check, another thread might have created the room
// while we waited for the lock.
if room, ok := rooms.roomTable[id]; ok {
rooms.Unlock()
return room, nil
}
if rooms.UsersEnabled && rooms.AuthorizeRoomCreation && !session.Authenticated() {
rooms.Unlock()
return nil, NewDataError("room_join_requires_account", "Room creation requires a user account")
}
room := NewRoomWorker(rooms, id, credentials)
rooms.roomTable[id] = room
rooms.Unlock()
go func() {
// Start room, this blocks until room expired.
room.Start()
// Cleanup room when we are done.
rooms.Lock()
defer rooms.Unlock()
delete(rooms.roomTable, id)
log.Printf("Cleaned up room '%s'\n", id)
}()
return room, nil
}
func (rooms *roomManager) GlobalUsers() []*roomUser {
if rooms.globalRoomID == "" {
return make([]*roomUser, 0)
}
rooms.RLock()
if room, ok := rooms.roomTable[rooms.globalRoomID]; ok {
rooms.RUnlock()
return room.Users()
}
rooms.RUnlock()
return make([]*roomUser, 0)
}

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

@ -0,0 +1,79 @@ @@ -0,0 +1,79 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 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/>.
*
*/
package main
import (
"testing"
)
func NewTestRoomManager() (RoomManager, *Config) {
config := &Config{}
return NewRoomManager(config, nil), config
}
func Test_RoomManager_JoinRoom_ReturnsAnErrorForUnauthenticatedSessionsWhenCreationRequiresAnAccount(t *testing.T) {
roomManager, config := NewTestRoomManager()
config.UsersEnabled = true
config.AuthorizeRoomCreation = true
unauthenticatedSession := &Session{}
_, err := roomManager.JoinRoom("foo", nil, unauthenticatedSession, nil)
assertDataError(t, err, "room_join_requires_account")
authenticatedSession := &Session{userid: "9870457"}
_, err = roomManager.JoinRoom("foo", nil, authenticatedSession, nil)
if err != nil {
t.Fatalf("Unexpected error %v joining room while authenticated", err)
}
_, err = roomManager.JoinRoom("foo", nil, unauthenticatedSession, nil)
if err != nil {
t.Fatalf("Unexpected error %v joining room while unauthenticated", err)
}
}
func Test_RoomManager_UpdateRoom_ReturnsAnErrorIfNoRoomHasBeenJoined(t *testing.T) {
roomManager, _ := NewTestRoomManager()
_, err := roomManager.UpdateRoom(&Session{}, nil)
assertDataError(t, err, "not_in_room")
}
func Test_RoomManager_UpdateRoom_ReturnsAnErrorIfUpdatingAnUnjoinedRoom(t *testing.T) {
roomManager, _ := NewTestRoomManager()
session := &Session{Hello: true, Roomid: "foo"}
_, err := roomManager.UpdateRoom(session, &DataRoom{Name: "bar"})
assertDataError(t, err, "not_in_room")
}
func Test_RoomManager_UpdateRoom_ReturnsACorrectlyTypedDocument(t *testing.T) {
roomManager, _ := NewTestRoomManager()
session := &Session{Hello: true, Roomid: "foo"}
room, err := roomManager.UpdateRoom(session, &DataRoom{Name: session.Roomid})
if err != nil {
t.Fatalf("Unexpected error %v updating room", err)
}
if room.Type != "Room" {
t.Errorf("Expected document type to be Room, but was %v", room.Type)
}
}

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

@ -22,7 +22,7 @@ @@ -22,7 +22,7 @@
package main
import (
"encoding/json"
"crypto/subtle"
"log"
"sync"
"time"
@ -33,39 +33,53 @@ const ( @@ -33,39 +33,53 @@ const (
roomExpiryDuration = 60 * time.Second
)
type RoomConnectionUpdate struct {
Id string
Sessionid string
Status bool
Connection *Connection
type RoomWorker interface {
Start()
SessionIDs() []string
Users() []*roomUser
Update(*DataRoom) error
GetUsers() []*DataSession
Broadcast(*Session, Buffer)
Join(*DataRoomCredentials, *Session, Sender) (*DataRoom, error)
Leave(*Session)
}
type RoomWorker struct {
type roomWorker struct {
// References.
h *Hub
manager *roomManager
// Data handling.
workers chan (func())
expired chan (bool)
connections map[string]*Connection
timer *time.Timer
mutex sync.RWMutex
workers chan (func())
expired chan (bool)
users map[string]*roomUser
timer *time.Timer
mutex sync.RWMutex
// Metadata.
Id string
Id string
credentials *DataRoomCredentials
}
func NewRoomWorker(h *Hub, id string) *RoomWorker {
type roomUser struct {
*Session
Sender
}
func NewRoomWorker(manager *roomManager, id string, credentials *DataRoomCredentials) RoomWorker {
log.Printf("Creating worker for room '%s'\n", id)
r := &RoomWorker{
h: h,
Id: id,
r := &roomWorker{
manager: manager,
Id: id,
workers: make(chan func(), roomMaxWorkers),
expired: make(chan bool),
users: make(map[string]*roomUser),
}
if credentials != nil && len(credentials.PIN) > 0 {
r.credentials = credentials
}
r.workers = make(chan func(), roomMaxWorkers)
r.expired = make(chan bool)
r.connections = make(map[string]*Connection)
// Create expire timer.
r.timer = time.AfterFunc(roomExpiryDuration, func() {
@ -76,7 +90,7 @@ func NewRoomWorker(h *Hub, id string) *RoomWorker { @@ -76,7 +90,7 @@ func NewRoomWorker(h *Hub, id string) *RoomWorker {
}
func (r *RoomWorker) Start() {
func (r *roomWorker) Start() {
// Main blocking worker.
L:
@ -90,7 +104,7 @@ L: @@ -90,7 +104,7 @@ L:
//fmt.Println("Work room expired", r.Id)
//fmt.Println("Work room expired", r.Id, len(r.connections))
r.mutex.RLock()
if len(r.connections) == 0 {
if len(r.users) == 0 {
// Cleanup room when it is empty.
r.mutex.RUnlock()
log.Printf("Room worker not in use - cleaning up '%s'\n", r.Id)
@ -107,19 +121,29 @@ L: @@ -107,19 +121,29 @@ L:
}
func (r *RoomWorker) GetConnections() []*Connection {
func (r *roomWorker) SessionIDs() []string {
r.mutex.RLock()
defer r.mutex.RUnlock()
sessions := make([]string, 0, len(r.users))
for id := range r.users {
sessions = append(sessions, id)
}
return sessions
}
func (r *roomWorker) Users() []*roomUser {
r.mutex.RLock()
defer r.mutex.RUnlock()
connections := make([]*Connection, 0, len(r.connections))
for _, connection := range r.connections {
connections = append(connections, connection)
users := make([]*roomUser, 0, len(r.users))
for _, user := range r.users {
users = append(users, user)
}
return connections
return users
}
func (r *RoomWorker) Run(f func()) bool {
func (r *roomWorker) Run(f func()) bool {
select {
case r.workers <- f:
@ -131,13 +155,30 @@ func (r *RoomWorker) Run(f func()) bool { @@ -131,13 +155,30 @@ func (r *RoomWorker) Run(f func()) bool {
}
func (r *RoomWorker) usersHandler(c *Connection) {
func (r *roomWorker) Update(room *DataRoom) error {
fault := make(chan error, 1)
worker := func() {
r.mutex.Lock()
if room.Credentials != nil {
if len(room.Credentials.PIN) > 0 {
r.credentials = room.Credentials
} else {
r.credentials = nil
}
}
r.mutex.Unlock()
fault <- nil
}
r.Run(worker)
return <-fault
}
func (r *roomWorker) GetUsers() []*DataSession {
out := make(chan []*DataSession, 1)
worker := func() {
sessions := &DataSessions{Type: "Users"}
var sl []*DataSession
appender := func(ec *Connection) bool {
ecsession := ec.Session
appender := func(user *roomUser) bool {
ecsession := user.Session
if ecsession != nil {
session := ecsession.Data()
session.Type = "Online"
@ -150,73 +191,95 @@ func (r *RoomWorker) usersHandler(c *Connection) { @@ -150,73 +191,95 @@ func (r *RoomWorker) usersHandler(c *Connection) {
return true
}
r.mutex.RLock()
sl = make([]*DataSession, 0, len(r.connections))
sl = make([]*DataSession, 0, len(r.users))
// Include connections in this room.
for _, ec := range r.connections {
if !appender(ec) {
for _, user := range r.users {
if !appender(user) {
break
}
}
r.mutex.RUnlock()
// Include connections to global room.
for _, ec := range c.h.GetGlobalConnections() {
for _, ec := range r.manager.GlobalUsers() {
if !appender(ec) {
break
}
}
sessions.Users = sl
sessionsJson := c.h.buffers.New()
encoder := json.NewEncoder(sessionsJson)
err := encoder.Encode(&DataOutgoing{From: c.Id, Data: sessions})
if err != nil {
log.Println("Users error while encoding JSON", err)
sessionsJson.Decref()
return
}
c.send(sessionsJson)
sessionsJson.Decref()
out <- sl
}
r.Run(worker)
return <-out
}
func (r *RoomWorker) broadcastHandler(m *MessageRequest) {
func (r *roomWorker) Broadcast(session *Session, message Buffer) {
worker := func() {
r.mutex.RLock()
defer r.mutex.RUnlock()
for id, ec := range r.connections {
if id == m.From {
for id, user := range r.users {
if id == session.Id {
// Skip broadcast to self.
continue
}
//fmt.Printf("%s\n", m.Message)
ec.send(m.Message)
user.Send(message)
}
m.Message.Decref()
message.Decref()
}
m.Message.Incref()
message.Incref()
r.Run(worker)
}
func (r *RoomWorker) connectionHandler(rcu *RoomConnectionUpdate) {
type joinResult struct {
*DataRoom
error
}
func (r *roomWorker) Join(credentials *DataRoomCredentials, session *Session, sender Sender) (*DataRoom, error) {
results := make(chan joinResult, 1)
worker := func() {
r.mutex.Lock()
defer r.mutex.Unlock()
if rcu.Status {
r.connections[rcu.Sessionid] = rcu.Connection
} else {
if _, ok := r.connections[rcu.Sessionid]; ok {
delete(r.connections, rcu.Sessionid)
if r.credentials == nil && credentials != nil {
results <- joinResult{nil, NewDataError("authorization_not_required", "No credentials may be provided for this room")}
r.mutex.Unlock()
return
} else if r.credentials != nil {
if credentials == nil {
results <- joinResult{nil, NewDataError("authorization_required", "Valid credentials are required to join this room")}
r.mutex.Unlock()
return
}
if len(r.credentials.PIN) != len(credentials.PIN) || subtle.ConstantTimeCompare([]byte(r.credentials.PIN), []byte(credentials.PIN)) != 1 {
results <- joinResult{nil, NewDataError("invalid_credentials", "The provided credentials are incorrect")}
r.mutex.Unlock()
return
}
}
}
r.users[session.Id] = &roomUser{session, sender}
// NOTE(lcooper): Needs to be a copy, else we risk races with
// a subsequent modification of room properties.
result := joinResult{&DataRoom{Name: r.Id}, nil}
r.mutex.Unlock()
results <- result
}
r.Run(worker)
result := <-results
return result.DataRoom, result.error
}
func (r *roomWorker) Leave(session *Session) {
worker := func() {
r.mutex.Lock()
defer r.mutex.Unlock()
if _, ok := r.users[session.Id]; ok {
delete(r.users, session.Id)
}
}
r.Run(worker)
}

124
src/app/spreed-webrtc-server/roomworker_test.go

@ -0,0 +1,124 @@ @@ -0,0 +1,124 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 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/>.
*
*/
package main
import (
"testing"
)
const (
testRoomName string = "a-room-name"
)
func NewTestRoomWorker() RoomWorker {
worker := NewRoomWorker(&roomManager{Config: &Config{}}, testRoomName, nil)
go worker.Start()
return worker
}
func NewTestRoomWorkerWithPIN(t *testing.T) (RoomWorker, string) {
pin := "asdf"
worker := NewRoomWorker(&roomManager{Config: &Config{}}, testRoomName, &DataRoomCredentials{PIN: pin})
go worker.Start()
return worker, pin
}
func Test_RoomWorker_Join_SucceedsWhenNoCredentialsAreRequired(t *testing.T) {
worker := NewTestRoomWorker()
_, err := worker.Join(nil, &Session{}, nil)
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
if userCount := len(worker.GetUsers()); userCount != 1 {
t.Errorf("Expected join to have been accepted but room contains %d users", userCount)
}
}
func Test_RoomWorker_Join_FailsIfCredentialsAreGivenWhenUnneeded(t *testing.T) {
worker := NewTestRoomWorker()
_, err := worker.Join(&DataRoomCredentials{}, &Session{}, nil)
assertDataError(t, err, "authorization_not_required")
if userCount := len(worker.GetUsers()); userCount != 0 {
t.Errorf("Expected join to have been rejected but room contains %d users", userCount)
}
}
func Test_RoomWorker_Join_FailsIfNoCredentialsAreGiven(t *testing.T) {
worker, _ := NewTestRoomWorkerWithPIN(t)
_, err := worker.Join(nil, &Session{}, nil)
assertDataError(t, err, "authorization_required")
if userCount := len(worker.GetUsers()); userCount != 0 {
t.Errorf("Expected join to have been rejected but room contains %d users", userCount)
}
}
func Test_RoomWorker_Join_FailsIfIncorrectCredentialsAreGiven(t *testing.T) {
worker, _ := NewTestRoomWorkerWithPIN(t)
_, err := worker.Join(&DataRoomCredentials{PIN: "adfs"}, &Session{}, nil)
assertDataError(t, err, "invalid_credentials")
if userCount := len(worker.GetUsers()); userCount != 0 {
t.Errorf("Expected join to have been rejected but room contains %d users", userCount)
}
}
func Test_RoomWorker_Join_SucceedsWhenTheCorrectPINIsGiven(t *testing.T) {
worker, pin := NewTestRoomWorkerWithPIN(t)
if _, err := worker.Join(&DataRoomCredentials{PIN: pin}, &Session{}, nil); err != nil {
t.Fatalf("Unexpected error %v", err)
}
if len(worker.GetUsers()) < 1 {
t.Error("Expected join to have been accepted but room contains no users")
}
}
func Test_RoomWorker_Update_AllowsClearingCredentials(t *testing.T) {
worker, _ := NewTestRoomWorkerWithPIN(t)
if err := worker.Update(&DataRoom{Credentials: &DataRoomCredentials{PIN: ""}}); err != nil {
t.Fatalf("Failed to update room: %v", err)
}
_, err := worker.Join(&DataRoomCredentials{}, &Session{}, nil)
assertDataError(t, err, "authorization_not_required")
}
func Test_RoomWorker_Update_RetainsCredentialsWhenOtherPropertiesAreUpdated(t *testing.T) {
worker, pin := NewTestRoomWorkerWithPIN(t)
if err := worker.Update(&DataRoom{}); err != nil {
t.Fatalf("Failed to update room: %v", err)
}
if _, err := worker.Join(&DataRoomCredentials{PIN: pin}, &Session{}, nil); err != nil {
t.Fatalf("Unexpected error joining room %v", err)
}
}

280
src/app/spreed-webrtc-server/server.go

@ -1,280 +0,0 @@ @@ -1,280 +0,0 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 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/>.
*
*/
package main
import (
"encoding/json"
"log"
"sync/atomic"
"time"
)
const (
maxConferenceSize = 100
)
type Server struct {
}
func (s *Server) OnRegister(c *Connection) {
//log.Println("OnRegister", c.id)
if token, err := c.h.EncodeSessionToken(c.Session.Token()); err == nil {
log.Println("Created new session token", len(token), token)
// Send stuff back.
s.Unicast(c, c.Id, &DataSelf{
Type: "Self",
Id: c.Id,
Sid: c.Session.Sid,
Userid: c.Session.Userid(),
Suserid: c.h.CreateSuserid(c.Session),
Token: token,
Version: c.h.version,
Turn: c.h.CreateTurnData(c.Id),
Stun: c.h.config.StunURIs,
})
} else {
log.Println("Error in OnRegister", c.Idx, err)
}
}
func (s *Server) OnUnregister(c *Connection) {
//log.Println("OnUnregister", c.id)
dsl := c.Session.DataSessionLeft("hard")
if c.Hello {
s.UpdateRoomConnection(c, &RoomConnectionUpdate{Id: c.Roomid})
s.Broadcast(c, dsl)
}
c.Session.RunForAllSubscribers(func(session *Session) {
log.Println("Notifying subscriber that we are gone", c.Id, session.Id)
s.Unicast(c, session.Id, dsl)
})
}
func (s *Server) OnText(c *Connection, b Buffer) {
//log.Printf("OnText from %d: %s\n", c.Id, b)
var msg DataIncoming
err := json.Unmarshal(b.Bytes(), &msg)
if err != nil {
log.Println("OnText error while decoding JSON", err)
log.Printf("JSON:\n%s\n", b)
return
}
switch msg.Type {
case "Self":
s.OnRegister(c)
case "Hello":
//log.Println("Hello", msg.Hello, c.Idx)
// TODO(longsleep): Filter room id and user agent.
s.UpdateSession(c, &SessionUpdate{Types: []string{"Roomid", "Ua"}, Roomid: msg.Hello.Id, Ua: msg.Hello.Ua})
if c.Hello && c.Roomid != msg.Hello.Id {
// Room changed.
s.UpdateRoomConnection(c, &RoomConnectionUpdate{Id: c.Roomid})
s.Broadcast(c, c.Session.DataSessionLeft("soft"))
}
c.Roomid = msg.Hello.Id
if c.h.config.DefaultRoomEnabled || !c.h.isDefaultRoomid(c.Roomid) {
c.Hello = true
s.UpdateRoomConnection(c, &RoomConnectionUpdate{Id: c.Roomid, Status: true})
s.Broadcast(c, c.Session.DataSessionJoined())
} else {
c.Hello = false
}
case "Offer":
// TODO(longsleep): Validate offer
s.Unicast(c, msg.Offer.To, msg.Offer)
case "Candidate":
// TODO(longsleep): Validate candidate
s.Unicast(c, msg.Candidate.To, msg.Candidate)
case "Answer":
// TODO(longsleep): Validate Answer
s.Unicast(c, msg.Answer.To, msg.Answer)
case "Users":
if c.Hello {
s.Users(c)
}
case "Authentication":
if msg.Authentication.Authentication != nil && s.Authenticate(c, msg.Authentication.Authentication) {
s.OnRegister(c)
if c.Hello {
s.Broadcast(c, c.Session.DataSessionStatus())
}
}
case "Bye":
s.Unicast(c, msg.Bye.To, msg.Bye)
case "Status":
//log.Println("Status", msg.Status)
s.UpdateSession(c, &SessionUpdate{Types: []string{"Status"}, Status: msg.Status.Status})
if c.Hello {
s.Broadcast(c, c.Session.DataSessionStatus())
}
case "Chat":
// TODO(longsleep): Limit sent chat messages per incoming connection.
if !msg.Chat.Chat.NoEcho {
s.Unicast(c, c.Id, msg.Chat)
}
msg.Chat.Chat.Time = time.Now().Format(time.RFC3339)
if msg.Chat.To == "" {
// TODO(longsleep): Check if chat broadcast is allowed.
if c.Hello {
atomic.AddUint64(&c.h.broadcastChatMessages, 1)
s.Broadcast(c, msg.Chat)
}
} else {
if msg.Chat.Chat.Status != nil && msg.Chat.Chat.Status.ContactRequest != nil {
err = s.ContactRequest(c, msg.Chat.To, msg.Chat.Chat.Status.ContactRequest)
if err != nil {
log.Println("Ignoring invalid contact request.", err)
return
}
msg.Chat.Chat.Status.ContactRequest.Userid = c.Session.Userid()
}
atomic.AddUint64(&c.h.unicastChatMessages, 1)
s.Unicast(c, msg.Chat.To, msg.Chat)
if msg.Chat.Chat.Mid != "" {
// Send out delivery confirmation status chat message.
s.Unicast(c, c.Id, &DataChat{To: msg.Chat.To, Type: "Chat", Chat: &DataChatMessage{Mid: msg.Chat.Chat.Mid, Status: &DataChatStatus{State: "sent"}}})
}
}
case "Conference":
// Check conference maximum size.
if len(msg.Conference.Conference) > maxConferenceSize {
log.Println("Refusing to create conference above limit.", len(msg.Conference.Conference))
} else {
// Send conference update to anyone.
for _, id := range msg.Conference.Conference {
if id != c.Id {
//log.Println("participant", id)
s.Unicast(c, id, msg.Conference)
}
}
}
case "Alive":
s.Alive(c, msg.Alive, msg.Iid)
case "Sessions":
s.Sessions(c, msg.Sessions.Sessions, msg.Iid)
default:
log.Println("OnText unhandled message type", msg.Type)
}
}
func (s *Server) Unicast(c *Connection, to string, m interface{}) {
outgoing := &DataOutgoing{From: c.Id, To: to, Data: m}
if !c.isClosing && c.Id != to {
outgoing.A = c.Session.Attestation()
}
b := c.h.buffers.New()
encoder := json.NewEncoder(b)
err := encoder.Encode(outgoing)
if err != nil {
b.Decref()
log.Println("Unicast error while encoding JSON", err)
return
}
//log.Println("Unicast", b)
var msg = &MessageRequest{From: c.Id, To: to, Message: b}
c.h.unicastHandler(msg)
b.Decref()
}
func (s *Server) Broadcast(c *Connection, m interface{}) {
b := c.h.buffers.New()
encoder := json.NewEncoder(b)
err := encoder.Encode(&DataOutgoing{From: c.Id, Data: m, A: c.Session.Attestation()})
if err != nil {
b.Decref()
log.Println("Broadcast error while encoding JSON", err)
return
}
if c.h.isGlobalRoomid(c.Roomid) {
c.h.RunForAllRooms(func(room *RoomWorker) {
var msg = &MessageRequest{From: c.Id, Message: b, Id: room.Id}
room.broadcastHandler(msg)
})
} else {
var msg = &MessageRequest{From: c.Id, Message: b, Id: c.Roomid}
room := c.h.GetRoom(c.Roomid)
room.broadcastHandler(msg)
}
b.Decref()
}
func (s *Server) Alive(c *Connection, alive *DataAlive, iid string) {
c.h.aliveHandler(c, alive, iid)
}
func (s *Server) Sessions(c *Connection, srq *DataSessionsRequest, iid string) {
c.h.sessionsHandler(c, srq, iid)
}
func (s *Server) UpdateSession(c *Connection, su *SessionUpdate) uint64 {
su.Id = c.Id
return c.h.sessionupdateHandler(su)
}
func (s *Server) ContactRequest(c *Connection, to string, cr *DataContactRequest) (err error) {
return c.h.contactrequestHandler(c, to, cr)
}
func (s *Server) Users(c *Connection) {
room := c.h.GetRoom(c.Roomid)
room.usersHandler(c)
}
func (s *Server) Authenticate(c *Connection, st *SessionToken) bool {
err := c.h.authenticateHandler(c.Session, st, "")
if err == nil {
log.Println("Authentication success", c.Id, c.Idx, c.Session.Userid)
return true
} else {
log.Println("Authentication failed", err, c.Id, c.Idx, st.Userid, st.Nonce)
return false
}
}
func (s *Server) UpdateRoomConnection(c *Connection, rcu *RoomConnectionUpdate) {
rcu.Sessionid = c.Id
rcu.Connection = c
room := c.h.GetRoom(c.Roomid)
room.connectionHandler(rcu)
}

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

@ -39,26 +39,28 @@ type Session struct { @@ -39,26 +39,28 @@ type Session struct {
Status interface{}
Nonce string
Prio int
Hello bool
Roomid string
mutex sync.RWMutex
userid string
fake bool
stamp int64
attestation *SessionAttestation
attestations *securecookie.SecureCookie
subscriptions map[string]*Session
subscribers map[string]*Session
h *Hub
}
func NewSession(h *Hub, id, sid string) *Session {
func NewSession(attestations *securecookie.SecureCookie, id, sid string) *Session {
session := &Session{
Id: id,
Sid: sid,
Prio: 100,
stamp: time.Now().Unix(),
attestations: attestations,
subscriptions: make(map[string]*Session),
subscribers: make(map[string]*Session),
h: h,
}
session.NewAttestation()
return session
@ -171,6 +173,13 @@ func (s *Session) Authorize(realm string, st *SessionToken) (string, error) { @@ -171,6 +173,13 @@ 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 {
s.mutex.Lock()
@ -288,35 +297,27 @@ func (s *Session) DataSessionStatus() *DataSession { @@ -288,35 +297,27 @@ func (s *Session) DataSessionStatus() *DataSession {
}
func (s *Session) NewAttestation() {
s.attestation = &SessionAttestation{
s: s,
}
s.attestation.Update()
}
func (s *Session) Attestation() (attestation string) {
s.mutex.RLock()
attestation = s.attestation.Token()
s.mutex.RUnlock()
return
}
func (s *Session) UpdateAttestation() {
s.mutex.Lock()
s.attestation.Update()
s.mutex.Unlock()
}
type SessionUpdate struct {
Id string
Types []string
Roomid string
Ua string
Prio int
Status interface{}
@ -336,39 +337,31 @@ type SessionAttestation struct { @@ -336,39 +337,31 @@ type SessionAttestation struct {
}
func (sa *SessionAttestation) Update() (string, error) {
token, err := sa.Encode()
if err == nil {
sa.token = token
sa.refresh = time.Now().Unix() + 180 // expires after 3 minutes
}
return token, err
}
func (sa *SessionAttestation) Token() (token string) {
if sa.refresh < time.Now().Unix() {
token, _ = sa.Update()
} else {
token = sa.token
}
return
}
func (sa *SessionAttestation) Encode() (string, error) {
return sa.s.h.attestations.Encode("attestation", sa.s.Id)
return sa.s.attestations.Encode("attestation", sa.s.Id)
}
func (sa *SessionAttestation) Decode(token string) (string, error) {
var id string
err := sa.s.h.attestations.Decode("attestation", token, &id)
err := sa.s.attestations.Decode("attestation", token, &id)
return id, err
}
func init() {

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

@ -0,0 +1,173 @@ @@ -0,0 +1,173 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 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/>.
*
*/
package main
import (
"crypto/sha256"
"net/http"
"sync"
"github.com/gorilla/securecookie"
)
type UserStats interface {
UserInfo(bool) (int, map[string]*DataUser)
}
type SessionManager interface {
UserStats
RetrieveUsersWith(func(*http.Request) (string, error))
CreateSession(*http.Request) *Session
DestroySession(*Session)
Authenticate(*Session, *SessionToken, string) error
GetUserSessions(session *Session, id string) []*DataSession
}
type sessionManager struct {
Tickets
sync.RWMutex
config *Config
userTable map[string]*User
fakesessionTable map[string]*Session
useridRetriever func(*http.Request) (string, error)
attestations *securecookie.SecureCookie
}
func NewSessionManager(config *Config, tickets Tickets, sessionSecret []byte) SessionManager {
sessionManager := &sessionManager{
tickets,
sync.RWMutex{},
config,
make(map[string]*User),
make(map[string]*Session),
nil,
nil,
}
sessionManager.attestations = securecookie.New(sessionSecret, nil)
sessionManager.attestations.MaxAge(300) // 5 minutes
sessionManager.attestations.HashFunc(sha256.New)
return sessionManager
}
func (sessionManager *sessionManager) UserInfo(details bool) (userCount int, users map[string]*DataUser) {
sessionManager.RLock()
defer sessionManager.RUnlock()
userCount = len(sessionManager.userTable)
if details {
users := make(map[string]*DataUser)
for userid, user := range sessionManager.userTable {
users[userid] = user.Data()
}
}
return
}
func (sessionManager *sessionManager) RetrieveUsersWith(retriever func(*http.Request) (string, error)) {
sessionManager.useridRetriever = retriever
}
func (sessionManager *sessionManager) CreateSession(request *http.Request) *Session {
request.ParseForm()
token := request.FormValue("t")
st := sessionManager.DecodeSessionToken(token)
var userid string
if sessionManager.config.UsersEnabled {
if sessionManager.useridRetriever != nil {
userid, _ = sessionManager.useridRetriever(request)
if userid == "" {
userid = st.Userid
}
}
}
session := NewSession(sessionManager.attestations, st.Id, st.Sid)
if userid != "" {
// XXX(lcooper): Should errors be handled here?
sessionManager.Authenticate(session, st, userid)
}
return session
}
func (sessionManager *sessionManager) DestroySession(session *Session) {
session.Close()
sessionManager.Lock()
if suserid := session.Userid(); suserid != "" {
user, ok := sessionManager.userTable[suserid]
if ok && user.RemoveSession(session) {
delete(sessionManager.userTable, suserid)
}
}
sessionManager.Unlock()
}
func (sessionManager *sessionManager) Authenticate(session *Session, st *SessionToken, userid string) error {
if err := session.Authenticate(sessionManager.Realm(), st, userid); err != nil {
return err
}
// Authentication success.
suserid := session.Userid()
sessionManager.Lock()
user, ok := sessionManager.userTable[suserid]
if !ok {
user = NewUser(suserid)
sessionManager.userTable[suserid] = user
}
sessionManager.Unlock()
user.AddSession(session)
return nil
}
func (sessionManager *sessionManager) GetUserSessions(session *Session, userid string) (users []*DataSession) {
var (
user *User
ok bool
)
sessionManager.RLock()
user, ok = sessionManager.userTable[userid]
sessionManager.RUnlock()
if !ok {
// No user. Create fake session.
sessionManager.Lock()
session, ok := sessionManager.fakesessionTable[userid]
if !ok {
st := sessionManager.FakeSessionToken(userid)
session = NewSession(sessionManager.attestations, st.Id, st.Sid)
session.SetUseridFake(st.Userid)
sessionManager.fakesessionTable[userid] = session
}
sessionManager.Unlock()
users = make([]*DataSession, 1, 1)
users[0] = session.Data()
} else {
// Add sessions for foreign user.
users = user.SubscribeSessions(session)
}
return
}

13
src/app/spreed-webrtc-server/sessions.go

@ -23,6 +23,7 @@ package main @@ -23,6 +23,7 @@ package main
import (
"encoding/json"
"errors"
"github.com/gorilla/mux"
"log"
"net/http"
@ -42,7 +43,8 @@ type SessionNonceRequest struct { @@ -42,7 +43,8 @@ type SessionNonceRequest struct {
}
type Sessions struct {
hub *Hub
SessionValidator
SessionStore
users *Users
}
@ -78,7 +80,7 @@ func (sessions *Sessions) Patch(request *http.Request) (int, interface{}, http.H @@ -78,7 +80,7 @@ func (sessions *Sessions) Patch(request *http.Request) (int, interface{}, http.H
}
// Make sure Sid matches session and is valid.
if !sessions.hub.ValidateSession(snr.Id, snr.Sid) {
if !sessions.ValidateSession(snr.Id, snr.Sid) {
log.Println("Session patch failed - validation failed.")
error = true
}
@ -104,7 +106,12 @@ func (sessions *Sessions) Patch(request *http.Request) (int, interface{}, http.H @@ -104,7 +106,12 @@ func (sessions *Sessions) Patch(request *http.Request) (int, interface{}, http.H
var nonce string
if !error {
// FIXME(longsleep): Not running this might reveal error state with a timing attack.
nonce, err = sessions.hub.sessiontokenHandler(&SessionToken{Id: snr.Id, Sid: snr.Sid, Userid: userid})
if session, ok := sessions.GetSession(snr.Id); ok {
nonce, err = session.Authorize(sessions.Realm(), &SessionToken{Id: snr.Id, Sid: snr.Sid, Userid: userid})
} else {
err = errors.New("no such session")
}
if err != nil {
log.Println("Session patch failed - handle failed.", err)
error = true

8
src/app/spreed-webrtc-server/stats.go

@ -33,11 +33,11 @@ type Stat struct { @@ -33,11 +33,11 @@ type Stat struct {
Hub *HubStat `json:"hub"`
}
func NewStat(details bool, h *Hub) *Stat {
func NewStat(details bool, statsGenerator StatsGenerator) *Stat {
stat := &Stat{
details: details,
Runtime: &RuntimeStat{},
Hub: h.Stat(details),
Hub: statsGenerator.Stat(details),
}
stat.Runtime.Read()
return stat
@ -69,12 +69,12 @@ func (stat *RuntimeStat) Read() { @@ -69,12 +69,12 @@ func (stat *RuntimeStat) Read() {
}
type Stats struct {
hub *Hub
StatsGenerator
}
func (stats *Stats) Get(request *http.Request) (int, interface{}, http.Header) {
details := request.Form.Get("details") == "1"
return 200, NewStat(details, stats.hub), http.Header{"Content-Type": {"application/json; charset=utf-8"}, "Access-Control-Allow-Origin": {"*"}}
return 200, NewStat(details, stats), http.Header{"Content-Type": {"application/json; charset=utf-8"}, "Access-Control-Allow-Origin": {"*"}}
}

104
src/app/spreed-webrtc-server/stats_manager.go

@ -0,0 +1,104 @@ @@ -0,0 +1,104 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 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/>.
*
*/
package main
import (
"sync/atomic"
)
type HubStat struct {
Rooms int `json:"rooms"`
Connections int `json:"connections"`
Sessions int `json:"sessions"`
Users int `json:"users"`
Count uint64 `json:"count"`
BroadcastChatMessages uint64 `json:"broadcastchatmessages"`
UnicastChatMessages uint64 `json:"unicastchatmessages"`
IdsInRoom map[string][]string `json:"idsinroom,omitempty"`
SessionsById map[string]*DataSession `json:"sessionsbyid,omitempty"`
UsersById map[string]*DataUser `json:"usersbyid,omitempty"`
ConnectionsByIdx map[string]string `json:"connectionsbyidx,omitempty"`
}
type ConnectionCounter interface {
CountConnection() uint64
}
type StatsCounter interface {
CountBroadcastChat()
CountUnicastChat()
}
type StatsGenerator interface {
Stat(details bool) *HubStat
}
type StatsManager interface {
ConnectionCounter
StatsCounter
StatsGenerator
}
type statsManager struct {
ClientStats
RoomStats
UserStats
connectionCount uint64
broadcastChatMessages uint64
unicastChatMessages uint64
}
func NewStatsManager(clientStats ClientStats, roomStats RoomStats, userStats UserStats) StatsManager {
return &statsManager{clientStats, roomStats, userStats, 0, 0, 0}
}
func (stats *statsManager) CountConnection() uint64 {
return atomic.AddUint64(&stats.connectionCount, 1)
}
func (stats *statsManager) CountBroadcastChat() {
atomic.AddUint64(&stats.broadcastChatMessages, 1)
}
func (stats *statsManager) CountUnicastChat() {
atomic.AddUint64(&stats.unicastChatMessages, 1)
}
func (stats *statsManager) Stat(details bool) *HubStat {
roomCount, roomSessionInfo := stats.RoomInfo(details)
clientCount, sessions, connections := stats.ClientInfo(details)
userCount, users := stats.UserInfo(details)
return &HubStat{
Rooms: roomCount,
Connections: clientCount,
Sessions: clientCount,
Users: userCount,
Count: atomic.LoadUint64(&stats.connectionCount),
BroadcastChatMessages: atomic.LoadUint64(&stats.broadcastChatMessages),
UnicastChatMessages: atomic.LoadUint64(&stats.unicastChatMessages),
IdsInRoom: roomSessionInfo,
SessionsById: sessions,
UsersById: users,
ConnectionsByIdx: connections,
}
}

130
src/app/spreed-webrtc-server/tickets.go

@ -0,0 +1,130 @@ @@ -0,0 +1,130 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 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/>.
*
*/
package main
import (
"crypto/aes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"log"
"github.com/gorilla/securecookie"
)
type SessionValidator interface {
Realm() string
ValidateSession(string, string) bool
}
type SessionEncoder interface {
EncodeSessionToken(*Session) (string, error)
EncodeSessionUserID(*Session) string
}
type Tickets interface {
SessionValidator
SessionEncoder
DecodeSessionToken(token string) (st *SessionToken)
FakeSessionToken(userid string) *SessionToken
}
type tickets struct {
*securecookie.SecureCookie
realm string
tokenName string
encryptionSecret []byte
}
func NewTickets(sessionSecret, encryptionSecret []byte, realm string) Tickets {
tickets := &tickets{
nil,
realm,
fmt.Sprintf("token@%s", realm),
encryptionSecret,
}
tickets.SecureCookie = securecookie.New(sessionSecret, encryptionSecret)
tickets.MaxAge(86400 * 30) // 30 days
tickets.HashFunc(sha256.New)
tickets.BlockFunc(aes.NewCipher)
return tickets
}
func (tickets *tickets) Realm() string {
return tickets.realm
}
func (tickets *tickets) DecodeSessionToken(token string) (st *SessionToken) {
var err error
if token != "" {
st = &SessionToken{}
err = tickets.Decode(tickets.tokenName, token, st)
if err != nil {
log.Println("Error while decoding session token", err)
}
}
if st == nil || err != nil {
sid := NewRandomString(32)
id, _ := tickets.Encode("id", sid)
st = &SessionToken{Id: id, Sid: sid}
log.Println("Created new session id", id)
}
return
}
func (tickets *tickets) FakeSessionToken(userid string) *SessionToken {
st := &SessionToken{}
st.Sid = fmt.Sprintf("fake-%s", NewRandomString(27))
st.Id, _ = tickets.Encode("id", st.Sid)
st.Userid = userid
log.Println("Created new fake session id", st.Id)
return st
}
func (tickets *tickets) ValidateSession(id, sid string) bool {
var decoded string
if err := tickets.Decode("id", id, &decoded); err != nil {
log.Println("Session validation error", err, id, sid)
return false
}
if decoded != sid {
log.Println("Session validation failed", id, sid)
return false
}
return true
}
func (tickets *tickets) EncodeSessionToken(session *Session) (string, error) {
return tickets.Encode(tickets.tokenName, session.Token())
}
func (tickets *tickets) EncodeSessionUserID(session *Session) (suserid string) {
if userid := session.Userid(); userid != "" {
m := hmac.New(sha256.New, tickets.encryptionSecret)
m.Write([]byte(userid))
suserid = base64.StdEncoding.EncodeToString(m.Sum(nil))
}
return
}

32
src/app/spreed-webrtc-server/users.go

@ -291,16 +291,21 @@ func (un *UserNonce) Response() (int, interface{}, http.Header) { @@ -291,16 +291,21 @@ func (un *UserNonce) Response() (int, interface{}, http.Header) {
}
type Users struct {
hub *Hub
SessionValidator
SessionManager
SessionStore
realm string
handler UsersHandler
}
func NewUsers(hub *Hub, mode, realm string, runtime phoenix.Runtime) *Users {
func NewUsers(sessionStore SessionStore, sessionValidator SessionValidator, sessionManager SessionManager, mode, realm string, runtime phoenix.Runtime) *Users {
var users = &Users{
hub: hub,
realm: realm,
sessionValidator,
sessionManager,
sessionStore,
realm,
nil,
}
var handler UsersHandler
@ -309,8 +314,8 @@ func NewUsers(hub *Hub, mode, realm string, runtime phoenix.Runtime) *Users { @@ -309,8 +314,8 @@ func NewUsers(hub *Hub, mode, realm string, runtime phoenix.Runtime) *Users {
// Create handler based on mode.
if handler, err = users.createHandler(mode, runtime); handler != nil && err == nil {
users.handler = handler
// Register handler Get at the hub.
users.hub.useridRetriever = func(request *http.Request) (userid string, err error) {
// Register handler Get.
sessionManager.RetrieveUsersWith(func(request *http.Request) (userid string, err error) {
userid, err = handler.Get(request)
if err != nil {
log.Printf("Failed to get userid from handler: %s", err)
@ -320,7 +325,7 @@ func NewUsers(hub *Hub, mode, realm string, runtime phoenix.Runtime) *Users { @@ -320,7 +325,7 @@ func NewUsers(hub *Hub, mode, realm string, runtime phoenix.Runtime) *Users {
}
}
return
}
})
log.Printf("Enabled users handler '%s'\n", mode)
} else if err != nil {
log.Printf("Failed to enable handler '%s': %s\n", mode, err)
@ -450,11 +455,20 @@ func (users *Users) Post(request *http.Request) (int, interface{}, http.Header) @@ -450,11 +455,20 @@ func (users *Users) Post(request *http.Request) (int, interface{}, http.Header)
userid := fmt.Sprintf("%s@%s", uuid.NewV4().String(), users.realm)
// Make sure Sid matches session and is valid.
if !users.hub.ValidateSession(snr.Id, snr.Sid) {
if !users.ValidateSession(snr.Id, snr.Sid) {
return 403, NewApiError("users_invalid_session", "Invalid session"), http.Header{"Content-Type": {"application/json"}}
}
nonce, err := users.hub.sessiontokenHandler(&SessionToken{Id: snr.Id, Sid: snr.Sid, Userid: userid})
var (
nonce string
err error
)
if session, ok := users.GetSession(snr.Id); ok {
nonce, err = session.Authorize(users.Realm(), &SessionToken{Id: snr.Id, Sid: snr.Sid, Userid: userid})
} else {
err = errors.New("no such session")
}
if err != nil {
return 400, NewApiError("users_request_failed", fmt.Sprintf("Error: %q", err)), http.Header{"Content-Type": {"application/json"}}
}

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

@ -22,9 +22,10 @@ @@ -22,9 +22,10 @@
package main
import (
"github.com/gorilla/websocket"
"log"
"net/http"
"github.com/gorilla/websocket"
)
const (
@ -49,10 +50,8 @@ var ( @@ -49,10 +50,8 @@ var (
}
)
func makeWsHubHandler(h *Hub) http.HandlerFunc {
func makeWSHandler(connectionCounter ConnectionCounter, sessionManager SessionManager, codec Codec, channellingAPI ChannellingAPI) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Validate incoming request.
if r.Method != "GET" {
w.WriteHeader(http.StatusMethodNotAllowed)
@ -68,30 +67,14 @@ func makeWsHubHandler(h *Hub) http.HandlerFunc { @@ -68,30 +67,14 @@ func makeWsHubHandler(h *Hub) http.HandlerFunc {
return
}
// Read request details.
r.ParseForm()
token := r.FormValue("t")
// Create a new connection instance.
c := NewConnection(h, ws, r)
if token != "" {
if err := c.reregister(token); err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
} else {
if err := c.register(); err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
session := sessionManager.CreateSession(r)
defer sessionManager.DestroySession(session)
client := NewClient(codec, channellingAPI, session)
conn := NewConnection(connectionCounter.CountConnection(), ws, client)
// Start pumps (readPump blocks).
go c.writePump()
c.readPump()
go conn.writePump()
conn.readPump()
}
}

1
src/i18n/helpers/languages.py

@ -16,6 +16,7 @@ import json @@ -16,6 +16,7 @@ import json
def main():
print """// This file is auto generated, do not modify.
"use strict";
define([], function() {
return %s;
});""" % json.dumps(LANGUAGES)

127
src/i18n/messages-de.po

@ -8,8 +8,8 @@ msgid "" @@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Spreed WebRTC 1.0\n"
"Report-Msgid-Bugs-To: simon@struktur.de\n"
"POT-Creation-Date: 2014-10-24 10:17+0200\n"
"PO-Revision-Date: 2014-10-23 11:28+0100\n"
"POT-Creation-Date: 2014-12-08 16:48+0100\n"
"PO-Revision-Date: 2014-12-08 16:48+0100\n"
"Last-Translator: Simon Eisenmann <simon@struktur.de>\n"
"Language-Team: struktur AG <opensource@struktur.de>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
@ -54,8 +54,8 @@ msgstr "Große Videos" @@ -54,8 +54,8 @@ msgstr "Große Videos"
msgid "Kiosk view"
msgstr "Kiosk-Ansicht"
msgid "Classroom"
msgstr "Klassenzimmer"
msgid "Auditorium"
msgstr "Auditorium"
msgid "Start chat"
msgstr "Chat starten"
@ -66,8 +66,8 @@ msgstr "Video-Anruf starten" @@ -66,8 +66,8 @@ msgstr "Video-Anruf starten"
msgid "Start audio conference"
msgstr "Audio-Konferenz starten"
msgid "No other users online"
msgstr "Niemand sonst online"
msgid "No one else here"
msgstr "Niemand sonst hier"
msgid "Take"
msgstr "Los"
@ -155,6 +155,9 @@ msgstr "" @@ -155,6 +155,9 @@ msgstr ""
"Betreten Sie einen Raum und klicken dann auf das Stern-Symbol eines "
"anderen Nutzers um eine Kontaktanfrage zu starten."
msgid "Edit contact"
msgstr "Kontakt bearbeiten"
msgid "Edit"
msgstr "Bearbeiten"
@ -246,6 +249,25 @@ msgstr "Bildschirm einpassen." @@ -246,6 +249,25 @@ msgstr "Bildschirm einpassen."
msgid "Profile"
msgstr "Profil"
msgid "Your name"
msgstr "Ihr Name"
msgid "Your picture"
msgstr "Ihr Bild"
msgid "Status message"
msgstr "Status Nachricht"
msgid "What's on your mind?"
msgstr "Was machen Sie gerade?"
msgid ""
"Your picture, name and status message identify yourself in calls, chats "
"and rooms."
msgstr ""
"Ihr Bild, Name und Status Nachricht repräsentiert Sie in Anrufen, Chats "
"und Räumen."
msgid "Your ID"
msgstr "Ihre ID"
@ -271,28 +293,6 @@ msgstr "Abmelden" @@ -271,28 +293,6 @@ msgstr "Abmelden"
msgid "Manage account"
msgstr "Konto verwalten"
msgid "Your picture"
msgstr "Ihr Bild"
msgid "Your name"
msgstr "Ihr Name"
msgid "Status message"
msgstr "Status Nachricht"
msgid "What's on your mind?"
msgstr "Was machen Sie gerade?"
msgid ""
"Your picture, name and status message identify yourself in calls, chats "
"and rooms."
msgstr ""
"Ihr Bild, Name und Status Nachricht repräsentiert Sie in Anrufen, Chats "
"und Räumen."
msgid "Manage contacts"
msgstr "Kontakte verwalten"
msgid "Media"
msgstr "Kamera / Mikrofon"
@ -396,20 +396,23 @@ msgstr "" @@ -396,20 +396,23 @@ msgstr ""
"Ihre ID bleibt dennoch gespeichert. Klicken Sie Ausloggen weiter oben um "
"die ID zu löschen."
msgid "Share by Email"
msgstr "Per E-Mail teilen"
msgid "Room link"
msgstr "Raum-Link"
msgid "Share on Facebook"
msgstr "Auf Facebook teilen"
msgid "Invite by Email"
msgstr "Per E-Mail einladen"
msgid "Share on Twitter"
msgstr "Auf Twitter teilen"
msgid "Invite with Facebook"
msgstr "Mit Facebook einladen"
msgid "Share on Google Plus"
msgstr "Auf Google Plus teilen"
msgid "Invite with Twitter"
msgstr "Mit Twitter einladen"
msgid "Share on XING"
msgstr "Auf XING teilen"
msgid "Invite with Google Plus"
msgstr "Mit Google Plus einladen"
msgid "Invite with XING"
msgstr "Mit XING einladen"
msgid "Initializing"
msgstr "Initialisiere"
@ -478,14 +481,24 @@ msgstr "Hier klicken für weitere Infos (Google Chrome)." @@ -478,14 +481,24 @@ msgstr "Hier klicken für weitere Infos (Google Chrome)."
msgid "Please set your user details and settings."
msgstr "Bitte vervollständigen Sie Ihre Daten und Einstellungen."
msgid "Create a room and talk together"
msgstr "Erstellen Sie Ihren Raum"
msgid "Enter a room name"
msgstr "Raum eingeben"
msgid "Random room name"
msgstr "Zufälliger Raum"
msgid "Creating room ..."
msgstr "Raum wird erstellt ..."
msgid "Enter room"
msgstr "Raum betreten"
msgid "Create"
msgstr "Erstellen"
msgid ""
"Enter the name of an existing room. You can create new rooms when you are"
" signed in."
msgstr ""
"Geben Sie den Namen eines existierenden Raums ein. Melden Sie sich an um "
"eigene Räume zu erstellen."
msgid "Room history"
msgstr "Raum-Verlauf"
msgid "Videos are shared with everyone in this call."
msgstr "Das Video wird bei allen Gesprächsteilnehmern angezeigt."
@ -595,11 +608,11 @@ msgstr " hat Ihren Anruf abgelehnt." @@ -595,11 +608,11 @@ msgstr " hat Ihren Anruf abgelehnt."
msgid " does not pick up."
msgstr " nimmt nicht ab."
msgid " tried to call you."
msgstr " hat versucht Sie anzurufen."
msgid " tried to call you"
msgstr " hat versucht Sie anzurufen"
msgid " called you."
msgstr " hat Sie angerufen."
msgid " called you"
msgstr " hat Sie angerufen"
msgid "Your browser is not supported. Please upgrade to a current version."
msgstr ""
@ -682,6 +695,9 @@ msgstr "Browsereinstellung" @@ -682,6 +695,9 @@ msgstr "Browsereinstellung"
msgid "Meet with me here:"
msgstr "Meeting:"
msgid "Room name"
msgstr "Raum-Name"
msgid "Unknown URL format. Please make sure to enter a valid YouTube URL."
msgstr "Unbekanntes URL-Format. Bitte geben Sie eine gültige YouTube URL ein."
@ -723,6 +739,21 @@ msgstr "" @@ -723,6 +739,21 @@ msgstr ""
"Der Zugriffscode konnte nicht überprueft werden. Bitte prüfen Sie Ihre "
"Internetverbindung."
#, python-format
msgid "PIN for room %s is now '%s'."
msgstr "PIN für Raum %s ist jetzt '%s'."
#, python-format
msgid "PIN lock has been removed from room %s."
msgstr "Raum %s ist nicht mehr PIN-geschützt."
#, python-format
msgid "Enter the PIN for room %s"
msgstr "Geben Sie die PIN für Raum %s ein"
msgid "Please sign in to create rooms."
msgstr "Bitte melden Sie sich an um Räume zu erstellen."
#, python-format
msgid "and %s"
msgstr "und %s"

111
src/i18n/messages-ja.po

@ -8,7 +8,7 @@ msgid "" @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Spreed WebRTC 1.0\n"
"Report-Msgid-Bugs-To: simon@struktur.de\n"
"POT-Creation-Date: 2014-10-24 10:17+0200\n"
"POT-Creation-Date: 2014-12-08 16:48+0100\n"
"PO-Revision-Date: 2014-04-23 22:25+0100\n"
"Last-Translator: Curt Frisemo <curt.frisemo@spreed.com>\n"
"Language-Team: Curt Frisemo <curt.frisemo@spreed.com>\n"
@ -54,7 +54,7 @@ msgstr "" @@ -54,7 +54,7 @@ msgstr ""
msgid "Kiosk view"
msgstr ""
msgid "Classroom"
msgid "Auditorium"
msgstr ""
msgid "Start chat"
@ -66,8 +66,8 @@ msgstr "テレビ電話を始める" @@ -66,8 +66,8 @@ msgstr "テレビ電話を始める"
msgid "Start audio conference"
msgstr "音声会議を始める"
msgid "No other users online"
msgstr "オンラインのユーザーはいません"
msgid "No one else here"
msgstr ""
msgid "Take"
msgstr ""
@ -157,6 +157,9 @@ msgid "" @@ -157,6 +157,9 @@ msgid ""
"clicking on the star icon next to a user entry."
msgstr ""
msgid "Edit contact"
msgstr ""
msgid "Edit"
msgstr ""
@ -246,6 +249,23 @@ msgstr "画面に合わせる" @@ -246,6 +249,23 @@ msgstr "画面に合わせる"
msgid "Profile"
msgstr ""
msgid "Your name"
msgstr "あなたの名前"
msgid "Your picture"
msgstr "あなたの写真"
msgid "Status message"
msgstr ""
msgid "What's on your mind?"
msgstr ""
msgid ""
"Your picture, name and status message identify yourself in calls, chats "
"and rooms."
msgstr ""
msgid "Your ID"
msgstr ""
@ -269,26 +289,6 @@ msgstr "" @@ -269,26 +289,6 @@ msgstr ""
msgid "Manage account"
msgstr ""
msgid "Your picture"
msgstr "あなたの写真"
msgid "Your name"
msgstr "あなたの名前"
msgid "Status message"
msgstr ""
msgid "What's on your mind?"
msgstr ""
msgid ""
"Your picture, name and status message identify yourself in calls, chats "
"and rooms."
msgstr ""
msgid "Manage contacts"
msgstr ""
msgid "Media"
msgstr ""
@ -390,20 +390,24 @@ msgid "" @@ -390,20 +390,24 @@ msgid ""
" ID."
msgstr ""
msgid "Share by Email"
msgid "Room link"
msgstr ""
#, fuzzy
msgid "Invite by Email"
msgstr "Eメールでシェア"
msgid "Share on Facebook"
msgstr "フェイスブックでシェア"
msgid "Invite with Facebook"
msgstr ""
msgid "Share on Twitter"
msgstr "ツィッターでシェア"
msgid "Invite with Twitter"
msgstr ""
msgid "Share on Google Plus"
msgstr "Google+でシェア"
msgid "Invite with Google Plus"
msgstr ""
msgid "Share on XING"
msgstr "XINGでシェア"
msgid "Invite with XING"
msgstr ""
msgid "Initializing"
msgstr "初期化中"
@ -470,14 +474,22 @@ msgstr "ここをクリックしてヘルプ表示(Google Chrome)" @@ -470,14 +474,22 @@ msgstr "ここをクリックしてヘルプ表示(Google Chrome)"
msgid "Please set your user details and settings."
msgstr "あなたのプロフィールとアプリの動作を設定してください."
msgid "Create a room and talk together"
msgid "Enter a room name"
msgstr ""
msgid "Random room name"
msgstr ""
#, fuzzy
msgid "Creating room ..."
msgstr "ルームへのリンクを作る..."
msgid "Enter room"
msgstr "現在のルーム"
msgid ""
"Enter the name of an existing room. You can create new rooms when you are"
" signed in."
msgstr ""
msgid "Create"
msgid "Room history"
msgstr ""
msgid "Videos are shared with everyone in this call."
@ -585,10 +597,12 @@ msgstr "着信拒否されました." @@ -585,10 +597,12 @@ msgstr "着信拒否されました."
msgid " does not pick up."
msgstr "は電話にでません."
msgid " tried to call you."
#, fuzzy
msgid " tried to call you"
msgstr "は電話しようとしました."
msgid " called you."
#, fuzzy
msgid " called you"
msgstr "から電話がありました."
#, fuzzy
@ -664,6 +678,10 @@ msgstr "ブラウザの言語を使用" @@ -664,6 +678,10 @@ msgstr "ブラウザの言語を使用"
msgid "Meet with me here:"
msgstr "ここで私と会う:"
#, fuzzy
msgid "Room name"
msgstr "あなたの名前"
msgid "Unknown URL format. Please make sure to enter a valid YouTube URL."
msgstr ""
@ -701,6 +719,21 @@ msgid "" @@ -701,6 +719,21 @@ msgid ""
"again."
msgstr "アクセスコードの確認に失敗しました.インターネット接続を確認してリトライしてください."
#, python-format
msgid "PIN for room %s is now '%s'."
msgstr ""
#, python-format
msgid "PIN lock has been removed from room %s."
msgstr ""
#, python-format
msgid "Enter the PIN for room %s"
msgstr ""
msgid "Please sign in to create rooms."
msgstr ""
#, fuzzy, python-format
msgid "and %s"
msgstr "と %2"

111
src/i18n/messages-ko.po

@ -8,7 +8,7 @@ msgid "" @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Spreed WebRTC 1.0\n"
"Report-Msgid-Bugs-To: simon@struktur.de\n"
"POT-Creation-Date: 2014-10-24 10:17+0200\n"
"POT-Creation-Date: 2014-12-08 16:48+0100\n"
"PO-Revision-Date: 2014-04-13 20:30+0900\n"
"Last-Translator: Curt Frisemo <curt.frisemo@spreed.com>\n"
"Language-Team: Curt Frisemo <curt.frisemo@spreed.com>\n"
@ -54,7 +54,7 @@ msgstr "" @@ -54,7 +54,7 @@ msgstr ""
msgid "Kiosk view"
msgstr ""
msgid "Classroom"
msgid "Auditorium"
msgstr ""
msgid "Start chat"
@ -66,8 +66,8 @@ msgstr "화상회의 시작" @@ -66,8 +66,8 @@ msgstr "화상회의 시작"
msgid "Start audio conference"
msgstr "음성회의 시작"
msgid "No other users online"
msgstr "온라인에 다른 대화상대 없음"
msgid "No one else here"
msgstr ""
msgid "Take"
msgstr ""
@ -157,6 +157,9 @@ msgid "" @@ -157,6 +157,9 @@ msgid ""
"clicking on the star icon next to a user entry."
msgstr ""
msgid "Edit contact"
msgstr ""
msgid "Edit"
msgstr ""
@ -246,6 +249,23 @@ msgstr "화면에 맟춤" @@ -246,6 +249,23 @@ msgstr "화면에 맟춤"
msgid "Profile"
msgstr ""
msgid "Your name"
msgstr "사용자 이름"
msgid "Your picture"
msgstr "사용자 사진"
msgid "Status message"
msgstr ""
msgid "What's on your mind?"
msgstr ""
msgid ""
"Your picture, name and status message identify yourself in calls, chats "
"and rooms."
msgstr ""
msgid "Your ID"
msgstr ""
@ -269,26 +289,6 @@ msgstr "" @@ -269,26 +289,6 @@ msgstr ""
msgid "Manage account"
msgstr ""
msgid "Your picture"
msgstr "사용자 사진"
msgid "Your name"
msgstr "사용자 이름"
msgid "Status message"
msgstr ""
msgid "What's on your mind?"
msgstr ""
msgid ""
"Your picture, name and status message identify yourself in calls, chats "
"and rooms."
msgstr ""
msgid "Manage contacts"
msgstr ""
msgid "Media"
msgstr ""
@ -390,20 +390,24 @@ msgid "" @@ -390,20 +390,24 @@ msgid ""
" ID."
msgstr ""
msgid "Share by Email"
msgid "Room link"
msgstr ""
#, fuzzy
msgid "Invite by Email"
msgstr "이메일로 공유"
msgid "Share on Facebook"
msgstr "Facebook에서 공유"
msgid "Invite with Facebook"
msgstr ""
msgid "Share on Twitter"
msgstr "Twitter에서 공유"
msgid "Invite with Twitter"
msgstr ""
msgid "Share on Google Plus"
msgstr "구글 플러스에서 공유"
msgid "Invite with Google Plus"
msgstr ""
msgid "Share on XING"
msgstr "Xing에서 공유"
msgid "Invite with XING"
msgstr ""
msgid "Initializing"
msgstr "초기화"
@ -470,14 +474,22 @@ msgstr "도움말을 원하면 여기를 클릭 하세요 (구글 크롬)" @@ -470,14 +474,22 @@ msgstr "도움말을 원하면 여기를 클릭 하세요 (구글 크롬)"
msgid "Please set your user details and settings."
msgstr "사용자의 세부상세와 설정을 지정하세요 "
msgid "Create a room and talk together"
msgid "Enter a room name"
msgstr ""
msgid "Random room name"
msgstr ""
#, fuzzy
msgid "Creating room ..."
msgstr "방 링크 만들기..."
msgid "Enter room"
msgstr "현재 방"
msgid ""
"Enter the name of an existing room. You can create new rooms when you are"
" signed in."
msgstr ""
msgid "Create"
msgid "Room history"
msgstr ""
msgid "Videos are shared with everyone in this call."
@ -585,10 +597,12 @@ msgstr "전화가 거부 되었습니다." @@ -585,10 +597,12 @@ msgstr "전화가 거부 되었습니다."
msgid " does not pick up."
msgstr "전화를 받지 않습니다."
msgid " tried to call you."
#, fuzzy
msgid " tried to call you"
msgstr "연결을 시도 중입니다"
msgid " called you."
#, fuzzy
msgid " called you"
msgstr "전화 드렸습니다."
#, fuzzy
@ -664,6 +678,10 @@ msgstr "브라우저 언어 사용" @@ -664,6 +678,10 @@ msgstr "브라우저 언어 사용"
msgid "Meet with me here:"
msgstr "나를 여기서 만납니다:"
#, fuzzy
msgid "Room name"
msgstr "사용자 이름"
msgid "Unknown URL format. Please make sure to enter a valid YouTube URL."
msgstr ""
@ -701,6 +719,21 @@ msgid "" @@ -701,6 +719,21 @@ msgid ""
"again."
msgstr "접속코드 확인이 실패 했습니다. 인터넷 연결을 확인하고 다시 시도해 주십시오. "
#, python-format
msgid "PIN for room %s is now '%s'."
msgstr ""
#, python-format
msgid "PIN lock has been removed from room %s."
msgstr ""
#, python-format
msgid "Enter the PIN for room %s"
msgstr ""
msgid "Please sign in to create rooms."
msgstr ""
#, fuzzy, python-format
msgid "and %s"
msgstr "그리고 %2$s"

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

@ -8,7 +8,7 @@ msgid "" @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Spreed WebRTC 1.0\n"
"Report-Msgid-Bugs-To: simon@struktur.de\n"
"POT-Creation-Date: 2014-10-24 10:17+0200\n"
"POT-Creation-Date: 2014-12-08 16:48+0100\n"
"PO-Revision-Date: 2014-05-21 09:54+0800\n"
"Last-Translator: Michael P.\n"
"Language-Team: Curt Frisemo <curt.frisemo@spreed.com>\n"
@ -54,7 +54,7 @@ msgstr "" @@ -54,7 +54,7 @@ msgstr ""
msgid "Kiosk view"
msgstr ""
msgid "Classroom"
msgid "Auditorium"
msgstr ""
msgid "Start chat"
@ -66,8 +66,8 @@ msgstr "开始视频通话" @@ -66,8 +66,8 @@ msgstr "开始视频通话"
msgid "Start audio conference"
msgstr "开始语音会议"
msgid "No other users online"
msgstr "无其他联系人在线"
msgid "No one else here"
msgstr ""
msgid "Take"
msgstr ""
@ -157,6 +157,9 @@ msgid "" @@ -157,6 +157,9 @@ msgid ""
"clicking on the star icon next to a user entry."
msgstr ""
msgid "Edit contact"
msgstr ""
msgid "Edit"
msgstr ""
@ -246,6 +249,23 @@ msgstr "适合屏幕" @@ -246,6 +249,23 @@ msgstr "适合屏幕"
msgid "Profile"
msgstr ""
msgid "Your name"
msgstr "您的名字"
msgid "Your picture"
msgstr "您的图片"
msgid "Status message"
msgstr ""
msgid "What's on your mind?"
msgstr ""
msgid ""
"Your picture, name and status message identify yourself in calls, chats "
"and rooms."
msgstr ""
msgid "Your ID"
msgstr ""
@ -269,26 +289,6 @@ msgstr "" @@ -269,26 +289,6 @@ msgstr ""
msgid "Manage account"
msgstr ""
msgid "Your picture"
msgstr "您的图片"
msgid "Your name"
msgstr "您的名字"
msgid "Status message"
msgstr ""
msgid "What's on your mind?"
msgstr ""
msgid ""
"Your picture, name and status message identify yourself in calls, chats "
"and rooms."
msgstr ""
msgid "Manage contacts"
msgstr ""
msgid "Media"
msgstr ""
@ -390,20 +390,24 @@ msgid "" @@ -390,20 +390,24 @@ msgid ""
" ID."
msgstr ""
msgid "Share by Email"
msgid "Room link"
msgstr ""
#, fuzzy
msgid "Invite by Email"
msgstr "电子邮件共享"
msgid "Share on Facebook"
msgstr "Facebook共享"
msgid "Invite with Facebook"
msgstr ""
msgid "Share on Twitter"
msgstr "Twitter共享"
msgid "Invite with Twitter"
msgstr ""
msgid "Share on Google Plus"
msgstr "Google Plus共享"
msgid "Invite with Google Plus"
msgstr ""
msgid "Share on XING"
msgstr "XING共享"
msgid "Invite with XING"
msgstr ""
msgid "Initializing"
msgstr "初始化"
@ -470,14 +474,22 @@ msgstr "点击这里获取帮助 (Google Chrome)" @@ -470,14 +474,22 @@ msgstr "点击这里获取帮助 (Google Chrome)"
msgid "Please set your user details and settings."
msgstr "请设定您的用户信息及设置"
msgid "Create a room and talk together"
msgid "Enter a room name"
msgstr ""
msgid "Random room name"
msgstr ""
#, fuzzy
msgid "Creating room ..."
msgstr "创建房间链接"
msgid "Enter room"
msgstr "當前房间"
msgid ""
"Enter the name of an existing room. You can create new rooms when you are"
" signed in."
msgstr ""
msgid "Create"
msgid "Room history"
msgstr ""
msgid "Videos are shared with everyone in this call."
@ -585,10 +597,12 @@ msgstr " 拒绝了您的呼叫。" @@ -585,10 +597,12 @@ msgstr " 拒绝了您的呼叫。"
msgid " does not pick up."
msgstr " 无人接听。"
msgid " tried to call you."
#, fuzzy
msgid " tried to call you"
msgstr " 曾呼叫您。"
msgid " called you."
#, fuzzy
msgid " called you"
msgstr " 曾与您通话。"
#, fuzzy
@ -663,6 +677,10 @@ msgstr "使用浏览器语言" @@ -663,6 +677,10 @@ msgstr "使用浏览器语言"
msgid "Meet with me here:"
msgstr "我们这里见:"
#, fuzzy
msgid "Room name"
msgstr "您的名字"
msgid "Unknown URL format. Please make sure to enter a valid YouTube URL."
msgstr ""
@ -700,6 +718,21 @@ msgid "" @@ -700,6 +718,21 @@ msgid ""
"again."
msgstr "接入码认证失败。请检查您的网络连接并重试。"
#, python-format
msgid "PIN for room %s is now '%s'."
msgstr ""
#, python-format
msgid "PIN lock has been removed from room %s."
msgstr ""
#, python-format
msgid "Enter the PIN for room %s"
msgstr ""
msgid "Please sign in to create rooms."
msgstr ""
#, python-format
msgid "and %s"
msgstr ""

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

@ -8,7 +8,7 @@ msgid "" @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Spreed WebRTC 1.0\n"
"Report-Msgid-Bugs-To: simon@struktur.de\n"
"POT-Creation-Date: 2014-10-24 10:17+0200\n"
"POT-Creation-Date: 2014-12-08 16:48+0100\n"
"PO-Revision-Date: 2014-05-21 09:55+0800\n"
"Last-Translator: Michael P.\n"
"Language-Team: Curt Frisemo <curt.frisemo@spreed.com>\n"
@ -54,7 +54,7 @@ msgstr "" @@ -54,7 +54,7 @@ msgstr ""
msgid "Kiosk view"
msgstr ""
msgid "Classroom"
msgid "Auditorium"
msgstr ""
msgid "Start chat"
@ -66,8 +66,8 @@ msgstr "開始視頻通話" @@ -66,8 +66,8 @@ msgstr "開始視頻通話"
msgid "Start audio conference"
msgstr "開始語音會議"
msgid "No other users online"
msgstr "無其他聯繫人在線"
msgid "No one else here"
msgstr ""
msgid "Take"
msgstr ""
@ -157,6 +157,9 @@ msgid "" @@ -157,6 +157,9 @@ msgid ""
"clicking on the star icon next to a user entry."
msgstr ""
msgid "Edit contact"
msgstr ""
msgid "Edit"
msgstr ""
@ -246,6 +249,23 @@ msgstr "適合屏幕" @@ -246,6 +249,23 @@ msgstr "適合屏幕"
msgid "Profile"
msgstr ""
msgid "Your name"
msgstr "您的名字"
msgid "Your picture"
msgstr "您的圖片"
msgid "Status message"
msgstr ""
msgid "What's on your mind?"
msgstr ""
msgid ""
"Your picture, name and status message identify yourself in calls, chats "
"and rooms."
msgstr ""
msgid "Your ID"
msgstr ""
@ -269,26 +289,6 @@ msgstr "" @@ -269,26 +289,6 @@ msgstr ""
msgid "Manage account"
msgstr ""
msgid "Your picture"
msgstr "您的圖片"
msgid "Your name"
msgstr "您的名字"
msgid "Status message"
msgstr ""
msgid "What's on your mind?"
msgstr ""
msgid ""
"Your picture, name and status message identify yourself in calls, chats "
"and rooms."
msgstr ""
msgid "Manage contacts"
msgstr ""
msgid "Media"
msgstr ""
@ -390,20 +390,24 @@ msgid "" @@ -390,20 +390,24 @@ msgid ""
" ID."
msgstr ""
msgid "Share by Email"
msgid "Room link"
msgstr ""
#, fuzzy
msgid "Invite by Email"
msgstr "電子郵件共享"
msgid "Share on Facebook"
msgstr "Facebook共享"
msgid "Invite with Facebook"
msgstr ""
msgid "Share on Twitter"
msgstr "Twitter共享"
msgid "Invite with Twitter"
msgstr ""
msgid "Share on Google Plus"
msgstr "Google Plus共享"
msgid "Invite with Google Plus"
msgstr ""
msgid "Share on XING"
msgstr "XING共享"
msgid "Invite with XING"
msgstr ""
msgid "Initializing"
msgstr "初始化"
@ -470,14 +474,22 @@ msgstr "點擊這裡獲取幫助 (Google Chrome)" @@ -470,14 +474,22 @@ msgstr "點擊這裡獲取幫助 (Google Chrome)"
msgid "Please set your user details and settings."
msgstr "請設定您的用戶信息及設置"
msgid "Create a room and talk together"
msgid "Enter a room name"
msgstr ""
msgid "Random room name"
msgstr ""
#, fuzzy
msgid "Creating room ..."
msgstr "創建房間連接 ..."
msgid "Enter room"
msgstr "當前房間"
msgid ""
"Enter the name of an existing room. You can create new rooms when you are"
" signed in."
msgstr ""
msgid "Create"
msgid "Room history"
msgstr ""
msgid "Videos are shared with everyone in this call."
@ -585,10 +597,12 @@ msgstr " 拒絕了您的呼叫" @@ -585,10 +597,12 @@ msgstr " 拒絕了您的呼叫"
msgid " does not pick up."
msgstr " 無人接聽。"
msgid " tried to call you."
#, fuzzy
msgid " tried to call you"
msgstr " 曾呼叫您"
msgid " called you."
#, fuzzy
msgid " called you"
msgstr " 曾與您通話"
#, fuzzy
@ -663,6 +677,10 @@ msgstr "使用瀏覽器語言" @@ -663,6 +677,10 @@ msgstr "使用瀏覽器語言"
msgid "Meet with me here:"
msgstr "我們這裡見:"
#, fuzzy
msgid "Room name"
msgstr "您的名字"
msgid "Unknown URL format. Please make sure to enter a valid YouTube URL."
msgstr ""
@ -700,6 +718,21 @@ msgid "" @@ -700,6 +718,21 @@ msgid ""
"again."
msgstr "接入碼認證錯誤。請檢查您的網絡連接并重試。"
#, python-format
msgid "PIN for room %s is now '%s'."
msgstr ""
#, python-format
msgid "PIN lock has been removed from room %s."
msgstr ""
#, python-format
msgid "Enter the PIN for room %s"
msgstr ""
msgid "Please sign in to create rooms."
msgstr ""
#, python-format
msgid "and %s"
msgstr ""

93
src/i18n/messages.pot

@ -9,7 +9,7 @@ msgid "" @@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Spreed WebRTC 1.0\n"
"Report-Msgid-Bugs-To: simon@struktur.de\n"
"POT-Creation-Date: 2014-10-24 10:17+0200\n"
"POT-Creation-Date: 2014-12-08 16:48+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -54,7 +54,7 @@ msgstr "" @@ -54,7 +54,7 @@ msgstr ""
msgid "Kiosk view"
msgstr ""
msgid "Classroom"
msgid "Auditorium"
msgstr ""
msgid "Start chat"
@ -66,7 +66,7 @@ msgstr "" @@ -66,7 +66,7 @@ msgstr ""
msgid "Start audio conference"
msgstr ""
msgid "No other users online"
msgid "No one else here"
msgstr ""
msgid "Take"
@ -153,6 +153,9 @@ msgid "" @@ -153,6 +153,9 @@ msgid ""
"clicking on the star icon next to a user entry."
msgstr ""
msgid "Edit contact"
msgstr ""
msgid "Edit"
msgstr ""
@ -242,47 +245,44 @@ msgstr "" @@ -242,47 +245,44 @@ msgstr ""
msgid "Profile"
msgstr ""
msgid "Your ID"
msgstr ""
msgid "Register"
msgid "Your name"
msgstr ""
msgid ""
"Authenticated by certificate. To log out you have to remove your "
"certificate from the browser."
msgid "Your picture"
msgstr ""
msgid "Sign in"
msgid "Status message"
msgstr ""
msgid "Create an account"
msgid "What's on your mind?"
msgstr ""
msgid "Sign out"
msgid ""
"Your picture, name and status message identify yourself in calls, chats "
"and rooms."
msgstr ""
msgid "Manage account"
msgid "Your ID"
msgstr ""
msgid "Your picture"
msgid "Register"
msgstr ""
msgid "Your name"
msgid ""
"Authenticated by certificate. To log out you have to remove your "
"certificate from the browser."
msgstr ""
msgid "Status message"
msgid "Sign in"
msgstr ""
msgid "What's on your mind?"
msgid "Create an account"
msgstr ""
msgid ""
"Your picture, name and status message identify yourself in calls, chats "
"and rooms."
msgid "Sign out"
msgstr ""
msgid "Manage contacts"
msgid "Manage account"
msgstr ""
msgid "Media"
@ -386,19 +386,22 @@ msgid "" @@ -386,19 +386,22 @@ msgid ""
" ID."
msgstr ""
msgid "Share by Email"
msgid "Room link"
msgstr ""
msgid "Invite by Email"
msgstr ""
msgid "Share on Facebook"
msgid "Invite with Facebook"
msgstr ""
msgid "Share on Twitter"
msgid "Invite with Twitter"
msgstr ""
msgid "Share on Google Plus"
msgid "Invite with Google Plus"
msgstr ""
msgid "Share on XING"
msgid "Invite with XING"
msgstr ""
msgid "Initializing"
@ -466,13 +469,21 @@ msgstr "" @@ -466,13 +469,21 @@ msgstr ""
msgid "Please set your user details and settings."
msgstr ""
msgid "Create a room and talk together"
msgid "Enter a room name"
msgstr ""
msgid "Random room name"
msgstr ""
msgid "Enter room"
msgstr ""
msgid "Creating room ..."
msgid ""
"Enter the name of an existing room. You can create new rooms when you are"
" signed in."
msgstr ""
msgid "Create"
msgid "Room history"
msgstr ""
msgid "Videos are shared with everyone in this call."
@ -579,10 +590,10 @@ msgstr "" @@ -579,10 +590,10 @@ msgstr ""
msgid " does not pick up."
msgstr ""
msgid " tried to call you."
msgid " tried to call you"
msgstr ""
msgid " called you."
msgid " called you"
msgstr ""
msgid "Your browser is not supported. Please upgrade to a current version."
@ -654,6 +665,9 @@ msgstr "" @@ -654,6 +665,9 @@ msgstr ""
msgid "Meet with me here:"
msgstr ""
msgid "Room name"
msgstr ""
msgid "Unknown URL format. Please make sure to enter a valid YouTube URL."
msgstr ""
@ -691,6 +705,21 @@ msgid "" @@ -691,6 +705,21 @@ msgid ""
"again."
msgstr ""
#, python-format
msgid "PIN for room %s is now '%s'."
msgstr ""
#, python-format
msgid "PIN lock has been removed from room %s."
msgstr ""
#, python-format
msgid "Enter the PIN for room %s"
msgstr ""
msgid "Please sign in to create rooms."
msgstr ""
#, python-format
msgid "and %s"
msgstr ""

2
src/styles/Makefile.am

@ -36,6 +36,8 @@ styles: @@ -36,6 +36,8 @@ styles:
$(CURDIR)/bootstrap.scss:$(STATIC)/css/bootstrap.min.css
$(SASS) --compass --scss $(SASSFLAGS) \
$(CURDIR)/font-awesome.scss:$(STATIC)/css/font-awesome.min.css
$(SASS) --compass --scss $(SASSFLAGS) \
$(CURDIR)/csp.scss:$(STATIC)/css/csp.min.css
styleshint:
@if [ "$(SASS)" = "" ]; then echo "Command 'sass' not found, required when checking styles"; exit 1; fi

49
src/styles/_shame.scss

@ -22,4 +22,53 @@ @@ -22,4 +22,53 @@
// Remove boostrap 3 modal scroll bar.
.modal {
overflow-y: auto;
}
// Toastr fontawesome support.
#toast-container > .toast {
background-image: none !important;
}
#toast-container > .toast:before {
position: fixed;
font-family: FontAwesome;
font-size: 20px;
line-height: 20px;
float: left;
color: #fff;
padding-right: 0.5em;
margin: auto 0.5em auto -1.5em;
}
#toast-container > .toast-warning:before {
content: "\f05a";
}
#toast-container > .toast-error:before {
content: "\f05a";
}
#toast-container > .toast-info:before {
content: "\f05a";
}
#toast-container > .toast-success:before {
content: "\f05a";
}
// No shadows for toastr.
#toast-container > :hover, #toast-container > div {
box-shadow: none !important;
}
// Update colors for toaster.
.toast-info {
background-color: #5bc0de;
}
// Update position of toastr close icon.
.toast-close-button {
top: -0.6em;
font-size: 1em;
}
// No opacity for toastr.
#toast-container > div {
opacity: 1;
filter: alpha(opacity=100);
}

35
src/styles/components/_audiovideo.scss

@ -169,14 +169,14 @@ @@ -169,14 +169,14 @@
overflow: hidden;
position: relative;
vertical-align: bottom;
visibility: hidden;
//visibility: hidden;
width: 100%;
&.withvideo {
visibility: visible;
//visibility: visible;
}
&.onlyaudio {
background: $video-onlyaudio-background;
visibility: visible;
//visibility: visible;
}
.onlyaudio {
color: $video-onlyaudio;
@ -190,12 +190,15 @@ @@ -190,12 +190,15 @@
text-align: center;
top: 45%;
}
&.onlyaudio video {
&.onlyaudio video, &.dummy video {
visibility: hidden;
}
&.onlyaudio .onlyaudio {
display: block;
}
&.dummy .onlyaudio {
display: block;
}
.peerActions {
bottom: 5%;
left: 40px;
@ -251,6 +254,25 @@ @@ -251,6 +254,25 @@
text-shadow: 0 0 5px black;
width: 40px;
}
button.renderer-auditorium {
position: relative;
span:before {
position:absolute;
left:50%;
top:50%;
margin-left: -0.8em;
margin-top: -0.5em;
content: "\f183";
}
span:after {
position:absolute;
top:50%;
right:50%;
margin-right: -0.9em;
margin-top: -0.5em;
content: "\f183";
}
}
}
.remoteVideo {
@ -384,7 +406,7 @@ @@ -384,7 +406,7 @@
}
}
.renderer-classroom {
.renderer-auditorium {
.remoteContainer {
border-left: 40px solid black;
}
@ -442,4 +464,5 @@ @@ -442,4 +464,5 @@
bottom: 8%;
}
}
}
}

2
src/styles/components/_buddylist.scss

@ -240,7 +240,7 @@ @@ -240,7 +240,7 @@
text-align: center;
vertical-align: middle;
line-height: 40px;
font-size: 1.6em;
font-size: $buddylist-action-font-size;
}
}

5
src/styles/components/_contactsmanager.scss

@ -47,9 +47,12 @@ @@ -47,9 +47,12 @@
overflow-y: auto;
}
.picture {
display: table-cell;
border-bottom: 0;
cursor: auto;
min-height: 46px;
position: static;
width: auto;
.buddyPicture {
margin: 0 0 0 10px;
}
@ -60,7 +63,7 @@ @@ -60,7 +63,7 @@
border-top: none;
}
.name {
width: 70%;
width: 40%;
text-align: left;
vertical-align: middle;
}

3
src/styles/components/_social.scss

@ -20,6 +20,9 @@ @@ -20,6 +20,9 @@
*/
.#{$fa-css-prefix} {
&.link {
color: $social-email-color;
}
&.email {
color: $social-email-color;
}

5
src/styles/global/skins/_light.scss → src/styles/csp.scss

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
/*
/*!
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
@ -19,5 +19,4 @@ @@ -19,5 +19,4 @@
*
*/
$skin-name: "Light Theme";
$skin-version: "0.0.1";
@import "libs/angular/angular-csp.scss";

4
src/styles/global/_overlaybar.scss

@ -28,6 +28,8 @@ @@ -28,6 +28,8 @@
text-shadow: 0 0 5px black;
user-select: none;
vertical-align: middle;
border-bottom: 1px solid #222;
border-top: 1px solid #222;
}
.overlaybar {
@ -57,6 +59,8 @@ @@ -57,6 +59,8 @@
&.notvisible {
background: transparent;
pointer-events: none;
border-bottom: 1px solid transparent;
border-top: 1px solid transparent;
&:hover {
background: transparent;
}

40
src/styles/global/_pages.scss

@ -35,16 +35,17 @@ @@ -35,16 +35,17 @@
text-shadow: 0 0 5px black;
max-width:600px;
min-height: 160px;
padding-left:160px;
padding-left:105px;
padding-right: 0px;
position: relative;
@include breakpt($breakpoint-medium) {
padding-left:0px;
margin: 0 10px;
padding-left:10px;
padding-right:20px;
margin: 0 auto;
}
h1 {
text-align: center;
margin-top: 0px;
white-space: nowrap;
@include breakpt($breakpoint-medium) {
white-space: normal;
@ -52,24 +53,23 @@ @@ -52,24 +53,23 @@
}
.welcome-container {
max-width: 450px;
margin: 0 auto;
}
.welcome-logo {
position: absolute;
left: 0px;
top: 0px;
top: 1px;
bottom: 0px;
width: 140px;
background: $scalable-logo no-repeat left;
width: 90px;
background: $scalable-logo no-repeat left top;
background-size: contain;
@include breakpt($breakpoint-medium) {
position: relative;
margin: 0 auto;
height: 70px;
width: 70px;
margin-top: 30px;
margin-bottom: 20px;
}
}
@ -88,6 +88,28 @@ @@ -88,6 +88,28 @@
a {
padding-right: .5em;
color: black;
user-select: none;
}
}
.room-link {
margin-top: -10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
a {
color: $welcome;
}
}
.rooms-history {
margin-top: 3em;
a {
&:hover {
text-decoration: none;
}
margin-right: .5em;
display: inline-block;
}
}

3
src/styles/global/_variables.scss

@ -20,8 +20,6 @@ @@ -20,8 +20,6 @@
*/
@import "compass/utilities/color/contrast";
@import "branding";
@import "skins/light";
// general
$background: #e5e5e5 !default;
@ -105,6 +103,7 @@ $buddylist-tab-background: $componentbg !default; @@ -105,6 +103,7 @@ $buddylist-tab-background: $componentbg !default;
$buddylist-action-background: rgba(255,255,255,0.5) !default;
$buddylist-buddy1: $componentfg1 !default;
$buddylist-buddy2: $componentfg2 !default;
$buddylist-action-font-size: 1.6em;
// chat
$chat-width: $pane-width !default;

91
src/styles/global/skins/_dark.scss

@ -1,91 +0,0 @@ @@ -1,91 +0,0 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 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/>.
*
*/
$skin-name: "Dark Theme";
$skin-version: "0.0.1";
$base-skin-color: #212121;
$contrasted-dark-default: #262625; // compass color utility
$contrasted-light-default: #FCFCFC; // compass color utility
// background
$main-background: url("../img/bg-tiles.jpg");
$main-background-retina: url("../img/bg-tiles_x2.jpg");
// menu background color
$menu-background-color: $base-skin-color;
$menu-background-color-accent: lighten($menu-background-color, 5%);
$componentfg1: contrast-color($menu-background-color);
$componentbg: $menu-background-color;
$buddylist-tab-background: $menu-background-color;
$chat-background: darken($menu-background-color, 5%);
$chat-msg-self-background: lighten($chat-background, 10%);
$chat-msg-remote-background: $chat-msg-self-background;
$fileinfo-background: darken($chat-msg-self-background, 10%);
$fileinfo-background-remote: darken($chat-msg-remote-background, 10%);
$buddylist-action-background: transparent;
$sidepanebg: $menu-background-color-accent;
$settings-background: $menu-background-color;
$fileinfo-icon-background-color: $chat-msg-self-background;
$fileinfo-icon-background-color-remote: $chat-msg-remote-background;
// font
$font-color: contrast-color($menu-background-color);
$font-color-accent: darken($font-color, 20%);
$chat-ctrl: $font-color;
$bar-btn-action-color: $font-color;
$chat-meta: $font-color-accent;
$chat-timestamp: $font-color-accent;
$form-help-text: $font-color-accent;
$bar-logo-text-desc: $font-color-accent;
$buddylist-browser: $font-color-accent;
$buddylist-tab-color: $font-color-accent;
$bar-btn-action-hover: darken($font-color, 20%);
$bar-btn-action-border: darken($font-color, 50%);
$bordercolor: lighten($menu-background-color, 10%);
$chat-msg-border: lighten($chat-background, 5%);
$fileinfo-border-remote: $chat-msg-remote-background;
$fileinfo-border: $chat-msg-self-background;
$overlaybar-btn: $font-color-accent;
$overlaybar-color: $font-color-accent;
// bootstrap vars
$text-color: $font-color;
$legend-color: $font-color;
$legend-border-color: $bordercolor;
$hr-border: $bordercolor;
$list-group-bg: $menu-background-color-accent;
$list-group-border: $bordercolor;
$list-group-hover-bg: $menu-background-color;
$list-group-link-color: $font-color-accent;
$input-bg: lighten($menu-background-color, 20%);
$input-color: $font-color;
$input-border: $bar-btn-action-border;
$progress-bg: lighten($input-bg, 30%);
$modal-content-bg: $menu-background-color-accent;
$dialog-header-neutral: $menu-background-color-accent !default;
$close-color: white;
$modal-header-border-color: $bordercolor;

6
src/styles/libs/_dialogs.scss

@ -26,7 +26,7 @@ @@ -26,7 +26,7 @@
.dialog-header-wait span, .dialog-header-wait h4 { color: #fff; }
.modal-content {
overflow:hidden;
.modal-body {
min-height: 160px;
}
}
.modal-backdrop.in {
opacity:0.2;
}

1
src/styles/libs/_libs.scss

@ -2,3 +2,4 @@ @@ -2,3 +2,4 @@
@import "hidpi";
@import "toastr/toastr.scss";
@import "dialogs";
@import "angular/angular.scss";

41
src/styles/libs/angular/angular-csp.scss

@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 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/>.
*
*/
[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak],
.ng-cloak, .x-ng-cloak,
.ng-hide {
display: none !important;
}
ng\:form {
display: block;
}
.ng-animate-block-transitions {
transition:0s all!important;
-webkit-transition:0s all!important;
}
/* show the element during a show/hide animation when the
* animation is ongoing, but the .ng-hide class is active */
.ng-hide-add-active, .ng-hide-remove {
display: block!important;
}

0
src/styles/global/_angular.scss → src/styles/libs/angular/angular.scss

44
src/styles/libs/toastr/toastr.scss

@ -1,12 +1,13 @@ @@ -1,12 +1,13 @@
/*
* Toastr
* Version 2.0.1
* Copyright 2012 John Papa and Hans Fjällemark.
* Copyright 2012-2014
* Authors: John Papa, Hans Fjällemark, and Tim Ferrell.
* All Rights Reserved.
* Use, reproduction, distribution, and modification of this code is subject to the terms and
* conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php
*
* Author: John Papa and Hans Fjällemark
* ARIA Support: Greta Krafsig
*
* Project: https://github.com/CodeSeven/toastr
*/
.toast-title {
@ -24,7 +25,6 @@ @@ -24,7 +25,6 @@
color: #cccccc;
text-decoration: none;
}
.toast-close-button {
position: relative;
right: -0.3em;
@ -48,7 +48,6 @@ @@ -48,7 +48,6 @@
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=40);
filter: alpha(opacity=40);
}
/*Additional properties for button version
iOS requires the button element instead of an anchor tag.
If you want the anchor version, it requires `href="#"`.*/
@ -59,6 +58,16 @@ button.toast-close-button { @@ -59,6 +58,16 @@ button.toast-close-button {
border: 0;
-webkit-appearance: none;
}
.toast-top-center {
top: 0;
right: 0;
width: 100%;
}
.toast-bottom-center {
bottom: 0;
right: 0;
width: 100%;
}
.toast-top-full-width {
top: 0;
right: 0;
@ -97,6 +106,8 @@ button.toast-close-button { @@ -97,6 +106,8 @@ button.toast-close-button {
box-sizing: border-box;
}
#toast-container > div {
position: relative;
overflow: hidden;
margin: 0 0 6px;
padding: 15px 15px 15px 50px;
width: 300px;
@ -134,6 +145,11 @@ button.toast-close-button { @@ -134,6 +145,11 @@ button.toast-close-button {
#toast-container > .toast-warning {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=") !important;
}
#toast-container.toast-top-center > div,
#toast-container.toast-bottom-center > div {
width: 300px;
margin: auto;
}
#toast-container.toast-top-full-width > div,
#toast-container.toast-bottom-full-width > div {
width: 96%;
@ -154,8 +170,20 @@ button.toast-close-button { @@ -154,8 +170,20 @@ button.toast-close-button {
.toast-warning {
background-color: #f89406;
}
.toast-progress {
position: absolute;
left: 0;
bottom: 0;
height: 4px;
background-color: #000000;
opacity: 0.4;
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=40);
filter: alpha(opacity=40);
}
/*Responsive Design*/
@media all and (max-width: 239px) {
@media all and (max-width: 240px) {
#toast-container > div {
padding: 8px 8px 8px 50px;
width: 11em;
@ -165,7 +193,7 @@ button.toast-close-button { @@ -165,7 +193,7 @@ button.toast-close-button {
top: -0.2em;
}
}
@media all and (min-width: 240px) and (max-width: 479px) {
@media all and (min-width: 241px) and (max-width: 480px) {
#toast-container > div {
padding: 8px 8px 8px 50px;
width: 18em;
@ -175,7 +203,7 @@ button.toast-close-button { @@ -175,7 +203,7 @@ button.toast-close-button {
top: -0.2em;
}
}
@media all and (min-width: 480px) and (max-width: 767px) {
@media all and (min-width: 481px) and (max-width: 768px) {
#toast-container > div {
padding: 15px 15px 15px 50px;
width: 25em;

1
src/styles/main.scss

@ -30,7 +30,6 @@ @@ -30,7 +30,6 @@
@import "global/loader";
@import "global/views";
@import "global/pages";
@import "global/angular";
@import "global/nicescroll";
@import "global/animations";
@import "global/overlaybar";

20
static/css/csp.min.css vendored

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
/*!
* Spreed WebRTC.
* Copyright (C) 2013-2014 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/>.
*
*/[ng\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide{display:none !important}ng\:form{display:block}.ng-animate-block-transitions{transition:0s all !important;-webkit-transition:0s all !important}.ng-hide-add-active,.ng-hide-remove{display:block !important}

2
static/css/main.min.css vendored

File diff suppressed because one or more lines are too long

59
static/js/app.js

@ -18,6 +18,8 @@ @@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define([
'require',
'jquery',
@ -38,6 +40,7 @@ define([ @@ -38,6 +40,7 @@ define([
'angular-animate',
'angular-humanize',
'angular-route',
'mobile-events'
], function(require, $, _, angular, modernizr, moment, services, directives, filters, controllers, languages) {
@ -65,12 +68,13 @@ define([ @@ -65,12 +68,13 @@ define([
var appConfig = {};
// Implement translation store.
var TranslationData = function(default_language) {
var TranslationData = function(default_language, launcher) {
// Create data structure.
this.data = {
locale_data: {}
};
this.lang = this.default_lang = default_language;
this.getHTTP = launcher.$http.get;
};
TranslationData.prototype.language = function() {
// Return language.
@ -97,25 +101,24 @@ define([ @@ -97,25 +101,24 @@ define([
};
TranslationData.prototype.load = function(domain, url) {
var that = this;
return $.ajax({
dataType: "json",
url: url,
success: function(data) {
return this.getHTTP(url).
success(function(data) {
//console.log("loaded translation data", data);
that.add(domain, data);
},
error: function(err, textStatus, errorThrown) {
console.warn("Failed to load translation data: " + errorThrown);
}).
error(function(data, status) {
console.warn("Failed to load translation data: " + status);
that.add(domain, null);
}
});
});
};
TranslationData.prototype.get = function() {
return this.data;
};
var translationData = new TranslationData("en");
var create = function(ms) {
var create = function(ms, launcher) {
// Create translation data instance.
var translationData = launcher.translationData = new TranslationData("en", launcher);
var modules = ['ui.bootstrap', 'ngSanitize', 'ngAnimate', 'ngHumanize', 'ngRoute'];
if (ms && ms.length) {
@ -179,11 +182,18 @@ define([ @@ -179,11 +182,18 @@ define([
};
var initialize = function(app) {
// Our API version as float. This value is incremented on
// breaking changes to plugins can check on it.
var apiversion = 1.1;
var deferred = $.Deferred();
var initialize = function(app, launcher) {
var globalContext = JSON.parse($("#globalcontext").text());
var deferred = launcher.$q.defer();
var globalContext = JSON.parse(document.getElementById("globalcontext").innerHTML);
if (!globalContext.Cfg.Version) {
globalContext.Cfg.Version = "unknown";
}
app.constant("globalContext", globalContext);
// Configure language.
@ -233,16 +243,19 @@ define([ @@ -233,16 +243,19 @@ define([
console.info("Selected language: "+lang);
// Set language and load default translations.
translationData.lang = lang;
var domain = "messages";
if (lang === translationData.default_lang) {
launcher.translationData.lang = lang;
var translationDomain = "messages";
if (lang === launcher.translationData.default_lang) {
// No need to load default language as it is built in.
translationData.add(domain, null);
launcher.translationData.add(translationDomain, null);
deferred.resolve();
} else {
// Load default translation catalog.
var url = require.toUrl('translation/' + domain + "-" + lang + '.json');
$.when(translationData.load(domain, url)).always(function() {
var url = require.toUrl('translation/'+translationDomain+"-"+lang+'.json');
launcher.translationData.load(translationDomain, url).then(function() {
deferred.resolve();
}, function() {
// Ignore errors.
deferred.resolve();
});
}
@ -250,7 +263,7 @@ define([ @@ -250,7 +263,7 @@ define([
// Set momemt language.
moment.lang(lang);
return deferred.promise();
return deferred.promise;
};
@ -259,7 +272,7 @@ define([ @@ -259,7 +272,7 @@ define([
initialize: initialize,
query: urlQuery,
config: appConfig,
translationData: translationData
apiversion: apiversion
};
});

5
static/js/base.js

@ -18,8 +18,9 @@ @@ -18,8 +18,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
// Helper module to put non dependency base libraries together.
define([
"use strict";
define([ // Helper module to put non dependency base libraries together.
'modernizr',
'moment',
'Howler',

14
static/js/controllers/chatroomcontroller.js

@ -18,15 +18,17 @@ @@ -18,15 +18,17 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
define(['underscore', 'moment', 'text!partials/fileinfo.html', 'text!partials/contactrequest.html', 'text!partials/geolocation.html'], function(_, moment, templateFileInfo, templateContactRequest, templateGeolocation) {
"use strict";
define(['jquery', 'underscore', 'moment', 'text!partials/fileinfo.html', 'text!partials/contactrequest.html', 'text!partials/geolocation.html'], function($, _, moment, templateFileInfo, templateContactRequest, templateGeolocation) {
// ChatroomController
return ["$scope", "$element", "$window", "safeMessage", "safeDisplayName", "$compile", "$filter", "translation", function($scope, $element, $window, safeMessage, safeDisplayName, $compile, $filter, translation) {
$scope.outputElement = $(".output", $element);
$scope.inputElement = $(".input", $element);
$scope.bodyElement = $(".chatbody", $element);
$scope.menuElement = $(".chatmenu", $element);
$scope.outputElement = $element.find(".output");
$scope.inputElement = $element.find(".input");
$scope.bodyElement = $element.find(".chatbody");
$scope.menuElement = $element.find(".chatmenu");
var lastSender = null;
var lastDate = null;
var lastMessageContainer = null;
@ -159,7 +161,7 @@ define(['underscore', 'moment', 'text!partials/fileinfo.html', 'text!partials/co @@ -159,7 +161,7 @@ define(['underscore', 'moment', 'text!partials/fileinfo.html', 'text!partials/co
$scope.canScroll = function() {
var o = $scope.outputElement.get(0);
var o = $scope.outputElement[0];
if ((o.clientHeight - 20) < o.scrollHeight) {
if (!scrollAfterInput && (o.clientHeight + 20) < (o.scrollHeight - o.scrollTop)) {
// Manually scrolled -> do nothing.

25
static/js/controllers/contactsmanagercontroller.js

@ -18,10 +18,20 @@ @@ -18,10 +18,20 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define([], function() {
// ContactsmanagerController
return ["$scope", "$modalInstance", "contactData", "data", "contacts", 'dialogs', 'translation', function($scope, $modalInstance, contactData, data, contacts, dialogs, translation) {
return ["$scope", "$modalInstance", "contactData", "data", "contacts", "dialogs", "translation", "mediaStream", "buddyData", function($scope, $modalInstance, contactData, data, contacts, dialogs, translation, mediaStream, buddyData) {
var getContactSessionId = function(userid) {
var session = null;
var scope = buddyData.lookup(userid, false, false);
if (scope) {
session = scope.session.get();
}
return session && session.Id ? session.Id : null;
};
$scope.header = data.header;
$scope.contacts = [];
$scope.openContactsManagerEdit = function(contact) {
@ -36,7 +46,6 @@ define([], function() { @@ -36,7 +46,6 @@ define([], function() {
}
);
};
var updateContacts = function() {
$scope.contacts = contactData.getAll();
};
@ -50,7 +59,17 @@ define([], function() { @@ -50,7 +59,17 @@ define([], function() {
contacts.e.on('contactremoved', function() {
updateContacts();
});
$scope.doCall = function(contact) {
mediaStream.webrtc.doCall(getContactSessionId(contact.Userid));
$modalInstance.close();
};
$scope.startChat = function(contact) {
$scope.$emit("startchat", getContactSessionId(contact.Userid), {
autofocus: true,
restore: true
});
$modalInstance.close();
};
}];
});

2
static/js/controllers/contactsmanagereditcontroller.js

@ -18,6 +18,8 @@ @@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define([], function() {
// ContactsmanagereditController

6
static/js/controllers/controllers.js

@ -18,22 +18,22 @@ @@ -18,22 +18,22 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define([
'underscore',
'controllers/mediastreamcontroller',
'controllers/statusmessagecontroller',
'controllers/chatroomcontroller',
'controllers/roomchangecontroller',
'controllers/usersettingscontroller',
'controllers/contactsmanagercontroller',
'controllers/contactsmanagereditcontroller'], function(_, MediastreamController, StatusmessageController, ChatroomController, RoomchangeController, UsersettingsController, ContactsmanagerController, ContactsmanagereditController) {
'controllers/contactsmanagereditcontroller'], function(_, MediastreamController, StatusmessageController, ChatroomController, UsersettingsController, ContactsmanagerController, ContactsmanagereditController) {
var controllers = {
MediastreamController: MediastreamController,
StatusmessageController: StatusmessageController,
ChatroomController: ChatroomController,
RoomchangeController: RoomchangeController,
UsersettingsController: UsersettingsController,
ContactsmanagerController: ContactsmanagerController,
ContactsmanagereditController: ContactsmanagereditController

257
static/js/controllers/mediastreamcontroller.js

@ -18,9 +18,11 @@ @@ -18,9 +18,11 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapter'], function(_, BigScreen, moment, sjcl, Modernizr) {
return ["$scope", "$rootScope", "$element", "$window", "$timeout", "safeDisplayName", "safeApply", "mediaStream", "appData", "playSound", "desktopNotify", "alertify", "toastr", "translation", "fileDownload", "localStorage", "screensharing", "userSettingsData", "localStatus", "dialogs", function($scope, $rootScope, $element, $window, $timeout, safeDisplayName, safeApply, mediaStream, appData, playSound, desktopNotify, alertify, toastr, translation, fileDownload, localStorage, screensharing, userSettingsData, localStatus, dialogs) {
"use strict";
define(['jquery', 'underscore', 'angular', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapter'], function($, _, angular, BigScreen, moment, sjcl, Modernizr) {
return ["$scope", "$rootScope", "$element", "$window", "$timeout", "safeDisplayName", "safeApply", "mediaStream", "appData", "playSound", "desktopNotify", "alertify", "toastr", "translation", "fileDownload", "localStorage", "screensharing", "userSettingsData", "localStatus", "dialogs", "rooms", "constraints", function($scope, $rootScope, $element, $window, $timeout, safeDisplayName, safeApply, mediaStream, appData, playSound, desktopNotify, alertify, toastr, translation, fileDownload, localStorage, screensharing, userSettingsData, localStatus, dialogs, rooms, constraints) {
/*console.log("route", $route, $routeParams, $location);*/
@ -86,29 +88,6 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte @@ -86,29 +88,6 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte
appData.set($scope);
var videoQualityMap = {
tiny: {
maxWidth: 80,
maxHeight: 45
},
low: {
maxWidth: 320,
maxHeight: 180
},
high: {
maxWidth: 640,
maxHeight: 360
},
hd: {
minWidth: 1280,
minHeight: 720
},
fullhd: {
minWidth: 1920,
minHeight: 1080
}
}
var displayName = safeDisplayName;
// Init STUN and TURN servers.
@ -147,6 +126,7 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte @@ -147,6 +126,7 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte
$scope.chatMessagesUnseen = 0;
$scope.autoAccept = null;
$scope.isCollapsed = true;
$scope.roomsHistory = [];
$scope.defaults = {
displayName: null,
buddyPicture: null,
@ -219,156 +199,31 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte @@ -219,156 +199,31 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte
console.warn("This is not a WebRTC capable browser.");
return;
}
var settings = $scope.master.settings;
// Create iceServers from scope settings.
// Create iceServers from scope.
var iceServers = [];
var iceServer;
if ($scope.stun.length) {
iceServer = createIceServers($scope.stun);
iceServer = $window.createIceServers($scope.stun);
if (iceServer.length) {
iceServers.push.apply(iceServers, iceServer);
}
}
if ($scope.turn.urls && $scope.turn.urls.length) {
iceServer = createIceServers($scope.turn.urls, $scope.turn.username, $scope.turn.password);
iceServer = $window.createIceServers($scope.turn.urls, $scope.turn.username, $scope.turn.password);
if (iceServer.length) {
iceServers.push.apply(iceServers, iceServer);
}
}
mediaStream.webrtc.settings.pcConfig.iceServers = iceServers;
var audioConstraints = [];
var videoConstraints = [];
var videoConstraintsMandatory = {};
var screensharingConstraints = [];
var pushmulti = function(arrays, data) {
_.each(arrays, function(a) {
a.push(data);
});
};
// Chrome only constraints.
if ($scope.isChrome) {
// Audio settings.
// For defaults in Chromium see https://code.google.com/p/webrtc/source/browse/trunk/talk/media/webrtc/webrtcvoiceengine.cc#225
// Experimental audio settings.
if (settings.experimental.enabled) {
audioConstraints.push({
googEchoCancellation: true // defaults to true
});
audioConstraints.push({
googEchoCancellation2: settings.experimental.audioEchoCancellation2 && true // defaults to false in Chrome
});
audioConstraints.push({
googAutoGainControl: true // defaults to true
});
audioConstraints.push({
googAutoGainControl2: settings.experimental.audioAutoGainControl2 && true // defaults to false in Chrome
});
audioConstraints.push({
googNoiseSuppression: true // defaults to true
});
audioConstraints.push({
googgNoiseSuppression2: settings.experimental.audioNoiseSuppression2 && true // defaults to false in Chrome
});
audioConstraints.push({
googHighpassFilter: true // defaults to true
});
audioConstraints.push({
googTypingNoiseDetection: settings.experimental.audioTypingNoiseDetection && true // defaults to true in Chrome
});
}
if ($scope.supported.renderToAssociatedSink) {
audioConstraints.push({
// When true uses the default communications device on Windows.
// https://codereview.chromium.org/155863003
googDucking: true // defaults to true on Windows.
});
audioConstraints.push({
// Chrome will start rendering mediastream output to an output device that's associated with
// the input stream that was opened via getUserMedia.
// https://chromiumcodereview.appspot.com/23558010
chromeRenderToAssociatedSink: settings.audioRenderToAssociatedSkin && true // defaults to false in Chrome
});
}
// Select microphone device by id.
if (settings.microphoneId) {
audioConstraints.push({
sourceId: settings.microphoneId
});
}
// Select camera by device id.
if (settings.cameraId) {
videoConstraints.push({
sourceId: settings.cameraId
});
}
// Video settings.
if (settings.experimental.enabled) {
// Experimental video settings.
pushmulti([videoConstraints, screensharingConstraints], {
// Changes the way the video encoding adapts to the available bandwidth.
// https://code.google.com/p/webrtc/issues/detail?id=3351
googLeakyBucket: settings.experimental.videoLeakyBucket && true // defaults to false in Chrome
});
pushmulti([videoConstraints, screensharingConstraints], {
// Removes the noise in the captured video stream at the expense of CPU.
googNoiseReduction: settings.experimental.videoNoiseReduction && true // defaults to false in Chrome
});
pushmulti([videoConstraints, screensharingConstraints], {
googCpuOveruseDetection: settings.experimental.videoCpuOveruseDetection && true // defaults to true in Chrome
});
}
// Video.
videoConstraintsMandatory = $.extend(videoConstraintsMandatory, videoQualityMap[settings.videoQuality]);
// Not supported as of Firefox 27.
if (settings.maxFrameRate && settings.maxFrameRate != "auto") {
videoConstraintsMandatory.maxFrameRate = parseInt(settings.maxFrameRate, 10);
}
}
// Apply the shit.
// Stereo.
mediaStream.webrtc.settings.stereo = settings.stereo;
mediaStream.webrtc.settings.mediaConstraints.video.mandatory = videoConstraintsMandatory;
mediaStream.webrtc.settings.mediaConstraints.video.optional = videoConstraints;
mediaStream.webrtc.settings.mediaConstraints.audio = {
optional: audioConstraints
};
mediaStream.webrtc.settings.pcConfig.iceServers = iceServers;
mediaStream.webrtc.settings.screensharing.mediaConstraints.video.optional = screensharingConstraints;
// Inject optional stuff.
var optionalPcConstraints = mediaStream.webrtc.settings.pcConstraints.optional = [];
if ($window.webrtcDetectedBrowser === "chrome") {
// NOTE(longsleep): We can always enable SCTP data channels, as we have a workaround
// using the "active" event for Firefox < 27.
// SCTP does not work correctly with Chrome 31. Require M32.
if ($window.webrtcDetectedVersion >= 32) {
// SCTP is supported from Chrome M31.
// No need to pass DTLS constraint as it is on by default in Chrome M31.
// For SCTP, reliable and ordered is true by default.
} else {
// Chrome < M32 does not yet do DTLS-SRTP by default whereas Firefox only
// does DTLS-SRTP. In order to get interop, you must supply Chrome
// with a PC constructor constraint to enable DTLS.
console.warn("Turning on SCTP combatibility - please update your Chrome.");
optionalPcConstraints.push({
DtlsSrtpKeyAgreement: true
});
}
}
//console.log("WebRTC settings", mediaStream.webrtc.settings);
// Refresh constraints.
constraints.refresh($scope.master.settings);
};
$scope.refreshWebrtcSettings(); // Call once for bootstrap.
@ -421,6 +276,8 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte @@ -421,6 +276,8 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte
} else {
$scope.loadedUser = false;
}
$scope.roomsHistory = [];
appData.e.triggerHandler("userSettingsLoaded", [$scope.loadedUser, $scope.user]);
$scope.reset();
};
@ -544,20 +401,18 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte @@ -544,20 +401,18 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte
// Unmark authorization process.
if (data.Userid) {
mediaStream.users.authorizing(false);
} else if (!mediaStream.users.authorizing()) {
appData.authorizing(false);
} else if (!appData.authorizing()) {
// Trigger user data load when not in authorizing phase.
$scope.loadUserSettings();
}
if (!$rootScope.roomid && $scope.master.settings.defaultRoom) {
// Select room if settings have an alternative default room.
if (rooms.inDefaultRoom() && $scope.master.settings.defaultRoom) {
console.log("Selecting default room from settings:", [$scope.master.settings.defaultRoom]);
mediaStream.changeRoom($scope.master.settings.defaultRoom, true);
rooms.joinByName($scope.master.settings.defaultRoom, true);
}
// Always apply room after self received to avoid double stuff.
mediaStream.applyRoom();
});
mediaStream.webrtc.e.on("peercall", function(event, peercall) {
@ -660,36 +515,36 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte @@ -660,36 +515,36 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte
}
};
mediaStream.connector.e.on("open error close", function(event, options) {
var t = event.type;
var opts = $.extend({}, options);
$scope.$on("room.joined", function(ev) {
// TODO(lcooper): Is it really needful to do this stuff?
$timeout.cancel(ttlTimeout);
if (!opts.soft) {
// Reset login information for anything not soft.
$scope.userid = $scope.suserid = null;
}
switch (t) {
connected = true;
reconnecting = false;
$scope.updateStatus(true);
});
mediaStream.connector.e.on("open error close", function(event) {
$timeout.cancel(ttlTimeout);
$scope.userid = $scope.suserid = null;
switch (event.type) {
case "open":
t = "waiting";
connected = true;
reconnecting = false;
$scope.updateStatus(true);
if (opts.soft) {
return;
}
$scope.setStatus("waiting");
break;
case "error":
if (reconnecting || connected) {
reconnecting = false;
reconnect();
return;
} else {
$scope.setStatus(event.type);
}
break;
case "close":
reconnect();
return;
break;
}
$scope.setStatus(t);
});
mediaStream.webrtc.e.on("waitforusermedia connecting", function(event, currentcall) {
@ -797,23 +652,6 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte @@ -797,23 +652,6 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte
if (mediaStream.connector.connected) {
$scope.setStatus("waiting");
}
if ($scope.roomstatus) {
$scope.layout.buddylist = true;
$scope.layout.buddylistAutoHide = false;
} else {
$scope.layout.buddylist = false;
$scope.layout.buddylistAutoHide = true;
}
});
$scope.$watch("roomstatus", function(roomstatus) {
if (roomstatus && !$scope.peer) {
$scope.layout.buddylist = true;
$scope.layout.buddylistAutoHide = false;
} else if (!$scope.layout.buddylistAutoHide) {
$scope.layout.buddylist = false;
$scope.layout.buddylistAutoHide = true;
}
});
mediaStream.webrtc.e.on("busy", function(event, from) {
@ -825,6 +663,7 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte @@ -825,6 +663,7 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte
});
mediaStream.webrtc.e.on("bye", function(event, reason, from) {
console.log("received bye", pickupTimeout, reason);
switch (reason) {
case "busy":
console.log("User is busy", reason, from);
@ -851,6 +690,13 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte @@ -851,6 +690,13 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte
console.log("User cannot accept call because of error");
alertify.dialog.alert(translation._("Oops") + "<br/>" + translation._("User hung up because of error."));
break;
case "abort":
console.log("Remote call was aborted before we did pick up");
$scope.$emit("notification", "abortbeforepickup", {
reason: reason,
from: from
});
break;
}
});
@ -885,10 +731,12 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte @@ -885,10 +731,12 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte
message = displayName(details.from) + translation._(" does not pick up.");
break;
case "incomingbusy":
toastr.info(moment().format("llll"), displayName(details.from) + translation._(" tried to call you."));
toastr.info(moment().format("lll"), displayName(details.from) + translation._(" tried to call you"));
break;
case "abortbeforepickup":
// Fall through
case "incomingpickuptimeout":
toastr.info(moment().format("llll"), displayName(details.from) + translation._(" called you."));
toastr.info(moment().format("lll"), displayName(details.from) + translation._(" called you"));
break;
}
if (message) {
@ -919,6 +767,17 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte @@ -919,6 +767,17 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte
$scope.chatMessagesUnseen = $scope.chatMessagesUnseen - count;
});
$scope.$on("room.joined", function(event, roomName) {
if (roomName) {
_.pull($scope.roomsHistory, roomName);
$scope.roomsHistory.unshift(roomName);
if ($scope.roomsHistory.length > 15) {
// Limit the history.
$scope.roomsHistory = $scope.roomsHistory.splice(0, 15);
}
}
});
_.defer(function() {
if (!Modernizr.websockets) {
alertify.dialog.alert(translation._("Your browser is not supported. Please upgrade to a current version."));

90
static/js/controllers/roomchangecontroller.js

@ -1,90 +0,0 @@ @@ -1,90 +0,0 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 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/>.
*
*/
define([], function() {
// RoomchangeController
return ["$scope", "$element", "$window", "mediaStream", "$http", "$timeout", function($scope, $element, $window, mediaStream, $http, $timeout) {
//console.log("Room change controller", $element, $scope.roomdata);
var url = mediaStream.url.api("rooms");
var ctrl = this;
ctrl.enabled = true;
ctrl.getRoom = function(cb) {
$http({
method: "POST",
url: url,
data: $.param({}),
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}).
success(function(data, status) {
cb(data);
}).
error(function() {
console.error("Failed to retrieve room link.");
cb({});
});
};
$scope.changeRoomToId = function(id) {
return mediaStream.changeRoom(id);
};
$scope.refreshRoom = function() {
if (ctrl.enabled) {
ctrl.getRoom(function(roomdata) {
console.info("Retrieved room data", roomdata);
$scope.roomdata = roomdata;
$element.find(".btn-roomcreate").get(0).focus();
});
}
};
$scope.$on("$destroy", function() {
//console.log("Room change controller destroyed");
ctrl.enabled = false;
});
$scope.roomdata = {};
$scope.$watch("roomdata.name", function(n) {
//console.log("roomdata.name changed", n);
if (!n) {
n = "";
}
var u = encodeURIComponent(n);
$scope.roomdata.url = "/" + u;
$scope.roomdata.link = mediaStream.url.room(n);
});
var roomDataLinkInput = $element.find(".roomdata-link-input");
if (roomDataLinkInput.length) {
$timeout(function() {
$scope.refreshRoom();
}, 100);
}
}];
});

2
static/js/controllers/statusmessagecontroller.js

@ -18,6 +18,8 @@ @@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define([], function() {
// StatusmessageController

11
static/js/controllers/usersettingscontroller.js

@ -18,6 +18,8 @@ @@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define([], function() {
// UsersettingsController
@ -25,7 +27,7 @@ define([], function() { @@ -25,7 +27,7 @@ define([], function() {
$scope.withUsersForget = true;
this.registerUserid = function(btn) {
this.registerUserid = function(event) {
var successHandler = function(data) {
console.info("Created new userid:", data.userid);
@ -38,8 +40,13 @@ define([], function() { @@ -38,8 +40,13 @@ define([], function() {
delete data.nonce;
};
var form = null;
if (event && event.target) {
form = event.target.form;
}
console.log("No userid - creating one ...");
mediaStream.users.register(btn.form, function(data) {
mediaStream.users.register(form, function(data) {
if (data.nonce) {
successHandler(data);
} else {

2
static/js/directives/audiolevel.js

@ -18,6 +18,8 @@ @@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define(['jquery', 'underscore'], function($, _) {
return ["$window", "mediaStream", "safeApply", "animationFrame", function($window, mediaStream, safeApply, animationFrame) {

283
static/js/directives/audiovideo.js

@ -18,6 +18,8 @@ @@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/audiovideopeer.html', 'bigscreen', 'webrtc.adapter'], function($, _, template, templatePeer, BigScreen) {
return ["$window", "$compile", "$filter", "mediaStream", "safeApply", "desktopNotify", "buddyData", "videoWaiter", "videoLayout", "animationFrame", function($window, $compile, $filter, mediaStream, safeApply, desktopNotify, buddyData, videoWaiter, videoLayout, animationFrame) {
@ -26,19 +28,33 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/ @@ -26,19 +28,33 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/
var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
var peers = {};
var events = $({});
var streams = {};
var calls = {};
var getStreamId = function(stream, currentcall) {
var id = currentcall.id + "-" + stream.id;
console.log("Created stream ID", id);
return id;
};
// Dummy stream.
var dummy = {
id: "defaultDummyStream"
};
$scope.container = $element.get(0);
$scope.container = $element[0];
$scope.layoutparent = $element.parent();
$scope.remoteVideos = $element.find(".remoteVideos").get(0);
$scope.localVideo = $element.find(".localVideo").get(0);
$scope.miniVideo = $element.find(".miniVideo").get(0);
$scope.mini = $element.find(".miniContainer").get(0);
$scope.remoteVideos = $element.find(".remoteVideos")[0];
$scope.localVideo = $element.find(".localVideo")[0];
$scope.miniVideo = $element.find(".miniVideo")[0];
$scope.mini = $element.find(".miniContainer")[0];
$scope.hasUsermedia = false;
$scope.isActive = false;
$scope.haveStreams = false;
$scope.peersTalking = {};
$scope.rendererName = $scope.defaultRendererName = "democrazy";
@ -46,86 +62,125 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/ @@ -46,86 +62,125 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/
$scope.addRemoteStream = function(stream, currentcall) {
//console.log("Add remote stream to scope", pc.id, stream);
// Create scope.
var subscope = $scope.$new(true);
var id = getStreamId(stream, currentcall);
if (streams.hasOwnProperty(id)) {
console.warn("Cowardly refusing to add stream id twice", id, currentcall);
return;
}
var subscope;
// Dummy replacement support.
if (calls.hasOwnProperty(currentcall.id)) {
subscope = calls[currentcall.id];
if (stream === dummy) {
return;
}
if (subscope.dummy) {
subscope.$apply(function() {
subscope.attachStream(stream);
});
return;
}
} else {
// Create scope.
subscope = $scope.$new();
calls[currentcall.id] = subscope;
}
//console.log("Add remote stream to scope", stream.id, stream, currentcall);
var peerid = subscope.peerid = currentcall.id;
buddyData.push(peerid);
subscope.unattached = true;
subscope.withvideo = false;
subscope.onlyaudio = false;
subscope.talking = false;
subscope.destroyed = false;
subscope.applyTalking = function(talking) {
subscope.talking = !! talking;
safeApply(subscope);
};
subscope.$on("active", function() {
console.log("Stream scope is now active", peerid);
events.triggerHandler("active." + peerid, [subscope, currentcall, stream]);
console.log("Stream scope is now active", id, peerid);
});
subscope.$on("$destroy", function() {
console.log("Destroyed scope for audiovideo", subscope);
console.log("Destroyed scope for stream", id, peerid);
subscope.destroyed = true;
});
console.log("Created stream scope", peerid);
console.log("Created stream scope", id, peerid);
// Add created scope.
peers[peerid] = subscope;
if (stream === dummy) {
subscope.dummy = true;
} else {
streams[id] = subscope;
}
// Render template.
peerTemplate(subscope, function(clonedElement, scope) {
$($scope.remoteVideos).append(clonedElement);
clonedElement.data("peerid", scope.peerid);
scope.element = clonedElement;
var video = clonedElement.find("video").get(0);
$window.attachMediaStream(video, stream);
// Waiter callbacks also count as connected, as browser support (FireFox 25) is not setting state changes properly.
videoWaiter.wait(video, stream, function(withvideo) {
if (scope.destroyed) {
console.log("Abort wait for video on destroyed scope.");
scope.attachStream = function(stream) {
if (stream === dummy) {
return;
}
if (withvideo) {
scope.$apply(function($scope) {
$scope.withvideo = true;
});
} else {
console.info("Incoming stream has no video tracks.");
scope.$apply(function($scope) {
$scope.onlyaudio = true;
});
}
scope.$emit("active", currentcall);
$scope.redraw();
}, function() {
if (scope.destroyed) {
console.log("No longer wait for video on destroyed scope.");
return;
}
console.warn("We did not receive video data for remote stream", currentcall, stream, video);
scope.$emit("active", currentcall);
$scope.redraw();
});
var video = clonedElement.find("video")[0];
$window.attachMediaStream(video, stream);
// Waiter callbacks also count as connected, as browser support (FireFox 25) is not setting state changes properly.
videoWaiter.wait(video, stream, function(withvideo) {
if (scope.destroyed) {
console.log("Abort wait for video on destroyed scope.");
return;
}
if (withvideo) {
scope.$apply(function($scope) {
$scope.withvideo = true;
});
} else {
console.info("Incoming stream has no video tracks.");
scope.$apply(function($scope) {
$scope.onlyaudio = true;
});
}
scope.$emit("active", currentcall);
$scope.redraw();
}, function() {
if (scope.destroyed) {
console.log("No longer wait for video on destroyed scope.");
return;
}
console.warn("We did not receive video data for remote stream", currentcall, stream, video);
scope.$emit("active", currentcall);
$scope.redraw();
});
scope.unattached = false;
scope.dummy = false;
};
scope.doChat = function() {
$scope.$emit("startchat", currentcall.id, {
autofocus: true,
restore: true
});
};
scope.attachStream(stream);
});
};
$scope.removeRemoteStream = function(stream, currentcall) {
var subscope = peers[currentcall.id];
//console.log("remove stream", stream, stream.id, currentcall);
var id = getStreamId(stream, currentcall);
var subscope = streams[id];
if (subscope) {
buddyData.pop(currentcall.id);
delete peers[currentcall.id];
delete streams[id];
//console.log("remove scope", subscope);
if (subscope.element) {
subscope.element.remove();
}
var callscope = calls[currentcall.id];
if (subscope === callscope) {
delete calls[currentcall.id];
}
subscope.$destroy();
$scope.redraw();
}
@ -134,17 +189,9 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/ @@ -134,17 +189,9 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/
// Talking updates receiver.
mediaStream.api.e.on("received.talking", function(event, id, from, talking) {
var scope = peers[from];
//console.log("received.talking", talking, scope);
if (scope) {
scope.applyTalking(talking);
} else {
console.log("Received talking state without scope -> adding event.", from, talking);
events.one("active." + from, function(event, scope) {
console.log("Applying previously received talking state", from, talking);
scope.applyTalking(talking);
});
}
$scope.$apply(function(scope) {
scope.peersTalking[from] = !!talking;
});
});
$scope.$on("active", function(currentcall) {
@ -171,69 +218,83 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/ @@ -171,69 +218,83 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/
//console.log("Toggle full screen", BigScreen.enabled, $scope.isActive, $scope.hasUsermedia);
if (BigScreen.enabled && ($scope.isActive || $scope.hasUsermedia)) {
$scope.layoutparent.toggleClass("fullscreen");
BigScreen.toggle($scope.layoutparent.get(0));
BigScreen.toggle($scope.layoutparent[0]);
}
};
mediaStream.webrtc.e.on("usermedia", function(event, usermedia) {
//console.log("XXXXXXXXXXXXXXXXXXXXXXXXX usermedia event", usermedia);
$scope.hasUsermedia = true;
usermedia.attachMediaStream($scope.localVideo);
var count = 0;
var waitForLocalVideo = function() {
if (!$scope.hasUsermedia) {
return;
}
if ($scope.localVideo.videoWidth > 0) {
$scope.localVideo.style.opacity = 1;
$scope.redraw();
} else {
count++;
if (count < 100) {
setTimeout(waitForLocalVideo, 100);
//console.log("XXXX XXXXXXXXXXXXXXXXXXXXX usermedia event", usermedia);
if ($scope.haveStreams) {
usermedia.attachMediaStream($scope.miniVideo);
$scope.redraw();
} else {
$scope.hasUsermedia = true;
usermedia.attachMediaStream($scope.localVideo);
var count = 0;
var waitForLocalVideo = function() {
if (!$scope.hasUsermedia || $scope.isActive) {
return;
}
if ($scope.localVideo.videoWidth > 0) {
$scope.localVideo.style.opacity = 1;
$scope.redraw();
} else {
console.warn("Timeout while waiting for local video.")
count++;
if (count < 100) {
setTimeout(waitForLocalVideo, 100);
} else {
console.warn("Timeout while waiting for local video.")
}
}
}
};
waitForLocalVideo();
};
waitForLocalVideo();
}
});
mediaStream.webrtc.e.on("done", function() {
$scope.hasUsermedia = false;
$scope.isActive = false;
if (BigScreen.enabled) {
BigScreen.exit();
}
_.delay(function() {
if ($scope.isActive) {
return;
$scope.$apply(function() {
$scope.hasUsermedia = false;
$scope.isActive = false;
$scope.peersTalking = {};
if (BigScreen.enabled) {
BigScreen.exit();
}
$scope.localVideo.src = '';
$scope.miniVideo.src = '';
$($scope.remoteVideos).children(".remoteVideo").remove();
}, 1500);
$($scope.mini).removeClass("visible");
$scope.localVideo.style.opacity = 0;
$scope.remoteVideos.style.opacity = 0;
$element.removeClass('active');
_.each(peers, function(scope, k) {
scope.$destroy();
delete peers[k];
_.delay(function() {
if ($scope.isActive) {
return;
}
$scope.localVideo.src = '';
$scope.miniVideo.src = '';
$($scope.remoteVideos).children(".remoteVideo").remove();
}, 1500);
$($scope.mini).removeClass("visible");
$scope.localVideo.style.opacity = 0;
$scope.remoteVideos.style.opacity = 0;
$element.removeClass('active');
_.each(streams, function(scope, k) {
scope.$destroy();
delete streams[k];
});
$scope.rendererName = $scope.defaultRendererName;
$scope.haveStreams = false;
});
$scope.rendererName = $scope.defaultRendererName;
});
mediaStream.webrtc.e.on("streamadded", function(event, stream, currentcall) {
console.log("Remote stream added.", stream, currentcall);
if (_.isEmpty(peers)) {
if (!$scope.haveStreams) {
//console.log("First stream");
$window.reattachMediaStream($scope.miniVideo, $scope.localVideo);
$scope.haveStreams = true;
}
$scope.addRemoteStream(stream, currentcall);
@ -246,8 +307,26 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/ @@ -246,8 +307,26 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/
});
mediaStream.webrtc.e.on("statechange", function(event, iceConnectionState, currentcall) {
if (!$scope.haveStreams) {
return;
}
switch (iceConnectionState) {
case "new":
case "checking":
case "connected":
case "completed":
case "failed":
$scope.addRemoteStream(dummy, currentcall);
break;
}
});
return {
peers: peers
streams: streams
};
}];

2
static/js/directives/bfi.js

@ -18,6 +18,8 @@ @@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define(["bootstrap-file-input"], function() {
// bfi

65
static/js/directives/buddylist.js

@ -18,10 +18,12 @@ @@ -18,10 +18,12 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define(['underscore', 'text!partials/buddylist.html'], function(_, template) {
// buddyList
return ["$compile", "buddyList", "mediaStream", "contacts", function($compile, buddyList, mediaStream, contacts) {
return ["buddyList", "api", "webrtc", "contacts", function(buddyList, api, webrtc, contacts) {
//console.log("buddyList directive");
@ -30,10 +32,34 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) { @@ -30,10 +32,34 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) {
$scope.layout.buddylist = false;
$scope.layout.buddylistAutoHide = true;
$scope.doCall = function(id) {
var inRoom = false;
var updateBuddyListVisibility = function() {
if (inRoom && !$scope.peer) {
$scope.layout.buddylist = true;
$scope.layout.buddylistAutoHide = false;
} else if (!$scope.layout.buddylistAutoHide) {
$scope.layout.buddylist = false;
$scope.layout.buddylistAutoHide = true;
}
};
mediaStream.webrtc.doCall(id);
webrtc.e.on("done", function() {
$scope.$apply(updateBuddyListVisibility);
});
$scope.$on("room.joined", function(ev) {
inRoom = true;
updateBuddyListVisibility();
});
$scope.$on("room.left", function(ev) {
inRoom = false;
buddylist.onClosed();
updateBuddyListVisibility();
});
$scope.doCall = function(id) {
webrtc.doCall(id);
};
$scope.doChat = function(id) {
@ -61,23 +87,6 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) { @@ -61,23 +87,6 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) {
};
/*
$scope.doAudioConference = function(id) {
$scope.updateAutoAccept(id);
mediaStream.api.sendChat(id, null, {
AutoCall: {
Type: "conference",
Id: mediaStream.connector.roomid
}
})
};*/
$scope.setRoomStatus = function(status) {
$scope.$emit("roomStatus", status);
};
var buddylist = $scope.buddylist = buddyList.buddylist($element, $scope, {});
var onJoined = _.bind(buddylist.onJoined, buddylist);
var onLeft = _.bind(buddylist.onLeft, buddylist);
@ -85,15 +94,14 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) { @@ -85,15 +94,14 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) {
var onContactAdded = _.bind(buddylist.onContactAdded, buddylist);
var onContactRemoved = _.bind(buddylist.onContactRemoved, buddylist);
var onContactUpdated = _.bind(buddylist.onContactUpdated, buddylist);
mediaStream.api.e.on("received.userleftorjoined", function(event, dataType, data) {
api.e.on("received.userleftorjoined", function(event, dataType, data) {
if (dataType === "Left") {
onLeft(data);
} else {
onJoined(data);
}
});
mediaStream.api.e.on("received.users", function(event, data) {
$scope.setRoomStatus(true);
api.e.on("received.users", function(event, data) {
var selfId = $scope.id;
_.each(data, function(p) {
if (p.Id !== selfId) {
@ -102,17 +110,10 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) { @@ -102,17 +110,10 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) {
});
$scope.$apply();
});
mediaStream.api.e.on("received.status", function(event, data) {
api.e.on("received.status", function(event, data) {
onStatus(data);
});
mediaStream.connector.e.on("closed error", function() {
$scope.setRoomStatus(false);
buddylist.onClosed();
});
// Request user list whenever the connection comes ready.
mediaStream.connector.ready(function() {
mediaStream.api.requestUsers();
});
// Contacts.
contacts.e.on("contactadded", function(event, data) {
onContactAdded(data);

14
static/js/directives/buddypicturecapture.js

@ -18,10 +18,12 @@ @@ -18,10 +18,12 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define(['jquery', 'underscore', 'text!partials/buddypicturecapture.html'], function($, _, template) {
// buddyPictureCapture
return ["$compile", function($compile) {
return ["$compile", "$window", function($compile, $window) {
var controller = ['$scope', 'safeApply', '$timeout', '$q', function($scope, safeApply, $timeout, $q) {
@ -124,13 +126,13 @@ define(['jquery', 'underscore', 'text!partials/buddypicturecapture.html'], funct @@ -124,13 +126,13 @@ define(['jquery', 'underscore', 'text!partials/buddypicturecapture.html'], funct
}]
};
}
getUserMedia({
$window.getUserMedia({
video: videoConstraints
}, function(stream) {
$scope.showTakePicture = true;
localStream = stream;
$scope.waitingForPermission = false;
attachMediaStream($scope.video, stream);
$window.attachMediaStream($scope.video, stream);
safeApply($scope);
videoAllowed.resolve(true);
}, function(error) {
@ -176,10 +178,10 @@ define(['jquery', 'underscore', 'text!partials/buddypicturecapture.html'], funct @@ -176,10 +178,10 @@ define(['jquery', 'underscore', 'text!partials/buddypicturecapture.html'], funct
var link = function($scope, $element, $attrs, modelController) {
$scope.video = $element.find("video").get(0);
$scope.video = $element.find("video")[0];
$scope.flash = $element.find(".videoFlash");
$scope.canvasPic = $element.find("canvas.videoPic").get(0);
$scope.canvasPrev = $element.find("canvas.videoPrev").get(0);
$scope.canvasPic = $element.find("canvas.videoPic")[0];
$scope.canvasPrev = $element.find("canvas.videoPrev")[0];
$($scope.canvasPic).attr($scope.captureSize);
$scope.save = function() {

46
static/js/directives/buddypictureupload.js

@ -18,11 +18,23 @@ @@ -18,11 +18,23 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define(['jquery', 'underscore', 'text!partials/buddypictureupload.html'], function($, _, template) {
// buddyPictureUpload
return ["$compile", function($compile) {
var getNumFromPx = function(px) {
var num = px.match(/[\-0-9]+/);
if (num) {
num = Number(num[0]);
} else {
num = 0;
}
return num;
};
var controller = ['$scope', 'safeApply', '$timeout', '$q', 'translation', function($scope, safeApply, $timeout, $q, translation) {
var previewWidth = 205;
@ -40,6 +52,7 @@ define(['jquery', 'underscore', 'text!partials/buddypictureupload.html'], functi @@ -40,6 +52,7 @@ define(['jquery', 'underscore', 'text!partials/buddypictureupload.html'], functi
$scope.upload = {
status: 0
};
$scope.aspectRatio = 1;
var completedUpload = function() {
$scope.upload.status = 100;
@ -49,6 +62,7 @@ define(['jquery', 'underscore', 'text!partials/buddypictureupload.html'], functi @@ -49,6 +62,7 @@ define(['jquery', 'underscore', 'text!partials/buddypictureupload.html'], functi
$scope.prevImage.onload = function() {
// clear old dimensions
this.style.cssText = null;
$scope.aspectRatio = this.width/this.height;
// get new dimensions
var dim = getAutoFitDimensions(this, {width: previewWidth, height: previewHeight});
this.style.width = dim.width + 'px';
@ -61,10 +75,6 @@ define(['jquery', 'underscore', 'text!partials/buddypictureupload.html'], functi @@ -61,10 +75,6 @@ define(['jquery', 'underscore', 'text!partials/buddypictureupload.html'], functi
$scope.prevImage.src = data;
};
var getNumFromPx = function(px) {
return px.match(/[\-0-9]+/) ? Number(px.match(/[\-0-9]+/)[0]) : 0;
};
// Auto fit by smallest dimension
var getAutoFitDimensions = function(from, to) {
if (!from.width && !from.height && !to.width && !to.height) {
@ -149,7 +159,7 @@ define(['jquery', 'underscore', 'text!partials/buddypictureupload.html'], functi @@ -149,7 +159,7 @@ define(['jquery', 'underscore', 'text!partials/buddypictureupload.html'], functi
//console.log('file progress', event);
var percentComplete = event.loaded/event.total * 100;
// show complete only when src is loaded in image element
if(percentComplete != 100) {
if (percentComplete !== 100) {
$scope.$apply(function(scope) {
$scope.upload.status = percentComplete;
});
@ -203,7 +213,7 @@ define(['jquery', 'underscore', 'text!partials/buddypictureupload.html'], functi @@ -203,7 +213,7 @@ define(['jquery', 'underscore', 'text!partials/buddypictureupload.html'], functi
var link = function($scope, $element) {
$scope.prevImage = $element.find("img.preview").get(0);
$scope.prevImage = $element.find("img.preview")[0];
$scope.clearInput = function() {
$element.find("input[type=file]")[0].value = "";
};
@ -216,17 +226,21 @@ define(['jquery', 'underscore', 'text!partials/buddypictureupload.html'], functi @@ -216,17 +226,21 @@ define(['jquery', 'underscore', 'text!partials/buddypictureupload.html'], functi
};
var pxDefaultMovementSpeed = 5;
// Return the correct image height based on changes to the image width.
var calcHeight = function(width) {
return (getNumFromPx(width) / $scope.aspectRatio) + 'px';
};
var incrementPx = function(num, pxToMove) {
if(pxToMove === undefined) {
if (pxToMove === undefined) {
pxToMove = pxDefaultMovementSpeed;
}
return ((Number(num.match(/[\-0-9]+/)) + pxToMove) + 'px');
return (getNumFromPx(num) + pxToMove) + 'px';
};
var decrementPx = function(num, pxToMove) {
if(pxToMove === undefined) {
if (pxToMove === undefined) {
pxToMove = pxDefaultMovementSpeed;
}
return ((Number(num.match(/[\-0-9]+/)) - pxToMove) + 'px');
return (getNumFromPx(num) - pxToMove) + 'px';
};
var moveImageUp = function(pxMove) {
$scope.prevImage.style.top = decrementPx($scope.prevImage.style.top, pxMove);
@ -241,15 +255,15 @@ define(['jquery', 'underscore', 'text!partials/buddypictureupload.html'], functi @@ -241,15 +255,15 @@ define(['jquery', 'underscore', 'text!partials/buddypictureupload.html'], functi
$scope.prevImage.style.left = incrementPx($scope.prevImage.style.left, pxMove);
};
var makeImageLarger = function() {
$scope.prevImage.style.height = incrementPx($scope.prevImage.style.height);
moveImageLeft(1);
$scope.prevImage.style.width = incrementPx($scope.prevImage.style.width);
$scope.prevImage.style.height = calcHeight($scope.prevImage.style.width);
moveImageLeft(1);
moveImageUp(2);
};
var makeImageSmaller = function() {
$scope.prevImage.style.height = decrementPx($scope.prevImage.style.height);
moveImageRight(1);
$scope.prevImage.style.width = decrementPx($scope.prevImage.style.width);
$scope.prevImage.style.height = calcHeight($scope.prevImage.style.width);
moveImageRight(1);
moveImageDown(2);
};
var changeImage = function(evt) {
@ -263,8 +277,8 @@ define(['jquery', 'underscore', 'text!partials/buddypictureupload.html'], functi @@ -263,8 +277,8 @@ define(['jquery', 'underscore', 'text!partials/buddypictureupload.html'], functi
}
};
$element.find(".fa-long-arrow-up").on('mousedown', null, {intervalNum: intervalNum, action: moveImageUp}, changeImage);
$element.find(".fa-long-arrow-down").on('mousedown', null, {intervalNum: intervalNum, action: moveImageDown}, changeImage);
$element.find(".fa-long-arrow-up").on('mousedown', null, {intervalNum: intervalNum, action: moveImageDown}, changeImage);
$element.find(".fa-long-arrow-down").on('mousedown', null, {intervalNum: intervalNum, action: moveImageUp}, changeImage);
$element.find(".fa-long-arrow-up").on('mouseup', null, {intervalNum: intervalNum}, changeImage);
$element.find(".fa-long-arrow-down").on('mouseup', null, {intervalNum: intervalNum}, changeImage);

40
static/js/directives/chat.js

@ -18,7 +18,9 @@ @@ -18,7 +18,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'], function(_, templateChat, templateChatroom) {
"use strict";
define(['jquery', 'underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'], function($, _, templateChat, templateChatroom) {
return ["$compile", "safeDisplayName", "mediaStream", "safeApply", "desktopNotify", "translation", "playSound", "fileUpload", "randomGen", "buddyData", "appData", "$timeout", "geolocation", function($compile, safeDisplayName, mediaStream, safeApply, desktopNotify, translation, playSound, fileUpload, randomGen, buddyData, appData, $timeout, geolocation) {
@ -44,16 +46,13 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'], @@ -44,16 +46,13 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'],
var res = [];
for (var i = 0; i < ctrl.visibleRooms.length; i++) {
var r = rooms[ctrl.visibleRooms[i]];
if (!r || r.id === ctrl.group) {
if (!r) {
continue;
}
res.push(r);
}
return res;
};
$scope.getGroupRoom = function() {
return rooms[ctrl.group];
};
mediaStream.api.e.on("received.chat", function(event, id, from, data, p2p) {
@ -182,22 +181,31 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'], @@ -182,22 +181,31 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'],
scope.showGroupRoom = function(settings, options) {
var stngs = $.extend({
title: translation._("Room chat")
title: translation._("Room chat"),
group: true
}, settings);
return scope.showRoom(controller.group, stngs, options);
};
scope.hideGroupRoom = function(settings, options) {
return scope.hideRoom(controller.group);
};
scope.showRoom = function(id, settings, opts) {
var options = $.extend({}, opts);
var subscope = controller.rooms[id];
var index = controller.visibleRooms.length;
if (!subscope) {
console.log("Create new chatroom", [id]);
controller.visibleRooms.push(id);
if (settings.group) {
controller.visibleRooms.unshift(id);
} else {
controller.visibleRooms.push(id);
}
subscope = controller.rooms[id] = scope.$new();
translation.inject(subscope);
subscope.id = id;
subscope.isgroupchat = id === controller.group ? true : false;
subscope.isgroupchat = !!settings.group;
subscope.index = index;
subscope.settings = settings;
subscope.visible = false;
@ -474,11 +482,6 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'], @@ -474,11 +482,6 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'],
scope.currentRoomActive = false;
}
if (!controller.visibleRooms.length) {
scope.showGroupRoom(null, {
restore: true,
noenable: true,
noactivate: true
});
// If last visible room was removed, hide chat.
scope.layout.chat = false;
}
@ -544,18 +547,23 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'], @@ -544,18 +547,23 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'],
scope.layout.chatMaximized = false;
});
scope.$on("room", function(event, room) {
scope.$on("room.updated", function(event, room) {
var subscope = scope.showGroupRoom(null, {
restore: true,
noenable: true,
noactivate: true
});
if (room) {
var msg = $("<span>").text(translation._("You are now in room %s ...", room));
if (scope.currentRoomName != room.Name) {
var msg = $("<span>").text(translation._("You are now in room %s ...", room.Name));
subscope.$broadcast("display", null, $("<i>").append(msg));
scope.currentRoomName = room.Name;
}
});
scope.$on("room.left", function(event) {
scope.hideGroupRoom();
scope.currentRoomName = null;
});
};
};

2
static/js/directives/contactrequest.js

@ -18,6 +18,8 @@ @@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define(['jquery', 'underscore'], function($, _) {
return ["translation", "buddyData", "contacts", function(translation, buddyData, contacts) {

2
static/js/directives/defaultdialog.js

@ -18,6 +18,8 @@ @@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define(['text!partials/defaultdialog.html'], function(defaultDialogTemplate) {
// defautlDialog

12
static/js/directives/directives.js

@ -18,6 +18,8 @@ @@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define([
'underscore',
@ -43,7 +45,9 @@ define([ @@ -43,7 +45,9 @@ define([
'directives/odfcanvas',
'directives/presentation',
'directives/youtubevideo',
'directives/bfi'], function(_, onEnter, onEscape, statusMessage, buddyList, buddyPictureCapture, buddyPictureUpload, settings, chat, audioVideo, usability, audioLevel, fileInfo, screenshare, roomBar, socialShare, page, contactRequest, defaultDialog, pdfcanvas, odfcanvas, presentation, youtubevideo, bfi) {
'directives/bfi',
'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) {
var directives = {
onEnter: onEnter,
@ -68,13 +72,15 @@ define([ @@ -68,13 +72,15 @@ define([
odfcanvas: odfcanvas,
presentation: presentation,
youtubevideo: youtubevideo,
bfi: bfi
bfi: bfi,
title: title,
welcome: welcome
};
var initialize = function(angModule) {
_.each(directives, function(directive, name) {
angModule.directive(name, directive);
})
});
};
return {

4
static/js/directives/fileinfo.js

@ -18,7 +18,9 @@ @@ -18,7 +18,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
define(['jquery', 'underscore'], function($, _) {
"use strict";
define(['jquery', 'underscore', 'moment'], function($, _, moment) {
return ["fileData", "fileUpload", "fileDownload", "mediaStream", "$window", "alertify", "translation", function(fileData, fileUpload, fileDownload, mediaStream, $window, alertify, translation) {

2
static/js/directives/odfcanvas.js

@ -18,6 +18,8 @@ @@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define(['require', 'underscore', 'jquery'], function(require, _, $) {
return ["$window", "$compile", "translation", "safeApply", function($window, $compile, translation, safeApply) {

2
static/js/directives/onenter.js

@ -18,6 +18,8 @@ @@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define([], function() {
// onEnter

2
static/js/directives/onescape.js

@ -18,6 +18,8 @@ @@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define([], function() {
// onEscape

52
static/js/directives/page.js

@ -18,48 +18,25 @@ @@ -18,48 +18,25 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define(['text!partials/page.html', 'text!partials/page/welcome.html'], function(template, welcome) {
return ["$templateCache", "mediaStream", function($templateCache, mediaStream) {
return ["$templateCache", "$timeout", "rooms", function($templateCache, $timeout, rooms) {
$templateCache.put('page/welcome.html', welcome);
var link = function(scope, element, attrs) {
scope.room = false;
scope.page = null;
if (mediaStream.config.DefaultRoomEnabled !== true) {
scope.$on("welcome", function() {
if (!scope.initialized) {
scope.initialized = true;
scope.refresh();
}
var link = function($scope, $element, attrs) {
$scope.$on("room.joined", function(event) {
// Show no page when joined a room.
$scope.page = null;
});
$scope.$on("room.random", function(ev, roomdata) {
// Show welcome page on room random events.
$timeout(function() {
$scope.page = "page/welcome.html";
});
scope.$on("room", function(event, room) {
scope.initialized = true;
scope.room = room !== null ? true : false;
scope.refresh();
});
scope.$watch("status", function(event) {
if (scope.initialized) {
scope.refresh();
}
});
scope.refresh = function() {
if (scope.roomid || scope.room || scope.status !== "waiting") {
scope.page = null;
} else {
scope.page = "page/welcome.html";
}
};
}
});
};
return {
@ -67,8 +44,7 @@ define(['text!partials/page.html', 'text!partials/page/welcome.html'], function( @@ -67,8 +44,7 @@ define(['text!partials/page.html', 'text!partials/page/welcome.html'], function(
replace: true,
template: template,
link: link
}
};
}];
});

2
static/js/directives/pdfcanvas.js

@ -18,6 +18,8 @@ @@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define(['require', 'underscore', 'jquery'], function(require, _, $) {
return ["$window", "$compile", "translation", "safeApply", function($window, $compile, translation, safeApply) {

14
static/js/directives/presentation.js

@ -18,6 +18,8 @@ @@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define(['jquery', 'underscore', 'text!partials/presentation.html', 'bigscreen'], function($, _, template, BigScreen) {
return ["$window", "mediaStream", "fileUpload", "fileDownload", "alertify", "translation", "randomGen", "fileData", function($window, mediaStream, fileUpload, fileDownload, alertify, translation, randomGen, fileData) {
@ -685,7 +687,7 @@ define(['jquery', 'underscore', 'text!partials/presentation.html', 'bigscreen'], @@ -685,7 +687,7 @@ define(['jquery', 'underscore', 'text!partials/presentation.html', 'bigscreen'],
if (elem) {
BigScreen.toggle(elem);
} else {
BigScreen.toggle(pane.get(0));
BigScreen.toggle(pane[0]);
}
}
@ -704,11 +706,13 @@ define(['jquery', 'underscore', 'text!partials/presentation.html', 'bigscreen'], @@ -704,11 +706,13 @@ define(['jquery', 'underscore', 'text!partials/presentation.html', 'bigscreen'],
};
mediaStream.webrtc.e.on("done", function() {
_.each($scope.availablePresentations, function(presentation) {
presentation.clear();
$scope.$apply(function() {
_.each($scope.availablePresentations, function(presentation) {
presentation.clear();
});
$scope.availablePresentations = [];
$scope.activeDownloads = [];
});
$scope.availablePresentations = [];
$scope.activeDownloads = [];
});
$(document).on("keyup", function(event) {

55
static/js/directives/roombar.js

@ -18,49 +18,59 @@ @@ -18,49 +18,59 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
define(['underscore', 'text!partials/roombar.html'], function(_, template) {
"use strict";
define(['underscore', 'angular', 'text!partials/roombar.html'], function(_, angular, template) {
// roomBar
return ["$window", "$rootScope", "mediaStream", function($window, $rootScope, mediaStream) {
return ["$window", "rooms", function($window, rooms) {
var link = function($scope, $element) {
var link = function($scope) {
var clearRoomName = function() {
$scope.currentRoomName = null;
$scope.newRoomName = "";
};
//console.log("roomBar directive link", arguments);
$scope.newroomid = $rootScope.roomid;
$scope.layout.roombar = false;
$scope.layout.roombar = true;
$scope.save = function() {
var roomid = mediaStream.changeRoom($scope.newroomid);
if (roomid !== $rootScope.roomid) {
$scope.roombarform.$setPristine();
if ($scope.roombarform.$invalid) {
return;
}
$scope.layout.roombar = false;
};
$scope.hitEnter = function(evt) {
if (angular.equals(evt.keyCode, 13)) {
$scope.save();
var roomName = rooms.joinByName($scope.newRoomName);
if (roomName !== $scope.currentRoomName) {
$scope.roombarform.$setPristine();
}
};
$scope.exit = function() {
$scope.newroomid = "";
$scope.newRoomName = "";
$scope.save();
};
$rootScope.$watch("roomid", function(newroomid, roomid) {
if (!newroomid) {
newroomid = "";
}
$scope.newroomid = newroomid;
$scope.$on("room.updated", function(ev, room) {
$scope.currentRoomName = $scope.newRoomName = room.Name;
});
$scope.$watch("newroomid", function(newroomid) {
if (newroomid === $rootScope.roomid) {
$scope.$on("room.left", clearRoomName);
$scope.$watch("newRoomName", function(name) {
if (name === $scope.currentRoomName) {
$scope.roombarform.$setPristine();
}
});
$scope.$watch("layout.roombar", function(value) {
$element.find("input").focus();
});
$scope.$watch("peer", function(peer) {
$scope.layout.roombar = !peer;
});
clearRoomName();
};
return {
@ -68,7 +78,6 @@ define(['underscore', 'text!partials/roombar.html'], function(_, template) { @@ -68,7 +78,6 @@ define(['underscore', 'text!partials/roombar.html'], function(_, template) {
replace: true,
scope: true,
template: template,
controller: "RoomchangeController",
link: link
}

9
static/js/directives/screenshare.js

@ -18,6 +18,8 @@ @@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define(['jquery', 'underscore', 'text!partials/screenshare.html', 'text!partials/screensharepeer.html', 'bigscreen', 'webrtc.adapter'], function($, _, template, templatePeer, BigScreen) {
return ["$window", "mediaStream", "$compile", "safeApply", "videoWaiter", "$timeout", "alertify", "translation", "screensharing", function($window, mediaStream, $compile, safeApply, videoWaiter, $timeout, alertify, translation, screensharing) {
@ -132,7 +134,7 @@ define(['jquery', 'underscore', 'text!partials/screenshare.html', 'text!partials @@ -132,7 +134,7 @@ define(['jquery', 'underscore', 'text!partials/screenshare.html', 'text!partials
peerTemplate(subscope, function(clonedElement, scope) {
pane.append(clonedElement);
scope.element = clonedElement;
var video = clonedElement.find("video").get(0);
var video = clonedElement.find("video")[0];
$window.attachMediaStream(video, stream);
videoWaiter.wait(video, stream, function() {
console.log("Screensharing size: ", video.videoWidth, video.videoHeight);
@ -246,7 +248,6 @@ define(['jquery', 'underscore', 'text!partials/screenshare.html', 'text!partials @@ -246,7 +248,6 @@ define(['jquery', 'underscore', 'text!partials/screenshare.html', 'text!partials
mediaStream.tokens.off(token, handler);
mediaStream.webrtc.e.off("statechange", updater);
handler = null;
updated = null;
// Send by to all connected peers.
_.each(screenshares, function(peerscreenshare) {
peerscreenshare.send({
@ -312,14 +313,14 @@ define(['jquery', 'underscore', 'text!partials/screenshare.html', 'text!partials @@ -312,14 +313,14 @@ define(['jquery', 'underscore', 'text!partials/screenshare.html', 'text!partials
if (elem) {
BigScreen.toggle(elem);
} else {
BigScreen.toggle(pane.get(0));
BigScreen.toggle(pane[0]);
}
}
};
mediaStream.webrtc.e.on("done", function() {
$scope.stopScreenshare();
$scope.$apply($scope.stopScreenshare);
});
$scope.$watch("layout.screenshare", function(newval, oldval) {

160
static/js/directives/settings.js

@ -18,14 +18,40 @@ @@ -18,14 +18,40 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, template) {
var videoQualityMap = {
tiny: {
maxWidth: 80,
maxHeight: 45
},
low: {
maxWidth: 320,
maxHeight: 180
},
high: {
maxWidth: 640,
maxHeight: 360
},
hd: {
minWidth: 1280,
minHeight: 720
},
fullhd: {
minWidth: 1920,
minHeight: 1080
}
};
return ["$compile", "mediaStream", function($compile, mediaStream) {
var controller = ['$scope', 'desktopNotify', 'mediaSources', 'safeApply', 'availableLanguages', 'translation', 'localStorage', 'userSettingsData', function($scope, desktopNotify, mediaSources, safeApply, availableLanguages, translation, localStorage, userSettingsData) {
var controller = ['$scope', 'desktopNotify', 'mediaSources', 'safeApply', 'availableLanguages', 'translation', 'localStorage', 'userSettingsData', 'constraints', 'appData', '$timeout', function($scope, desktopNotify, mediaSources, safeApply, availableLanguages, translation, localStorage, userSettingsData, constraints, appData, $timeout) {
$scope.layout.settings = false;
$scope.showAdvancedSettings = true;
$scope.autoshowSettings = true;
$scope.rememberSettings = true;
$scope.desktopNotify = desktopNotify;
$scope.mediaSources = mediaSources;
@ -85,28 +111,15 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t @@ -85,28 +111,15 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t
};
$scope.checkDefaultMediaSources = function() {
// Check if the stuff exists.
if ($scope.master.settings.microphoneId && !$scope.mediaSources.hasAudioId($scope.master.settings.microphoneId)) {
$scope.master.settings.microphoneId = null;
}
if ($scope.master.settings.cameraId && !$scope.mediaSources.hasVideoId($scope.master.settings.cameraId)) {
$scope.master.settings.cameraId = null;
}
var audio = $scope.mediaSources.audio;
var video = $scope.mediaSources.video;
if (!$scope.master.settings.microphoneId && audio.length > 0) {
$scope.master.settings.microphoneId = audio[0].id;
}
if (!$scope.master.settings.cameraId && video.length > 0) {
$scope.master.settings.cameraId = $scope.mediaSources.video[0].id;
}
//console.log("master sources updates", $scope.master);
$scope.refreshWebrtcSettings();
};
$scope.mediaSources.refresh(function() {
safeApply($scope, $scope.checkDefaultMediaSources);
});
$scope.$watch("layout.settings", function(showSettings, oldValue) {
if (showSettings) {
$scope.desktopNotify.refresh();
@ -125,23 +138,132 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t @@ -125,23 +138,132 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t
$scope.user.settings.cameraId = video[0].id;
}
});
$scope.refreshWebrtcSettings();
});
} else if (!showSettings && oldValue) {
$scope.saveSettings();
}
});
}];
$scope.maybeShowSettings = function() {
if ($scope.autoshowSettings) {
$scope.autoshowSettings = false;
if (!$scope.loadedUser) {
$scope.layout.settings = true;
}
}
};
$scope.$on("room.joined", function() {
$timeout($scope.maybeShowSettings);
});
appData.e.on("authenticationChanged", function() {
$scope.autoshowSettings = true;
$timeout($scope.maybeShowSettings);
});
constraints.e.on("refresh", function(event, constraints) {
var settings = $scope.master.settings;
// Assert that selected devices are there.
(function() {
var deferred = constraints.defer();
mediaSources.refresh(function() {
$scope.checkDefaultMediaSources();
// Select microphone device by id.
if (settings.microphoneId) {
constraints.add("audio", "sourceId", settings.microphoneId);
}
// Select camera by device id.
if (settings.cameraId) {
constraints.add("video", "sourceId", settings.cameraId);
}
if (!mediaSources.hasAudio()) {
constraints.disable('audio');
console.info("Disabled audio input as no audio source was found.");
}
if (!mediaSources.hasVideo()) {
constraints.disable('video');
console.info("Disabled video input as no video source was found.");
}
deferred.resolve("complete");
});
})();
// Chrome only constraints.
if ($scope.isChrome) {
// Chrome specific constraints overview:
// https://code.google.com/p/webrtc/source/browse/trunk/talk/app/webrtc/mediaconstraintsinterface.cc
// https://code.google.com/p/webrtc/source/browse/trunk/talk/app/webrtc/videosource.cc (video constraints)
// https://code.google.com/p/webrtc/source/browse/trunk/talk/app/webrtc/localaudiosource.cc (audio constraints)
// https://code.google.com/p/webrtc/source/browse/trunk/talk/app/webrtc/webrtcsession.cc (pc constraints)
var link = function($scope, $element) {};
// Experimental audio settings.
if (settings.experimental.enabled) {
constraints.add("audio", "googEchoCancellation", true); // defaults to true
constraints.add("audio", "googEchoCancellation2", settings.experimental.audioEchoCancellation2 && true); // defaults to false in Chrome
constraints.add("audio", "googAutoGainControl", true); // defaults to true
constraints.add("audio", "googAutoGainControl2", settings.experimental.audioAutoGainControl2 && true); // defaults to false in Chrome
constraints.add("audio", "googNoiseSuppression", true); // defaults to true
constraints.add("audio", "googgNoiseSuppression2", settings.experimental.audioNoiseSuppression2 && true); // defaults to false in Chrome
constraints.add("audio", "googHighpassFilter", true); // defaults to true
constraints.add("audio", "googTypingNoiseDetection", settings.experimental.audioTypingNoiseDetection && true); // defaults to true in Chrome
}
if ($scope.supported.renderToAssociatedSink) {
// When true uses the default communications device on Windows.
// https://codereview.chromium.org/155863003
constraints.add("audio", "googDucking", true); // defaults to true on Windows.
// Chrome will start rendering mediastream output to an output device that's associated with
// the input stream that was opened via getUserMedia.
// https://chromiumcodereview.appspot.com/23558010
constraints.add("audio", "chromeRenderToAssociatedSink", settings.audioRenderToAssociatedSkin && true); // defaults to false in Chrome
}
// Experimental video settings.
if (settings.experimental.enabled) {
// Changes the way the video encoding adapts to the available bandwidth.
// https://code.google.com/p/webrtc/issues/detail?id=3351
constraints.add(["video", "screensharing"], "googLeakyBucket", settings.experimental.videoLeakyBucket && true); // defaults to false in Chrome
// Removes the noise in the captured video stream at the expense of CPU.
constraints.add(["video", "screensharing"], "googNoiseReduction", settings.experimental.videoNoiseReduction && true); // defaults to false in Chrome
constraints.add("pc", "googCpuOveruseDetection", settings.experimental.videoCpuOveruseDetection && true); // defaults to true in Chrome
}
// Set video quality.
var videoQuality = videoQualityMap[settings.videoQuality];
if (videoQuality) {
_.forEach(videoQuality, function(v, k) {
constraints.add("video", k, v, true);
});
}
// Set max frame rate if any was selected.
if (settings.maxFrameRate && settings.maxFrameRate != "auto") {
constraints.add("video", "maxFrameRate", parseInt(settings.maxFrameRate, 10), true);
}
} else {
// Other browsers constraints (there are none as of now.);
}
});
}];
return {
scope: true,
restrict: 'E',
replace: true,
template: template,
controller: controller,
link: link
controller: controller
};
}];

22
static/js/directives/socialshare.js

@ -18,7 +18,9 @@ @@ -18,7 +18,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
define(['jquery', 'text!partials/socialshare.html'], function($, template) {
"use strict";
define(['text!partials/socialshare.html'], function(template) {
var urls = {
email: "mailto:?subject=_TEXT_%20_URL_",
@ -29,7 +31,7 @@ define(['jquery', 'text!partials/socialshare.html'], function($, template) { @@ -29,7 +31,7 @@ define(['jquery', 'text!partials/socialshare.html'], function($, template) {
};
// socialShare
return ["$window", "translation", function($window, translation) {
return ["$window", "translation", "rooms", "alertify", function($window, translation, rooms, alertify) {
var title = $window.encodeURIComponent($window.document.title);
var makeUrl = function(nw, target) {
@ -46,8 +48,15 @@ define(['jquery', 'text!partials/socialshare.html'], function($, template) { @@ -46,8 +48,15 @@ define(['jquery', 'text!partials/socialshare.html'], function($, template) {
template: template,
replace: true,
link: function($scope, $element, $attr) {
$element.on("click", "a", function(event) {
var nw = $(event.currentTarget).data("nw");
$scope.$on("room.updated", function(ev, room) {
$scope.roomlink = rooms.link(room);
});
$scope.$on("room.left", function(ev) {
$scope.roomlink = null;
});
$element.find("a").on("click", function(event) {
event.preventDefault();
var nw = event.currentTarget.getAttribute("data-nw");
var url = makeUrl(nw, $scope.roomlink);
if (url) {
if (nw === "email") {
@ -56,6 +65,11 @@ define(['jquery', 'text!partials/socialshare.html'], function($, template) { @@ -56,6 +65,11 @@ define(['jquery', 'text!partials/socialshare.html'], function($, template) {
} else {
$window.open(url, "social_" + nw, "menubar=no,toolbar=no,resizable=yes,width=600,height=600,scrollbars=yes");
}
} else {
if (nw === "link") {
//$window.alert("Room link: " + $scope.roomlink);
alertify.dialog.notify(translation._("Room link"), '<a href="'+$scope.roomlink+'" rel="external" target="_blank">'+$scope.roomlink+'</a>');
}
}
});
}

2
static/js/directives/statusmessage.js

@ -18,6 +18,8 @@ @@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define(['text!partials/statusmessage.html'], function(template) {
// statusMessage

50
static/js/directives/title.js

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 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([], function() {
return [function() {
var link = function($scope, $element, attrs) {
var originalText = $element.text();
var updateTitle = function(roomName) {
if (roomName) {
$element.text(roomName+ " - " + originalText);
} else {
$element.text(originalText);
}
};
$scope.$on("room.updated", function(ev, room) {
updateTitle(room.Name);
});
$scope.$on("room.left", function(ev) {
updateTitle();
});
};
return {
restrict: 'E',
replace: false,
link: link
};
}];
});

40
static/js/directives/usability.js

@ -18,19 +18,19 @@ @@ -18,19 +18,19 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define(['jquery', 'underscore', 'text!partials/usability.html'], function($, _, template) {
var MEDIA_CHECK = "1" // First version of media check flag.
return ["mediaStream", function(mediaStream) {
return [function() {
var controller = ['$scope', "mediaStream", "safeApply", "$timeout", "localStorage", "continueConnector", function($scope, mediaStream, safeApply, $timeout, localStorage, continueConnector) {
var controller = ['$scope', "webrtc", "safeApply", "$timeout", "localStorage", "continueConnector", function($scope, webrtc, safeApply, $timeout, localStorage, continueConnector) {
var pending = true;
var complete = false;
var initializer = null;
var ctrl = this;
ctrl.setInfo = function(info) {
$scope.usabilityInfo = info;
@ -46,16 +46,7 @@ define(['jquery', 'underscore', 'text!partials/usability.html'], function($, _, @@ -46,16 +46,7 @@ define(['jquery', 'underscore', 'text!partials/usability.html'], function($, _,
localStorage.setItem("mediastream-mediacheck", MEDIA_CHECK)
console.log("Continue with connect after media check ...");
continueDeferred.resolve();
if (mediaStream.config.DefaultRoomEnabled !== true) {
ctrl.setInfo("initializing");
initializer = $timeout(function() {
ctrl.setInfo("ok");
$scope.layout.settings = false;
$scope.$emit("welcome");
}, 1000);
} else {
ctrl.setInfo("ok");
}
ctrl.setInfo("ok");
complete = true;
} else {
ctrl.setInfo("denied");
@ -70,7 +61,7 @@ define(['jquery', 'underscore', 'text!partials/usability.html'], function($, _, @@ -70,7 +61,7 @@ define(['jquery', 'underscore', 'text!partials/usability.html'], function($, _,
// NOTE(longsleep): Checkin for media access makes only sense on
// Chrome for now, as its the only one which remembers this
// decision permanently for https.
mediaStream.webrtc.testMediaAccess($scope.continueConnect);
webrtc.testMediaAccess($scope.continueConnect);
} else {
$scope.continueConnect(true);
}
@ -97,19 +88,14 @@ define(['jquery', 'underscore', 'text!partials/usability.html'], function($, _, @@ -97,19 +88,14 @@ define(['jquery', 'underscore', 'text!partials/usability.html'], function($, _,
}
});
$scope.$on("room", function(event, room) {
//console.log("roomStatus", room !== null ? true : false);
$scope.$on("room.joined", function(event) {
if (complete) {
ctrl.setInfo("ok");
}
});
$scope.$on("room.left", function(event) {
if (complete) {
if (initializer !== null) {
$timeout.cancel(initializer);
initializer = null;
}
// Check if we should show settings per default when in a room.
if(room && !$scope.loadedUser) {
$scope.layout.settings = true;
} else {
$scope.layout.settings = false;
}
ctrl.setInfo("ok");
}
});

92
static/js/directives/welcome.js

@ -0,0 +1,92 @@ @@ -0,0 +1,92 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 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([], function() {
// welcome
return ["rooms", "$timeout", "mediaStream", "translation", function(rooms, $timeout, mediaStream, translation) {
function link($scope, $element) {
//console.log("xxx welcome", $scope.$id, $element);
var placeHolder = translation._("Room name");
$scope.randomRoom = rooms.randomRoom;
$scope.canCreateRooms = rooms.canCreateRooms;
$scope.joinRoomByName = function(name) {
if ($scope.welcome.$invalid) {
return;
}
if (!name) {
return;
}
rooms.joinByName(name);
};
var roomdata = rooms.getRandomRoom();
var recreate = true;
if (roomdata) {
$scope.roomdata = {name: roomdata.name, placeholder: roomdata.name ? roomdata.name : placeHolder};
recreate = false;
} else {
$scope.roomdata = {placeholder: placeHolder};
}
$scope.roomdataInput = {
name: ""
};
$scope.$watch("roomdata.name", function(name) {
$scope.roomdata.link = rooms.link({Name: name});
}, true);
$scope.$watch("roomdataInput.name", function(name) {
if (name === "") {
if (recreate) {
$scope.randomRoom();
} else {
recreate = true;
}
} else {
$scope.roomdata.name = name;
}
});
$scope.$on("room.random", function(event, roomdata) {
$scope.roomdata = {name: roomdata.name, last: roomdata.name, placeholder: roomdata.name ? roomdata.name : placeHolder};
$scope.roomdataInput.name = "";
});
$timeout(function() {
$element.find(".roomdata-link-input:visible:enabled:first").focus();
});
}
return {
restrict: 'EA',
link: link
}
}];
});

31
static/js/directives/youtubevideo.js

@ -18,17 +18,22 @@ @@ -18,17 +18,22 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], function($, _, template, BigScreen) {
return ["$window", "$document", "mediaStream", "alertify", "translation", "safeApply", "appData", function($window, $document, mediaStream, alertify, translation, safeApply, appData) {
return ["$window", "$document", "mediaStream", "alertify", "translation", "safeApply", "appData", "$q", function($window, $document, mediaStream, alertify, translation, safeApply, appData, $q) {
var YOUTUBE_IFRAME_API_URL = "//www.youtube.com/iframe_api";
var isYouTubeIframeAPIReady = $.Deferred();
$window.onYouTubeIframeAPIReady = function() {
console.log("YouTube IFrame ready");
isYouTubeIframeAPIReady.resolve();
};
var isYouTubeIframeAPIReady = (function() {
var d = $q.defer();
$window.onYouTubeIframeAPIReady = function() {
console.log("YouTube IFrame ready");
d.resolve();
};
return d.promise;
})();
var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
@ -68,7 +73,7 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], @@ -68,7 +73,7 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'],
$scope.volumebarVisible = true;
$scope.volume = null;
isYouTubeIframeAPIReady.done(function() {
isYouTubeIframeAPIReady.then(function() {
$scope.$apply(function(scope) {
scope.youtubeAPIReady = true;
});
@ -270,10 +275,10 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], @@ -270,10 +275,10 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'],
playerReady = $.Deferred();
}
isYouTubeIframeAPIReady.done(function() {
isYouTubeIframeAPIReady.then(function() {
if (!player) {
var origin = $window.location.protocol + "//" + $window.location.host;
player = new YT.Player("youtubeplayer", {
player = new $window.YT.Player("youtubeplayer", {
height: "390",
width: "640",
playerVars: {
@ -563,11 +568,7 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], @@ -563,11 +568,7 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'],
$scope.toggleFullscreen = function(elem) {
if (BigScreen.enabled) {
if (elem) {
BigScreen.toggle(elem);
} else {
BigScreen.toggle(pane.get(0));
}
BigScreen.toggle(elem);
}
};
@ -576,7 +577,7 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'], @@ -576,7 +577,7 @@ define(['jquery', 'underscore', 'text!partials/youtubevideo.html', 'bigscreen'],
var compile = function(tElement, tAttr) {
return function(scope, iElement, iAttrs, controller) {
$(iElement).on("dblclick", "#youtubecontainer", _.debounce(function(event) {
$(iElement).find("#youtubecontainer").on("dblclick", _.debounce(function(event) {
scope.toggleFullscreen(event.delegateTarget);
}, 100, true));
}

2
static/js/filters/buddyimagesrc.js

@ -18,6 +18,8 @@ @@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define(["underscore"], function(_) {
// Create URLs for blobs.

2
static/js/filters/displayconference.js

@ -18,6 +18,8 @@ @@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define([], function() {
// displayConference

2
static/js/filters/displayname.js

@ -18,6 +18,8 @@ @@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define([], function() {
// displayName

2
static/js/filters/displaynameforsession.js

@ -18,6 +18,8 @@ @@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define(["jquery"], function($) {
// displayNameForSession

2
static/js/filters/displayuserid.js

@ -18,6 +18,8 @@ @@ -18,6 +18,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"use strict";
define([], function() {
// displayUserid

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save