diff --git a/README.FRONTEND b/README.FRONTEND index 81290ab..0af679f 100644 --- a/README.FRONTEND +++ b/README.FRONTEND @@ -47,10 +47,12 @@ from the server; the field `kind` indicates the kind of message. Once you have joined a group (see below), the remaining callbacks may trigger. The `onuser` callback is used to indicate that a user has joined -or left the current group. The `onchat` callback indicates that a chat -message has been posted to the group, and `onclearchat` indicates that the -chat history has been cleared. Finally, `ondownstream` is called when the -server pushes a stream to the client; see the section below about streams. +or left the current group, or that their attributes have changed; the +user's state can be found in the `users` dictionary. The `onchat` +callback indicates that a chat message has been posted to the group, and +`onclearchat` indicates that the chat history has been cleared. Finally, +`ondownstream` is called when the server pushes a stream to the client; +see the section below about streams. You may now connect to the server. diff --git a/README.PROTOCOL b/README.PROTOCOL index 4d6dfd9..7d40fab 100644 --- a/README.PROTOCOL +++ b/README.PROTOCOL @@ -115,9 +115,10 @@ users a `user` message: ```javascript { type: 'user', - kind: 'add' or 'delete', + kind: 'add' or 'change' or 'delete', id: id, - username: username + username: username, + permissions: permissions } ``` diff --git a/diskwriter/diskwriter.go b/diskwriter/diskwriter.go index 6b9dacf..cdd66e9 100644 --- a/diskwriter/diskwriter.go +++ b/diskwriter/diskwriter.go @@ -71,7 +71,7 @@ func (client *Client) Permissions() group.ClientPermissions { return group.ClientPermissions{} } -func (client *Client) PushClient(id, username string, add bool) error { +func (client *Client) PushClient(id, username string, permissions group.ClientPermissions, kind string) error { return nil } diff --git a/group/client.go b/group/client.go index 5e58d2f..0fc8714 100644 --- a/group/client.go +++ b/group/client.go @@ -100,6 +100,6 @@ type Client interface { SetPermissions(ClientPermissions) OverridePermissions(*Group) bool PushConn(g *Group, id string, conn conn.Up, tracks []conn.UpTrack, replace string) error - PushClient(id, username string, add bool) error + PushClient(id, username string, permissions ClientPermissions, kind string) error Kick(id, user, message string) error } diff --git a/group/group.go b/group/group.go index 959caa2..ed3abbb 100644 --- a/group/group.go +++ b/group/group.go @@ -485,11 +485,13 @@ func AddClient(group string, c Client) (*Group, error) { g.clients[c.Id()] = c g.timestamp = time.Now() + id := c.Id() u := c.Username() - c.PushClient(c.Id(), u, true) + p := c.Permissions() + c.PushClient(c.Id(), u, p, "add") for _, cc := range clients { - c.PushClient(cc.Id(), cc.Username(), true) - cc.PushClient(c.Id(), u, true) + c.PushClient(cc.Id(), cc.Username(), cc.Permissions(), "add") + cc.PushClient(id, u, p, "add") } return g, nil @@ -535,7 +537,7 @@ func DelClient(c Client) { go func(clients []Client) { for _, cc := range clients { - cc.PushClient(c.Id(), c.Username(), false) + cc.PushClient(c.Id(), c.Username(), c.Permissions(), "delete") } }(clients) diff --git a/rtpconn/webclient.go b/rtpconn/webclient.go index a1a51dd..3f732f6 100644 --- a/rtpconn/webclient.go +++ b/rtpconn/webclient.go @@ -106,16 +106,13 @@ func (c *webClient) OverridePermissions(g *group.Group) bool { return false } -func (c *webClient) PushClient(id, username string, add bool) error { - kind := "add" - if !add { - kind = "delete" - } +func (c *webClient) PushClient(id, username string, permissions group.ClientPermissions, kind string) error { return c.write(clientMessage{ - Type: "user", - Kind: kind, - Id: id, - Username: username, + Type: "user", + Kind: kind, + Id: id, + Username: username, + Permissions: &permissions, }) } @@ -989,6 +986,14 @@ func handleAction(c *webClient, a interface{}) error { } } } + id := c.Id() + user := c.Username() + clients := g.GetClients(nil) + go func(clients []group.Client) { + for _, cc := range clients { + cc.PushClient(id, user, perms, "change") + } + }(clients) case kickAction: return group.KickError{ a.id, a.username, a.message, diff --git a/static/galene.js b/static/galene.js index 663f3e1..065eb60 100644 --- a/static/galene.js +++ b/static/galene.js @@ -293,13 +293,11 @@ function setConnected(connected) { let userbox = document.getElementById('profile'); let connectionbox = document.getElementById('login-container'); if(connected) { - resetUsers(); clearChat(); userbox.classList.remove('invisible'); connectionbox.classList.add('invisible'); displayUsername(); } else { - resetUsers(); fillLogin(); userbox.classList.add('invisible'); connectionbox.classList.remove('invisible'); @@ -1654,9 +1652,6 @@ function resizePeers() { } } -/** @type{Object} */ -let users = {}; - /** * Lexicographic order, with case differences secondary. * @param{string} a @@ -1683,9 +1678,6 @@ function stringCompare(a, b) { function addUser(id, name) { if(!name) name = null; - if(id in users) - throw new Error('Duplicate user id'); - users[id] = name; let div = document.getElementById('users'); let user = document.createElement('div'); @@ -1697,7 +1689,9 @@ function addUser(id, name) { let us = div.children; for(let i = 0; i < us.length; i++) { let child = us[i]; - let childname = users[child.id.slice('user-'.length)] || null; + let childuser = + serverConnection.users[child.id.slice('user-'.length)] || null; + let childname = (childuser && childuser.username) || null; if(!childname || stringCompare(childname, name) > 0) { div.insertBefore(user, child); return; @@ -1711,36 +1705,38 @@ function addUser(id, name) { * @param {string} id * @param {string} name */ -function delUser(id, name) { - if(!name) - name = null; - if(!(id in users)) - throw new Error('Unknown user id'); - if(users[id] !== name) - throw new Error('Inconsistent user name'); - delete(users[id]); +function changeUser(id, name) { + let user = document.getElementById('user-' + id); + if(!user) { + console.warn('Unknown user ' + id); + return; + } + user.textContent = name ? name : '(anon)'; +} + +/** + * @param {string} id + */ +function delUser(id) { let div = document.getElementById('users'); let user = document.getElementById('user-' + id); div.removeChild(user); } -function resetUsers() { - for(let id in users) - delUser(id, users[id]); -} - /** * @param {string} id * @param {string} kind - * @param {string} name */ -function gotUser(id, kind, name) { +function gotUser(id, kind) { switch(kind) { case 'add': - addUser(id, name); + addUser(id, serverConnection.users[id].username); break; case 'delete': - delUser(id, name); + delUser(id); + break; + case 'change': + changeUser(id, serverConnection.users[id].username); break; default: console.warn('Unknown user kind', kind); @@ -1986,8 +1982,10 @@ function addToChatbox(peerId, dest, nick, time, privileged, kind, message) { let header = document.createElement('p'); if(peerId || nick || dest) { let user = document.createElement('span'); + let u = serverConnection.users[dest]; + let name = (u && u.username); user.textContent = dest ? - `${nick||'(anon)'} \u2192 ${users[dest]||'(anon)'}` : + `${nick||'(anon)'} \u2192 ${name || '(anon)'}` : (nick || '(anon)'); user.classList.add('message-user'); header.appendChild(user); @@ -2246,11 +2244,12 @@ function parseCommand(line) { * @param {string} user */ function findUserId(user) { - if(user in users) + if(user in serverConnection.users) return user; - for(let id in users) { - if(users[id] === user) + for(let id in serverConnection.users) { + let u = serverConnection.users[id]; + if(u && u.username === user) return id; } return null; diff --git a/static/protocol.js b/static/protocol.js index a80659e..80f5f73 100644 --- a/static/protocol.js +++ b/static/protocol.js @@ -60,6 +60,12 @@ function newLocalId() { return id; } +/** + * @typedef {Object} user + * @property {string} username + * @property {Object} permissions + */ + /** * ServerConnection encapsulates a websocket connection to the server and * all the associated streams. @@ -81,8 +87,16 @@ function ServerConnection() { this.group = null; /** * The username we joined as. + * + * @type {string} */ this.username = null; + /** + * The set of users in this group, including ourself. + * + * @type {Object} + */ + this.users = {}; /** * The underlying websocket. * @@ -136,9 +150,10 @@ function ServerConnection() { */ this.onclose = null; /** - * onuser is called whenever a user is added or removed from the group + * onuser is called whenever a user in the group changes. The users + * array has already been updated. * - * @type{(this: ServerConnection, id: string, kind: string, username: string) => void} + * @type{(this: ServerConnection, id: string, kind: string) => void} */ this.onuser = null; /** @@ -260,6 +275,11 @@ ServerConnection.prototype.connect = async function(url) { let c = sc.down[id]; c.close(); } + for(let id in sc.users) { + delete(sc.users[id]); + if(sc.onuser) + sc.onuser.call(sc, id, 'delete'); + } if(sc.group && sc.onjoined) sc.onjoined.call(sc, 'leave', sc.group, {}, ''); sc.group = null; @@ -303,14 +323,51 @@ ServerConnection.prototype.connect = async function(url) { sc.username = m.username; sc.permissions = m.permissions || []; sc.rtcConfiguration = m.rtcConfiguration || null; + if(m.kind == 'leave') { + for(let id in sc.users) { + delete(sc.users[id]); + if(sc.onuser) + sc.onuser.call(sc, id, 'delete'); + } + } if(sc.onjoined) sc.onjoined.call(sc, m.kind, m.group, m.permissions || {}, m.value || null); break; case 'user': + switch(m.kind) { + case 'add': + if(m.id in sc.users) + console.warn(`Duplicate user ${m.id} ${m.username}`); + sc.users[m.id] = { + username: m.username, + permissions: m.permissions, + }; + break; + case 'change': + if(!(m.id in sc.users)) { + console.warn(`Unknown user ${m.id} ${m.username}`); + sc.users[m.id] = { + username: m.username, + permissions: m.permissions, + }; + } else { + sc.users[m.id].username = m.username; + sc.users[m.id].permissions = m.permissions; + } + break; + case 'delete': + if(!(m.id in sc.users)) + console.warn(`Unknown user ${m.id} ${m.username}`); + delete(sc.users[m.id]); + break; + default: + console.warn(`Unknown user action ${m.kind}`); + return; + } if(sc.onuser) - sc.onuser.call(sc, m.id, m.kind, m.username); + sc.onuser.call(sc, m.id, m.kind); break; case 'chat': if(sc.onchat)