1
Fork 0
mirror of https://github.com/jech/galene.git synced 2024-11-10 02:35:58 +01:00

Export group status in .status.json.

This commit is contained in:
Juliusz Chroboczek 2021-10-26 22:22:48 +02:00
parent 5e39c3a2a7
commit c7c3c9c6b0
5 changed files with 132 additions and 56 deletions

View file

@ -1,24 +1,5 @@
# 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
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

View file

@ -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
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 GetPublic() []Public { func GetStatus(g *Group, authentified bool) Status {
gs := make([]Public, 0)
Range(func(g *Group) bool {
desc := g.Description() desc := g.Description()
if desc.Public { d := Status{
locked, _ := g.Locked()
gs = append(gs, Public{
Name: g.name, Name: g.name,
DisplayName: desc.DisplayName, DisplayName: desc.DisplayName,
Description: desc.Description, Description: desc.Description,
Locked: locked, }
ClientCount: len(g.clients),
}) 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() []Status {
gs := make([]Status, 0)
Range(func(g *Group) bool {
if g.Description().Public {
gs = append(gs, GetStatus(g, false))
} }
return true return true
}) })

View file

@ -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
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 {
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 {
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 {

View file

@ -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) {
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() {
} }
} }
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());

View file

@ -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) {
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) {
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")