Files
realy/openapi/http-filter.go
2025-06-24 18:39:32 +01:00

205 lines
6.1 KiB
Go

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
})
}