Skip to content

Commit 8eda011

Browse files
authored
refactor Filter class and add Filter test suite (#33)
1 parent b8f2d79 commit 8eda011

File tree

4 files changed

+513
-37
lines changed

4 files changed

+513
-37
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ venv/
22
__pycache__/
33
nostr.egg-info/
44
dist/
5-
nostr/_version.py
5+
nostr/_version.py
6+
.DS_Store

nostr/event.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def __init__(
2828
signature: str=None) -> None:
2929
if not isinstance(content, str):
3030
raise TypeError("Argument 'content' must be of type str")
31-
31+
3232
self.public_key = public_key
3333
self.content = content
3434
self.created_at = created_at or int(time.time())

nostr/filter.py

Lines changed: 87 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,121 @@
11
from collections import UserList
2-
from .event import Event
2+
from typing import List
3+
4+
from .event import Event, EventKind
5+
6+
37

48
class Filter:
9+
"""
10+
NIP-01 filtering.
11+
12+
Explicitly supports "#e" and "#p" tag filters via `event_refs` and `pubkey_refs`.
13+
14+
Arbitrary NIP-12 single-letter tag filters are also supported via `add_arbitrary_tag`.
15+
If a particular single-letter tag gains prominence, explicit support should be
16+
added. For example:
17+
# arbitrary tag
18+
filter.add_arbitrary_tag('t', [hashtags])
19+
20+
# promoted to explicit support
21+
Filter(hashtag_refs=[hashtags])
22+
"""
523
def __init__(
624
self,
7-
ids: "list[str]"=None,
8-
kinds: "list[int]"=None,
9-
authors: "list[str]"=None,
10-
since: int=None,
11-
until: int=None,
12-
tags: "dict[str, list[str]]"=None,
13-
limit: int=None) -> None:
14-
self.IDs = ids
25+
event_ids: List[str] = None,
26+
kinds: List[EventKind] = None,
27+
authors: List[str] = None,
28+
since: int = None,
29+
until: int = None,
30+
event_refs: List[str] = None, # the "#e" attr; list of event ids referenced in an "e" tag
31+
pubkey_refs: List[str] = None, # The "#p" attr; list of pubkeys referenced in a "p" tag
32+
limit: int = None) -> None:
33+
self.event_ids = event_ids
1534
self.kinds = kinds
1635
self.authors = authors
1736
self.since = since
1837
self.until = until
19-
self.tags = tags
38+
self.event_refs = event_refs
39+
self.pubkey_refs = pubkey_refs
2040
self.limit = limit
2141

42+
self.tags = {}
43+
if self.event_refs:
44+
self.add_arbitrary_tag('e', self.event_refs)
45+
if self.pubkey_refs:
46+
self.add_arbitrary_tag('p', self.pubkey_refs)
47+
48+
49+
def add_arbitrary_tag(self, tag: str, values: list):
50+
"""
51+
Filter on any arbitrary tag with explicit handling for NIP-01 and NIP-12
52+
single-letter tags.
53+
"""
54+
# NIP-01 'e' and 'p' tags and any NIP-12 single-letter tags must be prefixed with "#"
55+
tag_key = tag if len(tag) > 1 else f"#{tag}"
56+
self.tags[tag_key] = values
57+
58+
2259
def matches(self, event: Event) -> bool:
23-
if self.IDs != None and event.id not in self.IDs:
60+
if self.event_ids is not None and event.id not in self.event_ids:
2461
return False
25-
if self.kinds != None and event.kind not in self.kinds:
62+
if self.kinds is not None and event.kind not in self.kinds:
2663
return False
27-
if self.authors != None and event.public_key not in self.authors:
64+
if self.authors is not None and event.public_key not in self.authors:
2865
return False
29-
if self.since != None and event.created_at < self.since:
66+
if self.since is not None and event.created_at < self.since:
3067
return False
31-
if self.until != None and event.created_at > self.until:
68+
if self.until is not None and event.created_at > self.until:
3269
return False
33-
if self.tags != None and len(event.tags) == 0:
70+
if (self.event_refs is not None or self.pubkey_refs is not None) and len(event.tags) == 0:
3471
return False
35-
if self.tags != None:
36-
e_tag_identifiers = [e_tag[0] for e_tag in event.tags]
72+
73+
if self.tags:
74+
e_tag_identifiers = set([e_tag[0] for e_tag in event.tags])
3775
for f_tag, f_tag_values in self.tags.items():
38-
if f_tag[1:] not in e_tag_identifiers:
76+
# Omit any NIP-01 or NIP-12 "#" chars on single-letter tags
77+
f_tag = f_tag.replace("#", "")
78+
79+
if f_tag not in e_tag_identifiers:
80+
# Event is missing a tag type that we're looking for
3981
return False
82+
83+
# Multiple values within f_tag_values are treated as OR search; an Event
84+
# needs to match only one.
85+
# Note: an Event could have multiple entries of the same tag type
86+
# (e.g. a reply to multiple people) so we have to check all of them.
87+
match_found = False
4088
for e_tag in event.tags:
41-
if e_tag[1] not in f_tag_values:
42-
return False
43-
89+
if e_tag[0] == f_tag and e_tag[1] in f_tag_values:
90+
match_found = True
91+
break
92+
if not match_found:
93+
return False
94+
4495
return True
4596

97+
4698
def to_json_object(self) -> dict:
4799
res = {}
48-
if self.IDs != None:
49-
res["ids"] = self.IDs
50-
if self.kinds != None:
100+
if self.event_ids is not None:
101+
res["ids"] = self.event_ids
102+
if self.kinds is not None:
51103
res["kinds"] = self.kinds
52-
if self.authors != None:
104+
if self.authors is not None:
53105
res["authors"] = self.authors
54-
if self.since != None:
106+
if self.since is not None:
55107
res["since"] = self.since
56-
if self.until != None:
108+
if self.until is not None:
57109
res["until"] = self.until
58-
if self.tags != None:
59-
for tag, values in self.tags.items():
60-
res[tag] = values
61-
if self.limit != None:
110+
if self.limit is not None:
62111
res["limit"] = self.limit
112+
if self.tags:
113+
res.update(self.tags)
63114

64115
return res
65-
116+
117+
118+
66119
class Filters(UserList):
67120
def __init__(self, initlist: "list[Filter]"=[]) -> None:
68121
super().__init__(initlist)
@@ -75,5 +128,4 @@ def match(self, event: Event):
75128
return False
76129

77130
def to_json_array(self) -> list:
78-
return [filter.to_json_object() for filter in self.data]
79-
131+
return [filter.to_json_object() for filter in self.data]

0 commit comments

Comments
 (0)