Commit c7c3c9c6 authored by Juliusz Chroboczek's avatar Juliusz Chroboczek

Export group status in .status.json.

parent 5e39c3a2
# Galène's protocol # Galène's protocol
Galène uses a symmetric, asynchronous protocol. In client-server
usage, some messages are only sent in the client to server or in the
server to client direction.
## Message syntax
All messages are sent as JSON objects. All fields except `type` are
optional; however, there are some fields that are common across multiple
message types.
- `type`, the type of the message;
- `kind`, the subtype of the message;
- `id`, the id of the object being manipulated;
- `source`, the client-id of the originating client;
- `username`, the username of the originating client;
- `dest`, the client-id of the destination client;
- `privileged`, set by the server to indicate that the originating client
had the `op` privilege at the time it sent the message.
## Data structures ## Data structures
### Group ### Group
...@@ -41,6 +22,48 @@ exactly one peer connection (PC) (multiple streams in a single PC are not ...@@ -41,6 +22,48 @@ exactly one peer connection (PC) (multiple streams in a single PC are not
allowed). The offerer is also the RTP sender (i.e. all tracks sent by the allowed). The offerer is also the RTP sender (i.e. all tracks sent by the
offerer are of type `sendonly`). offerer are of type `sendonly`).
Galène uses a symmetric, asynchronous protocol. In client-server
usage, some messages are only sent in the client to server or in the
server to client direction.
## Before connecting
Before it connects and joins a group, a client may perform an HTTP GET
request on the URL `/public-groups.json`. This yields a JSON array of
objects, one for each group that has been marked public in its
configuration file. Each object has the following fields:
- `name`: the group's name
- `displayName` (optional): a longer version of the name used for display;
- `description` (optional): a user-readable description.
- `locked`: true if the group is locked;
- `clientCount`: the number of clients currently in the group.
A client may also fetch the URL `/group/name/.status.json` to retrieve the
status of a single group. If the group has not been marked as public,
then the fields `locked` and `clientCount` are omitted.
## Connecting
The client connects to the websocket at `/ws`. Galene uses a symmetric,
asynchronous protocol: there are no requests and responses, and most
messages may be sent by either peer.
## Message syntax
All messages are sent as JSON objects. All fields except `type` are
optional; however, there are some fields that are common across multiple
message types:
- `type`, the type of the message;
- `kind`, the subtype of the message;
- `id`, the id of the object being manipulated;
- `source`, the client-id of the originating client;
- `username`, the username of the originating client;
- `dest`, the client-id of the destination client;
- `privileged`, set by the server to indicate that the originating client
had the `op` privilege at the time when it sent the message.
## Establishing and maintaining a connection ## Establishing and maintaining a connection
The peer establishing the connection (the WebSocket client) sends The peer establishing the connection (the WebSocket client) sends
......
...@@ -112,6 +112,12 @@ func (g *Group) Description() *Description { ...@@ -112,6 +112,12 @@ func (g *Group) Description() *Description {
return g.description return g.description
} }
func (g *Group) ClientCount() int {
g.mu.Lock()
defer g.mu.Unlock()
return len(g.clients)
}
func (g *Group) EmptyTime() time.Duration { func (g *Group) EmptyTime() time.Duration {
g.mu.Lock() g.mu.Lock()
defer g.mu.Unlock() defer g.mu.Unlock()
...@@ -1052,27 +1058,37 @@ func (desc *Description) GetPermission(group string, creds ClientCredentials) (C ...@@ -1052,27 +1058,37 @@ func (desc *Description) GetPermission(group string, creds ClientCredentials) (C
return p, ErrNotAuthorised return p, ErrNotAuthorised
} }
type Public struct { type Status struct {
Name string `json:"name"` Name string `json:"name"`
DisplayName string `json:"displayName,omitempty"` DisplayName string `json:"displayName,omitempty"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
Locked bool `json:"locked,omitempty"` Locked bool `json:"locked,omitempty"`
ClientCount int `json:"clientCount"` ClientCount *int `json:"clientCount,omitempty"`
}
func GetStatus(g *Group, authentified bool) Status {
desc := g.Description()
d := Status{
Name: g.name,
DisplayName: desc.DisplayName,
Description: desc.Description,
}
if authentified || desc.Public {
// these are considered private information
locked, _ := g.Locked()
count := g.ClientCount()
d.Locked = locked
d.ClientCount = &count
}
return d
} }
func GetPublic() []Public { func GetPublic() []Status {
gs := make([]Public, 0) gs := make([]Status, 0)
Range(func(g *Group) bool { Range(func(g *Group) bool {
desc := g.Description() if g.Description().Public {
if desc.Public { gs = append(gs, GetStatus(g, false))
locked, _ := g.Locked()
gs = append(gs, Public{
Name: g.name,
DisplayName: desc.DisplayName,
Description: desc.Description,
Locked: locked,
ClientCount: len(g.clients),
})
} }
return true return true
}) })
......
...@@ -111,7 +111,7 @@ type clientMessage struct { ...@@ -111,7 +111,7 @@ type clientMessage struct {
Password string `json:"password,omitempty"` Password string `json:"password,omitempty"`
Privileged bool `json:"privileged,omitempty"` Privileged bool `json:"privileged,omitempty"`
Permissions *group.ClientPermissions `json:"permissions,omitempty"` Permissions *group.ClientPermissions `json:"permissions,omitempty"`
Status map[string]interface{} `json:"status,omitempty"` Status interface{} `json:"status,omitempty"`
Group string `json:"group,omitempty"` Group string `json:"group,omitempty"`
Value interface{} `json:"value,omitempty"` Value interface{} `json:"value,omitempty"`
NoEcho bool `json:"noecho,omitempty"` NoEcho bool `json:"noecho,omitempty"`
...@@ -813,21 +813,6 @@ func (c *webClient) PushConn(g *group.Group, id string, up conn.Up, tracks []con ...@@ -813,21 +813,6 @@ 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, message := g.Locked(); locked {
if message == "" {
status["locked"] = true
} else {
status["locked"] = message
}
}
if dn := g.Description().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 {
...@@ -1120,11 +1105,11 @@ func handleAction(c *webClient, a interface{}) error { ...@@ -1120,11 +1105,11 @@ func handleAction(c *webClient, a interface{}) error {
Status: a.status, Status: a.status,
}) })
case joinedAction: case joinedAction:
var status map[string]interface{} var status interface{}
if a.group != "" { if a.group != "" {
g := group.Get(a.group) g := group.Get(a.group)
if g != nil { if g != nil {
status = getGroupStatus(g) status = group.GetStatus(g, true)
} }
} }
perms := c.permissions perms := c.permissions
...@@ -1149,7 +1134,7 @@ func handleAction(c *webClient, a interface{}) error { ...@@ -1149,7 +1134,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), Status: group.GetStatus(g, true),
RTCConfiguration: ice.ICEConfiguration(), RTCConfiguration: ice.ICEConfiguration(),
}) })
if !c.permissions.Present { if !c.permissions.Present {
......
...@@ -26,6 +26,9 @@ let group; ...@@ -26,6 +26,9 @@ let group;
/** @type {ServerConnection} */ /** @type {ServerConnection} */
let serverConnection; let serverConnection;
/** @type {Object} */
let groupStatus = {};
/** /**
* @typedef {Object} userpass * @typedef {Object} userpass
* @property {string} username * @property {string} username
...@@ -2149,6 +2152,7 @@ async function gotJoined(kind, group, perms, status, message) { ...@@ -2149,6 +2152,7 @@ async function gotJoined(kind, group, perms, status, message) {
return; return;
case 'join': case 'join':
case 'change': case 'change':
groupStatus = status;
setTitle((status && status.displayName) || capitalise(group)); setTitle((status && status.displayName) || capitalise(group));
displayUsername(); displayUsername();
setButtonsVisibility(); setButtonsVisibility();
...@@ -3095,11 +3099,22 @@ async function serverConnect() { ...@@ -3095,11 +3099,22 @@ async function serverConnect() {
} }
} }
function start() { async function start() {
group = decodeURIComponent( group = decodeURIComponent(
location.pathname.replace(/^\/[a-z]*\//, '').replace(/\/$/, '') location.pathname.replace(/^\/[a-z]*\//, '').replace(/\/$/, '')
); );
setTitle(capitalise(group)); /** @type {Object} */
try {
let r = await fetch(".status.json")
if(!r.ok)
throw new Error(`${r.status} ${r.statusText}`);
groupStatus = await r.json()
} catch(e) {
console.error(e);
return;
}
setTitle(groupStatus.displayName || capitalise(group));
addFilters(); addFilters();
setMediaChoices(false).then(e => reflectSettings()); setMediaChoices(false).then(e => reflectSettings());
......
...@@ -278,7 +278,11 @@ func groupHandler(w http.ResponseWriter, r *http.Request) { ...@@ -278,7 +278,11 @@ func groupHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
mungeHeader(w) if strings.HasSuffix(r.URL.Path, "/.status.json") {
groupStatusHandler(w, r)
return
}
name := parseGroupName("/group/", r.URL.Path) name := parseGroupName("/group/", r.URL.Path)
if name == "" { if name == "" {
notFound(w) notFound(w)
...@@ -290,7 +294,7 @@ func groupHandler(w http.ResponseWriter, r *http.Request) { ...@@ -290,7 +294,7 @@ func groupHandler(w http.ResponseWriter, r *http.Request) {
if os.IsNotExist(err) { if os.IsNotExist(err) {
notFound(w) notFound(w)
} else { } else {
log.Printf("addGroup: %v", err) log.Printf("group.Add: %v", err)
http.Error(w, "Internal server error", http.Error(w, "Internal server error",
http.StatusInternalServerError) http.StatusInternalServerError)
} }
...@@ -308,9 +312,42 @@ func groupHandler(w http.ResponseWriter, r *http.Request) { ...@@ -308,9 +312,42 @@ func groupHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
mungeHeader(w)
serveFile(w, r, filepath.Join(StaticRoot, "galene.html")) serveFile(w, r, filepath.Join(StaticRoot, "galene.html"))
} }
func groupStatusHandler(w http.ResponseWriter, r *http.Request) {
path := path.Dir(r.URL.Path)
name := parseGroupName("/group/", path)
if name == "" {
notFound(w)
return
}
g, err := group.Add(name, nil)
if err != nil {
if os.IsNotExist(err) {
notFound(w)
} else {
http.Error(w, "Internal server error",
http.StatusInternalServerError)
}
return
}
d := group.GetStatus(g, false)
w.Header().Set("content-type", "application/json")
w.Header().Set("cache-control", "no-cache")
if r.Method == "HEAD" {
return
}
e := json.NewEncoder(w)
e.Encode(d)
return
}
func publicHandler(w http.ResponseWriter, r *http.Request) { func publicHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json") w.Header().Set("content-type", "application/json")
w.Header().Set("cache-control", "no-cache") w.Header().Set("cache-control", "no-cache")
......
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