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:
parent
5e39c3a2a7
commit
c7c3c9c6b0
5 changed files with 132 additions and 56 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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());
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
Loading…
Reference in a new issue