Mobile: - The room list's title picks a space: All chats, or one space, which narrows the list to its rooms (and its subspaces' rooms). - A space browser over /hierarchy: the rooms of a space and of its subspaces, joined or joinable, with Join; for those who may, adding an existing room or a new one to the space. - New space in the new-chat sheet; invites to spaces show with the other invites and open the space when joined. - Fixes several setState calls that were handed a Future (reloading the members, sessions, push distributor, threads), which Flutter rejects. Server: /hierarchy left out children_state for a room without children. The spec requires it on every room (an empty list), and SpaceRoom's omitempty dropped empty lists too, despite its comment. The famedly Dart SDK fails to parse such a response. The client API now wraps each room so the field is always written; the federation response, which wants it left out of the children it lists, is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
120 lines
3.5 KiB
Go
120 lines
3.5 KiB
Go
package clientapi
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/dariuszjaniak/warp/server/internal/httputil"
|
|
"github.com/dariuszjaniak/warp/server/internal/matrix/id"
|
|
"github.com/dariuszjaniak/warp/server/internal/roomserver"
|
|
"github.com/dariuszjaniak/warp/server/internal/userapi"
|
|
)
|
|
|
|
// hierarchyResponse is what GET /hierarchy returns.
|
|
type hierarchyResponse struct {
|
|
Rooms []hierarchyRoom `json:"rooms"`
|
|
NextBatch string `json:"next_batch,omitempty"`
|
|
}
|
|
|
|
// hierarchyRoom is a room as /hierarchy shows it to a client:
|
|
// children_state is required there, an empty list for a room with no
|
|
// children. SpaceRoom leaves an empty one out, as the federation response
|
|
// wants for the children it lists; this field, being shallower, takes the
|
|
// JSON name over the embedded one.
|
|
type hierarchyRoom struct {
|
|
roomserver.SpaceRoom
|
|
ChildrenState []roomserver.SpaceChildEvent `json:"children_state"`
|
|
}
|
|
|
|
// hierarchyTokenPrefix marks a /hierarchy token: how far into the walk
|
|
// the next page starts.
|
|
const hierarchyTokenPrefix = "h"
|
|
|
|
// handleHierarchy implements GET /_matrix/client/v1/rooms/{roomID}/hierarchy.
|
|
func (c *ClientAPI) handleHierarchy(w http.ResponseWriter, r *http.Request, auth userapi.Authenticated) error {
|
|
roomID, err := pathRoomID(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
query := r.URL.Query()
|
|
|
|
limit, err := intParam(query.Get("limit"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
suggestedOnly, err := boolParam(query.Get("suggested_only"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
maxDepth := -1
|
|
if raw := query.Get("max_depth"); raw != "" {
|
|
if maxDepth, err = strconv.Atoi(raw); err != nil || maxDepth < 0 {
|
|
return httputil.InvalidParam("max_depth must be a non-negative integer")
|
|
}
|
|
}
|
|
from := 0
|
|
if raw := query.Get("from"); raw != "" {
|
|
body, ok := strings.CutPrefix(raw, hierarchyTokenPrefix)
|
|
if from, err = strconv.Atoi(body); !ok || err != nil || from < 0 {
|
|
return httputil.InvalidParam("%q is not a hierarchy token", raw)
|
|
}
|
|
}
|
|
|
|
page, err := c.rooms.Hierarchy(r.Context(), roomserver.HierarchyRequest{
|
|
RoomID: roomID,
|
|
User: auth.UserID,
|
|
SuggestedOnly: suggestedOnly,
|
|
MaxDepth: maxDepth,
|
|
From: from,
|
|
Limit: limit,
|
|
})
|
|
if err != nil {
|
|
return roomError(err)
|
|
}
|
|
resp := hierarchyResponse{Rooms: make([]hierarchyRoom, 0, len(page.Rooms))}
|
|
for _, room := range page.Rooms {
|
|
children := room.ChildrenState
|
|
if children == nil {
|
|
children = []roomserver.SpaceChildEvent{}
|
|
}
|
|
resp.Rooms = append(resp.Rooms, hierarchyRoom{SpaceRoom: room, ChildrenState: children})
|
|
}
|
|
if page.Next != 0 {
|
|
resp.NextBatch = hierarchyTokenPrefix + strconv.Itoa(page.Next)
|
|
}
|
|
httputil.WriteJSON(w, http.StatusOK, resp)
|
|
return nil
|
|
}
|
|
|
|
// handleRoomSummary implements
|
|
// GET /_matrix/client/v1/room_summary/{roomIDOrAlias}.
|
|
func (c *ClientAPI) handleRoomSummary(w http.ResponseWriter, r *http.Request, auth userapi.Authenticated) error {
|
|
target := r.PathValue("roomIDOrAlias")
|
|
via := r.URL.Query()["via"]
|
|
|
|
roomID := target
|
|
switch {
|
|
case strings.HasPrefix(target, "#"):
|
|
resolved, err := c.resolveAlias(r.Context(), target)
|
|
if err != nil {
|
|
return roomError(err)
|
|
}
|
|
roomID = resolved.RoomID
|
|
via = append(via, resolved.Servers...)
|
|
case id.ValidateRoomID(target) != nil:
|
|
return httputil.InvalidParam("%s is not a room ID or alias", target)
|
|
}
|
|
|
|
resp, err := c.rooms.RoomSummary(r.Context(), roomserver.RoomSummaryRequest{
|
|
RoomID: roomID,
|
|
User: auth.UserID,
|
|
Via: via,
|
|
})
|
|
if err != nil {
|
|
return roomError(err)
|
|
}
|
|
httputil.WriteJSON(w, http.StatusOK, resp)
|
|
return nil
|
|
}
|