package syncapi import ( "context" "fmt" "strconv" "strings" "github.com/dariuszjaniak/warp/server/internal/roomserver" "github.com/dariuszjaniak/warp/server/internal/storage" ) // GET /rooms/{roomID}/relations/{eventID} and GET /rooms/{roomID}/threads: // the children of an event, and the threads of a room. // // The reads are the roomserver's (relations.go there); what is here is the // token handling, which is this package's. Relations page exactly like // /messages and take the same tokens — a client may start paging an // event's children from a sync's prev_batch — so they use the same // stream-position token. Threads are ordered by their latest activity // rather than by any one stream, and have a token of their own. // DefaultRelationsLimit is the page size of /relations and /threads when // the client names none. const DefaultRelationsLimit = 50 // RelationsRequest is one page of an event's children. type RelationsRequest struct { RoomID string EventID string User string RelType string EventType string // From and To are pagination tokens, as for /messages. From string To string // Direction defaults to backwards, newest first, as the spec has it. Direction storage.TimelineDirection Limit int Recurse bool } // RelationsResponse is one page of an event's children. type RelationsResponse struct { Chunk []roomserver.ClientEvent `json:"chunk"` // NextBatch continues in the direction asked for; it is left out at // the end. NextBatch string `json:"next_batch,omitempty"` // PrevBatch is the token this page began at, when the client gave one. PrevBatch string `json:"prev_batch,omitempty"` // RecursionDepth is how deep a recursive read went. It is only there // when the client asked to recurse, which is how it learns the server // honoured the request. RecursionDepth int `json:"recursion_depth,omitempty"` } // Relations reads one page of an event's children. func (s *API) Relations(ctx context.Context, req RelationsRequest) (RelationsResponse, error) { from, err := ParseToken(req.From) if err != nil { return RelationsResponse{}, errInvalidTokenf("from: %v", err) } to, err := ParseToken(req.To) if err != nil { return RelationsResponse{}, errInvalidTokenf("to: %v", err) } direction := req.Direction if direction != storage.DirectionForward { direction = storage.DirectionBackward } limit := req.Limit if limit <= 0 { limit = DefaultRelationsLimit } limit = min(limit, MaxTimelineLimit) low, high := from.PDU, to.PDU if direction == storage.DirectionBackward { low, high = to.PDU, from.PDU } page, err := s.rooms.Relations(ctx, roomserver.RelationsRequest{ RoomID: req.RoomID, EventID: req.EventID, User: req.User, RelType: req.RelType, EventType: req.EventType, Low: low, High: high, Direction: direction, Limit: limit, Recurse: req.Recurse, }) if err != nil { return RelationsResponse{}, err } chunk := page.Events if direction == storage.DirectionBackward { chunk = reversed(chunk) } if chunk == nil { chunk = []roomserver.ClientEvent{} } out := RelationsResponse{Chunk: chunk} if req.From != "" { out.PrevBatch = req.From } if page.Limited { if direction == storage.DirectionBackward { out.NextBatch = pduToken(page.Start).String() } else { out.NextBatch = pduToken(page.End).String() } } if req.Recurse { out.RecursionDepth = page.Depth } return out, nil } // ThreadsRequest is one page of a room's threads. type ThreadsRequest struct { RoomID string User string // Include is "all" or "participated". Include string // From is a token from a previous page's next_batch. From string Limit int } // ThreadsResponse is one page of a room's threads. type ThreadsResponse struct { Chunk []roomserver.ClientEvent `json:"chunk"` NextBatch string `json:"next_batch,omitempty"` } // threadTokenPrefix marks a /threads token, which is a position in thread // activity rather than a sync token. const threadTokenPrefix = "t" // Threads reads one page of a room's threads. func (s *API) Threads(ctx context.Context, req ThreadsRequest) (ThreadsResponse, error) { var participated bool switch req.Include { case "", "all": case "participated": participated = true default: return ThreadsResponse{}, errInvalidParamf("include must be \"all\" or \"participated\"") } var before int64 if req.From != "" { raw, ok := strings.CutPrefix(req.From, threadTokenPrefix) value, err := strconv.ParseInt(raw, 10, 64) if !ok || err != nil { return ThreadsResponse{}, errInvalidTokenf("%q is not a /threads token", req.From) } before = value } limit := req.Limit if limit <= 0 { limit = DefaultRelationsLimit } limit = min(limit, MaxTimelineLimit) page, err := s.rooms.Threads(ctx, roomserver.ThreadsRequest{ RoomID: req.RoomID, User: req.User, Participated: participated, Before: before, Limit: limit, }) if err != nil { return ThreadsResponse{}, err } out := ThreadsResponse{Chunk: page.Roots} if out.Chunk == nil { out.Chunk = []roomserver.ClientEvent{} } if page.Next != 0 { out.NextBatch = fmt.Sprintf("%s%d", threadTokenPrefix, page.Next) } return out, nil }