Compare commits
No commits in common. "main" and "v1.1.0-aio.3" have entirely different histories.
main
...
v1.1.0-aio
8 changed files with 76 additions and 289 deletions
|
|
@ -24,8 +24,8 @@
|
|||
- [x] **NIP-04**: Encrypted Direct Message
|
||||
- if `AUTH` enabled: send only to the intended target
|
||||
- [x] **NIP-09**: Event Deletion
|
||||
- [x] `e` tags: delete events by id
|
||||
- [x] `a` tags: delete replaceable / addressable events by `kind:pubkey:d`
|
||||
- [x] 'e' tags: Delete regular events by event ID
|
||||
- [x] 'a' tags: Delete addressable events by address (kind:pubkey:d-identifier)
|
||||
- [x] **NIP-11**: Relay Information Document
|
||||
- > **Note**: the endpoint is NOT on the root level of the domain. It also includes a path (eg https://lnbits.link/nostrrelay/)
|
||||
- [ ] **NIP-12**: Generic Tag Queries
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "Nostr Relay",
|
||||
"version": "1.1.0-aio.4",
|
||||
"version": "1.1.0-aio.3",
|
||||
"short_description": "One click launch your own relay!",
|
||||
"tile": "/nostrrelay/static/image/nostrrelay.png",
|
||||
"min_lnbits_version": "1.4.0",
|
||||
|
|
|
|||
|
|
@ -113,8 +113,7 @@ class NostrClientConnection:
|
|||
await self.broadcast_event(self, e)
|
||||
else:
|
||||
logger.warning(
|
||||
"[NOSTRRELAY CLIENT] ❌ No broadcast_event callback available "
|
||||
f"for event {e.id}"
|
||||
f"[NOSTRRELAY CLIENT] ❌ No broadcast_event callback available for event {e.id}"
|
||||
)
|
||||
|
||||
async def _handle_message(self, data: list) -> list:
|
||||
|
|
@ -230,61 +229,66 @@ class NostrClientConnection:
|
|||
await self.websocket.send_text(json.dumps(data))
|
||||
|
||||
async def _handle_delete_event(self, event: NostrEvent):
|
||||
"""NIP-09: `e` tags name events by id, `a` tags name replaceable or
|
||||
addressable events by `kind:pubkey:d`. Only the author's own events
|
||||
are touched; for `a` tags only versions up to the deletion request's
|
||||
`created_at` are removed, so a later re-publication survives."""
|
||||
ids_to_delete: list[str] = []
|
||||
# NIP 09 - Handle both regular events (e tags) and parameterized replaceable events (a tags)
|
||||
|
||||
# Get event IDs from 'e' tags (for regular events)
|
||||
event_ids = [t[1] for t in event.tags if t[0] == "e"]
|
||||
|
||||
# Get event addresses from 'a' tags (for parameterized replaceable events)
|
||||
event_addresses = [t[1] for t in event.tags if t[0] == "a"]
|
||||
|
||||
ids_to_delete = []
|
||||
|
||||
# Handle regular event deletions (e tags)
|
||||
if event_ids:
|
||||
nostr_filter = NostrFilter(authors=[event.pubkey], ids=event_ids)
|
||||
events_to_delete = await get_events(self.relay_id, nostr_filter, False)
|
||||
ids_to_delete += [e.id for e in events_to_delete if not e.is_delete_event]
|
||||
|
||||
for address in (t[1] for t in event.tags if t[0] == "a"):
|
||||
address_filter = self._address_filter(address, event.pubkey)
|
||||
if not address_filter:
|
||||
logger.debug(f"NIP-09: ignoring address '{address}'")
|
||||
continue
|
||||
# An addressable kind with an empty `d` names the version whose
|
||||
# `d` tag is empty or missing, which no tag join can express, so
|
||||
# fetch tags and filter here.
|
||||
empty_d = address_filter.is_addressable and not address_filter.d
|
||||
events_to_delete = await get_events(
|
||||
self.relay_id, address_filter, include_tags=empty_d
|
||||
ids_to_delete.extend(
|
||||
[e.id for e in events_to_delete if not e.is_delete_event]
|
||||
)
|
||||
ids_to_delete += [
|
||||
e.id
|
||||
for e in events_to_delete
|
||||
if e.created_at <= event.created_at
|
||||
and (not empty_d or not e.has_tag_value_other_than("d", ""))
|
||||
]
|
||||
|
||||
# Handle parameterized replaceable event deletions (a tags)
|
||||
if event_addresses:
|
||||
for addr in event_addresses:
|
||||
# Parse address format: kind:pubkey:d-tag
|
||||
parts = addr.split(":")
|
||||
if len(parts) == 3:
|
||||
kind_str, addr_pubkey, d_tag = parts
|
||||
try:
|
||||
kind = int(kind_str)
|
||||
# Only delete if the address pubkey matches the deletion event author
|
||||
if addr_pubkey == event.pubkey:
|
||||
# NOTE: Use "#d" alias, not "d" directly (Pydantic Field alias)
|
||||
nostr_filter = NostrFilter(
|
||||
authors=[addr_pubkey],
|
||||
kinds=[kind],
|
||||
**{"#d": [d_tag]}, # Use alias to set d field
|
||||
)
|
||||
events_to_delete = await get_events(
|
||||
self.relay_id, nostr_filter, False
|
||||
)
|
||||
ids_to_delete.extend(
|
||||
[
|
||||
e.id
|
||||
for e in events_to_delete
|
||||
if not e.is_delete_event
|
||||
]
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Deletion request pubkey mismatch: {addr_pubkey} != {event.pubkey}"
|
||||
)
|
||||
except ValueError:
|
||||
logger.warning(f"Invalid kind in address: {addr}")
|
||||
else:
|
||||
logger.warning(
|
||||
f"Invalid address format (expected kind:pubkey:d-tag): {addr}"
|
||||
)
|
||||
|
||||
# Only mark events as deleted if we found specific IDs
|
||||
if ids_to_delete:
|
||||
await mark_events_deleted(self.relay_id, NostrFilter(ids=ids_to_delete))
|
||||
|
||||
@staticmethod
|
||||
def _address_filter(address: str, author: str) -> NostrFilter | None:
|
||||
# `kind:pubkey:d`; the `d` value may itself contain ':'.
|
||||
parts = address.split(":", 2)
|
||||
if len(parts) != 3 or not parts[0].isdigit():
|
||||
return None
|
||||
kind, pubkey, d_tag = int(parts[0]), parts[1], parts[2]
|
||||
if pubkey != author:
|
||||
return None
|
||||
# NIP-01: replaceable are 0, 3 and 10000-19999; addressable 30000-39999.
|
||||
replaceable = kind in (0, 3) or 10000 <= kind < 20000
|
||||
addressable = 30000 <= kind < 40000
|
||||
if not (replaceable or addressable):
|
||||
return None
|
||||
if d_tag:
|
||||
# `d` is a Pydantic alias field; it must be set through the alias.
|
||||
return NostrFilter(
|
||||
authors=[author], kinds=[kind], **{"#d": [d_tag]} # type: ignore[arg-type]
|
||||
)
|
||||
return NostrFilter(authors=[author], kinds=[kind])
|
||||
|
||||
async def _handle_request(
|
||||
self, subscription_id: str, nostr_filter: NostrFilter
|
||||
) -> list:
|
||||
|
|
|
|||
|
|
@ -26,17 +26,10 @@ class NostrEvent(BaseModel):
|
|||
size: int = 0
|
||||
|
||||
def nostr_dict(self) -> dict:
|
||||
# Only the NIP-01 fields go on the wire; storage columns such as
|
||||
# relay_id, publisher and size must not leak into EVENT responses.
|
||||
return {
|
||||
"id": self.id,
|
||||
"pubkey": self.pubkey,
|
||||
"created_at": self.created_at,
|
||||
"kind": self.kind,
|
||||
"tags": self.tags,
|
||||
"content": self.content,
|
||||
"sig": self.sig,
|
||||
}
|
||||
_nostr_dict = dict(self)
|
||||
_nostr_dict.pop("relay_id")
|
||||
_nostr_dict.pop("publisher")
|
||||
return _nostr_dict
|
||||
|
||||
def serialize(self) -> list:
|
||||
return [0, self.pubkey, self.created_at, self.kind, self.tags, self.content]
|
||||
|
|
@ -127,8 +120,5 @@ class NostrEvent(BaseModel):
|
|||
def has_tag_value(self, tag_name: str, tag_value: str) -> bool:
|
||||
return tag_value in self.tag_values(tag_name)
|
||||
|
||||
def has_tag_value_other_than(self, tag_name: str, value: str) -> bool:
|
||||
return any(t[0] == tag_name and t[1] != value for t in self.tags if len(t) > 1)
|
||||
|
||||
def is_direct_message_for_pubkey(self, pubkey: str) -> bool:
|
||||
return self.is_direct_message and self.has_tag_value("p", pubkey)
|
||||
|
|
|
|||
|
|
@ -64,10 +64,6 @@ class NostrFilter(BaseModel):
|
|||
and (not self.until)
|
||||
)
|
||||
|
||||
@property
|
||||
def is_addressable(self) -> bool:
|
||||
return all(30000 <= k < 40000 for k in self.kinds) and len(self.kinds) > 0
|
||||
|
||||
def enforce_limit(self, limit: int):
|
||||
if not self.limit or self.limit > limit:
|
||||
self.limit = limit
|
||||
|
|
|
|||
|
|
@ -37,18 +37,6 @@ class MockWebSocket(WebSocket):
|
|||
async def send_text(self, data: str):
|
||||
self.sent_messages.append(data)
|
||||
|
||||
async def wait_for_messages(self, count: int, timeout: float = 5.0):
|
||||
"""Wait until at least `count` messages were sent to this socket."""
|
||||
loop = asyncio.get_running_loop()
|
||||
deadline = loop.time() + timeout
|
||||
while len(self.sent_messages) < count:
|
||||
if loop.time() > deadline:
|
||||
raise AssertionError(
|
||||
f"Expected {count} messages, got {len(self.sent_messages)} "
|
||||
f"after {timeout}s: {self.sent_messages}"
|
||||
)
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
async def wire_mock_data(self, data: dict):
|
||||
await self.fake_wire.put(dumps(data))
|
||||
|
||||
|
|
@ -110,7 +98,7 @@ async def alice_wires_meta_and_post01(ws_alice: MockWebSocket):
|
|||
await ws_alice.wire_mock_data(alice["post01"])
|
||||
await ws_alice.wire_mock_data(alice["post01"])
|
||||
await ws_alice.wire_mock_data(alice["meta_update"])
|
||||
await ws_alice.wait_for_messages(4)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
assert (
|
||||
len(ws_alice.sent_messages) == 4
|
||||
|
|
@ -125,6 +113,8 @@ async def alice_wires_meta_and_post01(ws_alice: MockWebSocket):
|
|||
alice["meta_update_response"]
|
||||
), "Alice: Expected confirmation for meta update"
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
|
||||
async def bob_wires_meta_and_folows_alice(ws_bob: MockWebSocket):
|
||||
ws_bob.sent_messages.clear()
|
||||
|
|
@ -132,7 +122,8 @@ async def bob_wires_meta_and_folows_alice(ws_bob: MockWebSocket):
|
|||
await ws_bob.wire_mock_data(bob["meta"])
|
||||
await ws_bob.wire_mock_data(bob["request_meta_alice"])
|
||||
await ws_bob.wire_mock_data(bob["request_posts_alice"])
|
||||
await ws_bob.wait_for_messages(5)
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
assert len(ws_bob.sent_messages) == 5, "Bob: Expected 5 confirmations to be sent"
|
||||
assert ws_bob.sent_messages[0] == dumps(
|
||||
|
|
@ -158,9 +149,9 @@ async def bob_wires_contact_list(ws_alice: MockWebSocket, ws_bob: MockWebSocket)
|
|||
|
||||
await ws_bob.wire_mock_data(bob["contact_list_create"])
|
||||
await ws_bob.wire_mock_data(bob["contact_list_update"])
|
||||
await ws_bob.wait_for_messages(2)
|
||||
await asyncio.sleep(0.1)
|
||||
await ws_alice.wire_mock_data(alice["subscribe_to_bob_contact_list"])
|
||||
await ws_alice.wait_for_messages(2)
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert (
|
||||
len(ws_bob.sent_messages) == 2
|
||||
|
|
@ -190,8 +181,7 @@ async def alice_wires_post02_____bob_is_notified(
|
|||
ws_alice.sent_messages.clear()
|
||||
|
||||
await ws_alice.wire_mock_data(alice["post02"])
|
||||
await ws_alice.wait_for_messages(1)
|
||||
await ws_bob.wait_for_messages(1)
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert ws_alice.sent_messages[0] == dumps(
|
||||
alice["post02_response_ok"]
|
||||
|
|
@ -208,9 +198,9 @@ async def bob_likes_post01_____alice_subscribes_and_receives_notifications(
|
|||
ws_bob.sent_messages.clear()
|
||||
|
||||
await ws_bob.wire_mock_data(bob["like_post01"])
|
||||
await ws_bob.wait_for_messages(1)
|
||||
await asyncio.sleep(0.1)
|
||||
await ws_alice.wire_mock_data(alice["subscribe_reactions_to_me"])
|
||||
await ws_alice.wait_for_messages(2)
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert (
|
||||
len(ws_alice.sent_messages) == 2
|
||||
|
|
@ -237,8 +227,7 @@ async def bob_likes_and_comments_____alice_receives_notifications(
|
|||
|
||||
await ws_bob.wire_mock_data(bob["like_post02"])
|
||||
await ws_bob.wire_mock_data(bob["comment_on_alice_post01"])
|
||||
await ws_bob.wait_for_messages(2)
|
||||
await ws_alice.wait_for_messages(2)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
assert (
|
||||
len(ws_bob.sent_messages) == 2
|
||||
|
|
@ -273,8 +262,7 @@ async def bob_writes_to_alice(ws_alice: MockWebSocket, ws_bob: MockWebSocket):
|
|||
ws_bob.sent_messages.clear()
|
||||
|
||||
await ws_bob.wire_mock_data(bob["direct_message01"])
|
||||
await ws_bob.wait_for_messages(1)
|
||||
await ws_alice.wait_for_messages(1)
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert (
|
||||
len(ws_bob.sent_messages) == 1
|
||||
|
|
@ -299,7 +287,7 @@ async def alice_writes_to_bob(ws_alice: MockWebSocket, ws_bob: MockWebSocket):
|
|||
ws_bob.sent_messages.clear()
|
||||
|
||||
await ws_alice.wire_mock_data(alice["direct_message01"])
|
||||
await ws_alice.wait_for_messages(1)
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert (
|
||||
len(ws_alice.sent_messages) == 1
|
||||
|
|
@ -310,7 +298,7 @@ async def alice_writes_to_bob(ws_alice: MockWebSocket, ws_bob: MockWebSocket):
|
|||
assert len(ws_bob.sent_messages) == 0, "Bob: no subscription, no message"
|
||||
|
||||
await ws_bob.wire_mock_data(bob["subscribe_to_direct_messages"])
|
||||
await ws_bob.wait_for_messages(2)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
assert (
|
||||
len(ws_bob.sent_messages) == 2
|
||||
|
|
@ -333,7 +321,7 @@ async def alice_deletes_post01__bob_is_notified(
|
|||
):
|
||||
ws_bob.sent_messages.clear()
|
||||
await ws_bob.wire_mock_data(bob["request_posts_alice"])
|
||||
await ws_bob.wait_for_messages(3)
|
||||
await asyncio.sleep(0.1)
|
||||
assert (
|
||||
len(ws_bob.sent_messages) == 3
|
||||
), "Bob: Expected two posts from Alice plus and EOSE"
|
||||
|
|
@ -342,10 +330,9 @@ async def alice_deletes_post01__bob_is_notified(
|
|||
ws_bob.sent_messages.clear()
|
||||
|
||||
await ws_bob.wire_mock_data(bob["subscribe_to_delete_from_alice"])
|
||||
await ws_bob.wait_for_messages(1)
|
||||
await asyncio.sleep(0.1)
|
||||
await ws_alice.wire_mock_data(alice["delete_post01"])
|
||||
await ws_alice.wait_for_messages(1)
|
||||
await ws_bob.wait_for_messages(2)
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
assert (
|
||||
len(ws_alice.sent_messages) == 1
|
||||
|
|
@ -366,7 +353,7 @@ async def alice_deletes_post01__bob_is_notified(
|
|||
|
||||
ws_bob.sent_messages.clear()
|
||||
await ws_bob.wire_mock_data(bob["request_posts_alice"])
|
||||
await ws_bob.wait_for_messages(2)
|
||||
await asyncio.sleep(0.1)
|
||||
assert (
|
||||
len(ws_bob.sent_messages) == 2
|
||||
), "Bob: Expected one posts from Alice plus and EOSE"
|
||||
|
|
|
|||
|
|
@ -1,137 +0,0 @@
|
|||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from ..crud import create_event, get_events
|
||||
from ..relay.client_connection import NostrClientConnection
|
||||
from ..relay.event import NostrEvent
|
||||
from ..relay.filter import NostrFilter
|
||||
from ..relay.relay import RelaySpec
|
||||
|
||||
ALICE = "a" * 64
|
||||
BOB = "b" * 64
|
||||
SIG = "0" * 128
|
||||
|
||||
|
||||
def _connection(relay_id: str) -> NostrClientConnection:
|
||||
conn = NostrClientConnection(relay_id=relay_id, websocket=MagicMock())
|
||||
conn.get_client_config = lambda: RelaySpec()
|
||||
conn._send_msg = AsyncMock() # type: ignore[method-assign]
|
||||
return conn
|
||||
|
||||
|
||||
def _event(relay_id, id_, pubkey, kind, created_at, tags=None) -> NostrEvent:
|
||||
return NostrEvent(
|
||||
id=id_.rjust(64, "0"),
|
||||
relay_id=relay_id,
|
||||
publisher=pubkey,
|
||||
pubkey=pubkey,
|
||||
created_at=created_at,
|
||||
kind=kind,
|
||||
tags=tags or [],
|
||||
content="",
|
||||
sig=SIG,
|
||||
)
|
||||
|
||||
|
||||
async def _remaining(relay_id: str) -> set[str]:
|
||||
events = await get_events(relay_id, NostrFilter(kinds=[1, 3, 30023]), False)
|
||||
return {e.id.lstrip("0") or "0" for e in events}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_tag_deletes_only_the_addressed_event():
|
||||
relay_id = "nip09_a_tag"
|
||||
await create_event(_event(relay_id, "1", ALICE, 30023, 100, [["d", "one"]]))
|
||||
await create_event(_event(relay_id, "2", ALICE, 30023, 100, [["d", "two"]]))
|
||||
await create_event(_event(relay_id, "3", BOB, 30023, 100, [["d", "one"]]))
|
||||
|
||||
await _connection(relay_id)._handle_delete_event(
|
||||
_event(relay_id, "9", ALICE, 5, 200, [["a", f"30023:{ALICE}:one"]])
|
||||
)
|
||||
|
||||
assert await _remaining(relay_id) == {"2", "3"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_tag_keeps_versions_newer_than_the_request():
|
||||
relay_id = "nip09_until"
|
||||
await create_event(_event(relay_id, "1", ALICE, 30023, 100, [["d", "one"]]))
|
||||
await create_event(_event(relay_id, "2", ALICE, 30023, 300, [["d", "one"]]))
|
||||
|
||||
await _connection(relay_id)._handle_delete_event(
|
||||
_event(relay_id, "9", ALICE, 5, 200, [["a", f"30023:{ALICE}:one"]])
|
||||
)
|
||||
|
||||
assert await _remaining(relay_id) == {"2"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_tag_for_replaceable_kind_has_empty_d():
|
||||
relay_id = "nip09_replaceable"
|
||||
await create_event(_event(relay_id, "1", ALICE, 3, 100))
|
||||
await create_event(_event(relay_id, "2", ALICE, 1, 100))
|
||||
|
||||
await _connection(relay_id)._handle_delete_event(
|
||||
_event(relay_id, "9", ALICE, 5, 200, [["a", f"3:{ALICE}:"]])
|
||||
)
|
||||
|
||||
assert await _remaining(relay_id) == {"2"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_tag_for_non_addressable_kind_is_ignored():
|
||||
relay_id = "nip09_kind1"
|
||||
await create_event(_event(relay_id, "1", ALICE, 1, 100))
|
||||
|
||||
await _connection(relay_id)._handle_delete_event(
|
||||
_event(relay_id, "9", ALICE, 5, 200, [["a", f"1:{ALICE}:"]])
|
||||
)
|
||||
|
||||
assert await _remaining(relay_id) == {"1"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_tag_d_value_may_contain_colons():
|
||||
relay_id = "nip09_colon"
|
||||
await create_event(_event(relay_id, "1", ALICE, 30023, 100, [["d", "a:b:c"]]))
|
||||
|
||||
await _connection(relay_id)._handle_delete_event(
|
||||
_event(relay_id, "9", ALICE, 5, 200, [["a", f"30023:{ALICE}:a:b:c"]])
|
||||
)
|
||||
|
||||
assert await _remaining(relay_id) == set()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_e_tag_deletes_only_own_events():
|
||||
relay_id = "nip09_e_tag"
|
||||
await create_event(_event(relay_id, "1", ALICE, 1, 100))
|
||||
await create_event(_event(relay_id, "2", BOB, 1, 100))
|
||||
|
||||
await _connection(relay_id)._handle_delete_event(
|
||||
_event(
|
||||
relay_id,
|
||||
"9",
|
||||
ALICE,
|
||||
5,
|
||||
200,
|
||||
[["e", "1".rjust(64, "0")], ["e", "2".rjust(64, "0")]],
|
||||
)
|
||||
)
|
||||
|
||||
assert await _remaining(relay_id) == {"2"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_tag_with_empty_d_on_addressable_kind_is_specific():
|
||||
relay_id = "nip09_empty_d"
|
||||
await create_event(_event(relay_id, "1", ALICE, 30023, 100))
|
||||
await create_event(_event(relay_id, "2", ALICE, 30023, 100, [["d", ""]]))
|
||||
await create_event(_event(relay_id, "3", ALICE, 30023, 100, [["d", "one"]]))
|
||||
|
||||
await _connection(relay_id)._handle_delete_event(
|
||||
_event(relay_id, "9", ALICE, 5, 200, [["a", f"30023:{ALICE}:"]])
|
||||
)
|
||||
|
||||
assert await _remaining(relay_id) == {"3"}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from ..relay.client_connection import NostrClientConnection
|
||||
from ..relay.relay import RelaySpec
|
||||
|
||||
RELAY_ID = "relay_req_filters"
|
||||
|
||||
|
||||
def _connection(spec: RelaySpec | None = None) -> NostrClientConnection:
|
||||
conn = NostrClientConnection(relay_id=RELAY_ID, websocket=MagicMock())
|
||||
conn.get_client_config = lambda: spec or RelaySpec()
|
||||
conn._send_msg = AsyncMock() # type: ignore[method-assign]
|
||||
return conn
|
||||
|
||||
|
||||
def _subscriptions(conn: NostrClientConnection) -> list[str | None]:
|
||||
return [f.subscription_id for f in conn.filters]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multi_filter_req_keeps_every_filter():
|
||||
conn = _connection()
|
||||
|
||||
await conn._handle_message(["REQ", "sub", {"kinds": [1]}, {"kinds": [3]}])
|
||||
|
||||
assert _subscriptions(conn) == ["sub", "sub"]
|
||||
assert sorted(f.kinds for f in conn.filters) == [[1], [3]]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_new_req_replaces_the_subscription():
|
||||
conn = _connection()
|
||||
await conn._handle_message(["REQ", "sub", {"kinds": [1]}, {"kinds": [3]}])
|
||||
await conn._handle_message(["REQ", "other", {"kinds": [7]}])
|
||||
|
||||
await conn._handle_message(["REQ", "sub", {"kinds": [0]}])
|
||||
|
||||
assert _subscriptions(conn) == ["other", "sub"]
|
||||
assert [f.kinds for f in conn.filters] == [[7], [0]]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_filter_cap_is_enforced():
|
||||
conn = _connection(RelaySpec(maxClientFilters=2))
|
||||
|
||||
await conn._handle_message(["REQ", "a", {"kinds": [1]}])
|
||||
await conn._handle_message(["REQ", "b", {"kinds": [1]}])
|
||||
responses = await conn._handle_message(["REQ", "c", {"kinds": [1]}])
|
||||
|
||||
assert _subscriptions(conn) == ["a", "b"]
|
||||
assert responses == [["NOTICE", "Maximum number of filters (2) exceeded."]]
|
||||
Loading…
Add table
Add a link
Reference in a new issue