package syncapi import ( "context" "github.com/dariuszjaniak/warp/server/internal/roomserver" "github.com/dariuszjaniak/warp/server/internal/storage" ) // GET /rooms/{roomID}/messages: a room's history, a page at a time. // // It is the other half of /sync. A sync hands a client the live stream and, // when a client has been away, a `limited` timeline with a gap in it; // /messages is how the client closes that gap, and how it scrolls back // through a room it has just opened. The tokens are the same ones — a sync's // prev_batch is a valid ?from= here, and an ?end= from here is a valid // ?since= for /sync — which is what makes the two compose. // MessagesRequest is one page of a room's history. type MessagesRequest struct { RoomID string // User is the full user ID asking, and Localpart is their localpart, // for resolving a filter ID. User string Localpart string // From is where to page from. Empty means the end of the room for a // backwards page and the start of it for a forwards one, which is what // a client opening a room sends. From string // To bounds the far end of the page. Empty is unbounded. To string // Direction is "b" to page backwards, which is what scrolling up does, // or "f" to page forwards. The spec requires it. Direction storage.TimelineDirection // Limit caps the events returned. Limit int // Filter is the ?filter= parameter: a filter ID or an inline // RoomEventFilter as JSON. Filter string } // MessagesResponse is one page. type MessagesResponse struct { // Start is the token this page began at, which is the ?from= the // client sent back to it. Start string `json:"start"` // End is the token for the next page. It is left out when the page // reached the end of what the client may see, which is how a client // knows to stop scrolling. End string `json:"end,omitempty"` Chunk []roomserver.ClientEvent `json:"chunk"` // State is the state of the events in the chunk: the member events of // their senders, when the filter asked for lazy loading. A client // needs them to render a name against a message from somebody who is // no longer in the room. State []roomserver.ClientEvent `json:"state,omitempty"` } // Messages answers one page of a room's history. func (s *API) Messages(ctx context.Context, req MessagesRequest) (MessagesResponse, error) { from, err := ParseToken(req.From) if err != nil { return MessagesResponse{}, errInvalidTokenf("from: %v", err) } to, err := ParseToken(req.To) if err != nil { return MessagesResponse{}, errInvalidTokenf("to: %v", err) } filter, err := s.messageFilter(ctx, req.Localpart, req.Filter) if err != nil { return MessagesResponse{}, err } direction := req.Direction if direction != storage.DirectionForward { // Backwards is the default: the spec makes `dir` required, but // every client that leaves it out means "scroll up". direction = storage.DirectionBackward } limit := req.Limit if limit <= 0 { limit = DefaultTimelineLimit } limit = min(limit, MaxTimelineLimit) // Which end of the window the client's two tokens are depends on which // way it is paging: scrolling up, `from` is the ceiling and `to` the // floor, and scrolling down it is the other way round. An unset bound // is zero, which the read already takes as "unbounded at that end" — // so paging backwards from the end of the room needs nothing special. low, high := from.PDU, to.PDU if direction == storage.DirectionBackward { low, high = to.PDU, from.PDU } timeline, err := s.rooms.Timeline(ctx, roomserver.TimelineRequest{ RoomID: req.RoomID, User: req.User, Low: low, High: high, Direction: direction, Limit: limit, Filter: filter.TimelineFilter(), }) if err != nil { return MessagesResponse{}, err } // The chunk is in the direction asked for: the spec says a backwards // page is in reverse chronological order, because that is the order a // client scrolling up consumes it in. The read returns events oldest // first — which is what /sync wants — so a backwards page is turned // round here. chunk := timeline.Events if direction == storage.DirectionBackward { chunk = reversed(chunk) } if chunk == nil { chunk = []roomserver.ClientEvent{} } out := MessagesResponse{ Start: pduToken(from.PDU).String(), Chunk: chunk, } // `from` is optional, and without it `start` has to name the end of // the room the page actually began at rather than position zero: a // client that paged backwards from the end and then used `start` to // page forwards again would otherwise be handed the whole room. if req.From == "" { if direction == storage.DirectionBackward { out.Start = pduToken(timeline.End).String() } else { out.Start = pduToken(timeline.Start).String() } } // `end` is left out at the end of the room, which is how a client // knows to stop asking. Reading backwards the far end is Start, the // position below the oldest event; reading forwards it is End. // // The read fetches one more row than the limit, so `Limited` is the // whole answer to "is there more": a page that came back exactly full // and happened to be the last one still correctly has no `end`. if timeline.Limited { if direction == storage.DirectionBackward { out.End = pduToken(timeline.Start).String() } else { out.End = pduToken(timeline.End).String() } } if filter.LazyLoadMembers() { state, err := s.rooms.MemberEvents(ctx, req.RoomID, sendersOf(chunk)) if err != nil { return MessagesResponse{}, err } out.State = state } return out, nil } // messageFilter resolves the ?filter= of /messages. // // The parameter is a RoomEventFilter here rather than the whole filter that // /sync takes — the spec defines it that way, because a filter about // presence or account data would mean nothing to a room's history. A filter // ID, though, names a full filter, so both shapes have to be accepted: an // inline one is wrapped into the room's timeline slot, and a stored one is // used as it stands. func (s *API) messageFilter(ctx context.Context, localpart, raw string) (Filter, error) { if raw == "" { return Filter{}, nil } if raw[0] != '{' { return s.filterFor(ctx, localpart, raw) } var roomEvents RoomEventFilter if err := unmarshalStrict([]byte(raw), &roomEvents); err != nil { return Filter{}, errInvalidParamf("filter: %v", err) } return Filter{Room: &RoomFilter{Timeline: &roomEvents}}, nil } // sendersOf lists the distinct senders of a chunk, in the order they first // appear. func sendersOf(events []roomserver.ClientEvent) []string { seen := make(map[string]struct{}, len(events)) out := make([]string, 0, len(events)) for _, e := range events { if _, dup := seen[e.Sender]; dup { continue } seen[e.Sender] = struct{}{} out = append(out, e.Sender) } return out }