diff --git a/client_manager.py b/client_manager.py index 22c0825..dc11202 100644 --- a/client_manager.py +++ b/client_manager.py @@ -1,11 +1,10 @@ -import asyncio import json -from typing import Any, Callable, List, Union +from typing import Any, Callable, List from fastapi import WebSocket from loguru import logger -from .crud import create_event, get_events +from .crud import create_event, delete_events, get_events from .models import NostrEvent, NostrEventType, NostrFilter @@ -23,8 +22,7 @@ class NostrClientManager: async def broadcast_event(self, source: "NostrClientConnection", event: NostrEvent): for client in self.clients: if client != source: - sent = await client.notify_event(event) - print("### sent", sent, event.id) + await client.notify_event(event) class NostrClientConnection: @@ -38,13 +36,12 @@ class NostrClientConnection: await self.websocket.accept() while True: json_data = await self.websocket.receive_text() - print('### received', json_data) + print('### received: ', json_data) try: data = json.loads(json_data) resp = await self.__handle_message(data) for r in resp: - print('### sent query', json.dumps(r)) await self.websocket.send_text(json.dumps(r)) except Exception as e: logger.warning(e) @@ -53,7 +50,6 @@ class NostrClientConnection: for filter in self.filters: if filter.matches(event): resp = event.serialize_response(filter.subscription_id) - print('### sent notify', json.dumps(resp)) await self.websocket.send_text(json.dumps(resp)) return True return False @@ -76,18 +72,28 @@ class NostrClientConnection: return [] - async def __handle_event(self, e: "NostrEvent"): + async def __handle_event(self, e: NostrEvent): resp_nip20: List[Any] = ["ok", e.id] try: e.check_signature() await create_event("111", e) await self.broadcast_event(self, e) + if e.is_delete_event(): + await self.__delete_event(e) resp_nip20 += [True, ""] except Exception as ex: resp_nip20 += [False, f"error: failed to create event"] await self.websocket.send_text(json.dumps(resp_nip20)) + async def __delete_event(self, event: NostrEvent): + # NIP 09 + filter = NostrFilter(authors=[event.pubkey]) + filter.ids = [t[1] for t in event.tags if t[0] == "e"] + events_to_delete = await get_events("111", filter, False) + ids = [e.id for e in events_to_delete] + await delete_events("111", ids) + async def __handle_request(self, subscription_id: str, filter: NostrFilter) -> List: filter.subscription_id = subscription_id self.remove_filter(subscription_id) diff --git a/models.py b/models.py index db757e5..c7ec48f 100644 --- a/models.py +++ b/models.py @@ -48,6 +48,9 @@ class NostrEvent(BaseModel): id = hashlib.sha256(data.encode()).hexdigest() return id + def is_delete_event(self) -> bool: + return self.kind == 5 + def check_signature(self): event_id = self.event_id if self.id != event_id: diff --git a/tests/fixture/clients.json b/tests/fixture/clients.json index e5bfadf..08a056e 100644 --- a/tests/fixture/clients.json +++ b/tests/fixture/clients.json @@ -98,6 +98,38 @@ "28c96b6e80681c18a690e0e0dc6ca4e72b9d291d1d2576bc8949a07bb4bee225", true, "" + ], + "delete_post01": [ + "EVENT", + { + "kind": 5, + "content": "deleted", + "tags": [ + [ + "e", + "05741bda9079cdf66f3be977a4d31287366470d1337b1aeb09506da4fbf7cd85" + ], + [ + "e", + "mock-id", + "" + ], + [ + "e", + "bb34749ffd3eb0e393e54cc90b61a7dd5f34108d4931467861d20281c0b7daea" + ] + ], + "created_at": 1675427798, + "pubkey": "0b29ecc73ba400e5b4bd1e4cb0d8f524e9958345749197ca21c8da38d0622816", + "id": "2751f2ee0f894268c61300c5b1a1a434f49a33a467a6f4516f10a82a1848f093", + "sig": "8e972ba7f1ce9d11ba5d49fdd48db4a92ea999790eb604e6a7f01868a26a70a8e96e1f9e104d8f77a5aa7f29e94119e33117b4cc8a5ff9e50ec8c23eeccd94e9" + } + ], + "delete_post01_response": [ + "ok", + "2751f2ee0f894268c61300c5b1a1a434f49a33a467a6f4516f10a82a1848f093", + true, + "" ] }, "bob": { @@ -254,6 +286,19 @@ ], "limit": 400 } + ], + "subscribe_to_delete_from_alice": [ + "REQ", + "notifications:delete", + { + "kinds": [ + 5 + ], + "authors": [ + "0b29ecc73ba400e5b4bd1e4cb0d8f524e9958345749197ca21c8da38d0622816" + ], + "limit": 400 + } ] } } \ No newline at end of file diff --git a/tests/test_clients.py b/tests/test_clients.py index 8fd46f7..3af109c 100644 --- a/tests/test_clients.py +++ b/tests/test_clients.py @@ -1,5 +1,5 @@ import asyncio -from json import dumps +from json import dumps, loads import pytest from fastapi import WebSocket @@ -56,6 +56,9 @@ async def test_alice_and_bob(): await alice_writes_to_bob(ws_alice, ws_bob) + await alice_deletes_post01__bob_is_notified(ws_alice, ws_bob) + + def init_clients(): client_manager = NostrClientManager() @@ -230,7 +233,7 @@ async def bob_writes_to_alice(ws_alice: MockWebSocket, ws_bob: MockWebSocket): ), "Alice: Wrong direct message received" -async def alice_writes_to_bob(ws_alice, ws_bob): +async def alice_writes_to_bob(ws_alice: MockWebSocket, ws_bob: MockWebSocket): ws_alice.sent_messages.clear() ws_bob.sent_messages.clear() @@ -262,3 +265,46 @@ async def alice_writes_to_bob(ws_alice, ws_bob): assert ws_bob.sent_messages[1] == dumps( ["EOSE", "notifications:d685447c43c7c18dbbea61923cf0b63e1ab46bed"] ), "Bob: Received all stored events" + +async def alice_deletes_post01__bob_is_notified(ws_alice: MockWebSocket, ws_bob:MockWebSocket): + ws_bob.sent_messages.clear() + await ws_bob.wire_mock_data(bob["request_posts_alice"]) + await asyncio.sleep(0.1) + assert ( + len(ws_bob.sent_messages) == 3 + ), "Bob: Expected two posts from Alice plus and EOSE" + + ws_alice.sent_messages.clear() + ws_bob.sent_messages.clear() + + await ws_bob.wire_mock_data(bob["subscribe_to_delete_from_alice"]) + await asyncio.sleep(0.1) + await ws_alice.wire_mock_data(alice["delete_post01"]) + await asyncio.sleep(0.1) + + assert ( + len(ws_alice.sent_messages) == 1 + ), "Alice: Expected confirmation for delete post01" + assert ws_alice.sent_messages[0] == dumps( + alice["delete_post01_response"] + ), "Alice: Wrong confirmation for delete post01" + + assert len(ws_bob.sent_messages) == 2, "Bob: Expects 2 messages for delete post01" + assert ws_bob.sent_messages[0] == dumps( + ["EOSE", "notifications:delete"] + ), "Bob: Expect no delete notification on subscribe" + assert loads(ws_bob.sent_messages[1]) == [ + "EVENT", + "notifications:delete", + alice["delete_post01"][1], + ], "Bob: Expect delete notification later on" + + ws_bob.sent_messages.clear() + await ws_bob.wire_mock_data(bob["request_posts_alice"]) + await asyncio.sleep(0.1) + assert ( + len(ws_bob.sent_messages) == 2 + ), "Bob: Expected one posts from Alice plus and EOSE" + + +