tinyrelayfile
tinyrelay/protocol/nostr/filter.go/

protocol/nostr/filter.go raw
1package nostr
2
3import (
4 "encoding/json"
5 "fmt"
6 "strings"
7)
8
9// Filter describes the event fields and tags a subscription or query accepts.
10// Fields combine with AND; values within a list are alternatives. Nil IDs,
11// Authors and Kinds leave that field unrestricted, while an empty non-nil list
12// matches nothing. Tags uses names without the wire prefix #. Since and Until
13// are inclusive Unix timestamps. Limit controls a historical result batch;
14// Matches evaluates each event independently of that limit.
15type Filter struct {
16 IDs []string `json:"ids,omitempty"`
17 Authors []string `json:"authors,omitempty"`
18 Kinds []int `json:"kinds,omitempty"`
19 Tags map[string][]string `json:"-"`
20 Since *int64 `json:"since,omitempty"`
21 Until *int64 `json:"until,omitempty"`
22 Limit *int `json:"limit,omitempty"`
23 Search string `json:"search,omitempty"`
24}
25
26// ParseFilter decodes supported Nostr filter fields, including #tag keys, and
27// checks their JSON types.
28func ParseFilter(raw []byte) (Filter, error) {
29 var values map[string]json.RawMessage
30 if err := json.Unmarshal(raw, &values); err != nil {
31 return Filter{}, errorsFilter("filter must be an object")
32 }
33 f := Filter{Tags: make(map[string][]string)}
34 for key, value := range values {
35 switch {
36 case key == "ids", key == "authors":
37 if !isJSONArray(value) {
38 return Filter{}, errorsFilter(key + " must be a list of strings")
39 }
40 var out []string
41 if err := json.Unmarshal(value, &out); err != nil {
42 return Filter{}, errorsFilter(key + " must be a list of strings")
43 }
44 if key == "ids" {
45 f.IDs = out
46 } else {
47 f.Authors = out
48 }
49 case key == "kinds":
50 if !isJSONArray(value) {
51 return Filter{}, errorsFilter("kinds must be a list of integers")
52 }
53 if err := json.Unmarshal(value, &f.Kinds); err != nil {
54 return Filter{}, errorsFilter("kinds must be a list of integers")
55 }
56 case key == "since", key == "until":
57 var n int64
58 if err := json.Unmarshal(value, &n); err != nil {
59 return Filter{}, errorsFilter(key + " must be an integer")
60 }
61 if key == "since" {
62 f.Since = &n
63 } else {
64 f.Until = &n
65 }
66 case key == "limit":
67 var n int
68 if err := json.Unmarshal(value, &n); err != nil {
69 return Filter{}, errorsFilter("limit must be an integer")
70 }
71 f.Limit = &n
72 case key == "search":
73 if err := json.Unmarshal(value, &f.Search); err != nil {
74 return Filter{}, errorsFilter("search must be a string")
75 }
76 case len(key) == 2 && key[0] == '#':
77 if !isJSONArray(value) {
78 return Filter{}, errorsFilter(key + " must be a list of strings")
79 }
80 var out []string
81 if err := json.Unmarshal(value, &out); err != nil {
82 return Filter{}, errorsFilter(key + " must be a list of strings")
83 }
84 f.Tags[key[1:]] = out
85 }
86 }
87 return f, nil
88}
89
90// MarshalJSON encodes tag constraints with their Nostr #name wire keys.
91func (f Filter) MarshalJSON() ([]byte, error) {
92 values := make(map[string]json.RawMessage, 7+len(f.Tags))
93 if f.IDs != nil {
94 values["ids"] = mustRaw(f.IDs)
95 }
96 if f.Authors != nil {
97 values["authors"] = mustRaw(f.Authors)
98 }
99 if f.Kinds != nil {
100 values["kinds"] = mustRaw(f.Kinds)
101 }
102 if f.Since != nil {
103 values["since"] = mustRaw(f.Since)
104 }
105 if f.Until != nil {
106 values["until"] = mustRaw(f.Until)
107 }
108 if f.Limit != nil {
109 values["limit"] = mustRaw(f.Limit)
110 }
111 if f.Search != "" {
112 values["search"] = mustRaw(f.Search)
113 }
114 for key, tags := range f.Tags {
115 values["#"+key] = mustRaw(tags)
116 }
117 return json.Marshal(values)
118}
119
120// Matches reports whether e satisfies every populated constraint in f.
121// Search terms are matched against content without classifying event kinds.
122// Callers must enforce visibility and authorization before returning matches.
123func Matches(f Filter, e Event) bool {
124 if f.IDs != nil && !contains(f.IDs, e.ID) || f.Authors != nil && !contains(f.Authors, e.PubKey) || f.Kinds != nil && !containsInt(f.Kinds, e.Kind) {
125 return false
126 }
127 if f.Since != nil && e.CreatedAt < *f.Since || f.Until != nil && e.CreatedAt > *f.Until {
128 return false
129 }
130 for name, values := range f.Tags {
131 if !tagMatch(e, name, values) {
132 return false
133 }
134 }
135 terms := SearchTerms(f.Search)
136 if len(terms) == 0 {
137 return true
138 }
139
140 content := strings.ToLower(e.Content)
141 for _, term := range terms {
142 if !strings.Contains(content, strings.ToLower(term)) {
143 return false
144 }
145 }
146 return true
147}
148
149// SearchTerms splits a search query into terms and drops protocol-style
150// name:value tokens.
151func SearchTerms(query string) []string { return slicesFilter(strings.Fields(query)) }
152
153func slicesFilter(words []string) []string {
154 out := make([]string, 0, len(words))
155 for _, word := range words {
156 if !strings.Contains(word, ":") {
157 out = append(out, word)
158 }
159 }
160 return out
161}
162func contains(values []string, want string) bool {
163 for _, value := range values {
164 if value == want {
165 return true
166 }
167 }
168 return false
169}
170func containsInt(values []int, want int) bool {
171 for _, value := range values {
172 if value == want {
173 return true
174 }
175 }
176 return false
177}
178func tagMatch(e Event, name string, values []string) bool {
179 for _, tag := range e.Tags {
180 if len(tag) > 1 && tag[0] == name && contains(values, tag[1]) {
181 return true
182 }
183 }
184 return false
185}
186func mustRaw(value any) json.RawMessage { data, _ := json.Marshal(value); return data }
187func errorsFilter(message string) error { return fmt.Errorf("%s", message) }
188
189func isJSONArray(value json.RawMessage) bool {
190 trimmed := strings.TrimSpace(string(value))
191 return len(trimmed) > 0 && trimmed[0] == '['
192}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs