From d4fd2467d691a69a0ba75348086424b9fb33a297 Mon Sep 17 00:00:00 2001 From: bndw Date: Sat, 7 Feb 2026 15:20:57 -0800 Subject: wip --- filter.go | 224 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 filter.go (limited to 'filter.go') diff --git a/filter.go b/filter.go new file mode 100644 index 0000000..dde04a5 --- /dev/null +++ b/filter.go @@ -0,0 +1,224 @@ +package nostr + +import ( + "encoding/json" + "strings" +) + +// Filter represents a subscription filter as defined in NIP-01. +type Filter struct { + IDs []string `json:"ids,omitempty"` + Kinds []int `json:"kinds,omitempty"` + Authors []string `json:"authors,omitempty"` + Tags map[string][]string `json:"-"` // Custom marshaling for #e, #p, etc. + Since *int64 `json:"since,omitempty"` + Until *int64 `json:"until,omitempty"` + Limit int `json:"limit,omitempty"` +} + +// MarshalJSON implements json.Marshaler for Filter. +// Converts Tags map to #e, #p format. +func (f Filter) MarshalJSON() ([]byte, error) { + // Create a map for custom marshaling + m := make(map[string]interface{}) + + if len(f.IDs) > 0 { + m["ids"] = f.IDs + } + if len(f.Kinds) > 0 { + m["kinds"] = f.Kinds + } + if len(f.Authors) > 0 { + m["authors"] = f.Authors + } + if f.Since != nil { + m["since"] = *f.Since + } + if f.Until != nil { + m["until"] = *f.Until + } + if f.Limit > 0 { + m["limit"] = f.Limit + } + + // Add tag filters with # prefix + for key, values := range f.Tags { + if len(values) > 0 { + m["#"+key] = values + } + } + + return json.Marshal(m) +} + +// UnmarshalJSON implements json.Unmarshaler for Filter. +// Extracts #e, #p format into Tags map. +func (f *Filter) UnmarshalJSON(data []byte) error { + // First unmarshal into a raw map + var raw map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + // Extract known fields + if v, ok := raw["ids"]; ok { + if err := json.Unmarshal(v, &f.IDs); err != nil { + return err + } + } + if v, ok := raw["kinds"]; ok { + if err := json.Unmarshal(v, &f.Kinds); err != nil { + return err + } + } + if v, ok := raw["authors"]; ok { + if err := json.Unmarshal(v, &f.Authors); err != nil { + return err + } + } + if v, ok := raw["since"]; ok { + var since int64 + if err := json.Unmarshal(v, &since); err != nil { + return err + } + f.Since = &since + } + if v, ok := raw["until"]; ok { + var until int64 + if err := json.Unmarshal(v, &until); err != nil { + return err + } + f.Until = &until + } + if v, ok := raw["limit"]; ok { + if err := json.Unmarshal(v, &f.Limit); err != nil { + return err + } + } + + // Extract tag filters (fields starting with #) + f.Tags = make(map[string][]string) + for key, value := range raw { + if strings.HasPrefix(key, "#") { + tagKey := strings.TrimPrefix(key, "#") + var values []string + if err := json.Unmarshal(value, &values); err != nil { + return err + } + f.Tags[tagKey] = values + } + } + + return nil +} + +// Matches checks if an event matches this filter. +func (f *Filter) Matches(event *Event) bool { + // Check IDs (prefix match) + if len(f.IDs) > 0 { + found := false + for _, id := range f.IDs { + if strings.HasPrefix(event.ID, id) { + found = true + break + } + } + if !found { + return false + } + } + + // Check authors (prefix match) + if len(f.Authors) > 0 { + found := false + for _, author := range f.Authors { + if strings.HasPrefix(event.PubKey, author) { + found = true + break + } + } + if !found { + return false + } + } + + // Check kinds + if len(f.Kinds) > 0 { + found := false + for _, kind := range f.Kinds { + if event.Kind == kind { + found = true + break + } + } + if !found { + return false + } + } + + // Check since + if f.Since != nil && event.CreatedAt < *f.Since { + return false + } + + // Check until + if f.Until != nil && event.CreatedAt > *f.Until { + return false + } + + // Check tag filters + for tagKey, values := range f.Tags { + if len(values) == 0 { + continue + } + found := false + for _, val := range values { + if event.Tags.ContainsValue(tagKey, val) { + found = true + break + } + } + if !found { + return false + } + } + + return true +} + +// Clone creates a deep copy of the filter. +func (f *Filter) Clone() *Filter { + clone := &Filter{ + Limit: f.Limit, + } + + if f.IDs != nil { + clone.IDs = make([]string, len(f.IDs)) + copy(clone.IDs, f.IDs) + } + if f.Kinds != nil { + clone.Kinds = make([]int, len(f.Kinds)) + copy(clone.Kinds, f.Kinds) + } + if f.Authors != nil { + clone.Authors = make([]string, len(f.Authors)) + copy(clone.Authors, f.Authors) + } + if f.Since != nil { + since := *f.Since + clone.Since = &since + } + if f.Until != nil { + until := *f.Until + clone.Until = &until + } + if f.Tags != nil { + clone.Tags = make(map[string][]string) + for k, v := range f.Tags { + clone.Tags[k] = make([]string, len(v)) + copy(clone.Tags[k], v) + } + } + + return clone +} -- cgit v1.2.3