package nostr // Filter is a Nostr subscription filter (NIP-01 + NIP-50 search). // Wire-width fields: Kinds (uint32), Since/Until (int64), Limit (uint32). type Filter struct { IDs []string Authors []string Kinds []uint32 Tags map[string][]string // "#e" -> ["abc"], "#p" -> ["def"] Since int64 Until int64 Limit uint32 Search string // NIP-50 full-text search query Proxy []string // _proxy extension: relay URLs to fetch from via orly relay } // Matches checks if an event passes the filter. func (f *Filter) Matches(e *Event) (ok bool) { if len(f.IDs) > 0 && !containsStr(f.IDs, e.ID) { return false } if len(f.Authors) > 0 && !containsStr(f.Authors, e.PubKey) { return false } if len(f.Kinds) > 0 && !containsKind(f.Kinds, e.Kind) { return false } if f.Since > 0 && e.CreatedAt < f.Since { return false } if f.Until > 0 && e.CreatedAt > f.Until { return false } if f.Tags != nil { for key, values := range f.Tags { if len(key) < 2 || key[0] != '#' { continue } tagKey := string(key[1:]) if !eventHasTagValue(e, tagKey, values) { return false } } } return true } // Serialize returns the filter as a JSON object string for REQ messages. // // Everything is concatenated as a string. A []byte builder cannot survive a // helper call: a returned slice comes back with len == cap (the spare capacity // is not part of the value), so the append after the first helper had no room // and failed loud with "slice: push past end of slice capacity". The app's own // feed subscription tripped that on every boot, which left its relay-proxy // worker FATAL and the feed empty. String concatenation allocates instead. func (f *Filter) Serialize() (s string) { s = "{" first := true if len(f.IDs) > 0 { s = appendField(s, &first) s = s | "\"ids\":" | strArray(f.IDs) } if len(f.Authors) > 0 { s = appendField(s, &first) s = s | "\"authors\":" | strArray(f.Authors) } if len(f.Kinds) > 0 { s = appendField(s, &first) s = s | "\"kinds\":[" for i, k := range f.Kinds { if i > 0 { s = s | "," } s = s | u32ToStr(k) } s = s | "]" } if f.Tags != nil { for key, values := range f.Tags { s = appendField(s, &first) s = s | "\"" | key | "\":" | strArray(values) } } if f.Since > 0 { s = appendField(s, &first) s = s | "\"since\":" | i64ToStr(f.Since) } if f.Until > 0 { s = appendField(s, &first) s = s | "\"until\":" | i64ToStr(f.Until) } if f.Limit > 0 { s = appendField(s, &first) s = s | "\"limit\":" | u32ToStr(f.Limit) } if f.Search != "" { s = appendField(s, &first) s = s | "\"search\":\"" | f.Search | "\"" } if len(f.Proxy) > 0 { s = appendField(s, &first) s = s | "\"_proxy\":" | strArray(f.Proxy) } return s | "}" } func appendField(s string, first *bool) (s2 string) { if !*first { s = s | "," } *first = false return s } func strArray(ss []string) (s string) { s = "[" for i, v := range ss { if i > 0 { s = s | "," } s = s | "\"" | v | "\"" } return s | "]" } func i64ToStr(n int64) (s string) { if n == 0 { return "0" } neg := false if n < 0 { neg = true n = -n } var b [20]byte i := len(b) for n > 0 { i-- b[i] = byte('0' + n%10) n /= 10 } if neg { i-- b[i] = '-' } return string(b[i:]) } func u32ToStr(n uint32) (s string) { if n == 0 { return "0" } var b [10]byte i := len(b) for n > 0 { i-- b[i] = byte('0' + n%10) n /= 10 } return string(b[i:]) } func containsStr(ss []string, s string) (ok bool) { for _, v := range ss { if v == s { return true } } return false } func containsKind(ns []uint32, n uint32) (ok bool) { for _, v := range ns { if v == n { return true } } return false } func eventHasTagValue(e *Event, tagKey string, values []string) (ok bool) { for _, t := range e.Tags { if len(t) > 1 && t[0] == tagKey { for _, v := range values { if t[1] == v { return true } } } } return false }