Commit c7c3c9c6 authored by Juliusz Chroboczek's avatar Juliusz Chroboczek

Export group status in .status.json.

parent 5e39c3a2
# 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
### Group
......@@ -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
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
The peer establishing the connection (the WebSocket client) sends
......
......@@ -112,6 +112,12 @@ func (g *Group) Description() *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 {
g.mu.Lock()
defer g.mu.Unlock()
......@@ -1052,27 +1058,37 @@ func (desc *Description) GetPermission(group string, creds ClientCredentials) (C
return p, ErrNotAuthorised
}
type Public struct {
type Status struct {
Name string `json:"name"`
DisplayName string `json:"displayName,omitempty"`
Description string `json:"description,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 {
gs := make([]Public, 0)
func GetPublic() []Status {
gs := make([]Status, 0)
Range(func(g *Group) bool {
desc := g.Description()
if desc.Public {
locked, _ := g.Locked()
gs = append(gs, Public{
Name: g.name,
DisplayName: desc.DisplayName,
Description: desc.Description,
Locked: locked,
ClientCount: len(g.clients),
})
if g.Description().Public {
gs = append(gs, GetStatus(g, false))
}
return true
})
......
......@@ -111,7 +111,7 @@ type clientMessage struct {
Password string `json:"password,omitempty"`
Privileged bool `json:"privileged,omitempty"`
Permissions *group.ClientPermissions `json:"permissions,omitempty"`
Status map[string]interface{} `json:"status,omitempty"`
Status interface{} `json:"status,omitempty"`
Group string `json:"group,omitempty"`
Value interface{} `json:"value,omitempty"`
NoEcho bool `json:"noecho,omitempty"`
......@@ -813,21 +813,6 @@ 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, 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 {
err := conn.SetReadDeadline(time.Now().Add(15 * time.Second))
if err != nil {
......@@ -1120,11 +1105,11 @@ func handleAction(c *webClient, a interface{}) error {
Status: a.status,
})
case joinedAction:
var status map[string]interface{}
var status interface{}
if a.group != "" {
g := group.Get(a.group)
if g != nil {
status = getGroupStatus(g)
status = group.GetStatus(g, true)
}
}
perms := c.permissions
......@@ -1149,7 +1134,7 @@ func handleAction(c *webClient, a interface{}) error {
Group: g.Name(),
Username: c.username,
Permissions: &perms,
Status: getGroupStatus(g),
Status: group.GetStatus(g, true),
RTCConfiguration: ice.ICEConfiguration(),
})
if !c.permissions.Present {
......
......@@ -26,6 +26,9 @@ let group;
/** @type {ServerConnection} */
let serverConnection;
/** @type {Object} */
let groupStatus = {};
/**
* @typedef {Object} userpass
* @property {string} username
......@@ -2149,6 +2152,7 @@ async function gotJoined(kind, group, perms, status, message) {
return;
case 'join':
case 'change':
groupStatus = status;
setTitle((status && status.displayName) || capitalise(group));
displayUsername();
setButtonsVisibility();
......@@ -3095,11 +3099,22 @@ async function serverConnect() {
}
}
function start() {
async function start() {
group = decodeURIComponent(
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();
setMediaChoices(false).then(e => reflectSettings());
......
......@@ -278,7 +278,11 @@ func groupHandler(w http.ResponseWriter, r *http.Request) {
return
}
mungeHeader(w)
if strings.HasSuffix(r.URL.Path, "/.status.json") {
groupStatusHandler(w, r)
return
}
name := parseGroupName("/group/", r.URL.Path)
if name == "" {
notFound(w)
......@@ -290,7 +294,7 @@ func groupHandler(w http.ResponseWriter, r *http.Request) {
if os.IsNotExist(err) {
notFound(w)
} else {
log.Printf("addGroup: %v", err)
log.Printf("group.Add: %v", err)
http.Error(w, "Internal server error",
http.StatusInternalServerError)
}
......@@ -308,9 +312,42 @@ func groupHandler(w http.ResponseWriter, r *http.Request) {
return
}
mungeHeader(w)
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) {
w.Header().Set("content-type", "application/json")
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