Commit 52a26327 authored by Juliusz Chroboczek's avatar Juliusz Chroboczek

Implement group status.

We now inform clients of the status of a group (locked, etc.).  Also
cleans up the handling of administrative messages, which solves the
issue of receiving "user" before "joined".
parent d78a750b
...@@ -207,6 +207,7 @@ following fields are allowed: ...@@ -207,6 +207,7 @@ following fields are allowed:
respectively with operator privileges, with presenter privileges, and respectively with operator privileges, with presenter privileges, and
as passive listeners; as passive listeners;
- `public`: if true, then the group is visible on the landing page; - `public`: if true, then the group is visible on the landing page;
- `displayName`: a human-friendly version of the group name;
- `description`: a human-readable description of the group; this is - `description`: a human-readable description of the group; this is
displayed on the landing page for public groups; displayed on the landing page for public groups;
- `contact`: a human-readable contact for this group, such as an e-mail - `contact`: a human-readable contact for this group, such as an e-mail
......
...@@ -100,12 +100,15 @@ its permissions or in the recommended RTC configuration. ...@@ -100,12 +100,15 @@ its permissions or in the recommended RTC configuration.
group: group, group: group,
username: username, username: username,
permissions: permissions, permissions: permissions,
status: status,
rtcConfiguration: RTCConfiguration rtcConfiguration: RTCConfiguration
} }
``` ```
The `permissions` field is an array of strings that may contain the values The `permissions` field is an array of strings that may contain the values
`present`, `op` and `record`. `present`, `op` and `record`. The `status` field is a dictionary that
contains random information that can be usefully displayed in the user
interface.
## Maintaining group membership ## Maintaining group membership
......
...@@ -77,7 +77,7 @@ func (client *Client) Status() map[string]interface{} { ...@@ -77,7 +77,7 @@ func (client *Client) Status() map[string]interface{} {
return nil return nil
} }
func (client *Client) PushClient(id, username string, permissions *group.ClientPermissions, status map[string]interface{}, kind string) error { func (client *Client) PushClient(group, kind, id, username string, permissions group.ClientPermissions, status map[string]interface{}) error {
return nil return nil
} }
...@@ -103,6 +103,10 @@ func (client *Client) Kick(id, user, message string) error { ...@@ -103,6 +103,10 @@ func (client *Client) Kick(id, user, message string) error {
return err return err
} }
func (client *Client) Joined(group, kind string) error {
return nil
}
func (client *Client) PushConn(g *group.Group, id string, up conn.Up, tracks []conn.UpTrack, replace string) error { func (client *Client) PushConn(g *group.Group, id string, up conn.Up, tracks []conn.UpTrack, replace string) error {
if client.group != g { if client.group != g {
return nil return nil
......
...@@ -102,6 +102,7 @@ type Client interface { ...@@ -102,6 +102,7 @@ type Client interface {
OverridePermissions(*Group) bool OverridePermissions(*Group) bool
PushConn(g *Group, id string, conn conn.Up, tracks []conn.UpTrack, replace string) error PushConn(g *Group, id string, conn conn.Up, tracks []conn.UpTrack, replace string) error
RequestConns(target Client, g *Group, id string) error RequestConns(target Client, g *Group, id string) error
PushClient(id, username string, permissions *ClientPermissions, status map[string]interface{}, kind string) error Joined(group, kind string) error
PushClient(group, kind, id, username string, permissions ClientPermissions, status map[string]interface{}) error
Kick(id, user, message string) error Kick(id, user, message string) error
} }
...@@ -92,12 +92,17 @@ func (g *Group) Locked() (bool, string) { ...@@ -92,12 +92,17 @@ func (g *Group) Locked() (bool, string) {
func (g *Group) SetLocked(locked bool, message string) { func (g *Group) SetLocked(locked bool, message string) {
g.mu.Lock() g.mu.Lock()
defer g.mu.Unlock()
if locked { if locked {
g.locked = &message g.locked = &message
} else { } else {
g.locked = nil g.locked = nil
} }
clients := g.getClientsUnlocked(nil)
g.mu.Unlock()
for _, c := range clients {
c.Joined(g.Name(), "change")
}
} }
func (g *Group) Public() bool { func (g *Group) Public() bool {
...@@ -118,6 +123,12 @@ func (g *Group) AllowRecording() bool { ...@@ -118,6 +123,12 @@ func (g *Group) AllowRecording() bool {
return g.description.AllowRecording return g.description.AllowRecording
} }
func (g *Group) DisplayName() string {
g.mu.Lock()
defer g.mu.Unlock()
return g.description.DisplayName
}
var groups struct { var groups struct {
mu sync.Mutex mu sync.Mutex
groups map[string]*Group groups map[string]*Group
...@@ -293,8 +304,16 @@ func APIFromNames(names []string) (*webrtc.API, error) { ...@@ -293,8 +304,16 @@ func APIFromNames(names []string) (*webrtc.API, error) {
} }
func Add(name string, desc *Description) (*Group, error) { func Add(name string, desc *Description) (*Group, error) {
g, notify, err := add(name, desc)
for _, c := range notify {
c.Joined(g.Name(), "change")
}
return g, err
}
func add(name string, desc *Description) (*Group, []Client, error) {
if name == "" || strings.HasSuffix(name, "/") { if name == "" || strings.HasSuffix(name, "/") {
return nil, UserError("illegal group name") return nil, nil, UserError("illegal group name")
} }
groups.mu.Lock() groups.mu.Lock()
...@@ -311,7 +330,7 @@ func Add(name string, desc *Description) (*Group, error) { ...@@ -311,7 +330,7 @@ func Add(name string, desc *Description) (*Group, error) {
if desc == nil { if desc == nil {
desc, err = GetDescription(name) desc, err = GetDescription(name)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
} }
...@@ -321,9 +340,10 @@ func Add(name string, desc *Description) (*Group, error) { ...@@ -321,9 +340,10 @@ func Add(name string, desc *Description) (*Group, error) {
clients: make(map[string]Client), clients: make(map[string]Client),
timestamp: time.Now(), timestamp: time.Now(),
} }
autoLockKick(g, g.getClientsUnlocked(nil)) clients := g.getClientsUnlocked(nil)
autoLockKick(g, clients)
groups.groups[name] = g groups.groups[name] = g
return g, nil return g, clients, nil
} }
g.mu.Lock() g.mu.Lock()
...@@ -332,7 +352,7 @@ func Add(name string, desc *Description) (*Group, error) { ...@@ -332,7 +352,7 @@ func Add(name string, desc *Description) (*Group, error) {
if desc != nil { if desc != nil {
g.description = desc g.description = desc
} else if !descriptionChanged(name, g.description) { } else if !descriptionChanged(name, g.description) {
return g, nil return g, nil, nil
} }
desc, err = GetDescription(name) desc, err = GetDescription(name)
...@@ -341,12 +361,13 @@ func Add(name string, desc *Description) (*Group, error) { ...@@ -341,12 +361,13 @@ func Add(name string, desc *Description) (*Group, error) {
log.Printf("Reading group %v: %v", name, err) log.Printf("Reading group %v: %v", name, err)
} }
deleteUnlocked(g) deleteUnlocked(g)
return nil, err return nil, nil, err
} }
g.description = desc g.description = desc
autoLockKick(g, g.getClientsUnlocked(nil)) clients := g.getClientsUnlocked(nil)
autoLockKick(g, clients)
return g, nil return g, clients, nil
} }
func Range(f func(g *Group) bool) { func Range(f func(g *Group) bool) {
...@@ -511,15 +532,19 @@ func AddClient(group string, c Client) (*Group, error) { ...@@ -511,15 +532,19 @@ func AddClient(group string, c Client) (*Group, error) {
g.clients[c.Id()] = c g.clients[c.Id()] = c
g.timestamp = time.Now() g.timestamp = time.Now()
c.Joined(g.Name(), "join")
id := c.Id() id := c.Id()
u := c.Username() u := c.Username()
p := c.Permissions() p := c.Permissions()
s := c.Status() s := c.Status()
c.PushClient(c.Id(), u, &p, s, "add") c.PushClient(g.Name(), "add", c.Id(), u, p, s)
for _, cc := range clients { for _, cc := range clients {
pp := cc.Permissions() pp := cc.Permissions()
c.PushClient(cc.Id(), cc.Username(), &pp, cc.Status(), "add") c.PushClient(
cc.PushClient(id, u, &p, s, "add") g.Name(), "add", cc.Id(), cc.Username(), pp, cc.Status(),
)
cc.PushClient(g.Name(), "add", id, u, p, s)
} }
return g, nil return g, nil
...@@ -539,6 +564,11 @@ func autoLockKick(g *Group, clients []Client) { ...@@ -539,6 +564,11 @@ func autoLockKick(g *Group, clients []Client) {
if g.description.Autolock && g.locked == nil { if g.description.Autolock && g.locked == nil {
m := "this group is locked" m := "this group is locked"
g.locked = &m g.locked = &m
go func(clients []Client) {
for _, c := range clients {
c.Joined(g.Name(), "change")
}
}(g.getClientsUnlocked(nil))
} }
if g.description.Autokick { if g.description.Autokick {
...@@ -552,23 +582,22 @@ func DelClient(c Client) { ...@@ -552,23 +582,22 @@ func DelClient(c Client) {
return return
} }
g.mu.Lock() g.mu.Lock()
defer g.mu.Unlock()
if g.clients[c.Id()] != c { if g.clients[c.Id()] != c {
log.Printf("Deleting unknown client") log.Printf("Deleting unknown client")
g.mu.Unlock()
return return
} }
delete(g.clients, c.Id()) delete(g.clients, c.Id())
g.timestamp = time.Now() g.timestamp = time.Now()
clients := g.getClientsUnlocked(nil) clients := g.getClientsUnlocked(nil)
g.mu.Unlock()
go func(clients []Client) { c.Joined(g.Name(), "leave")
for _, cc := range clients { for _, cc := range clients {
cc.PushClient(c.Id(), "", nil, nil, "delete") cc.PushClient(
} g.Name(), "delete", c.Id(), "", ClientPermissions{}, nil,
}(clients) )
}
autoLockKick(g, clients) autoLockKick(g, clients)
} }
...@@ -740,6 +769,9 @@ type Description struct { ...@@ -740,6 +769,9 @@ type Description struct {
modTime time.Time `json:"-"` modTime time.Time `json:"-"`
fileSize int64 `json:"-"` fileSize int64 `json:"-"`
// The user-friendly group name
DisplayName string `json:"displayName,omitempty"`
// A user-readable description of the group. // A user-readable description of the group.
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
......
...@@ -111,14 +111,9 @@ func (c *webClient) OverridePermissions(g *group.Group) bool { ...@@ -111,14 +111,9 @@ func (c *webClient) OverridePermissions(g *group.Group) bool {
return false return false
} }
func (c *webClient) PushClient(id, username string, permissions *group.ClientPermissions, status map[string]interface{}, kind string) error { func (c *webClient) PushClient(group, kind, id, username string, permissions group.ClientPermissions, status map[string]interface{}) error {
return c.write(clientMessage{ return c.action(pushClientAction{
Type: "user", group, kind, id, username, permissions, status,
Kind: kind,
Id: id,
Username: username,
Permissions: permissions,
Status: status,
}) })
} }
...@@ -804,6 +799,17 @@ func (c *webClient) PushConn(g *group.Group, id string, up conn.Up, tracks []con ...@@ -804,6 +799,17 @@ func (c *webClient) PushConn(g *group.Group, id string, up conn.Up, tracks []con
return nil return nil
} }
func getGroupStatus(g *group.Group) map[string]interface{} {
status := make(map[string]interface{})
if locked, _ := g.Locked(); locked {
status["locked"] = true
}
if dn := g.DisplayName(); dn != "" {
status["displayName"] = dn
}
return status
}
func readMessage(conn *websocket.Conn, m *clientMessage) error { func readMessage(conn *websocket.Conn, m *clientMessage) error {
err := conn.SetReadDeadline(time.Now().Add(15 * time.Second)) err := conn.SetReadDeadline(time.Now().Add(15 * time.Second))
if err != nil { if err != nil {
...@@ -880,8 +886,22 @@ type connectionFailedAction struct { ...@@ -880,8 +886,22 @@ type connectionFailedAction struct {
id string id string
} }
type pushClientAction struct {
group string
kind string
id string
username string
permissions group.ClientPermissions
status map[string]interface{}
}
type permissionsChangedAction struct{} type permissionsChangedAction struct{}
type joinedAction struct {
group string
kind string
}
type kickAction struct { type kickAction struct {
id string id string
username string username string
...@@ -1067,6 +1087,37 @@ func handleAction(c *webClient, a interface{}) error { ...@@ -1067,6 +1087,37 @@ func handleAction(c *webClient, a interface{}) error {
"unknown connection") "unknown connection")
} }
case pushClientAction:
if a.group != c.group.Name() {
log.Printf("got client for wrong group")
return nil
}
return c.write(clientMessage{
Type: "user",
Kind: a.kind,
Id: a.id,
Username: a.username,
Permissions: &a.permissions,
Status: a.status,
})
case joinedAction:
var status map[string]interface{}
if a.group != "" {
g := group.Get(a.group)
if g != nil {
status = getGroupStatus(g)
}
}
perms := c.permissions
return c.write(clientMessage{
Type: "joined",
Kind: a.kind,
Group: a.group,
Username: c.username,
Permissions: &perms,
Status: status,
RTCConfiguration: ice.ICEConfiguration(),
})
case permissionsChangedAction: case permissionsChangedAction:
g := c.Group() g := c.Group()
if g == nil { if g == nil {
...@@ -1079,6 +1130,7 @@ func handleAction(c *webClient, a interface{}) error { ...@@ -1079,6 +1130,7 @@ func handleAction(c *webClient, a interface{}) error {
Group: g.Name(), Group: g.Name(),
Username: c.username, Username: c.username,
Permissions: &perms, Permissions: &perms,
Status: getGroupStatus(g),
RTCConfiguration: ice.ICEConfiguration(), RTCConfiguration: ice.ICEConfiguration(),
}) })
if !c.permissions.Present { if !c.permissions.Present {
...@@ -1101,7 +1153,9 @@ func handleAction(c *webClient, a interface{}) error { ...@@ -1101,7 +1153,9 @@ func handleAction(c *webClient, a interface{}) error {
clients := g.GetClients(nil) clients := g.GetClients(nil)
go func(clients []group.Client) { go func(clients []group.Client) {
for _, cc := range clients { for _, cc := range clients {
cc.PushClient(id, user, &perms, s, "change") cc.PushClient(
g.Name(), "change", id, user, perms, s,
)
} }
}(clients) }(clients)
case kickAction: case kickAction:
...@@ -1212,6 +1266,10 @@ func (c *webClient) Kick(id, user, message string) error { ...@@ -1212,6 +1266,10 @@ func (c *webClient) Kick(id, user, message string) error {
return c.action(kickAction{id, user, message}) return c.action(kickAction{id, user, message})
} }
func (c *webClient) Joined(group, kind string) error {
return c.action(joinedAction{group, kind})
}
func kickClient(g *group.Group, id, user, dest string, message string) error { func kickClient(g *group.Group, id, user, dest string, message string) error {
client := g.GetClient(dest) client := g.GetClient(dest)
if client == nil { if client == nil {
...@@ -1243,14 +1301,6 @@ func handleClientMessage(c *webClient, m clientMessage) error { ...@@ -1243,14 +1301,6 @@ func handleClientMessage(c *webClient, m clientMessage) error {
return group.ProtocolError("you are not joined") return group.ProtocolError("you are not joined")
} }
leaveGroup(c) leaveGroup(c)
perms := c.permissions
return c.write(clientMessage{
Type: "joined",
Kind: "leave",
Group: m.Group,
Username: c.username,
Permissions: &perms,
})
} }
if m.Kind != "join" { if m.Kind != "join" {
...@@ -1298,18 +1348,6 @@ func handleClientMessage(c *webClient, m clientMessage) error { ...@@ -1298,18 +1348,6 @@ func handleClientMessage(c *webClient, m clientMessage) error {
}) })
} }
c.group = g c.group = g
perms := c.permissions
err = c.write(clientMessage{
Type: "joined",
Kind: "join",
Group: m.Group,
Username: c.username,
Permissions: &perms,
RTCConfiguration: ice.ICEConfiguration(),
})
if err != nil {
return err
}
h := c.group.GetChatHistory() h := c.group.GetChatHistory()
for _, m := range h { for _, m := range h {
err := c.write(clientMessage{ err := c.write(clientMessage{
...@@ -1608,8 +1646,10 @@ func handleClientMessage(c *webClient, m clientMessage) error { ...@@ -1608,8 +1646,10 @@ func handleClientMessage(c *webClient, m clientMessage) error {
status := c.Status() status := c.Status()
go func(clients []group.Client) { go func(clients []group.Client) {
for _, cc := range clients { for _, cc := range clients {
cc.PushClient(id, user, &perms, status, cc.PushClient(
"change") g.Name(), "change",
id, user, perms, status,
)
} }
}(g.GetClients(nil)) }(g.GetClients(nil))
default: default:
......
...@@ -2091,12 +2091,35 @@ function displayUsername() { ...@@ -2091,12 +2091,35 @@ function displayUsername() {
let presentRequested = null; let presentRequested = null;
/**
* @param {string} [title]
*/
function setTitle(title) {
function set(title) {
document.title = title;
document.getElementById('title').textContent = title;
}
if(title) {
set(title);
return;
}
let t = group.charAt(0).toUpperCase() + group.slice(1);
if(t) {
set(t);
return;
}
set('Galène');
}
/** /**
* @this {ServerConnection} * @this {ServerConnection}
* @param {string} group * @param {string} group
* @param {Object<string,boolean>} perms * @param {Object<string,boolean>} perms
* @param {Object<string,any>} status
* @param {string} message
*/ */
async function gotJoined(kind, group, perms, message) { async function gotJoined(kind, group, perms, status, message) {
let present = presentRequested; let present = presentRequested;
presentRequested = null; presentRequested = null;
...@@ -2108,7 +2131,7 @@ async function gotJoined(kind, group, perms, message) { ...@@ -2108,7 +2131,7 @@ async function gotJoined(kind, group, perms, message) {
return; return;
case 'redirect': case 'redirect':
this.close(); this.close();
document.location = message; document.location.href = message;
return; return;
case 'leave': case 'leave':
this.close(); this.close();
...@@ -2116,6 +2139,7 @@ async function gotJoined(kind, group, perms, message) { ...@@ -2116,6 +2139,7 @@ async function gotJoined(kind, group, perms, message) {
return; return;
case 'join': case 'join':
case 'change': case 'change':
setTitle(status.displayName || group);
displayUsername(); displayUsername();
setButtonsVisibility(); setButtonsVisibility();
if(kind === 'change') if(kind === 'change')
...@@ -3061,12 +3085,7 @@ async function serverConnect() { ...@@ -3061,12 +3085,7 @@ async function serverConnect() {
function start() { function start() {
group = decodeURIComponent(location.pathname.replace(/^\/[a-z]*\//, '')); group = decodeURIComponent(location.pathname.replace(/^\/[a-z]*\//, ''));
let title = group.charAt(0).toUpperCase() + group.slice(1); setTitle();
if(group !== '') {
document.title = title;
document.getElementById('title').textContent = title;
}
addFilters(); addFilters();
setMediaChoices(false).then(e => reflectSettings()); setMediaChoices(false).then(e => reflectSettings());
......
...@@ -164,7 +164,7 @@ function ServerConnection() { ...@@ -164,7 +164,7 @@ function ServerConnection() {
* *
* kind is one of 'join', 'fail', 'change' or 'leave'. * kind is one of 'join', 'fail', 'change' or 'leave'.
* *
* @type{(this: ServerConnection, kind: string, group: string, permissions: Object<string,boolean>, message: string) => void} * @type{(this: ServerConnection, kind: string, group: string, permissions: Object<string,boolean>, status: Object<string,any>, message: string) => void}
*/ */
this.onjoined = null; this.onjoined = null;
/** /**
...@@ -284,7 +284,7 @@ ServerConnection.prototype.connect = async function(url) { ...@@ -284,7 +284,7 @@ ServerConnection.prototype.connect = async function(url) {
sc.onuser.call(sc, id, 'delete'); sc.onuser.call(sc, id, 'delete');
} }
if(sc.group && sc.onjoined) if(sc.group && sc.onjoined)
sc.onjoined.call(sc, 'leave', sc.group, {}, ''); sc.onjoined.call(sc, 'leave', sc.group, {}, {}, '');
sc.group = null; sc.group = null;
sc.username = null; sc.username = null;
if(sc.onclose) if(sc.onclose)
...@@ -336,6 +336,7 @@ ServerConnection.prototype.connect = async function(url) { ...@@ -336,6 +336,7 @@ ServerConnection.prototype.connect = async function(url) {
if(sc.onjoined) if(sc.onjoined)
sc.onjoined.call(sc, m.kind, m.group, sc.onjoined.call(sc, m.kind, m.group,
m.permissions || {}, m.permissions || {},
m.status,
m.value || null); m.value || null);
break; break;
case 'user': case 'user':
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment