diff --git a/README b/README index c92a93b..16bcff7 100644 --- a/README +++ b/README @@ -207,6 +207,7 @@ following fields are allowed: respectively with operator privileges, with presenter privileges, and as passive listeners; - `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 displayed on the landing page for public groups; - `contact`: a human-readable contact for this group, such as an e-mail diff --git a/README.PROTOCOL b/README.PROTOCOL index 50a9901..6150f05 100644 --- a/README.PROTOCOL +++ b/README.PROTOCOL @@ -100,12 +100,15 @@ its permissions or in the recommended RTC configuration. group: group, username: username, permissions: permissions, + status: status, rtcConfiguration: RTCConfiguration } ``` 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 diff --git a/diskwriter/diskwriter.go b/diskwriter/diskwriter.go index 5928f64..f3e084f 100644 --- a/diskwriter/diskwriter.go +++ b/diskwriter/diskwriter.go @@ -77,7 +77,7 @@ func (client *Client) Status() map[string]interface{} { 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 } @@ -103,6 +103,10 @@ func (client *Client) Kick(id, user, message string) error { 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 { if client.group != g { return nil diff --git a/group/client.go b/group/client.go index 9b2400b..f3c080f 100644 --- a/group/client.go +++ b/group/client.go @@ -102,6 +102,7 @@ type Client interface { OverridePermissions(*Group) bool PushConn(g *Group, id string, conn conn.Up, tracks []conn.UpTrack, replace 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 } diff --git a/group/group.go b/group/group.go index fac1d07..8b230d1 100644 --- a/group/group.go +++ b/group/group.go @@ -92,12 +92,17 @@ func (g *Group) Locked() (bool, string) { func (g *Group) SetLocked(locked bool, message string) { g.mu.Lock() - defer g.mu.Unlock() if locked { g.locked = &message } else { g.locked = nil } + clients := g.getClientsUnlocked(nil) + g.mu.Unlock() + + for _, c := range clients { + c.Joined(g.Name(), "change") + } } func (g *Group) Public() bool { @@ -118,6 +123,12 @@ func (g *Group) AllowRecording() bool { return g.description.AllowRecording } +func (g *Group) DisplayName() string { + g.mu.Lock() + defer g.mu.Unlock() + return g.description.DisplayName +} + var groups struct { mu sync.Mutex groups map[string]*Group @@ -293,8 +304,16 @@ func APIFromNames(names []string) (*webrtc.API, 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, "/") { - return nil, UserError("illegal group name") + return nil, nil, UserError("illegal group name") } groups.mu.Lock() @@ -311,7 +330,7 @@ func Add(name string, desc *Description) (*Group, error) { if desc == nil { desc, err = GetDescription(name) if err != nil { - return nil, err + return nil, nil, err } } @@ -321,9 +340,10 @@ func Add(name string, desc *Description) (*Group, error) { clients: make(map[string]Client), timestamp: time.Now(), } - autoLockKick(g, g.getClientsUnlocked(nil)) + clients := g.getClientsUnlocked(nil) + autoLockKick(g, clients) groups.groups[name] = g - return g, nil + return g, clients, nil } g.mu.Lock() @@ -332,7 +352,7 @@ func Add(name string, desc *Description) (*Group, error) { if desc != nil { g.description = desc } else if !descriptionChanged(name, g.description) { - return g, nil + return g, nil, nil } desc, err = GetDescription(name) @@ -341,12 +361,13 @@ func Add(name string, desc *Description) (*Group, error) { log.Printf("Reading group %v: %v", name, err) } deleteUnlocked(g) - return nil, err + return nil, nil, err } 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) { @@ -511,15 +532,19 @@ func AddClient(group string, c Client) (*Group, error) { g.clients[c.Id()] = c g.timestamp = time.Now() + c.Joined(g.Name(), "join") + id := c.Id() u := c.Username() p := c.Permissions() 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 { pp := cc.Permissions() - c.PushClient(cc.Id(), cc.Username(), &pp, cc.Status(), "add") - cc.PushClient(id, u, &p, s, "add") + c.PushClient( + g.Name(), "add", cc.Id(), cc.Username(), pp, cc.Status(), + ) + cc.PushClient(g.Name(), "add", id, u, p, s) } return g, nil @@ -539,6 +564,11 @@ func autoLockKick(g *Group, clients []Client) { if g.description.Autolock && g.locked == nil { m := "this group is locked" g.locked = &m + go func(clients []Client) { + for _, c := range clients { + c.Joined(g.Name(), "change") + } + }(g.getClientsUnlocked(nil)) } if g.description.Autokick { @@ -552,23 +582,22 @@ func DelClient(c Client) { return } g.mu.Lock() - defer g.mu.Unlock() - if g.clients[c.Id()] != c { log.Printf("Deleting unknown client") + g.mu.Unlock() return } delete(g.clients, c.Id()) g.timestamp = time.Now() - clients := g.getClientsUnlocked(nil) + g.mu.Unlock() - go func(clients []Client) { - for _, cc := range clients { - cc.PushClient(c.Id(), "", nil, nil, "delete") - } - }(clients) - + c.Joined(g.Name(), "leave") + for _, cc := range clients { + cc.PushClient( + g.Name(), "delete", c.Id(), "", ClientPermissions{}, nil, + ) + } autoLockKick(g, clients) } @@ -740,6 +769,9 @@ type Description struct { modTime time.Time `json:"-"` fileSize int64 `json:"-"` + // The user-friendly group name + DisplayName string `json:"displayName,omitempty"` + // A user-readable description of the group. Description string `json:"description,omitempty"` diff --git a/rtpconn/webclient.go b/rtpconn/webclient.go index 4fe284f..2f43afe 100644 --- a/rtpconn/webclient.go +++ b/rtpconn/webclient.go @@ -111,14 +111,9 @@ func (c *webClient) OverridePermissions(g *group.Group) bool { return false } -func (c *webClient) PushClient(id, username string, permissions *group.ClientPermissions, status map[string]interface{}, kind string) error { - return c.write(clientMessage{ - Type: "user", - Kind: kind, - Id: id, - Username: username, - Permissions: permissions, - Status: status, +func (c *webClient) PushClient(group, kind, id, username string, permissions group.ClientPermissions, status map[string]interface{}) error { + return c.action(pushClientAction{ + group, kind, id, username, permissions, status, }) } @@ -804,6 +799,17 @@ func (c *webClient) PushConn(g *group.Group, id string, up conn.Up, tracks []con 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 { err := conn.SetReadDeadline(time.Now().Add(15 * time.Second)) if err != nil { @@ -880,8 +886,22 @@ type connectionFailedAction struct { id string } +type pushClientAction struct { + group string + kind string + id string + username string + permissions group.ClientPermissions + status map[string]interface{} +} + type permissionsChangedAction struct{} +type joinedAction struct { + group string + kind string +} + type kickAction struct { id string username string @@ -1067,6 +1087,37 @@ func handleAction(c *webClient, a interface{}) error { "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: g := c.Group() if g == nil { @@ -1079,6 +1130,7 @@ func handleAction(c *webClient, a interface{}) error { Group: g.Name(), Username: c.username, Permissions: &perms, + Status: getGroupStatus(g), RTCConfiguration: ice.ICEConfiguration(), }) if !c.permissions.Present { @@ -1101,7 +1153,9 @@ func handleAction(c *webClient, a interface{}) error { clients := g.GetClients(nil) go func(clients []group.Client) { for _, cc := range clients { - cc.PushClient(id, user, &perms, s, "change") + cc.PushClient( + g.Name(), "change", id, user, perms, s, + ) } }(clients) case kickAction: @@ -1212,6 +1266,10 @@ func (c *webClient) Kick(id, user, message string) error { 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 { client := g.GetClient(dest) if client == nil { @@ -1243,14 +1301,6 @@ func handleClientMessage(c *webClient, m clientMessage) error { return group.ProtocolError("you are not joined") } 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" { @@ -1298,18 +1348,6 @@ func handleClientMessage(c *webClient, m clientMessage) error { }) } 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() for _, m := range h { err := c.write(clientMessage{ @@ -1608,8 +1646,10 @@ func handleClientMessage(c *webClient, m clientMessage) error { status := c.Status() go func(clients []group.Client) { for _, cc := range clients { - cc.PushClient(id, user, &perms, status, - "change") + cc.PushClient( + g.Name(), "change", + id, user, perms, status, + ) } }(g.GetClients(nil)) default: diff --git a/static/galene.js b/static/galene.js index 2c8e356..78e2b87 100644 --- a/static/galene.js +++ b/static/galene.js @@ -2091,12 +2091,35 @@ function displayUsername() { 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} * @param {string} group * @param {Object} perms + * @param {Object} status + * @param {string} message */ -async function gotJoined(kind, group, perms, message) { +async function gotJoined(kind, group, perms, status, message) { let present = presentRequested; presentRequested = null; @@ -2108,7 +2131,7 @@ async function gotJoined(kind, group, perms, message) { return; case 'redirect': this.close(); - document.location = message; + document.location.href = message; return; case 'leave': this.close(); @@ -2116,6 +2139,7 @@ async function gotJoined(kind, group, perms, message) { return; case 'join': case 'change': + setTitle(status.displayName || group); displayUsername(); setButtonsVisibility(); if(kind === 'change') @@ -3061,12 +3085,7 @@ async function serverConnect() { function start() { group = decodeURIComponent(location.pathname.replace(/^\/[a-z]*\//, '')); - let title = group.charAt(0).toUpperCase() + group.slice(1); - if(group !== '') { - document.title = title; - document.getElementById('title').textContent = title; - } - + setTitle(); addFilters(); setMediaChoices(false).then(e => reflectSettings()); diff --git a/static/protocol.js b/static/protocol.js index c6269d9..6ca269b 100644 --- a/static/protocol.js +++ b/static/protocol.js @@ -164,7 +164,7 @@ function ServerConnection() { * * kind is one of 'join', 'fail', 'change' or 'leave'. * - * @type{(this: ServerConnection, kind: string, group: string, permissions: Object, message: string) => void} + * @type{(this: ServerConnection, kind: string, group: string, permissions: Object, status: Object, message: string) => void} */ this.onjoined = null; /** @@ -284,7 +284,7 @@ ServerConnection.prototype.connect = async function(url) { sc.onuser.call(sc, id, 'delete'); } if(sc.group && sc.onjoined) - sc.onjoined.call(sc, 'leave', sc.group, {}, ''); + sc.onjoined.call(sc, 'leave', sc.group, {}, {}, ''); sc.group = null; sc.username = null; if(sc.onclose) @@ -336,6 +336,7 @@ ServerConnection.prototype.connect = async function(url) { if(sc.onjoined) sc.onjoined.call(sc, m.kind, m.group, m.permissions || {}, + m.status, m.value || null); break; case 'user':