package syncapi import ( "context" "github.com/dariuszjaniak/warp/server/internal/roomserver" "github.com/dariuszjaniak/warp/server/internal/storage" ) // GET /rooms/{roomID}/context/{eventID}: one event and what surrounds it. // // It is what a permalink opens, and what jumping to a search result or a // reply does. The answer is the event, a few events either side of it, the // tokens to go on paging in both directions, and the state of the room as // of the event — because a client that jumps into the middle of a room's // history has none of it. // ContextRequest is one context read. type ContextRequest struct { RoomID string EventID string User string Localpart string // Limit is how many events to return in total, including the event // itself. The spec's default is 10, and the events either side of the // target get half each. Limit int // Filter is the ?filter= parameter, a RoomEventFilter as for // /messages. Filter string } // ContextResponse is the event and its surroundings. type ContextResponse struct { // Start and End are the tokens to page backwards and forwards from. Start string `json:"start,omitempty"` End string `json:"end,omitempty"` // EventsBefore and EventsAfter are oldest-first and oldest-first // respectively — the spec says EventsBefore is in reverse // chronological order, which is what a client scrolling up consumes. EventsBefore []roomserver.ClientEvent `json:"events_before"` Event roomserver.ClientEvent `json:"event"` EventsAfter []roomserver.ClientEvent `json:"events_after"` // State is the room's state as of the event, which is what a client // needs to render the names and avatars of the people in the chunk. State []roomserver.ClientEvent `json:"state"` } // DefaultContextLimit is the spec's default for /context. const DefaultContextLimit = 10 // Context answers one context read. func (s *API) Context(ctx context.Context, req ContextRequest) (ContextResponse, error) { filter, err := s.messageFilter(ctx, req.Localpart, req.Filter) if err != nil { return ContextResponse{}, err } limit := req.Limit if limit <= 0 { limit = DefaultContextLimit } limit = min(limit, MaxTimelineLimit) // The target event is read first, and through the roomserver, so that // the visibility check happens before anything else: an event the user // may not see has to come back as unknown rather than as a context // with an empty middle. target, err := s.rooms.Event(ctx, req.RoomID, req.User, req.EventID) if err != nil { return ContextResponse{}, err } // The target's own position. The roomserver's timeline reads are // bounded by position rather than by event ID, so it has to be found // before either side can be read — which is one extra read, and the // reason /context costs more than /messages. position, err := s.rooms.EventPosition(ctx, req.RoomID, req.User, req.EventID) if err != nil { return ContextResponse{}, err } // Half the budget each side, with the odd one going backwards: a // client opening a permalink scrolls up from it more often than down. after := limit / 2 before := limit - after - 1 if before < 0 { before = 0 } out := ContextResponse{ Event: target, EventsBefore: []roomserver.ClientEvent{}, EventsAfter: []roomserver.ClientEvent{}, State: []roomserver.ClientEvent{}, } if before > 0 { earlier, err := s.rooms.Timeline(ctx, roomserver.TimelineRequest{ RoomID: req.RoomID, User: req.User, // Strictly before the target: High is inclusive, and the // target itself is reported separately. High: position - 1, Direction: storage.DirectionBackward, Limit: before, Filter: filter.TimelineFilter(), }) if err != nil { return ContextResponse{}, err } // The spec wants this half newest first: it is what a client // prepends as the user scrolls up. out.EventsBefore = reversed(earlier.Events) out.Start = pduToken(earlier.Start).String() } if out.Start == "" { out.Start = pduToken(position - 1).String() } if after > 0 { later, err := s.rooms.Timeline(ctx, roomserver.TimelineRequest{ RoomID: req.RoomID, User: req.User, // Strictly after the target: Low is exclusive. Low: position, Direction: storage.DirectionForward, Limit: after, Filter: filter.TimelineFilter(), }) if err != nil { return ContextResponse{}, err } out.EventsAfter = later.Events out.End = pduToken(later.End).String() } if out.End == "" { out.End = pduToken(position).String() } state, err := s.contextState(ctx, req, filter, out) if err != nil { return ContextResponse{}, err } out.State = state return out, nil } // contextState is the state a context read reports. // // With lazy loading it is the member events of everybody in the chunk, // which is all a client needs to render it. Without, it is the room's whole // state as of the event — which for a large room is most of the response, // and is why every client asks for lazy loading. func (s *API) contextState(ctx context.Context, req ContextRequest, filter Filter, resp ContextResponse) ([]roomserver.ClientEvent, error) { if filter.LazyLoadMembers() { chunk := make([]roomserver.ClientEvent, 0, len(resp.EventsBefore)+len(resp.EventsAfter)+1) chunk = append(chunk, resp.EventsBefore...) chunk = append(chunk, resp.Event) chunk = append(chunk, resp.EventsAfter...) return s.rooms.MemberEvents(ctx, req.RoomID, sendersOf(chunk)) } return s.rooms.StateBeforeEvent(ctx, req.RoomID, req.User, req.EventID) } // reversed returns a copy of events in the opposite order. func reversed(events []roomserver.ClientEvent) []roomserver.ClientEvent { out := make([]roomserver.ClientEvent, len(events)) for i, e := range events { out[len(events)-1-i] = e } return out }