package roomserver import ( "context" "slices" "github.com/dariuszjaniak/warp/server/internal/matrix/event" "github.com/dariuszjaniak/warp/server/internal/storage" "github.com/dariuszjaniak/warp/server/internal/tracing" ) // Full-text search: the part of /search that decides where a user may // look. // // The index holds every searchable event of every room on the server, and // the query against it is the store's. What is decided here is the set of // rooms, each bounded to what the user may read of it — the same visible // range a timeline read is clamped to, so a user who left a room finds what // was said while they were in it and nothing since, and a user who joined a // `joined` room finds nothing from before they came. // SearchRequest is one page of a search. type SearchRequest struct { User string Term string // Keys are the fields to match; empty means all three. Keys []string // Rooms and NotRooms narrow the rooms searched, as a filter's rooms // and not_rooms do. Rooms []string NotRooms []string // Filter narrows by type and sender. Filter storage.TimelineFilter Order storage.SearchOrder Offset int Before int64 Limit int } // SearchResult is one event a search found. type SearchResult struct { Event ClientEvent // Position is the event's stream position, which a search by recency // pages by. Position int64 Rank float64 } // Search runs one page of a search. func (r *API) Search(ctx context.Context, req SearchRequest) (_ []SearchResult, _ int, err error) { ctx, span := tracing.Start(ctx, "roomserver.search") defer func() { tracing.End(span, err) }() memberships, err := r.UserMemberships(ctx, req.User, []string{event.MembershipJoin, event.MembershipLeave, event.MembershipBan}) if err != nil { return nil, 0, err } ranges := make([]storage.SearchRange, 0, len(memberships)) for _, m := range memberships { if len(req.Rooms) > 0 && !slices.Contains(req.Rooms, m.RoomID) { continue } if slices.Contains(req.NotRooms, m.RoomID) { continue } visible, err := r.VisibleRange(ctx, m.RoomID, req.User) if err != nil { return nil, 0, err } if !visible.Allowed { continue } ranges = append(ranges, storage.SearchRange{RoomID: m.RoomID, Low: visible.From, High: visible.To}) } if len(ranges) == 0 { return nil, 0, nil } found, total, err := r.store.Search(ctx, storage.SearchRequest{ Term: req.Term, Rooms: ranges, Keys: req.Keys, Filter: req.Filter, Order: req.Order, Offset: req.Offset, Before: req.Before, Limit: req.Limit, }) if err != nil { return nil, 0, err } out := make([]SearchResult, 0, len(found)) for _, f := range found { room, err := r.store.Room(ctx, f.Event.RoomID) if err != nil { return nil, 0, err } rules, err := r.rules(room) if err != nil { return nil, 0, err } e, err := parseEvent(f.Event, rules) if err != nil { return nil, 0, err } out = append(out, SearchResult{ Event: r.clientEvent(f.Event, e, f.Event.RoomID), Position: f.Event.StreamOrdering, Rank: f.Rank, }) } return out, total, nil }