package openapi import ( "bytes" "errors" "net/http" "sort" "github.com/danielgtaylor/huma/v2" "realy.lol/chk" "realy.lol/context" "realy.lol/event" "realy.lol/filter" "realy.lol/filters" "realy.lol/hex" "realy.lol/httpauth" "realy.lol/kind" "realy.lol/kinds" "realy.lol/log" "realy.lol/realy/helpers" "realy.lol/store" "realy.lol/tag" "realy.lol/tags" "realy.lol/timestamp" ) // SimpleFilter is the main parts of a filter.T that relate to event store indexes. type SimpleFilter struct { Kinds []int `json:"kinds,omitempty" doc:"array of kind numbers to match on"` Authors []string `json:"authors,omitempty" doc:"array of author pubkeys to match on (hex encoded)"` Tags [][]string `json:"tags,omitempty" doc:"array of tags to match on (first key of each '#x' and terms to match from the second field of the event tag)"` } // FilterInput is the parameters for a Filter HTTP API call. type FilterInput struct { Auth string `header:"Authorization" doc:"nostr nip-98 (and expiring variant)" required:"false"` Since int64 `query:"since" doc:"timestamp of the oldest events to return (inclusive)"` Until int64 `query:"until" doc:"timestamp of the newest events to return (inclusive)"` Limit uint `query:"limit" doc:"maximum number of results to return"` Sort string `query:"sort" enum:"asc,desc" default:"desc" doc:"sort order by created_at timestamp"` Body SimpleFilter `body:"filter" doc:"filter criteria to match for events to return"` } // ToFilter converts a SimpleFilter input to a regular nostr filter.T. func (fi FilterInput) ToFilter() (f *filter.T, err error) { f = filter.New() var ks []*kind.T for _, k := range fi.Body.Kinds { ks = append(ks, kind.New(k)) } f.Kinds = kinds.New(ks...) var as [][]byte for _, a := range fi.Body.Authors { var b []byte if b, err = hex.Dec(a); chk.E(err) { return } as = append(as, b) } f.Authors = tag.New(as...) var ts []*tag.T for _, t := range fi.Body.Tags { ts = append(ts, tag.New(t...)) } f.Tags = tags.New(ts...) if fi.Limit != 0 { f.Limit = &fi.Limit } if fi.Since != 0 { f.Since = timestamp.New(fi.Since) } if fi.Until != 0 { f.Until = timestamp.New(fi.Until) } return } // FilterOutput is a list of event Ids that match the query in the sort order requested. type FilterOutput struct { Body []string `doc:"list of event Ids that mach the query in the sort order requested"` } // RegisterFilter is the implementation of the HTTP API Filter method. func (x *Operations) RegisterFilter(api huma.API) { name := "Filter" description := "Search for events and receive a sorted list of event Ids (one of authors, kinds or tags must be present)" path := x.path + "/filter" scopes := []string{"user", "read"} method := http.MethodPost huma.Register(api, huma.Operation{ OperationID: name, Summary: name, Path: path, Method: method, Tags: []string{"events"}, Description: helpers.GenerateDescription(description, scopes), Security: []map[string][]string{{"auth": scopes}}, }, func(ctx context.T, input *FilterInput) (output *FilterOutput, err error) { r := ctx.Value("http-request").(*http.Request) remote := helpers.GetRemoteFromReq(r) log.I.S(input) var f *filter.T if f, err = input.ToFilter(); chk.E(err) { err = huma.Error422UnprocessableEntity(err.Error()) return } log.I.F("%s", f.Marshal(nil)) if len(input.Body.Authors) < 1 && len(input.Body.Kinds) < 1 && len(input.Body.Tags) < 1 { err = huma.Error400BadRequest( "cannot process filter with none of Authors/Kinds/Tags") return } var valid bool var pubkey []byte valid, pubkey, err = httpauth.CheckAuth(r) // if there is an error but not that the token is missing, or there is no error // but the signature is invalid, return error that request is unauthorized. if err != nil && !errors.Is(err, httpauth.ErrMissingKey) { err = huma.Error400BadRequest(err.Error()) return } err = nil if !valid { err = huma.Error401Unauthorized("Authorization header is invalid") return } allowed := filters.New(f) var accepted, modified bool allowed, accepted, modified = x.Server.AcceptReq(x.Context(), r, nil, filters.New(f), remote) if !accepted { err = huma.Error401Unauthorized("Auth to get access for this filter") return } else if modified { log.D.F("filter modified %s", allowed.F[0]) } if len(allowed.F) == 0 { err = huma.Error401Unauthorized("all kinds in event restricted; auth to get access for this filter") return } sto := x.Storage() var ok bool var quer store.Querier if quer, ok = sto.(store.Querier); !ok { err = huma.Error501NotImplemented("simple filter request not implemented") return } var evs []store.IdTsPk if evs, err = quer.QueryForIds(x.Context(), allowed.F[0]); chk.E(err) { err = huma.Error500InternalServerError("error querying for events", err) return } if input.Limit > 0 { evs = evs[:input.Limit] } switch input.Sort { case "asc": sort.Slice(evs, func(i, j int) bool { return evs[i].Ts < evs[j].Ts }) case "desc": sort.Slice(evs, func(i, j int) bool { return evs[i].Ts > evs[j].Ts }) } if len(pubkey) > 0 { // remove events from results if we find the user's mute list, that are present // on this list var mutes event.Ts if mutes, err = sto.QueryEvents(x.Context(), &filter.T{Authors: tag.New(pubkey), Kinds: kinds.New(kind.MuteList)}); !chk.E(err) { var mutePubs [][]byte for _, ev := range mutes { for _, t := range ev.Tags.ToSliceOfTags() { if bytes.Equal(t.Key(), []byte("p")) { var p []byte if p, err = hex.Dec(string(t.Value())); chk.E(err) { continue } mutePubs = append(mutePubs, p) } } } var tmp []store.IdTsPk next: for _, ev := range evs { for _, pk := range mutePubs { if bytes.Equal(ev.Pub, pk) { continue next } } tmp = append(tmp, ev) } // log.I.ToSliceOfBytes("done") evs = tmp } } output = &FilterOutput{} for _, ev := range evs { output.Body = append(output.Body, hex.Enc(ev.Id)) } return }) }