filter.go raw

   1  package nostr
   2  
   3  import (
   4  	"errors"
   5  	"strconv"
   6  	"strings"
   7  )
   8  
   9  // Filter is a NIP-01 subscription filter.
  10  //
  11  // Zero values mean "unset" for Since/Until/Limit; empty slices mean "no
  12  // constraint". Tags is keyed by the single-letter tag name (the filter key
  13  // "#e" is stored as "e").
  14  type Filter struct {
  15  	IDs     []string
  16  	Authors []string
  17  	Kinds   []int
  18  	Tags    map[string][]string
  19  	Since   int64
  20  	Until   int64
  21  	Limit   int
  22  	Search  string
  23  
  24  	// Graph is the optional _graph filter extension. When set the filter is a
  25  	// graph traversal request rather than a NIP-01 event query.
  26  	Graph *GraphQuery
  27  }
  28  
  29  var errBadFilter = errors.New("nostr: malformed filter")
  30  
  31  // Matches applies the filter to an event. The Search term is not evaluated
  32  // here: the store resolves it through the word index and only then calls
  33  // Matches on the candidates.
  34  func (f *Filter) Matches(e *Event) bool {
  35  	if len(f.IDs) > 0 && !containsString(f.IDs, Hex(e.ID[:])) {
  36  		return false
  37  	}
  38  	if len(f.Authors) > 0 && !containsString(f.Authors, Hex(e.PubKey[:])) {
  39  		return false
  40  	}
  41  	if len(f.Kinds) > 0 && !containsInt(f.Kinds, e.Kind) {
  42  		return false
  43  	}
  44  	if f.Since != 0 && e.CreatedAt < f.Since {
  45  		return false
  46  	}
  47  	if f.Until != 0 && e.CreatedAt > f.Until {
  48  		return false
  49  	}
  50  	for name, values := range f.Tags {
  51  		if len(name) != 1 {
  52  			// NIP-01 only defines single-letter tag filters. A multi-letter
  53  			// filter can never be satisfied by the index, so it cannot match.
  54  			return false
  55  		}
  56  		matched := false
  57  		for _, t := range e.Tags {
  58  			if t.Name() != name || len(t) < 2 {
  59  				continue
  60  			}
  61  			if containsString(values, t.Value()) {
  62  				matched = true
  63  				break
  64  			}
  65  		}
  66  		if !matched {
  67  			return false
  68  		}
  69  	}
  70  	return true
  71  }
  72  
  73  // Selectivity returns a rough ordering hint used to choose a driving index:
  74  // higher means the filter constrains more.
  75  func (f *Filter) HasTagFilters() bool { return len(f.Tags) > 0 }
  76  
  77  // SingleLetterTags returns the tag filters as a flat list of (name, values).
  78  func (f *Filter) SingleLetterTags() map[string][]string { return f.Tags }
  79  
  80  func containsString(haystack []string, needle string) bool {
  81  	for _, s := range haystack {
  82  		if s == needle {
  83  			return true
  84  		}
  85  	}
  86  	return false
  87  }
  88  
  89  func containsInt(haystack []int, needle int) bool {
  90  	for _, n := range haystack {
  91  		if n == needle {
  92  			return true
  93  		}
  94  	}
  95  	return false
  96  }
  97  
  98  // AppendJSON appends the filter's JSON form (used by tests and diagnostics).
  99  func (f *Filter) AppendJSON(dst []byte) []byte {
 100  	dst = append(dst, '{')
 101  	first := true
 102  	add := func() {
 103  		if !first {
 104  			dst = append(dst, ',')
 105  		}
 106  		first = false
 107  	}
 108  	if len(f.IDs) > 0 {
 109  		add()
 110  		dst = append(dst, `"ids":`...)
 111  		dst = appendStringArray(dst, f.IDs)
 112  	}
 113  	if len(f.Authors) > 0 {
 114  		add()
 115  		dst = append(dst, `"authors":`...)
 116  		dst = appendStringArray(dst, f.Authors)
 117  	}
 118  	if len(f.Kinds) > 0 {
 119  		add()
 120  		dst = append(dst, `"kinds":[`...)
 121  		for i, k := range f.Kinds {
 122  			if i > 0 {
 123  				dst = append(dst, ',')
 124  			}
 125  			dst = strconv.AppendInt(dst, int64(k), 10)
 126  		}
 127  		dst = append(dst, ']')
 128  	}
 129  	for name, values := range f.Tags {
 130  		add()
 131  		dst = append(dst, `"#`...)
 132  		dst = append(dst, name...)
 133  		dst = append(dst, `":`...)
 134  		dst = appendStringArray(dst, values)
 135  	}
 136  	if f.Since != 0 {
 137  		add()
 138  		dst = append(dst, `"since":`...)
 139  		dst = strconv.AppendInt(dst, f.Since, 10)
 140  	}
 141  	if f.Until != 0 {
 142  		add()
 143  		dst = append(dst, `"until":`...)
 144  		dst = strconv.AppendInt(dst, f.Until, 10)
 145  	}
 146  	if f.Limit != 0 {
 147  		add()
 148  		dst = append(dst, `"limit":`...)
 149  		dst = strconv.AppendInt(dst, int64(f.Limit), 10)
 150  	}
 151  	if f.Search != "" {
 152  		add()
 153  		dst = append(dst, `"search":`...)
 154  		dst = AppendJSONString(dst, f.Search)
 155  	}
 156  	if f.Graph != nil {
 157  		add()
 158  		dst = append(dst, `"_graph":[`...)
 159  		dst = AppendJSONString(dst, f.Graph.Seed)
 160  		dst = append(dst, ',')
 161  		dst = strconv.AppendInt(dst, int64(f.Graph.Depth), 10)
 162  		dst = append(dst, ',')
 163  		dst = AppendJSONString(dst, f.Graph.Edge)
 164  		dst = append(dst, ',')
 165  		dst = AppendJSONString(dst, f.Graph.Direction)
 166  		dst = append(dst, ']')
 167  	}
 168  	return append(dst, '}')
 169  }
 170  
 171  func appendStringArray(dst []byte, values []string) []byte {
 172  	dst = append(dst, '[')
 173  	for i, v := range values {
 174  		if i > 0 {
 175  			dst = append(dst, ',')
 176  		}
 177  		dst = AppendJSONString(dst, v)
 178  	}
 179  	return append(dst, ']')
 180  }
 181  
 182  // String renders the filter for logs.
 183  func (f *Filter) String() string { return string(f.AppendJSON(nil)) }
 184  
 185  // TagsKey normalizes a filter tag key: callers may pass "#e" or "e".
 186  func TagsKey(key string) string {
 187  	return strings.TrimPrefix(key, "#")
 188  }
 189  
 190  // Clone returns a deep copy so that a filter can be handed to another domain
 191  // without either side sharing slice or map storage.
 192  func (f *Filter) Clone() Filter {
 193  	out := *f
 194  	out.IDs = append([]string(nil), f.IDs...)
 195  	out.Authors = append([]string(nil), f.Authors...)
 196  	out.Kinds = append([]int(nil), f.Kinds...)
 197  	if f.Tags != nil {
 198  		out.Tags = make(map[string][]string, len(f.Tags))
 199  		for k, values := range f.Tags {
 200  			out.Tags[k] = append([]string(nil), values...)
 201  		}
 202  	}
 203  	if f.Graph != nil {
 204  		q := *f.Graph
 205  		out.Graph = &q
 206  	}
 207  	return out
 208  }
 209