mirror of
https://github.com/jech/galene.git
synced 2024-12-22 15:25:48 +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 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 GetPublic() []Public {
|
||||
gs := make([]Public, 0)
|
||||
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() []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")
|
||||
|
|
Loading…
Reference in a new issue