+
diff --git a/server.conf.in b/server.conf.in
index 9a9257d1..8b8c1d27 100644
--- a/server.conf.in
+++ b/server.conf.in
@@ -60,6 +60,11 @@ listen = 127.0.0.1:8080
; Session secret to use for session id generator. 32 or 64 bytes of random data
; are recommented.
sessionSecret = the-default-secret-do-not-keep-me
+; Encryption secret protecting the data in generated server side tokens. Use
+; 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. When you change
+; the encryption secret, stored authentications, sessions and contacts become
+; invalid.
+encryptionSecret = tne-default-encryption-block-key
; Full path to a text file containig client tokens which a user needs to enter
; when accessing the web client. Each line in this file represents a valid
; token.
diff --git a/src/app/spreed-webrtc-server/channeling.go b/src/app/spreed-webrtc-server/channeling.go
index c5e96f3a..726d3005 100644
--- a/src/app/spreed-webrtc-server/channeling.go
+++ b/src/app/spreed-webrtc-server/channeling.go
@@ -50,6 +50,7 @@ type DataSelf struct {
Id string
Sid string
Userid string
+ Suserid string
Token string
Version string
Turn *DataTurn
@@ -66,12 +67,19 @@ type DataTurn struct {
type DataSession struct {
Type string
Id string
- Userid string `json:"Userid,omitempty"`
- Ua string `json:"Ua,omitempty"`
- Token string `json:"Token,omitempty"`
- Version string `json:"Version,omitempty"`
- Rev uint64 `json:"Rev,omitempty"`
+ Userid string `json:",omitempty"`
+ Ua string `json:",omitempty"`
+ Token string `json:",omitempty"`
+ Version string `json:",omitempty"`
+ Rev uint64 `json:",omitempty"`
+ Prio int `json:",omitempty"`
Status interface{}
+ stamp int64
+}
+
+type DataUser struct {
+ Id string
+ Sessions int
}
type DataBye struct {
@@ -92,16 +100,41 @@ type DataChat struct {
}
type DataChatMessage struct {
- Mid string
Message string
Time string
- NoEcho bool `json:"NoEcho,omitempty"`
- Status interface{}
+ NoEcho bool `json:",omitempty"`
+ Mid string `json:",omitempty"`
+ Status *DataChatStatus
}
-type DataChatMessageStatus struct {
- State string
- Mid string
+type DataChatStatus struct {
+ Typing string `json:",omitempty"`
+ State string `json:",omitempty"`
+ Mid string `json:",omitempty"`
+ SeenMids []string `json:",omitempty"`
+ FileInfo *DataFileInfo `json:",omitempty"`
+ ContactRequest *DataContactRequest `json:",omitempty"`
+ AutoCall *DataAutoCall `json:",omitempty"`
+}
+
+type DataFileInfo struct {
+ Id string `json:"id"`
+ Chunks uint64 `json:"chunks"`
+ Name string `json:"name"`
+ Size uint64 `json:"size"`
+ Type string `json:"type"`
+}
+
+type DataContactRequest struct {
+ Id string
+ Success bool
+ Userid string `json:",omitempty"`
+ Token string `json:",omitempty"`
+}
+
+type DataAutoCall struct {
+ Id string
+ Type string
}
type DataIncoming struct {
@@ -116,19 +149,26 @@ type DataIncoming struct {
Conference *DataConference
Alive *DataAlive
Authentication *DataAuthentication
+ Sessions *DataSessions
+ Iid string `json:",omitempty"`
}
type DataOutgoing struct {
Data interface{}
From string
To string
+ Iid string `json:",omitempty"`
}
type DataSessions struct {
+ Type string
+ Sessions *DataSessionsRequest `json:",omitempty"`
+ Users []*DataSession
+}
+
+type DataSessionsRequest struct {
+ Token string
Type string
- Users []*DataSession
- Index uint64
- Batch uint64
}
type DataConference struct {
diff --git a/src/app/spreed-webrtc-server/contact.go b/src/app/spreed-webrtc-server/contact.go
new file mode 100644
index 00000000..995f92d6
--- /dev/null
+++ b/src/app/spreed-webrtc-server/contact.go
@@ -0,0 +1,29 @@
+/*
+ * 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
.
+ *
+ */
+
+package main
+
+import ()
+
+type Contact struct {
+ A string
+ B string
+}
diff --git a/src/app/spreed-webrtc-server/hub.go b/src/app/spreed-webrtc-server/hub.go
index 68ab5b26..777eecbd 100644
--- a/src/app/spreed-webrtc-server/hub.go
+++ b/src/app/spreed-webrtc-server/hub.go
@@ -23,6 +23,7 @@ package main
import (
"bytes"
+ "crypto/aes"
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
@@ -56,11 +57,13 @@ 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"`
}
@@ -69,9 +72,11 @@ type Hub struct {
connectionTable map[string]*Connection
sessionTable map[string]*Session
roomTable map[string]*RoomWorker
+ userTable map[string]*User
version string
config *Config
sessionSecret []byte
+ encryptionSecret []byte
turnSecret []byte
tickets *securecookie.SecureCookie
count uint64
@@ -83,29 +88,39 @@ type Hub struct {
realm string
tokenName string
useridRetriever func(*http.Request) (string, error)
+ contacts *securecookie.SecureCookie
}
-func NewHub(version string, config *Config, sessionSecret, turnSecret, realm string) *Hub {
+func NewHub(version string, config *Config, sessionSecret, encryptionSecret, turnSecret, realm string) *Hub {
h := &Hub{
- connectionTable: make(map[string]*Connection),
- sessionTable: make(map[string]*Session),
- roomTable: make(map[string]*RoomWorker),
- version: version,
- config: config,
- sessionSecret: []byte(sessionSecret),
- turnSecret: []byte(turnSecret),
- realm: realm,
+ connectionTable: make(map[string]*Connection),
+ sessionTable: make(map[string]*Session),
+ roomTable: make(map[string]*RoomWorker),
+ userTable: make(map[string]*User),
+ version: version,
+ config: config,
+ sessionSecret: []byte(sessionSecret),
+ encryptionSecret: []byte(encryptionSecret),
+ turnSecret: []byte(turnSecret),
+ realm: realm,
}
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))
}
- h.tickets = securecookie.New(h.sessionSecret, nil)
+ 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.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.MaxAge(0)
+ h.contacts.HashFunc(sha256.New)
+ h.contacts.BlockFunc(aes.NewCipher)
return h
}
@@ -117,6 +132,7 @@ func (h *Hub) Stat(details bool) *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),
@@ -136,6 +152,11 @@ func (h *Hub) Stat(details bool) *HubStat {
sessions[sessionid] = session.Data()
}
stat.SessionsById = sessions
+ users := make(map[string]*DataUser)
+ for userid, user := range h.userTable {
+ users[userid] = user.Data()
+ }
+ stat.UsersById = users
connections := make(map[string]string)
for id, connection := range h.connectionTable {
connections[fmt.Sprintf("%d", connection.Idx)] = id
@@ -166,6 +187,16 @@ 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))
+ }
+ return
+}
+
func (h *Hub) CreateSession(request *http.Request, st *SessionToken) *Session {
// NOTE(longsleep): Is it required to make this a secure cookie,
@@ -183,8 +214,8 @@ func (h *Hub) CreateSession(request *http.Request, st *SessionToken) *Session {
if st == nil {
sid := NewRandomString(32)
id, _ := h.tickets.Encode("id", sid)
- session = NewSession(id, sid, userid)
- log.Println("Created new session id", len(id), id, sid, userid)
+ session = NewSession(id, sid)
+ log.Println("Created new session id", len(id), id, sid)
} else {
if userid == "" {
userid = st.Userid
@@ -192,7 +223,11 @@ func (h *Hub) CreateSession(request *http.Request, st *SessionToken) *Session {
if !usersEnabled {
userid = ""
}
- session = NewSession(st.Id, st.Sid, userid)
+ session = NewSession(st.Id, st.Sid)
+ }
+
+ if userid != "" {
+ h.authenticateHandler(session, st, userid)
}
return session
@@ -337,8 +372,18 @@ func (h *Hub) unregisterHandler(c *Connection) {
return
}
session := c.Session
+ suserid := session.Userid()
delete(h.connectionTable, c.Id)
delete(h.sessionTable, c.Id)
+ if session != nil && suserid != "" {
+ user, ok := h.userTable[suserid]
+ if ok {
+ empty := user.RemoveSession(session)
+ if empty {
+ delete(h.userTable, suserid)
+ }
+ }
+ }
h.mutex.Unlock()
if session != nil {
h.buddyImages.Delete(session.Id)
@@ -362,11 +407,11 @@ func (h *Hub) unicastHandler(m *MessageRequest) {
}
-func (h *Hub) aliveHandler(c *Connection, alive *DataAlive) {
+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})
+ err := encoder.Encode(&DataOutgoing{From: c.Id, Data: alive, Iid: iid})
if err != nil {
log.Println("Alive error while encoding JSON", err)
aliveJson.Decref()
@@ -377,6 +422,59 @@ func (h *Hub) aliveHandler(c *Connection, alive *DataAlive) {
}
+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()
+ defer h.mutex.RUnlock()
+ user, ok := h.userTable[userid]
+ if !ok {
+ return
+ }
+ // Add sessions for forein user.
+ users = user.SessionsData()
+ default:
+ log.Println("Unkown incoming sessions request type", srq.Type)
+ }
+
+ 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)
@@ -385,7 +483,6 @@ func (h *Hub) sessionupdateHandler(s *SessionUpdate) uint64 {
h.mutex.RUnlock()
var rev uint64
if ok {
- rev = session.Update(s)
if s.Status != nil {
status, ok := s.Status.(map[string]interface{})
if ok && status["buddyPicture"] != nil {
@@ -398,6 +495,7 @@ func (h *Hub) sessionupdateHandler(s *SessionUpdate) uint64 {
}
}
}
+ rev = session.Update(s)
} else {
log.Printf("Update data for unknown user %s\n", s.Id)
}
@@ -423,3 +521,91 @@ func (h *Hub) sessiontokenHandler(st *SessionToken) (string, error) {
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)
+ }
+
+ return err
+
+}
+
+func (h *Hub) contactrequestHandler(c *Connection, to string, cr *DataContactRequest) error {
+
+ var err error
+
+ if cr.Success {
+ // Client replied with success.
+ // Decode Token and make sure c.Session.Userid and the to Session.Userid are a match.
+ contact := &Contact{}
+ err = h.contacts.Decode("contact", cr.Token, contact)
+ if err != nil {
+ return err
+ }
+ suserid := c.Session.Userid()
+ if suserid == "" {
+ return errors.New("no userid")
+ }
+ h.mutex.RLock()
+ session, ok := h.sessionTable[to]
+ h.mutex.RUnlock()
+ if !ok {
+ return errors.New("unknown to session for confirm")
+ }
+ userid := session.Userid()
+ if userid == "" {
+ return errors.New("to has no userid for confirm")
+ }
+ if suserid != contact.A {
+ return errors.New("contact mismatch in a")
+ }
+ if userid != contact.B {
+ return errors.New("contact mismatch in b")
+ }
+ } else {
+ if cr.Token != "" {
+ // Client replied with no success.
+ // Remove token.
+ cr.Token = ""
+ } else {
+ // New request.
+ // Create Token with flag and c.Session.Userid and the to Session.Userid.
+ suserid := c.Session.Userid()
+ if suserid == "" {
+ return errors.New("no userid")
+ }
+ h.mutex.RLock()
+ session, ok := h.sessionTable[to]
+ h.mutex.RUnlock()
+ if !ok {
+ return errors.New("unknown to session")
+ }
+ userid := session.Userid()
+ if userid == "" {
+ return errors.New("to has no userid")
+ }
+ if userid == suserid {
+ return errors.New("to userid cannot be the same as own userid")
+ }
+ // Create object.
+ contact := &Contact{userid, suserid}
+ // Serialize.
+ cr.Token, err = h.contacts.Encode("contact", contact)
+ }
+ }
+
+ return err
+
+}
diff --git a/src/app/spreed-webrtc-server/main.go b/src/app/spreed-webrtc-server/main.go
index 46d9eebd..3f1111b3 100644
--- a/src/app/spreed-webrtc-server/main.go
+++ b/src/app/spreed-webrtc-server/main.go
@@ -211,6 +211,11 @@ func runner(runtime phoenix.Runtime) error {
return fmt.Errorf("No sessionSecret in config file.")
}
+ encryptionSecret, err := runtime.GetString("app", "encryptionSecret")
+ if err != nil {
+ return fmt.Errorf("No encryptionSecret in config file.")
+ }
+
tokenFile, err := runtime.GetString("app", "tokenFile")
if err == nil {
if !httputils.HasFilePath(path.Clean(tokenFile)) {
@@ -340,7 +345,7 @@ func runner(runtime phoenix.Runtime) error {
computedRealm := fmt.Sprintf("%s.%s", serverRealm, serverToken)
// Create our hub instance.
- hub := NewHub(runtimeVersion, config, sessionSecret, turnSecret, computedRealm)
+ hub := NewHub(runtimeVersion, config, sessionSecret, encryptionSecret, turnSecret, computedRealm)
// Set number of go routines if it is 1
if goruntime.GOMAXPROCS(0) == 1 {
diff --git a/src/app/spreed-webrtc-server/server.go b/src/app/spreed-webrtc-server/server.go
index fb45e090..5e3577d0 100644
--- a/src/app/spreed-webrtc-server/server.go
+++ b/src/app/spreed-webrtc-server/server.go
@@ -44,7 +44,8 @@ func (s *Server) OnRegister(c *Connection) {
Type: "Self",
Id: c.Id,
Sid: c.Session.Sid,
- Userid: c.Session.Userid,
+ Userid: c.Session.Userid(),
+ Suserid: c.h.CreateSuserid(c.Session),
Token: token,
Version: c.h.version,
Turn: c.h.CreateTurnData(c.Id),
@@ -67,7 +68,7 @@ func (s *Server) OnUnregister(c *Connection) {
func (s *Server) OnText(c *Connection, b Buffer) {
- //log.Printf("OnText from %d: %s\n", c.id, b)
+ //log.Printf("OnText from %d: %s\n", c.Id, b)
var msg DataIncoming
err := json.Unmarshal(b.Bytes(), &msg)
if err != nil {
@@ -106,7 +107,7 @@ func (s *Server) OnText(c *Connection, b Buffer) {
// TODO(longsleep): Validate Answer
s.Unicast(c, msg.Answer.To, msg.Answer)
case "Users":
- if c.h.config.DefaultRoomEnabled || !c.h.isDefaultRoomid(c.Roomid) {
+ if c.Hello {
s.Users(c)
}
case "Authentication":
@@ -121,7 +122,7 @@ func (s *Server) OnText(c *Connection, b Buffer) {
case "Status":
//log.Println("Status", msg.Status)
s.UpdateSession(c, &SessionUpdate{Types: []string{"Status"}, Status: msg.Status.Status})
- if c.h.config.DefaultRoomEnabled || !c.h.isDefaultRoomid(c.Roomid) {
+ if c.Hello {
s.Broadcast(c, c.Session.DataSessionStatus())
}
case "Chat":
@@ -132,16 +133,24 @@ func (s *Server) OnText(c *Connection, b Buffer) {
msg.Chat.Chat.Time = time.Now().Format(time.RFC3339)
if msg.Chat.To == "" {
// TODO(longsleep): Check if chat broadcast is allowed.
- if c.h.config.DefaultRoomEnabled || !c.h.isDefaultRoomid(c.Roomid) {
+ 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: &DataChatMessageStatus{State: "sent"}}})
+ 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":
@@ -158,7 +167,9 @@ func (s *Server) OnText(c *Connection, b Buffer) {
}
}
case "Alive":
- s.Alive(c, msg.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)
}
@@ -181,9 +192,15 @@ func (s *Server) Unicast(c *Connection, to string, m interface{}) {
b.Decref()
}
-func (s *Server) Alive(c *Connection, alive *DataAlive) {
+func (s *Server) Alive(c *Connection, alive *DataAlive, iid string) {
- c.h.aliveHandler(c, alive)
+ c.h.aliveHandler(c, alive, iid)
+
+}
+
+func (s *Server) Sessions(c *Connection, srq *DataSessionsRequest, iid string) {
+
+ c.h.sessionsHandler(c, srq, iid)
}
@@ -194,6 +211,12 @@ func (s *Server) UpdateSession(c *Connection, su *SessionUpdate) uint64 {
}
+func (s *Server) ContactRequest(c *Connection, to string, cr *DataContactRequest) (err error) {
+
+ return c.h.contactrequestHandler(c, to, cr)
+
+}
+
func (s *Server) Broadcast(c *Connection, m interface{}) {
b := c.h.buffers.New()
@@ -228,9 +251,9 @@ func (s *Server) Users(c *Connection) {
func (s *Server) Authenticate(c *Connection, st *SessionToken) bool {
- err := c.Session.Authenticate(c.h.realm, st)
+ err := c.h.authenticateHandler(c.Session, st, "")
if err == nil {
- log.Println("Authentication success", c.Id, c.Idx, st.Userid)
+ 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)
diff --git a/src/app/spreed-webrtc-server/session.go b/src/app/spreed-webrtc-server/session.go
index 13dfe2a2..a2ccb07b 100644
--- a/src/app/spreed-webrtc-server/session.go
+++ b/src/app/spreed-webrtc-server/session.go
@@ -26,6 +26,7 @@ import (
"fmt"
"github.com/gorilla/securecookie"
"sync"
+ "time"
)
var sessionNonces *securecookie.SecureCookie
@@ -33,21 +34,23 @@ var sessionNonces *securecookie.SecureCookie
type Session struct {
Id string
Sid string
- Userid string
- Roomid string
Ua string
UpdateRev uint64
Status interface{}
Nonce string
+ Prio int
mutex sync.RWMutex
+ userid string
+ stamp int64
}
-func NewSession(id, sid, userid string) *Session {
+func NewSession(id, sid string) *Session {
return &Session{
- Id: id,
- Sid: sid,
- Userid: userid,
+ Id: id,
+ Sid: sid,
+ Prio: 100,
+ stamp: time.Now().Unix(),
}
}
@@ -61,12 +64,12 @@ func (s *Session) Update(update *SessionUpdate) uint64 {
//fmt.Println("type update", key)
switch key {
- case "Roomid":
- s.Roomid = update.Roomid
case "Ua":
s.Ua = update.Ua
case "Status":
s.Status = update.Status
+ case "Prio":
+ s.Prio = update.Prio
}
}
@@ -76,19 +79,6 @@ func (s *Session) Update(update *SessionUpdate) uint64 {
}
-func (s *Session) Apply(st *SessionToken) uint64 {
-
- s.mutex.Lock()
- defer s.mutex.Unlock()
- s.Id = st.Id
- s.Sid = st.Sid
- s.Userid = st.Userid
-
- s.UpdateRev++
- return s.UpdateRev
-
-}
-
func (s *Session) Authorize(realm string, st *SessionToken) (string, error) {
s.mutex.Lock()
@@ -97,7 +87,7 @@ func (s *Session) Authorize(realm string, st *SessionToken) (string, error) {
if s.Id != st.Id || s.Sid != st.Sid {
return "", errors.New("session id mismatch")
}
- if s.Userid != "" {
+ if s.userid != "" {
return "", errors.New("session already authenticated")
}
@@ -109,35 +99,41 @@ func (s *Session) Authorize(realm string, st *SessionToken) (string, error) {
}
-func (s *Session) Authenticate(realm string, st *SessionToken) error {
+func (s *Session) Authenticate(realm string, st *SessionToken, userid string) error {
s.mutex.Lock()
defer s.mutex.Unlock()
- if s.Userid != "" {
+ if s.userid != "" {
return errors.New("session already authenticated")
}
- if s.Nonce == "" || s.Nonce != st.Nonce {
- return errors.New("nonce validation failed")
- }
- var userid string
- err := sessionNonces.Decode(fmt.Sprintf("%s@%s", s.Sid, realm), st.Nonce, &userid)
- if err != nil {
- return err
- }
- if st.Userid != userid {
- return errors.New("user id mismatch")
+ if userid == "" {
+ if s.Nonce == "" || s.Nonce != st.Nonce {
+ return errors.New("nonce validation failed")
+ }
+ err := sessionNonces.Decode(fmt.Sprintf("%s@%s", s.Sid, realm), st.Nonce, &userid)
+ if err != nil {
+ return err
+ }
+ if st.Userid != userid {
+ return errors.New("user id mismatch")
+ }
+ s.Nonce = ""
}
- s.Nonce = ""
- s.Userid = st.Userid
+ s.userid = userid
+ s.stamp = time.Now().Unix()
s.UpdateRev++
return nil
}
func (s *Session) Token() *SessionToken {
- return &SessionToken{Id: s.Id, Sid: s.Sid, Userid: s.Userid}
+
+ s.mutex.RLock()
+ defer s.mutex.RUnlock()
+
+ return &SessionToken{Id: s.Id, Sid: s.Sid, Userid: s.userid}
}
func (s *Session) Data() *DataSession {
@@ -147,14 +143,22 @@ func (s *Session) Data() *DataSession {
return &DataSession{
Id: s.Id,
- Userid: s.Userid,
+ Userid: s.userid,
Ua: s.Ua,
Status: s.Status,
Rev: s.UpdateRev,
+ Prio: s.Prio,
+ stamp: s.stamp,
}
}
+func (s *Session) Userid() string {
+
+ return s.userid
+
+}
+
func (s *Session) DataSessionLeft(state string) *DataSession {
s.mutex.RLock()
@@ -176,8 +180,9 @@ func (s *Session) DataSessionJoined() *DataSession {
return &DataSession{
Type: "Joined",
Id: s.Id,
- Userid: s.Userid,
+ Userid: s.userid,
Ua: s.Ua,
+ Prio: s.Prio,
}
}
@@ -190,9 +195,10 @@ func (s *Session) DataSessionStatus() *DataSession {
return &DataSession{
Type: "Status",
Id: s.Id,
- Userid: s.Userid,
+ Userid: s.userid,
Status: s.Status,
Rev: s.UpdateRev,
+ Prio: s.Prio,
}
}
@@ -202,6 +208,7 @@ type SessionUpdate struct {
Types []string
Roomid string
Ua string
+ Prio int
Status interface{}
}
diff --git a/src/app/spreed-webrtc-server/user.go b/src/app/spreed-webrtc-server/user.go
new file mode 100644
index 00000000..057e37a6
--- /dev/null
+++ b/src/app/spreed-webrtc-server/user.go
@@ -0,0 +1,112 @@
+/*
+ * 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
.
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "sort"
+ "sync"
+)
+
+type User struct {
+ Id string
+ sessionTable map[string]*Session
+ mutex sync.RWMutex
+}
+
+func NewUser(id string) *User {
+
+ user := &User{
+ Id: id,
+ sessionTable: make(map[string]*Session),
+ }
+ return user
+
+}
+
+// Return true if first session.
+func (u *User) AddSession(s *Session) bool {
+ first := false
+ u.mutex.Lock()
+ u.sessionTable[s.Id] = s
+ if len(u.sessionTable) == 1 {
+ fmt.Println("First session registered for user", u.Id)
+ first = true
+ }
+ u.mutex.Unlock()
+ return first
+}
+
+// Return true if no session left.
+func (u *User) RemoveSession(s *Session) bool {
+ last := false
+ u.mutex.Lock()
+ delete(u.sessionTable, s.Id)
+ if len(u.sessionTable) == 0 {
+ fmt.Println("Last session unregistered for user", u.Id)
+ last = true
+ }
+ u.mutex.Unlock()
+ return last
+}
+
+func (u *User) Data() *DataUser {
+ u.mutex.RLock()
+ defer u.mutex.RUnlock()
+ return &DataUser{
+ Id: u.Id,
+ Sessions: len(u.sessionTable),
+ }
+}
+
+func (u *User) SessionsData() []*DataSession {
+
+ sessions := make([]*DataSession, 0, len(u.sessionTable))
+ u.mutex.RLock()
+ defer u.mutex.RUnlock()
+ for _, session := range u.sessionTable {
+ sessions = append(sessions, session.Data())
+ }
+ sort.Sort(ByPrioAndStamp(sessions))
+ return sessions
+
+}
+
+type ByPrioAndStamp []*DataSession
+
+func (a ByPrioAndStamp) Len() int {
+ return len(a)
+}
+
+func (a ByPrioAndStamp) Swap(i, j int) {
+ a[i], a[j] = a[j], a[i]
+}
+
+func (a ByPrioAndStamp) Less(i, j int) bool {
+ if a[i].Prio < a[j].Prio {
+ return true
+ }
+ if a[i].Prio == a[j].Prio {
+ return a[i].stamp < a[j].stamp
+ }
+ return false
+}
diff --git a/src/app/spreed-webrtc-server/users.go b/src/app/spreed-webrtc-server/users.go
index 4bc552dd..ed70687f 100644
--- a/src/app/spreed-webrtc-server/users.go
+++ b/src/app/spreed-webrtc-server/users.go
@@ -74,10 +74,11 @@ func (uh *UsersSharedsecretHandler) Get(request *http.Request) (userid string, e
func (uh *UsersSharedsecretHandler) Validate(snr *SessionNonceRequest, request *http.Request) (string, error) {
// Parse UseridCombo.
- useridCombo := strings.SplitN(snr.UseridCombo, ":", 2)
- if len(useridCombo) != 2 {
+ useridCombo := strings.SplitN(snr.UseridCombo, ":", 3)
+ if len(useridCombo) < 2 {
return "", errors.New("invalid useridcombo")
}
+ // TODO(longsleep): Add support for third field which provides the username.
expirationString, userid := useridCombo[0], useridCombo[1]
expiration, err := strconv.ParseInt(expirationString, 10, 64)
diff --git a/src/styles/components/_buddylist.scss b/src/styles/components/_buddylist.scss
index d7f7d6fc..8eeaff88 100644
--- a/src/styles/components/_buddylist.scss
+++ b/src/styles/components/_buddylist.scss
@@ -113,7 +113,7 @@
cursor: pointer;
display: block;
font-size: 13px;
- height: 66px;
+ min-height: 66px;
overflow: hidden;
position: relative;
text-align: left;
@@ -126,15 +126,21 @@
}
.buddy {
- &.withSubline .buddy1 {
+ &.withSubline .buddy1, &.contact .buddy1 {
top: 15px;
}
- &.withSubline .buddy2 {
+ &.withSubline .buddy2, &.contact .buddy2 {
display: block;
}
&.hovered .buddyactions {
right: 0;
}
+ & .fa.contact:before {
+ content: "\f006";
+ }
+ &.contact .fa.contact:before {
+ content: "\f005";
+ }
.buddyPicture {
background: $actioncolor1;
border-radius: 2px;
@@ -158,6 +164,16 @@
top: 0;
}
}
+ .buddyPictureSmall {
+ margin:0px;
+ margin-left:-4px;
+ height:30px;
+ width:30px;
+ margin-right:4px;
+ .#{$fa-css-prefix} {
+ line-height: 30px;
+ }
+ }
.buddy1 {
color: $componentfg1;
font-weight: bold;
@@ -181,11 +197,19 @@
white-space: nowrap;
display: none;
}
+ .buddy3 {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ display: inline-block;
+ width:160px;
+ text-align:left;
+ }
}
.buddy .buddyactions {
background: $buddylist-action-background;
- bottom: 0px;
+ height: 66px;
line-height: 66px;
position: absolute;
right: -125px;
@@ -199,4 +223,34 @@
.#{$fa-css-prefix} {
font-size: 2em;
}
+ i.fa {
+ pointer-events: none;
+ }
}
+
+.buddy .buddysessions {
+ margin-top: 56px;
+ max-height:0px;
+ transition-property: max-height;
+ transition-duration: 1s;
+ transition-delay: .1s;
+ ul {
+ padding-top: 10px;
+ margin-left: 20px;
+ padding-left: 0px;
+ border-left: 1px dotted $bordercolor;
+ }
+ ul li {
+ margin-bottom: 1px;
+ margin-top: 1px;
+ margin-left: -4px;
+ list-style-type: none;
+ }
+ .currentsession .buddy3 {
+ font-weight: bold;
+ }
+}
+
+.buddy.hovered .buddysessions {
+ max-height:999px;
+}
\ No newline at end of file
diff --git a/src/styles/components/_chat.scss b/src/styles/components/_chat.scss
index 4da09c0b..a76ab562 100644
--- a/src/styles/components/_chat.scss
+++ b/src/styles/components/_chat.scss
@@ -439,7 +439,7 @@
}
}
.typinghint {
- padding: 2px 6px 0 6px;
+ padding: 0 6px 0 6px;
white-space: nowrap;
overflow: hidden;
font-size:.8em;
diff --git a/src/styles/global/_base.scss b/src/styles/global/_base.scss
index cbd1681c..63825c38 100644
--- a/src/styles/global/_base.scss
+++ b/src/styles/global/_base.scss
@@ -131,4 +131,5 @@ a {
font-size: 1.1em;
margin-top: 80px;
text-shadow: 0 0 5px black;
+ max-width:500px;
}
diff --git a/static/js/controllers/chatroomcontroller.js b/static/js/controllers/chatroomcontroller.js
index 74c66656..0b814c2e 100644
--- a/static/js/controllers/chatroomcontroller.js
+++ b/static/js/controllers/chatroomcontroller.js
@@ -18,7 +18,7 @@
* along with this program. If not, see
.
*
*/
-define(['underscore', 'moment', 'text!partials/fileinfo.html'], function(_, moment, templateFileInfo) {
+define(['underscore', 'moment', 'text!partials/fileinfo.html', 'text!partials/contactrequest.html'], function(_, moment, templateFileInfo, templateContactRequest) {
// ChatroomController
return ["$scope", "$element", "$window", "safeMessage", "safeDisplayName", "$compile", "$filter", "translation", function($scope, $element, $window, safeMessage, safeDisplayName, $compile, $filter, translation) {
@@ -45,6 +45,7 @@ define(['underscore', 'moment', 'text!partials/fileinfo.html'], function(_, mome
var displayName = safeDisplayName;
var buddyImageSrc = $filter("buddyImageSrc");
var fileInfo = $compile(templateFileInfo);
+ var contactRequest = $compile(templateContactRequest);
var knowMessage = {
r: {},
@@ -285,17 +286,17 @@ define(['underscore', 'moment', 'text!partials/fileinfo.html'], function(_, mome
$scope.showmessage = function(from, timestamp, message, nodes) {
- var userid = $scope.$parent.$parent.id;
+ var sessonid = $scope.$parent.$parent.id;
// Prepare message to display.
var s = [];
if (message) {
s.push(message);
- $scope.$emit("incoming", message, from, userid);
+ $scope.$emit("incoming", message, from, sessonid);
}
var is_new_message = lastSender !== from;
- var is_self = from === userid;
+ var is_self = from === sessonid;
var extra_css = "";
var title = null;
@@ -373,7 +374,7 @@ define(['underscore', 'moment', 'text!partials/fileinfo.html'], function(_, mome
$scope.$on("received", function(event, from, data) {
- var userid = $scope.$parent.$parent.id;
+ var sessionid = $scope.$parent.$parent.id;
var mid = data.Mid || null;
switch (data.Type) {
@@ -394,9 +395,10 @@ define(['underscore', 'moment', 'text!partials/fileinfo.html'], function(_, mome
// Definitions.
var message = null;
var nodes = null;
- var fromself = from === userid;
+ var fromself = from === sessionid;
var noop = false;
var element = null;
+ var subscope;
var timestamp = data.Time;
if (!timestamp) {
@@ -441,7 +443,7 @@ define(['underscore', 'moment', 'text!partials/fileinfo.html'], function(_, mome
// File offers.
if (data.Status.FileInfo) {
- var subscope = $scope.$new();
+ subscope = $scope.$new();
subscope.info = data.Status.FileInfo;
subscope.from = from;
fileInfo(subscope, function(clonedElement, scope) {
@@ -451,6 +453,39 @@ define(['underscore', 'moment', 'text!partials/fileinfo.html'], function(_, mome
noop = true;
}
+ // Contact request.
+ if (data.Status.ContactRequest) {
+ subscope = $scope.$new();
+ subscope.request = data.Status.ContactRequest;
+ subscope.fromself = fromself;
+ contactRequest(subscope, function(clonedElement, scope) {
+ var text;
+ if (fromself) {
+ if (scope.request.Userid) {
+ if (scope.request.Success) {
+ text = translation._("You accepted the contact request.");
+ } else {
+ text = translation._("You rejected the contact request.");
+ }
+ } else {
+ text = translation._("You sent a contact request.");
+ }
+ } else {
+ if (scope.request.Success) {
+ text = translation._("Your contact request was accepted.");
+ } else{
+ if (scope.request.Token) {
+ text = translation._("Incoming contact request.");
+ } else {
+ text = translation._("Your contact request was rejected.");
+ }
+ }
+ }
+ element = $scope.showmessage(from, timestamp, text, clonedElement);
+ });
+ noop = true;
+ }
+
// Ignore unknown status messages.
if (message === null && nodes === null) {
noop = true;
diff --git a/static/js/controllers/controllers.js b/static/js/controllers/controllers.js
index 6d3411d8..24b7c56b 100644
--- a/static/js/controllers/controllers.js
+++ b/static/js/controllers/controllers.js
@@ -24,13 +24,15 @@ define([
'controllers/mediastreamcontroller',
'controllers/statusmessagecontroller',
'controllers/chatroomcontroller',
- 'controllers/roomchangecontroller'], function(_, MediastreamController, StatusmessageController, ChatroomController, RoomchangeController) {
+ 'controllers/roomchangecontroller',
+ 'controllers/usersettingscontroller'], function(_, MediastreamController, StatusmessageController, ChatroomController, RoomchangeController, UsersettingsController) {
var controllers = {
MediastreamController: MediastreamController,
StatusmessageController: StatusmessageController,
ChatroomController: ChatroomController,
- RoomchangeController: RoomchangeController
+ RoomchangeController: RoomchangeController,
+ UsersettingsController: UsersettingsController
};
var initialize = function(angModule) {
diff --git a/static/js/controllers/mediastreamcontroller.js b/static/js/controllers/mediastreamcontroller.js
index 17322dcb..1cd9f07f 100644
--- a/static/js/controllers/mediastreamcontroller.js
+++ b/static/js/controllers/mediastreamcontroller.js
@@ -20,7 +20,7 @@
*/
define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function(_, BigScreen, moment, sjcl) {
- return ["$scope", "$rootScope", "$element", "$window", "$timeout", "safeDisplayName", "safeApply", "mediaStream", "appData", "playSound", "desktopNotify", "alertify", "toastr", "translation", "fileDownload", function($scope, $rootScope, $element, $window, $timeout, safeDisplayName, safeApply, mediaStream, appData, playSound, desktopNotify, alertify, toastr, translation, fileDownload) {
+ return ["$scope", "$rootScope", "$element", "$window", "$timeout", "safeDisplayName", "safeApply", "mediaStream", "appData", "playSound", "desktopNotify", "alertify", "toastr", "translation", "fileDownload", "localStorage", function($scope, $rootScope, $element, $window, $timeout, safeDisplayName, safeApply, mediaStream, appData, playSound, desktopNotify, alertify, toastr, translation, fileDownload, localStorage) {
/*console.log("route", $route, $routeParams, $location);*/
@@ -123,6 +123,7 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function
$scope.status = "initializing";
$scope.id = null;
$scope.userid = null;
+ $scope.suserid = null;
$scope.peer = null;
$scope.dialing = null;
$scope.conference = null;
@@ -138,6 +139,7 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function
$scope.master = {
displayName: null,
buddyPicture: null,
+ message: null,
settings: {
videoQuality: "high",
stereo: true,
@@ -174,7 +176,8 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function
// This is the user status.
var status = {
displayName: $scope.master.displayName || null,
- buddyPicture: $scope.master.buddyPicture || null
+ buddyPicture: $scope.master.buddyPicture || null,
+ message: $scope.master.message || null
}
if (_.isEqual(status, cache.status)) {
console.log("Status update skipped, as status has not changed.")
@@ -344,7 +347,6 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function
});
// Load stuff from localStorage.
- // TODO(longsleep): Put localStorage into Angular service.
var storedUser = localStorage.getItem("mediastream-user");
console.log("Found stored user data:", storedUser);
if (storedUser) {
@@ -371,6 +373,7 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function
safeApply($scope, function(scope) {
scope.id = scope.myid = data.Id;
scope.userid = data.Userid;
+ scope.suserid = data.Suserid;
scope.turn = data.Turn;
scope.stun = data.Stun;
scope.refreshWebrtcSettings();
@@ -402,6 +405,7 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function
delete data.nonce;
}, function(data, status) {
console.error("Failed to authorize session", status, data);
+ mediaStream.users.forget();
});
}
}
@@ -427,6 +431,8 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function
}, 0);
}
+ appData.e.triggerHandler("selfReceived", data);
+
});
mediaStream.webrtc.e.on("peercall", function(event, peercall) {
@@ -545,7 +551,7 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function
if (opts.soft) {
return;
}
- $scope.userid = null;
+ $scope.userid = $scope.suserid = null;
break;
case "error":
if (reconnecting || connected) {
@@ -621,9 +627,12 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function
});
$scope.$watch("userid", function(userid) {
+ var suserid;
if (userid) {
- console.info("Session is now authenticated:", userid);
+ suserid = $scope.suserid;
+ console.info("Session is now authenticated:", userid, suserid);
}
+ appData.e.triggerHandler("authenticationChanged", [userid, suserid]);
});
// Apply all layout stuff as classes to our element.
@@ -659,8 +668,23 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'webrtc.adapter'], function
if (mediaStream.connector.connected) {
$scope.setStatus("waiting");
}
- $scope.layout.buddylist = true;
- $scope.layout.buddylistAutoHide = false;
+ 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) {
diff --git a/static/js/controllers/usersettingscontroller.js b/static/js/controllers/usersettingscontroller.js
new file mode 100644
index 00000000..2bbc5c8d
--- /dev/null
+++ b/static/js/controllers/usersettingscontroller.js
@@ -0,0 +1,66 @@
+/*
+ * 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
.
+ *
+ */
+define([], function() {
+
+ // UsersettingsController
+ return ["$scope", "$element", "mediaStream", "safeApply", function($scope, $element, mediaStream, safeApply) {
+
+ $scope.withUsersForget = true;
+
+ this.registerUserid = function(btn) {
+
+ var successHandler = function(data) {
+ console.info("Created new userid:", data.userid);
+ // If the server provided us a nonce, we can do everthing on our own.
+ mediaStream.users.store(data);
+ $scope.loadedUserlogin = true;
+ safeApply($scope);
+ // Directly authenticate ourselves with the provided nonce.
+ mediaStream.api.requestAuthentication(data.userid, data.nonce);
+ delete data.nonce;
+ };
+
+ console.log("No userid - creating one ...");
+ mediaStream.users.register(btn.form, function(data) {
+ if (data.nonce) {
+ successHandler(data);
+ } else {
+ // No nonce received. So this means something we cannot do on our own.
+ // Make are GET request and retrieve nonce that way and let the
+ // browser/server do the rest.
+ mediaStream.users.authorize(data, successHandler, function(data, status) {
+ console.error("Failed to get nonce after create", status, data);
+ });
+ }
+ }, function(data, status) {
+ console.error("Failed to create userid", status, data);
+ });
+
+ };
+
+ this.forgetUserid = function() {
+ mediaStream.users.forget();
+ mediaStream.connector.forgetAndReconnect();
+ };
+
+ }];
+
+});
diff --git a/static/js/directives/audiolevel.js b/static/js/directives/audiolevel.js
index e8f91d02..0ab7de42 100644
--- a/static/js/directives/audiolevel.js
+++ b/static/js/directives/audiolevel.js
@@ -18,11 +18,10 @@
* along with this program. If not, see
.
*
*/
-define(['jquery', 'underscore', 'rAF'], function($, _) {
+define(['jquery', 'underscore'], function($, _) {
- return ["$window", "mediaStream", "safeApply", function($window, mediaStream, safeApply) {
+ return ["$window", "mediaStream", "safeApply", "animationFrame", function($window, mediaStream, safeApply, animationFrame) {
- var requestAnimationFrame = $window.requestAnimationFrame;
var webrtc = mediaStream.webrtc;
// Consider anyting lower than this % as no audio.
@@ -43,21 +42,22 @@ define(['jquery', 'underscore', 'rAF'], function($, _) {
// Own audio level indicator.
var element = $element[0];
+ var width = 0;
this.update = _.bind(function() {
- if (this.active) {
- requestAnimationFrame(this.update);
- }
- var width = 0;
- if (webrtc.usermedia.audioLevel) {
- width = Math.round(100 * webrtc.usermedia.audioLevel);
- // Hide low volumes.
- if (width < threshhold) {
+ if (this.active || width > 0) {
+ if (webrtc.usermedia.audioLevel) {
+ width = Math.round(100 * webrtc.usermedia.audioLevel);
+ // Hide low volumes.
+ if (width < threshhold) {
+ width = 0;
+ }
+ } else {
width = 0;
}
+ element.style.width = width + '%';
}
- element.style.width = width + '%';
}, this);
- this.update();
+ animationFrame.register(this.update);
// Talking state.
this.audioActivityHistory = [];
diff --git a/static/js/directives/audiovideo.js b/static/js/directives/audiovideo.js
index 160b12c1..60243c1d 100644
--- a/static/js/directives/audiovideo.js
+++ b/static/js/directives/audiovideo.js
@@ -18,11 +18,10 @@
* along with this program. If not, see
.
*
*/
-define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/audiovideopeer.html', 'bigscreen', 'injectCSS', 'webrtc.adapter', 'rAF'], function($, _, template, templatePeer, BigScreen) {
+define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/audiovideopeer.html', 'bigscreen', 'injectCSS', 'webrtc.adapter'], function($, _, template, templatePeer, BigScreen) {
- return ["$window", "$compile", "$filter", "mediaStream", "safeApply", "desktopNotify", "buddyData", "videoWaiter", "videoLayout", function($window, $compile, $filter, mediaStream, safeApply, desktopNotify, buddyData, videoWaiter, videoLayout) {
+ return ["$window", "$compile", "$filter", "mediaStream", "safeApply", "desktopNotify", "buddyData", "videoWaiter", "videoLayout", "animationFrame", function($window, $compile, $filter, mediaStream, safeApply, desktopNotify, buddyData, videoWaiter, videoLayout, animationFrame) {
- var requestAnimationFrame = $window.requestAnimationFrame;
var peerTemplate = $compile(templatePeer);
var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
@@ -316,9 +315,8 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/
needsRedraw = false;
redraw();
}
- requestAnimationFrame(update);
}
- _.defer(update);
+ animationFrame.register(update);
}
diff --git a/static/js/directives/buddylist.js b/static/js/directives/buddylist.js
index 515b311d..e3c71d5f 100644
--- a/static/js/directives/buddylist.js
+++ b/static/js/directives/buddylist.js
@@ -21,14 +21,14 @@
define(['underscore', 'text!partials/buddylist.html'], function(_, template) {
// buddyList
- return ["$compile", "buddyList", "mediaStream", function($compile, buddyList, mediaStream) {
+ return ["$compile", "buddyList", "mediaStream", "contacts", function($compile, buddyList, mediaStream, contacts) {
//console.log("buddyList directive");
var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
$scope.layout.buddylist = false;
- $scope.enabled = false;
+ $scope.layout.buddylistAutoHide = true;
$scope.doCall = function(id) {
@@ -46,33 +46,44 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) {
};
+ $scope.doContactRequest = function(id) {
+
+ //console.log("doContact", id);
+ $scope.$emit("requestcontact", id, {
+ restore: true
+ });
+
+ };
+
+ $scope.doContactRemove = function(userid) {
+
+ contacts.remove(userid);
+
+ };
+
+ /*
$scope.doAudioConference = function(id) {
$scope.updateAutoAccept(id);
mediaStream.api.sendChat(id, null, {
- type: "conference",
- id: mediaStream.connector.roomid
+ AutoCall: {
+ Type: "conference",
+ Id: mediaStream.connector.roomid
+ }
})
- };
+ };*/
$scope.setRoomStatus = function(status) {
- if (status !== $scope.enabled) {
- $scope.enabled = status;
- $scope.$emit("roomStatus", status);
- }
- if (status && !$scope.layout.buddylistAutoHide) {
- $scope.layout.buddylist = true
- }
+ $scope.$emit("roomStatus", status);
};
- //XXX(longsleep): Debug leftover ?? Remove this.
- window.doAudioConference = $scope.doAudioConference;
-
var buddylist = $scope.buddylist = buddyList.buddylist($element, $scope, {});
var onJoined = _.bind(buddylist.onJoined, buddylist);
var onLeft = _.bind(buddylist.onLeft, buddylist);
var onStatus = _.bind(buddylist.onStatus, buddylist);
+ var onContactAdded = _.bind(buddylist.onContactAdded, buddylist);
+ var onContactRemoved = _.bind(buddylist.onContactRemoved, buddylist);
mediaStream.api.e.on("received.userleftorjoined", function(event, dataType, data) {
if (dataType === "Left") {
onLeft(data);
@@ -97,11 +108,17 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) {
$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);
+ });
+ contacts.e.on("contactremoved", function(event, data) {
+ onContactRemoved(data);
+ });
}];
diff --git a/static/js/directives/chat.js b/static/js/directives/chat.js
index c40dc455..c58916c1 100644
--- a/static/js/directives/chat.js
+++ b/static/js/directives/chat.js
@@ -20,7 +20,7 @@
*/
define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'], function(_, templateChat, templateChatroom) {
- return ["$compile", "safeDisplayName", "mediaStream", "safeApply", "desktopNotify", "translation", "playSound", "fileUpload", "randomGen", "buddyData", "$timeout", function($compile, safeDisplayName, mediaStream, safeApply, desktopNotify, translation, playSound, fileUpload, randomGen, buddyData, $timeout) {
+ return ["$compile", "safeDisplayName", "mediaStream", "safeApply", "desktopNotify", "translation", "playSound", "fileUpload", "randomGen", "buddyData", "appData", "$timeout", function($compile, safeDisplayName, mediaStream, safeApply, desktopNotify, translation, playSound, fileUpload, randomGen, buddyData, appData, $timeout) {
var displayName = safeDisplayName;
var group_chat_id = "";
@@ -138,6 +138,33 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'],
});
+ $scope.$parent.$on("requestcontact", function(event, id, options) {
+
+ if (id !== group_chat_id) {
+ // Make sure the contact id is valid.
+ var ad = appData.get();
+ if (!ad.userid) {
+ // Unable to add contacts as we have no own userid.
+ console.log("You need to log in to add contacts.");
+ return;
+ }
+ var bd = buddyData.get(id);
+ if (!bd || !bd.session || !bd.session.Userid || ad.userid === bd.session.Userid) {
+ console.log("This contact cannot be added.");
+ return;
+ }
+ var subscope = $scope.showRoom(id, {
+ title: translation._("Chat with")
+ }, options);
+ subscope.sendChatServer(id, "Contact request", {
+ ContactRequest: {
+ Id: randomGen.random({hex: true})
+ }
+ });
+ }
+
+ });
+
}];
var compile = function(tElement, tAttrs) {
@@ -205,47 +232,59 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'],
subscope.sendChat = function(to, message, status, mid, noloop) {
//console.log("send chat", to, scope.peer);
var peercall = mediaStream.webrtc.findTargetCall(to);
- if (message && !mid) {
- mid = randomGen.random({
- hex: true
- });
- }
if (peercall && peercall.peerconnection.datachannelReady) {
subscope.p2p(true);
// Send out stuff through data channel.
- _.delay(function() {
- mediaStream.api.apply("sendChat", {
- send: function(type, data) {
- // We also send to self, to display our own stuff.
- if (!noloop) {
- mediaStream.api.received({
- Type: data.Type,
- Data: data,
- From: mediaStream.api.id,
- To: peercall.id
- });
- }
- return peercall.peerconnection.send(data);
- }
- })(to, message, status, mid);
- }, 100);
-
+ return subscope.sendChatPeer2Peer(peercall, to, message, status, mid, noloop);
} else {
subscope.p2p(false);
- _.delay(function() {
- mediaStream.api.send2("sendChat", function(type, data) {
+ return subscope.sendChatServer(to, message, status, mid, noloop);
+ }
+ return mid;
+ };
+ subscope.sendChatPeer2Peer = function(peercall, to, message, status, mid, noloop) {
+ if (message && !mid) {
+ mid = randomGen.random({
+ hex: true
+ });
+ }
+ _.delay(function() {
+ mediaStream.api.apply("sendChat", {
+ send: function(type, data) {
+ // We also send to self, to display our own stuff.
if (!noloop) {
- //console.log("looped to self", type, data);
mediaStream.api.received({
Type: data.Type,
Data: data,
From: mediaStream.api.id,
- To: to
+ To: peercall.id
});
}
- })(to, message, status, mid);
- }, 100);
+ return peercall.peerconnection.send(data);
+ }
+ })(to, message, status, mid);
+ }, 100);
+ return mid;
+ };
+ subscope.sendChatServer = function(to, message, status, mid, noloop) {
+ if (message && !mid) {
+ mid = randomGen.random({
+ hex: true
+ });
}
+ _.delay(function() {
+ mediaStream.api.send2("sendChat", function(type, data) {
+ if (!noloop) {
+ //console.log("looped to self", type, data);
+ mediaStream.api.received({
+ Type: data.Type,
+ Data: data,
+ From: mediaStream.api.id,
+ To: to
+ });
+ }
+ })(to, message, status, mid);
+ }, 100);
return mid;
};
subscope.p2p = function(state) {
@@ -295,8 +334,8 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'],
}
safeApply(subscope);
});
- subscope.$on("incoming", function(event, message, from, userid) {
- if (from !== userid) {
+ subscope.$on("incoming", function(event, message, from, sessionid) {
+ if (from !== sessionid) {
subscope.pending++;
scope.$emit("chatincoming", subscope.id);
}
@@ -304,7 +343,7 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'],
var room = event.targetScope.id;
// Make sure we are not in group chat or the message is from ourselves
// before we beep and shout.
- if (!subscope.isgroupchat && from !== userid) {
+ if (!subscope.isgroupchat && from !== sessionid) {
playSound.play("message1");
desktopNotify.notify(translation._("Message from ") + displayName(from), message);
}
diff --git a/static/js/directives/contactrequest.js b/static/js/directives/contactrequest.js
new file mode 100644
index 00000000..ef5358a7
--- /dev/null
+++ b/static/js/directives/contactrequest.js
@@ -0,0 +1,72 @@
+/*
+ * 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
.
+ *
+ */
+define(['jquery', 'underscore'], function($, _) {
+
+ return ["translation", "buddyData", "contacts", function(translation, buddyData, contacts) {
+
+ var controller = ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
+
+ $scope.state = "request";
+ $scope.doAccept = function() {
+ $scope.state = "accepted";
+ $scope.doContact(true);
+ };
+
+ $scope.doReject = function() {
+ $scope.state = "rejected";
+ $scope.doContact(false);
+ };
+
+ $scope.doContact = function(success) {
+ var r = $scope.request;
+ r.Success = !!success;
+ $scope.sendChatServer($scope.id, "Contact request answer", {
+ ContactRequest: r
+ });
+ };
+
+ $scope.addContact = function(request, status) {
+ contacts.add(request, status)
+ };
+
+ // Add support for contacts on controller creation.
+ var request = $scope.request;
+ if (request.Success && request.Userid && request.Token) {
+ var buddy = buddyData.lookup($scope.id);
+ var status = {};
+ if (buddy) {
+ $.extend(status, buddy.status);
+ }
+ $scope.addContact(request, status);
+ }
+
+ }];
+
+ return {
+ scope: true,
+ restrict: 'EAC',
+ controller: controller,
+ replace: false
+ }
+
+ }];
+
+});
diff --git a/static/js/directives/directives.js b/static/js/directives/directives.js
index 510fd6e3..9b4f2ff4 100644
--- a/static/js/directives/directives.js
+++ b/static/js/directives/directives.js
@@ -35,7 +35,8 @@ define([
'directives/screenshare',
'directives/roombar',
'directives/socialshare',
- 'directives/page'], function(_, onEnter, onEscape, statusMessage, buddyList, buddyPicture, settings, chat, audioVideo, usability, audioLevel, fileInfo, screenshare, roomBar, socialShare, page) {
+ 'directives/page',
+ 'directives/contactrequest'], function(_, onEnter, onEscape, statusMessage, buddyList, buddyPicture, settings, chat, audioVideo, usability, audioLevel, fileInfo, screenshare, roomBar, socialShare, page, contactRequest) {
var directives = {
onEnter: onEnter,
@@ -52,7 +53,8 @@ define([
screenshare: screenshare,
roomBar: roomBar,
socialShare: socialShare,
- page: page
+ page: page,
+ contactRequest: contactRequest
};
var initialize = function(angModule) {
diff --git a/static/js/directives/settings.js b/static/js/directives/settings.js
index 6716aa05..7d36757e 100644
--- a/static/js/directives/settings.js
+++ b/static/js/directives/settings.js
@@ -22,7 +22,7 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t
return ["$compile", "mediaStream", function($compile, mediaStream) {
- var controller = ['$scope', 'desktopNotify', 'mediaSources', 'safeApply', 'availableLanguages', 'translation', function($scope, desktopNotify, mediaSources, safeApply, availableLanguages, translation) {
+ var controller = ['$scope', 'desktopNotify', 'mediaSources', 'safeApply', 'availableLanguages', 'translation', 'localStorage', function($scope, desktopNotify, mediaSources, safeApply, availableLanguages, translation, localStorage) {
$scope.layout.settings = false;
$scope.showAdvancedSettings = true;
@@ -74,42 +74,6 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t
});
};
- $scope.registerUserid = function(btn) {
-
- var successHandler = function(data) {
- console.info("Created new userid:", data.userid);
- // If the server provided us a nonce, we can do everthing on our own.
- mediaStream.users.store(data);
- $scope.loadedUserlogin = true;
- safeApply($scope);
- // Directly authenticate ourselves with the provided nonce.
- mediaStream.api.requestAuthentication(data.userid, data.nonce);
- delete data.nonce;
- };
-
- console.log("No userid - creating one ...");
- mediaStream.users.register(btn.form, function(data) {
- if (data.nonce) {
- successHandler(data);
- } else {
- // No nonce received. So this means something we cannot do on our own.
- // Make are GET request and retrieve nonce that way and let the
- // browser/server do the rest.
- mediaStream.users.authorize(data, successHandler, function(data, status) {
- console.error("Failed to get nonce after create", status, data);
- });
- }
- }, function(data, status) {
- console.error("Failed to create userid", status, data);
- });
-
- };
-
- $scope.forgetUserid = function() {
- mediaStream.users.forget();
- mediaStream.connector.forgetAndReconnect();
- };
-
$scope.checkDefaultMediaSources = function() {
if ($scope.master.settings.microphoneId && !$scope.mediaSources.hasAudioId($scope.master.settings.microphoneId)) {
$scope.master.settings.microphoneId = null;
diff --git a/static/js/directives/usability.js b/static/js/directives/usability.js
index a75aa58a..aace59a3 100644
--- a/static/js/directives/usability.js
+++ b/static/js/directives/usability.js
@@ -24,7 +24,7 @@ define(['jquery', 'underscore', 'text!partials/usability.html'], function($, _,
return ["mediaStream", function(mediaStream) {
- var controller = ['$scope', "mediaStream", "safeApply", "$timeout", function($scope, mediaStream, safeApply, $timeout) {
+ var controller = ['$scope', "mediaStream", "safeApply", "$timeout", "localStorage", function($scope, mediaStream, safeApply, $timeout, localStorage) {
var pending = true;
var complete = false;
diff --git a/static/js/filters/buddyimagesrc.js b/static/js/filters/buddyimagesrc.js
index cded8615..852a71c0 100644
--- a/static/js/filters/buddyimagesrc.js
+++ b/static/js/filters/buddyimagesrc.js
@@ -79,18 +79,18 @@ define(["underscore"], function(_) {
var scope = buddyData.lookup(id);
if (scope) {
- var status = scope.status;
- if (status) {
- if (status.buddyPictureLocalUrl) {
- return status.buddyPictureLocalUrl;
- } else if (status.buddyPicture) {
+ var display = scope.display;
+ if (display) {
+ if (display.buddyPictureLocalUrl) {
+ return display.buddyPictureLocalUrl;
+ } else if (display.buddyPicture) {
var url = urls[id];
if (url) {
revokeURL(id, url);
}
// New data -> new url.
- var blob = dataURLToBlob(status.buddyPicture);
- url = status.buddyPictureLocalUrl = urls[id] = blobToObjectURL(blob);
+ var blob = dataURLToBlob(display.buddyPicture);
+ url = display.buddyPictureLocalUrl = urls[id] = blobToObjectURL(blob);
return url;
}
}
diff --git a/static/js/filters/displayname.js b/static/js/filters/displayname.js
index ee104a19..f3695968 100644
--- a/static/js/filters/displayname.js
+++ b/static/js/filters/displayname.js
@@ -35,8 +35,8 @@ define([], function() {
}
var scope = buddyData.lookup(id);
if (scope) {
- if (scope.displayName) {
- return scope.displayName;
+ if (scope.display.displayName) {
+ return scope.display.displayName;
}
return user_text + " " + scope.buddyIndex;
} else {
diff --git a/static/js/mediastream/api.js b/static/js/mediastream/api.js
index 7ed60a44..c7f8b110 100644
--- a/static/js/mediastream/api.js
+++ b/static/js/mediastream/api.js
@@ -29,6 +29,7 @@ define(['jquery', 'underscore'], function($, _) {
this.sid = null;
this.session = {};
this.connector = connector;
+ this.iids= 0;
this.e = $({});
@@ -74,7 +75,7 @@ define(['jquery', 'underscore'], function($, _) {
Type: type
};
payload[type] = data;
- //console.log("<<<<<<<<<<<<", JSON.stringify(payload));
+ //console.log("<<<<<<<<<<<<", JSON.stringify(payload, null, 2));
this.connector.send(payload, noqueue);
};
@@ -91,6 +92,21 @@ define(['jquery', 'underscore'], function($, _) {
return this.apply(name, obj);
};
+ Api.prototype.request = function(type, data, cb) {
+
+ var payload = {
+ Type: type
+ }
+ payload[type] = data;
+ if (cb) {
+ var iid = ""+(this.iids++);
+ payload.Iid = iid;
+ this.e.one(iid+".request", cb);
+ }
+ this.connector.send(payload);
+
+ }
+
// Helper hack function to send API requests to other destinations.
// Simply provide an alternative send function on the obj Object.
Api.prototype.apply = function(name, obj) {
@@ -105,9 +121,16 @@ define(['jquery', 'underscore'], function($, _) {
this.last_receive = now;
this.last_receive_overdue = false;
+ var iid = d.Iid;
var data = d.Data;
var dataType = data.Type;
+ if (iid) {
+ // Shortcut for iid registered responses.
+ this.e.triggerHandler(iid+".request", [dataType, data]);
+ return;
+ }
+
switch (dataType) {
case "Self":
console.log("Self received", data);
@@ -321,6 +344,20 @@ define(['jquery', 'underscore'], function($, _) {
return this.send("Alive", data);
};
+ Api.prototype.sendSessions = function(token, type, cb) {
+
+ var data = {
+ Type: "Sessions",
+ Sessions: {
+ Type: type,
+ Token: token
+ }
+ }
+
+ return this.request("Sessions", data, cb);
+
+ };
+
return Api;
});
diff --git a/static/js/services/animationframe.js b/static/js/services/animationframe.js
new file mode 100644
index 00000000..dd6f43b4
--- /dev/null
+++ b/static/js/services/animationframe.js
@@ -0,0 +1,51 @@
+/*
+ * 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
.
+ *
+ */
+define(["underscore", "rAF"], function(_) {
+
+ // animationFrame
+ return ["$window", function($window) {
+
+ var requestAnimationFrame = $window.requestAnimationFrame;
+ var registry = [];
+
+ var caller = function(f) {
+ f();
+ };
+ var worker = function() {
+ registry.forEach(caller)
+ requestAnimationFrame(worker);
+ };
+
+ // Public api.
+ var animationFrame = {
+ register: function(f) {
+ registry.push(f);
+ }
+ };
+
+ // Auto start worker.
+ _.defer(worker);
+
+ return animationFrame;
+
+ }];
+
+});
diff --git a/static/js/services/appdata.js b/static/js/services/appdata.js
index 2344904c..de688591 100644
--- a/static/js/services/appdata.js
+++ b/static/js/services/appdata.js
@@ -18,13 +18,18 @@
* along with this program. If not, see
.
*
*/
-define([], function() {
+define(["jquery"], function($) {
+
+ // appData.e events
+ // - authenticationChanged(userid)
+ // - selfReceived(self)
// appData
return [function() {
var data = {
- data: null
+ data: null,
+ e: $({})
}
var appData = {
get: function() {
@@ -33,7 +38,8 @@ define([], function() {
set: function(d) {
data.data = d;
return d;
- }
+ },
+ e: data.e
}
return appData;
diff --git a/static/js/services/buddydata.js b/static/js/services/buddydata.js
index 16a2447e..26f68052 100644
--- a/static/js/services/buddydata.js
+++ b/static/js/services/buddydata.js
@@ -21,7 +21,7 @@
define(['underscore'], function(underscore) {
// buddyData
- return [function() {
+ return ["contactData", function(contactData) {
var scopes = {};
var brain = {};
@@ -61,16 +61,35 @@ define(['underscore'], function(underscore) {
}
return 0;
},
- get: function(id, createInParent, afterCreateCallback) {
+ get: function(id, createInParent, afterCreateCallback, userid) {
if (scopes.hasOwnProperty(id)) {
+ //console.log("found id scope", id);
return scopes[id];
} else if (!createInParent && pushed.hasOwnProperty(id)) {
return pushed[id].scope;
} else {
+ var scope;
+ if (userid && scopes.hasOwnProperty(userid)) {
+ scope = scopes[userid];
+ if (createInParent) {
+ scopes[id] = scope;
+ }
+ //console.log("found userid scope", userid);
+ return scope;
+ }
if (createInParent) {
+ //console.log("creating scope", id, userid);
// If we have a parent we can create a new scope.
- var scope = scopes[id] = createInParent.$new();
+ scope = scopes[id] = createInParent.$new();
+ if (userid) {
+ scopes[userid] = scope;
+ }
scope.buddyIndex = ++count;
+ if (userid) {
+ scope.contact = contactData.get(userid);
+ } else {
+ scope.contact = null;
+ }
scope.buddyIndexSortable = ("0000000" + scope.buddyIndex).slice(-7);
if (pushed.hasOwnProperty(id)) {
// Refresh pushed scope reference.
@@ -101,13 +120,17 @@ define(['underscore'], function(underscore) {
del: function(id, hard) {
var scope = scopes[id];
if (scope) {
- scope.$destroy();
- brain[id] = scope;
+ if (!hard) {
+ brain[id] = scope;
+ }
delete scopes[id];
return scope;
} else {
return null;
}
+ },
+ set: function(id, scope) {
+ scopes[id] = scope;
}
};
return buddyData;
diff --git a/static/js/services/buddylist.js b/static/js/services/buddylist.js
index 4b4b9f57..4ab1224a 100644
--- a/static/js/services/buddylist.js
+++ b/static/js/services/buddylist.js
@@ -18,7 +18,7 @@
* along with this program. If not, see
.
*
*/
-define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!partials/buddyactions.html', 'text!partials/buddyactionsforaudiomixer.html', 'rAF'], function(_, Modernizr, AvlTree, templateBuddy, templateBuddyActions, templateBuddyActionsForAudioMixer) {
+define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!partials/buddyactions.html', 'text!partials/buddyactionsforaudiomixer.html'], function(_, Modernizr, AvlTree, templateBuddy, templateBuddyActions, templateBuddyActionsForAudioMixer) {
var BuddyTree = function() {
@@ -31,7 +31,8 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!
BuddyTree.prototype.create = function(id, scope) {
- var sort = scope.displayName ? scope.displayName : "session " + scope.buddyIndexSortable + " " + id;
+ var display = scope.display || {};
+ var sort = display.displayName ? display.displayName : "session " + scope.buddyIndexSortable + " " + id;
var data = {
id: id,
sort: sort + "z" + id
@@ -66,6 +67,12 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!
};
+ BuddyTree.prototype.check = function(id) {
+
+ return this.data.hasOwnProperty(id);
+
+ };
+
/**
* Returns undefined when no change required. Position result otherwise.
*/
@@ -102,6 +109,18 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!
};
+ BuddyTree.prototype.traverse = function(cb) {
+
+ return this.tree.inOrderTraverse(cb);
+
+ };
+
+ BuddyTree.prototype.keys = function() {
+
+ return _.keys(this.data);
+
+ };
+
BuddyTree.prototype.clear = function() {
this.tree.clear();
@@ -110,9 +129,7 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!
};
// buddyList
- return ["$window", "$compile", "playSound", "buddyData", "fastScroll", "mediaStream", function($window, $compile, playSound, buddyData, fastScroll, mediaStream) {
-
- var requestAnimationFrame = $window.requestAnimationFrame;
+ return ["$window", "$compile", "playSound", "buddyData", "buddySession", "fastScroll", "mediaStream", "animationFrame", "$q", function($window, $compile, playSound, buddyData, buddySession, fastScroll, mediaStream, animationFrame, $q) {
var buddyTemplate = $compile(templateBuddy);
var buddyActions = $compile(templateBuddyActions);
@@ -138,25 +155,24 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!
$element.on("mouseenter mouseleave", ".buddy", _.bind(function(event) {
// Hover handler for on Buddy actions.
var buddyElement = $(event.currentTarget);
- this.hover(buddyElement, event.type === "mouseenter" ? true : false, buddyElement.scope().session.Id);
+ this.hover(buddyElement, event.type === "mouseenter" ? true : false);
}, this));
$element.on("click", ".buddy", _.bind(function(event) {
var buddyElement = $(event.currentTarget);
- buddyElement.scope().doDefault();
+ this.click(buddyElement, event.target);
}, this));
$element.attr("data-xthreshold", "10");
$element.on("swipeleft", ".buddy", _.bind(function(event) {
event.preventDefault();
var buddyElement = $(event.currentTarget);
- this.hover(buddyElement, !buddyElement.hasClass("hovered"), buddyElement.scope().session.Id);
+ this.hover(buddyElement, !buddyElement.hasClass("hovered"));
}, this));
$window.setInterval(_.bind(this.soundLoop, this), 500);
var update = _.bind(function refreshBuddies() {
this.refreshBuddies();
- requestAnimationFrame(update);
}, this);
- requestAnimationFrame(update);
+ animationFrame.register(update);
};
@@ -185,23 +201,63 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!
};
- Buddylist.prototype.onBuddyScope = function(scope) {
+ Buddylist.prototype.onBuddyScopeCreated = function(scope, data) {
+ // Init scope with our stuff.
scope.element = null;
- scope.doDefault = function() {
- var id = scope.session.Id;
- if (scope.status.isMixer) {
- return scope.doAudioConference(id);
- }
- return scope.doCall(id);
- };
+ scope.contact = null;
+ scope.display = {};
+ scope.session = buddySession.create(data);
scope.$on("$destroy", function() {
+ //console.log("destroyed");
scope.element = null;
scope.killed = true;
});
};
+ Buddylist.prototype.onBuddySessionUserid = function(scope, sourceSession) {
+
+ //console.log("session with userid", sourceSession);
+
+ var userid = sourceSession.Userid;
+ /*
+ if (userid === scope.userid) {
+ // The source session has our own userid, ignore it.
+
+ }*/
+ var targetScope = buddyData.get(userid);
+ if (!targetScope) {
+ // No scope for this userid yet - set us.
+ buddyData.set(userid, scope);
+ //console.log("set scope with userid", sourceSession);
+ return;
+ }
+ var session = targetScope.session;
+ if (sourceSession === session) {
+ // No action.
+ //console.log("source session same as target");
+ return;
+ }
+ // Merge sessions.
+ session.merge(sourceSession);
+ // Cleanup old from tree and DOM.
+ var id = sourceSession.Id;
+ this.tree.remove(id);
+ if (targetScope !== scope) {
+ if (scope.element) {
+ this.lefts[id] = scope.element;
+ //console.log("destroying", id, scope.element);
+ scope.$destroy();
+ }
+ buddyData.set(id, targetScope);
+ delete this.actionElements[id];
+ return targetScope;
+ }
+ return;
+
+ };
+
Buddylist.prototype.soundLoop = function() {
if (this.playSoundLeft) {
@@ -335,72 +391,160 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!
};
+ Buddylist.prototype.dumpBuddyPictureToBlob = function(scope, data) {
+
+ if (!data) {
+ data = this.dumpBuddyPictureToString(scope);
+ if (!data) {
+ return null;
+ }
+ }
+ // NOTE(longsleep): toBlob is not widely supported narf ..
+ // see: https://code.google.com/p/chromium/issues/detail?id=67587
+ var parts = data.match(/data:([^;]*)(;base64)?,([0-9A-Za-z+\/]+)/);
+ var binStr = atob(parts[3]);
+ var buf = new ArrayBuffer(binStr.length);
+ var view = new Uint8Array(buf);
+ for (var i = 0; i < view.length; i++) {
+ view[i] = binStr.charCodeAt(i);
+ }
+ return new Blob([view], {'type': parts[1]});
+
+ };
+
+ Buddylist.prototype.dumpBuddyPictureToString = function(scope) {
+
+ var img = scope.element.find(".buddyPicture img").get(0);
+ if (img) {
+ var canvas = $window.document.createElement("canvas");
+ canvas.width = img.width;
+ canvas.height = img.height;
+ var ctx = canvas.getContext("2d");
+ ctx.drawImage(img, 0, 0);
+ return canvas.toDataURL("image/jpeg");
+ }
+ return null;
+
+ };
+
+ Buddylist.prototype.setDisplay = function(id, scope, data, queueName) {
+
+ var status = data.Status;
+ var display = scope.display;
+ // Set display.name.
+ display.displayName = status.displayName;
+ // Set display.picture.
+ display.buddyPicture = status.buddyPicture;
+ this.updateBuddyPicture(display);
+ // Set display subline.
+ this.updateSubline(display, status.message);
+ // Add to render queue when no element exists.
+ if (!scope.element) {
+ var before = this.tree.add(id, scope);
+ this.queue.push([queueName, id, before]);
+ this.playSoundJoined = true;
+ }
+
+ };
+
+ Buddylist.prototype.updateDisplay = function(id, scope, data, queueName) {
+
+ var status = data.Status;
+ var display = scope.display;
+ // Update display name.
+ var displayName = display.displayName;
+ if (status.displayName) {
+ display.displayName = status.displayName;
+ } else {
+ display.displayName = null;
+ }
+ // Add to status queue if sorting has changed.
+ if (displayName !== status.displayName) {
+ var before = this.tree.update(id, scope);
+ this.queue.push([queueName, id, before]);
+ }
+ // Update display subline.
+ if (status.message) {
+ this.updateSubline(display, status.message);
+ }
+ // Update display picture.
+ if (status.buddyPicture) {
+ display.buddyPicture = status.buddyPicture || null;
+ this.updateBuddyPicture(display);
+ }
+
+ };
+
+ Buddylist.prototype.updateSubline = function(display, s) {
+
+ if (!s || s === "__e") {
+ display.subline = "";
+ return;
+ }
+ if (s.length > 20) {
+ display.sublineFull = s;
+ s = s.substr(0, 20) + "...";
+ } else {
+ display.sublineFull = null;
+ }
+ display.subline = s;
+
+ };
+
Buddylist.prototype.onStatus = function(data) {
- //console.log("onStatus", status);
+ //console.log("onStatus", data);
var id = data.Id;
- var scope = buddyData.get(id, this.$scope, _.bind(this.onBuddyScope, this));
+ var scope = buddyData.get(id, this.$scope, _.bind(function(scope) {
+ this.onBuddyScopeCreated(scope, data);
+ }, this), data.Userid);
// Update session.
- scope.session.Userid = data.Userid;
- scope.session.Rev = data.Rev;
- // Update status.
- if (scope.status && scope.status.Rev >= data.Rev) {
- console.warn("Received old status update in status", data.Rev, scope.status.Rev);
- } else {
- scope.status = data.Status;
- this.updateBuddyPicture(scope.status);
- var displayName = scope.displayName;
- if (scope.status.displayName) {
- scope.displayName = scope.status.displayName;
- } else {
- scope.displayName = null;
- }
- if (displayName !== scope.displayName) {
- var before = this.tree.update(id, scope);
- this.queue.push(["status", id, before]);
+ var sessionData = scope.session.update(id, data, _.bind(function(session) {
+ //console.log("Session is now authenticated", session);
+ var newscope = this.onBuddySessionUserid(scope, session);
+ if (newscope) {
+ scope = newscope;
}
- scope.$apply();
+ }, this));
+ if (sessionData) {
+ // onStatus for main session.
+ this.updateDisplay(id, scope, sessionData, "status");
}
+ scope.$apply();
+ return scope;
};
- Buddylist.prototype.onJoined = function(data) {
+ Buddylist.prototype.onJoined = function(data, noApply) {
//console.log("Joined", data);
var id = data.Id;
- var scope = buddyData.get(id, this.$scope, _.bind(this.onBuddyScope, this));
- // Create session.
- scope.session = {
- Id: data.Id,
- Userid: data.Userid,
- Ua: data.Ua,
- Rev: 0
- };
- // Add status.
+ var scope = buddyData.get(id, this.$scope, _.bind(function(scope) {
+ this.onBuddyScopeCreated(scope, data);
+ }, this), data.Userid);
+ // Update session.
buddyCount++;
- if (data.Status) {
- if (scope.status && scope.status.Rev >= data.Status.Rev) {
- console.warn("Received old status update in join", data.Status.Rev, scope.status.Rev);
- } else {
- scope.status = data.Status;
- scope.displayName = scope.status.displayName;
- this.updateBuddyPicture(scope.status);
+ var sessionData = scope.session.update(id, data, _.bind(function(session) {
+ //console.log("Session is now authenticated", session);
+ var newscope = this.onBuddySessionUserid(scope, session);
+ if (newscope) {
+ scope = newscope;
}
+ }, this));
+ if (sessionData && sessionData.Status) {
+ this.setDisplay(id, scope, sessionData, "joined");
+ } else if (!noApply) {
+ scope.$apply();
}
- //console.log("Joined scope", scope, scope.element);
- if (!scope.element) {
- var before = this.tree.add(id, scope);
- this.queue.push(["joined", id, before]);
- this.playSoundJoined = true;
- }
+ return scope;
};
- Buddylist.prototype.onLeft = function(data) {
- //console.log("Left", session);
+ Buddylist.prototype.onLeft = function(data, force, noApply) {
+
+ //console.log("Left", data);
var id = data.Id;
- this.tree.remove(id);
var scope = buddyData.get(id);
if (!scope) {
//console.warn("Trying to remove buddy with no registered scope", session);
@@ -409,30 +553,191 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!
if (buddyCount > 0) {
buddyCount--;
}
- if (scope.element) {
- this.lefts[id] = scope.element;
- this.playSoundLeft = true;
- }
+ // Remove current id from tree.
+ this.tree.remove(id);
buddyData.del(id);
- delete this.actionElements[id];
+ // Remove session.
+ var session = scope.session;
+ if ((session.remove(id) && scope.contact === null) || force) {
+ // No session left. Cleanup.
+ if (scope.element) {
+ this.lefts[id] = scope.element;
+ this.playSoundLeft = true;
+ }
+ if (session.Userid) {
+ buddyData.del(session.Userid, true);
+ }
+ delete this.actionElements[id];
+ scope.$destroy();
+ } else {
+ // Update display stuff if a session is left. This can
+ // return no session in case when we got this as contact.
+ var sessionData = session.get();
+ if (sessionData) {
+ this.updateDisplay(sessionData.Id, scope, sessionData, "status");
+ } else if (scope.contact) {
+ var contact = scope.contact;
+ // Use it with userid as id in tree.
+ if (!this.tree.check(contact.Userid)) {
+ this.tree.add(contact.Userid, scope);
+ buddyCount++;
+ }
+ if (contact.Status !== null && !contact.Status.message) {
+ // Remove status message.
+ contact.Status.message = "__e";
+ }
+ this.updateDisplay(contact.Userid, scope, contact, "status");
+ }
+ if (!noApply) {
+ scope.$apply();
+ }
+ }
+ return scope;
};
Buddylist.prototype.onClosed = function() {
//console.log("Closed");
- this.$element.empty();
- buddyCount = 0;
- buddyData.clear();
- this.tree.clear();
- this.actionElements = {};
+
+ // Remove pending stuff from queue.
this.queue = [];
+ // Trigger left events for all sessions.
+ var data = {};
+ var sessions = buddySession.sessions();
+ for (var id in sessions) {
+ if (sessions.hasOwnProperty(id)) {
+ //console.log("close id", id);
+ data.Id = id;
+ this.onLeft(data, false, true);
+ }
+ }
+
+ };
+
+ Buddylist.prototype.onContactAdded = function(contact) {
+
+ //console.log("onContactAdded", contact);
+ var userid = contact.Userid;
+
+ var scope = buddyData.get(userid);
+ if (scope) {
+ scope.contact = contact;
+ var sessionData = scope.session.get();
+ if (sessionData) {
+ if (contact.Status === null && sessionData.Status) {
+ // Update contact status with session.Status
+ var status = contact.Status = _.extend({}, sessionData.Status);
+ // Remove status message.
+ delete status.message;
+ // Convert buddy image.
+ if (status.buddyPicture) {
+ var img = this.dumpBuddyPictureToString(scope);
+ if (img) {
+ status.buddyPicture = img;
+ } else {
+ delete status.buddyPicture;
+ }
+ }
+ console.log("Injected status into contact", contact);
+ }
+ this.updateDisplay(sessionData.Id, scope, contact, "status");
+ scope.$apply();
+ }
+ } else {
+ // Create new scope for contact.
+ scope = this.onJoined({
+ Id: contact.Userid,
+ Userid: contact.Userid,
+ Status: _.extend({}, contact.Status)
+ });
+ scope.contact = contact;
+ }
+
};
- Buddylist.prototype.hover = function(buddyElement, hover, id) {
+ Buddylist.prototype.onContactRemoved = function(contact) {
+
+ //console.log("onContactRemoved", contact);
+ var userid = contact.Userid;
+
+ var scope = buddyData.get(userid);
+ if (scope) {
+ scope.contact = null;
+ // Remove with left when no session for this userid.
+ var sessionData = scope.session.get();
+ if (!sessionData) {
+ // Force left.
+ this.onLeft({Id: userid}, true);
+ }
+ }
+
+ };
+
+ Buddylist.prototype.click = function(buddyElement, target) {
+
+ //console.log("click handler", buddyElement, target);
+ var action = $(target).data("action");
+ if (!action) {
+ // Make call the default action.
+ action = "call";
+ }
- //console.log("hover handler", event, hover, id);
+ var scope = buddyElement.scope();
+ var session = scope.session;
+ var contact = scope.contact;
+
+ var promise = (function() {
+ var deferred = $q.defer();
+ var sessionData = session.get()
+ if (!sessionData) {
+ // Find session with help of contact.
+ if (contact && contact.Token) {
+ mediaStream.api.sendSessions(contact.Token, "contact", function(event, type, data) {
+ //console.log("oooooooooooooooo", type, data);
+ if (data.Users && data.Users.length > 0) {
+ var s = data.Users[0];
+ buddyData.set(s.Id, scope);
+ deferred.resolve(s.Id);
+ }
+ });
+ }
+ } else {
+ deferred.resolve(sessionData.Id);
+ }
+ return deferred.promise;
+ })();
+
+ switch (action) {
+ case "call":
+ promise.then(function(id) {
+ scope.doCall(id);
+ });
+ break;
+ case "chat":
+ promise.then(function(id) {
+ scope.doChat(id);
+ });
+ break;
+ case "contact":
+ if (contact) {
+ scope.doContactRemove(contact.Userid);
+ } else {
+ promise.then(function(id) {
+ scope.doContactRequest(id);
+ });
+ }
+ break;
+ }
+
+ };
+
+ Buddylist.prototype.hover = function(buddyElement, hover) {
+
+ //console.log("hover handler", buddyElement, hover);
+ var scope = buddyElement.scope();
+ var id = scope.session.Id;
var buddy = $(buddyElement);
var actionElements = this.actionElements;
var elem;
@@ -453,11 +758,10 @@ define(['underscore', 'modernizr', 'avltree', 'text!partials/buddy.html', 'text!
if (elem) {
buddy.addClass("hovered");
} else {
- var scope = buddyData.get(id);
var template = buddyActions;
- if (scope.status.isMixer) {
- template = buddyActionsForAudioMixer;
- }
+ //if (scope.status.autoCalls && _.indexOf(scope.status.autoCalls, "conference") !== -1) {
+ // template = buddyActionsForAudioMixer;
+ //}
//console.log("scope", scope, id);
template(scope, _.bind(function(clonedElement, $scope) {
actionElements[id] = clonedElement;
diff --git a/static/js/services/buddysession.js b/static/js/services/buddysession.js
new file mode 100644
index 00000000..d45fcd86
--- /dev/null
+++ b/static/js/services/buddysession.js
@@ -0,0 +1,191 @@
+/*
+ * 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
.
+ *
+ */
+define(["underscore"], function(_) {
+
+ // buddySession
+ return [function() {
+
+ var sessions = {};
+ var serials = 0;
+
+ var BuddySession = function(data) {
+ this.serial = serials++;
+ this.sessions = {};
+ this.count = 0;
+ //console.log("creating session", this.serial, data.Id, data.Userid, this);
+ var id = data.Id;
+ if (data.Id) {
+ var userid = data.Userid || null;
+ if (id === userid) {
+ // Add as default with userid.
+ this.use(userid, data);
+ } else {
+ // Add as session.
+ var sessionData = this.add(id, data);
+ if (userid) {
+ this.auth(userid, sessionData);
+ }
+ }
+ } else {
+ this.use(null, {});
+ }
+ };
+
+ BuddySession.prototype.add = function(id, data) {
+ this.sessions[id] = data;
+ if (this.count === 0) {
+ this.use(id, data);
+ }
+ this.count++;
+ sessions[id] = this;
+ return data;
+ };
+
+ BuddySession.prototype.rm = function(id) {
+ delete this.sessions[id];
+ this.count--;
+ delete sessions[id];
+ };
+
+ BuddySession.prototype.get = function(id) {
+ if (!id) {
+ id = this.Id;
+ }
+ return this.sessions[id];
+ };
+
+ BuddySession.prototype.use = function(id, data) {
+ if (id) {
+ this.Id = id;
+ } else {
+ this.Id = null;
+ }
+ //console.log("Use session as default", id, data, this);
+ };
+
+ BuddySession.prototype.remove = function(id, onEmptyCallback) {
+
+ this.rm(id);
+ if (id === this.Id) {
+ var sessions = this.sessions;
+ var sessionData;
+ for (var sd in sessions) {
+ if (sessions.hasOwnProperty(sd)) {
+ sessionData = sessions[sd];
+ break;
+ }
+ }
+ if (sessionData) {
+ this.use(sessionData.Id, sessionData);
+ } else {
+ //console.log("Last session removed", sessions);
+ if (this.Userid) {
+ //console.log("Using userid as session id");
+ this.use(this.Userid);
+ } else {
+ this.use(null);
+ }
+ return true;
+ }
+ }
+ return false;
+
+ };
+
+ BuddySession.prototype.update = function(id, data, onUseridCallback) {
+
+ var userid = data.Userid;
+ //console.log("session update", id, userid, this, data);
+
+ var sessionData
+ if (id === userid) {
+ // Fake updates from userid ids.
+ sessionData = data;
+ } else {
+ sessionData = this.get(id);
+ if (!sessionData) {
+ sessionData = this.add(id, data);
+ }
+ }
+ if (userid) {
+ this.auth(userid, sessionData, onUseridCallback);
+ }
+ if (data !== sessionData) {
+ if (data.Rev) {
+ sessionData.Rev = data.Rev;
+ }
+ if (data.Status) {
+ sessionData.Status = data.Status;
+ }
+ }
+
+ if (id === this.Id) {
+ return sessionData;
+ } else {
+ return null;
+ }
+
+ };
+
+ BuddySession.prototype.auth = function(userid, sessionData, onUseridCallback) {
+
+ if (!this.Userid) {
+ this.Userid = userid;
+ //console.log("Session now has a user id", this.Id, userid);
+ }
+ // Trigger callback if defined and not triggered before.
+ if (onUseridCallback && !sessionData.auth) {
+ onUseridCallback(this);
+ sessionData.auth = true;
+ }
+
+ };
+
+ BuddySession.prototype.merge = function(otherSession) {
+ if (!this.Userid) {
+ console.error("Refusing to merge into session as we have no userid", this, otherSession);
+ return;
+ }
+ if (otherSession.Userid !== this.Userid) {
+ console.error("Refusing to merge other session with different userid", otherSession, this);
+ return;
+ }
+ _.each(otherSession.sessions, _.bind(function(s, id) {
+ if (this.sessions.hasOwnProperty(id)) {
+ return;
+ }
+ this.add(id, s);
+ }, this));
+ console.log("Merged sessions", this, otherSession);
+ };
+
+ return {
+ create: function(data) {
+ return new BuddySession(data);
+ },
+ sessions: function() {
+ return sessions;
+ }
+ };
+
+ }];
+
+});
diff --git a/static/js/services/contactdata.js b/static/js/services/contactdata.js
new file mode 100644
index 00000000..20cf81e9
--- /dev/null
+++ b/static/js/services/contactdata.js
@@ -0,0 +1,103 @@
+/*
+ * 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
.
+ *
+ */
+define(['underscore', 'jquery'], function(underscore, $) {
+
+ // contactData
+ return [function() {
+
+ var contacts = {};
+ var users = {};
+ var count = 0;
+
+ var contactData = {
+ clear: function(cb) {
+ _.each(users, _.bind(function(idx, userid) {
+ var contact = contacts[idx];
+ if (cb && contact) {
+ cb(contact);
+ }
+ this.remove(userid);
+ }, this));
+ count = 0;
+ },
+ addByRequest: function(request, status) {
+ //console.log("addByRequest", request, status);
+ var userid = request.Userid;
+ var token = request.Token;
+ var id;
+ if (users.hasOwnProperty(userid)) {
+ // Existing contact. Replace it.
+ id = users[userid];
+ } else {
+ id = String(count++);
+ users[userid] = id;
+ }
+ var contact = contacts[id] = {
+ Id: "contact-"+id,
+ Userid: userid,
+ Token: token,
+ Status: null
+ }
+ return contact;
+ },
+ addByData: function(data) {
+ //console.log("addByData", data.Userid, data);
+ var userid = data.Userid;
+ if (users.hasOwnProperty(userid)) {
+ id = users[userid]
+ } else {
+ id = String(count++);
+ users[userid] = id;
+ }
+ var contact = contacts[id] = data;
+ contact.Id = id;
+ return contact;
+ },
+ get: function(userid) {
+ if (users.hasOwnProperty(userid)) {
+ var id = users[userid];
+ return contacts[id];
+ }
+ return null;
+ },
+ remove: function(userid) {
+ if (users.hasOwnProperty(userid)) {
+ var id = users[userid];
+ delete contacts[id];
+ }
+ delete users[userid];
+ },
+ getById: function(id) {
+ if (id.indexOf("contact-") === 0) {
+ id = id.substr(8)
+ }
+ if (contacts.hasOwnProperty(id)) {
+ return contacts[id];
+ }
+ return null
+ }
+ };
+
+ return contactData;
+
+ }];
+
+});
diff --git a/static/js/services/contacts.js b/static/js/services/contacts.js
new file mode 100644
index 00000000..4f900221
--- /dev/null
+++ b/static/js/services/contacts.js
@@ -0,0 +1,265 @@
+/*
+ * 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
.
+ *
+ */
+define(['underscore', 'jquery', 'modernizr', 'sjcl'], function(underscore, $, Modernizr, sjcl) {
+
+ var Database = function(name) {
+ this.version = 3;
+ this.ready = false;
+ this.db = null;
+ this.name = name;
+ this.e = $({});
+ var request = indexedDB.open(this.name, this.version);
+ var that = this;
+ request.onupgradeneeded = function(event) {
+ var db = event.target.result;
+ var transaction = event.target.transaction;
+ transaction.onerror = _.bind(that.onerror, that);
+ that.init(db);
+ console.log("Created contacts database.")
+ };
+ request.onsuccess = _.bind(that.onsuccess, that);
+ };
+ Database.prototype.init = function(db) {
+ var createOrUpdateStore = function(name, obj) {
+ if (db.objectStoreNames.contains(name)) {
+ // TODO(longsleep): Migrate data.
+ db.deleteObjectStore(name);
+ }
+ db.createObjectStore(name, obj);
+ }
+ // Create our object stores.
+ createOrUpdateStore("contacts", {
+ // We use id field as our unique identifier.
+ keyPath: "id"
+ });
+ };
+ Database.prototype.onerror = function(event) {
+ console.log("IndexDB database error", event);
+ };
+ Database.prototype.onsuccess = function(event) {
+ this.db = event.target.result;
+ this.ready = true;
+ console.log("Openend database", this.db);
+ this.e.triggerHandler("ready");
+ };
+ Database.prototype.put = function(storename, data, successCallback, errorCallback) {
+ var transaction = this.db.transaction(storename, "readwrite");
+ var store = transaction.objectStore(storename);
+ var request = store.put(data);
+ if (!errorCallback) {
+ errorCallback = _.bind(this.onerror, this);
+ }
+ transaction.onerror = request.onerror = errorCallback;
+ if (successCallback) {
+ request.onsuccess = successCallback;
+ }
+ return request;
+ };
+ Database.prototype.delete = function(storename, id, successCallback, errorCallback) {
+ var transaction = this.db.transaction(storename, "readwrite");
+ var store = transaction.objectStore(storename);
+ var request = store.delete(id);
+ if (!errorCallback) {
+ errorCallback = _.bind(this.onerror, this);
+ }
+ transaction.onerror = request.onerror = errorCallback;
+ if (successCallback) {
+ request.onsuccess = successCallback;
+ }
+ return request;
+ };
+ Database.prototype.all = function(storename, iteratorCallback, errorCallback) {
+ var transaction = this.db.transaction(storename);
+ var store = transaction.objectStore(storename);
+ var keyRange = IDBKeyRange.lowerBound(0);
+ var cursorRequest = store.openCursor(keyRange);
+ cursorRequest.onsuccess = function(event) {
+ var result = event.target.result;
+ if (!result) {
+ return;
+ }
+ //console.log("read data idb", result, event);
+ iteratorCallback(result.value);
+ result.continue();
+ };
+ if (!errorCallback) {
+ errorCallback = _.bind(this.onerror, this);
+ }
+ transaction.onerror = cursorRequest.onerror = errorCallback;
+ return cursorRequest;
+ };
+ Database.prototype.close = function() {
+ // TODO(longsleep): Database close.
+ this.e.off();
+ if (this.db) {
+ this.db.close();
+ this.db = null;
+ this.ready = false;
+ }
+ _.defer(_.bind(function() {
+ this.e.triggerHandler("closed");
+ }, this));
+ };
+
+ // contacts
+ return ["appData", "contactData", "mediaStream", function(appData, contactData, mediaStream) {
+
+ var Contacts = function() {
+
+ this.e = $({});
+ this.userid = null;
+ this.key = null;
+ this.database = null;
+
+ appData.e.on("authenticationChanged", _.bind(function(event, userid, suserid) {
+ // TODO(longsleep): Avoid creating empty databases. Create db on store only.
+ var database = this.open(userid, suserid);
+ if (database && userid) {
+ // Load when database is ready and userid is available.
+ if (database.ready) {
+ _.defer(_.bind(function() {
+ if (this.database === database) {
+ this.load();
+ }
+ }, this));
+ } else {
+ database.e.one("ready", _.bind(function() {
+ if (this.database === database) {
+ this.load();
+ }
+ }, this));
+ }
+ }
+ }, this));
+
+ };
+
+ Contacts.prototype.open = function(userid, suserid) {
+
+ if (this.database && (!userid || this.userid !== userid)) {
+ // Unload existing contacts.
+ this.unload();
+ // Close existing database.
+ this.database.close();
+ this.database = null;
+ }
+ if (userid) {
+ if (!Modernizr.indexeddb) {
+ return;
+ }
+ // Create secure key for hashing and encryption.
+ this.key = sjcl.codec.base64.fromBits(sjcl.hash.sha256.hash(suserid+mediaStream.config.Token));
+ // Create database name for user which.
+ var id = "mediastream-" + this.id(userid);
+ console.log("Open of database:", id);
+ var database = this.database = new Database(id);
+ return database;
+ } else {
+ this.database = null;
+ return null;
+ }
+
+ };
+
+ Contacts.prototype.id = function(userid) {
+
+ var hmac = new sjcl.misc.hmac(this.key);
+ return sjcl.codec.base64.fromBits(hmac.encrypt(userid));
+
+ };
+
+ Contacts.prototype.encrypt = function(data) {
+
+ return sjcl.encrypt(this.key, JSON.stringify(data));
+
+ };
+
+ Contacts.prototype.decrypt = function(data) {
+
+ var result;
+ try {
+ var s = sjcl.decrypt(this.key, data);
+ result = JSON.parse(s);
+ } catch(err) {
+ console.error("Failed to decrypt contact data", err);
+ }
+ return result;
+
+ };
+
+ Contacts.prototype.load = function() {
+ if (this.database) {
+ console.log("Load contacts from storage", this);
+ var remove = [];
+ this.database.all("contacts", _.bind(function(data) {
+ var d = this.decrypt(data.contact);
+ if (d) {
+ var contact = contactData.addByData(d);
+ // TODO(longsleep): Convert buddyImage string to Blob.
+ this.e.triggerHandler("contactadded", d);
+ } else {
+ // Remove empty or invalid entries automatically.
+ remove.push(data.id);
+ }
+ }, this));
+ // Remove marked entries.
+ if (remove.length) {
+ _.each(remove, _.bind(function(id) {
+ this.database.delete("contacts", id);
+ }, this));
+ }
+ }
+ };
+
+ Contacts.prototype.unload = function() {
+ contactData.clear(_.bind(function(contact) {
+ this.e.triggerHandler("contactremoved", contact);
+ }, this));
+ };
+
+ Contacts.prototype.add = function(request, status) {
+ var contact = contactData.addByRequest(request, status);
+ this.e.triggerHandler("contactadded", contact);
+ if (this.database) {
+ this.database.put("contacts", {
+ id: this.id(contact.Userid),
+ contact: this.encrypt(contact)
+ })
+ }
+ };
+
+ Contacts.prototype.remove = function(userid) {
+ var contact = contactData.get(userid);
+ console.log("contacts remove", userid, contact);
+ if (contact) {
+ contactData.remove(userid);
+ if (this.database) {
+ this.database.delete("contacts", this.id(userid));
+ }
+ this.e.triggerHandler("contactremoved", contact);
+ }
+ };
+
+ return new Contacts();
+
+ }];
+
+});
diff --git a/static/js/services/localstorage.js b/static/js/services/localstorage.js
new file mode 100644
index 00000000..dbf2332f
--- /dev/null
+++ b/static/js/services/localstorage.js
@@ -0,0 +1,73 @@
+/*
+ * 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
.
+ *
+ */
+define(["modernizr"], function(Modernizr) {
+
+ // localStorage
+ return ["$window", function($window) {
+
+ // PersistentStorage (c)2014 struktur AG. MIT license.
+ var PersistentStorage = function(prefix) {
+ this.prefix = prefix ? prefix : "ps";
+ this.isPersistentStorage = true;
+ };
+ PersistentStorage.prototype.setItem = function(key, data) {
+ var name = this.prefix+"_"+key;
+ $window.document.cookie = name + "=" + data + "; path=/";
+ };
+ PersistentStorage.prototype.getItem = function(key) {
+ var name = this.prefix+"_"+key+"=";
+ var ca = $window.document.cookie.split(';');
+ for (var i=0; i
-
+
+
{{session.Id|displayName}}
-
{{session.Ua}}
+
({{session.count}}) {{display.subline}}
diff --git a/static/partials/buddyactions.html b/static/partials/buddyactions.html
index 6fdb45e8..c3e7d74b 100644
--- a/static/partials/buddyactions.html
+++ b/static/partials/buddyactions.html
@@ -1,4 +1,11 @@
-
-
-
+
diff --git a/static/partials/buddylist.html b/static/partials/buddylist.html
index 6f3b436c..ee1c088a 100644
--- a/static/partials/buddylist.html
+++ b/static/partials/buddylist.html
@@ -1,4 +1,4 @@
-