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