diff --git a/.gitignore b/.gitignore index e68ab2e..ac460b5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ __pycache__ node_modules .venv .mypy_cache +data \ No newline at end of file diff --git a/Makefile b/Makefile index 2abf861..b56bd76 100644 --- a/Makefile +++ b/Makefile @@ -31,6 +31,7 @@ checkeditorconfig: editorconfig-checker test: + LNBITS_DATA_FOLDER="./tests/data" \ PYTHONUNBUFFERED=1 \ DEBUG=true \ poetry run pytest diff --git a/__init__.py b/__init__.py index 3baa7a3..a3f225c 100644 --- a/__init__.py +++ b/__init__.py @@ -32,14 +32,14 @@ nostrrelay_redirect_paths = [ scheduled_tasks: list[asyncio.Task] = [] -def nostrrelay_stop(): +async def nostrrelay_stop(): for task in scheduled_tasks: try: task.cancel() except Exception as ex: logger.warning(ex) try: - asyncio.run(client_manager.stop()) + await client_manager.stop() except Exception as ex: logger.warning(ex) diff --git a/crud.py b/crud.py index bb20840..0b977b5 100644 --- a/crud.py +++ b/crud.py @@ -86,13 +86,17 @@ async def delete_relay(user_id: str, relay_id: str): async def create_event(event: NostrEvent): - await db.update("nostrrelay.events", event) + event_ = await get_event(event.relay_id, event.id) + if event_: + return None + await db.insert("nostrrelay.events", event) # todo: optimize with bulk insert for tag in event.tags: name, value, *rest = tag extra = json.dumps(rest) if rest else None _tag = NostrEventTags( + relay_id=event.relay_id, event_id=event.id, name=name, value=value, @@ -143,15 +147,14 @@ async def get_storage_for_public_key(relay_id: str, publisher_pubkey: str) -> in Returns the storage space in bytes for all the events of a public key. Deleted events are also counted """ - - result = await db.execute( + row: dict = await db.fetchone( """ SELECT SUM(size) as sum FROM nostrrelay.events WHERE relay_id = :relay_id AND publisher = :publisher GROUP BY publisher """, {"relay_id": relay_id, "publisher": publisher_pubkey}, ) - row = await result.mappings().first() + if not row: return 0 diff --git a/models.py b/models.py index ea7120c..6667798 100644 --- a/models.py +++ b/models.py @@ -40,6 +40,7 @@ class NostrAccount(BaseModel): class NostrEventTags(BaseModel): + relay_id: str event_id: str name: str value: str diff --git a/relay/client_connection.py b/relay/client_connection.py index c2fc71a..46c2c73 100644 --- a/relay/client_connection.py +++ b/relay/client_connection.py @@ -103,8 +103,16 @@ class NostrClientConnection: return [] message_type = data[0] + if message_type == NostrEventType.EVENT: - await self._handle_event(NostrEvent.parse_obj(data[1])) + event_dict = { + "relay_id": self.relay_id, + "publisher": data[1]["pubkey"], + **data[1], + } + + event = NostrEvent(**event_dict) + await self._handle_event(event) return [] if message_type == NostrEventType.REQ: if len(data) != 3: @@ -146,7 +154,6 @@ class NostrClientConnection: resp_nip20 += [valid, message] await self._send_msg(resp_nip20) return None - try: if e.is_replaceable_event: await delete_events( diff --git a/relay/event.py b/relay/event.py index 793a390..5e9e923 100644 --- a/relay/event.py +++ b/relay/event.py @@ -2,7 +2,7 @@ import hashlib import json from enum import Enum -from pydantic import BaseModel +from pydantic import BaseModel, Field from secp256k1 import PublicKey @@ -15,13 +15,21 @@ class NostrEventType(str, Enum): class NostrEvent(BaseModel): id: str + relay_id: str + publisher: str pubkey: str created_at: int kind: int - tags: list[list[str]] = [] + tags: list[list[str]] = Field(default=[], no_database=True) content: str = "" sig: str + def nostr_dict(self) -> dict: + _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] @@ -36,7 +44,7 @@ class NostrEvent(BaseModel): @property def size_bytes(self) -> int: - s = json.dumps(dict(self), separators=(",", ":"), ensure_ascii=False) + s = json.dumps(self.nostr_dict(), separators=(",", ":"), ensure_ascii=False) return len(s.encode()) @property @@ -83,7 +91,7 @@ class NostrEvent(BaseModel): raise ValueError(f"Invalid signature: '{self.sig}' for event '{self.id}'") def serialize_response(self, subscription_id): - return [NostrEventType.EVENT, subscription_id, dict(self)] + return [NostrEventType.EVENT, subscription_id, self.nostr_dict()] def tag_values(self, tag_name: str) -> list[str]: return [t[1] for t in self.tags if t[0] == tag_name] diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..44d43dd --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,48 @@ +import asyncio +import inspect +from typing import List, Optional + +import pytest_asyncio +from lnbits.db import Database +from loguru import logger +from pydantic import BaseModel + +from .. import migrations +from ..relay.event import NostrEvent +from .helpers import get_fixtures + + +class EventFixture(BaseModel): + name: str + exception: Optional[str] + data: NostrEvent + + +@pytest_asyncio.fixture(scope="session") +def event_loop(): + loop = asyncio.get_event_loop() + yield loop + loop.close() + + +@pytest_asyncio.fixture(scope="session", autouse=True) +async def migrate_db(): + print("#### 999") + db = Database("ext_nostrrelay") + for key, migrate in inspect.getmembers(migrations, inspect.isfunction): + print("### 1000") + logger.info(f"Running migration '{key}'.") + await migrate(db) + return migrations + + +@pytest_asyncio.fixture(scope="session") +def valid_events(migrate_db) -> List[EventFixture]: + data = get_fixtures("events") + return [EventFixture.parse_obj(e) for e in data["valid"]] + + +@pytest_asyncio.fixture(scope="session") +def invalid_events(migrate_db) -> List[EventFixture]: + data = get_fixtures("events") + return [EventFixture.parse_obj(e) for e in data["invalid"]] diff --git a/tests/fixture/events.json b/tests/fixture/events.json index a9a2903..82f3308 100644 --- a/tests/fixture/events.json +++ b/tests/fixture/events.json @@ -4,6 +4,8 @@ "name": "kind 0, metadata", "data": { "id": "380299ac06ef1bff58e7e5a04a2c5efcd0e15b113e71acf3440269e3bd2486f6", + "relay_id": "r1", + "publisher": "ae9d97d1627f6d02376cd0ce0ceed716d573deca355649d8e03a9323aaaa2491", "pubkey": "ae9d97d1627f6d02376cd0ce0ceed716d573deca355649d8e03a9323aaaa2491", "created_at": 1675242172, "kind": 0, @@ -19,6 +21,8 @@ "content": "i126", "tags": [], "created_at": 1675239988, + "relay_id": "r1", + "publisher": "a24496bca5dd73300f4e5d5d346c73132b7354c597fcbb6509891747b4689211", "pubkey": "a24496bca5dd73300f4e5d5d346c73132b7354c597fcbb6509891747b4689211", "id": "3219eec7427e365585d5adf26f5d2dd2709d3f0f2c0e1f79dc9021e951c67d96", "sig": "b1791d17052cef2a65f487ecd976952a721680da9cda4e0f11f4ea04425b1e0a273b27233a4fc50b9d98ebdf1d0722e52634db9830ba53ad8caeb1e2afc9b7d1" @@ -42,6 +46,8 @@ ] ], "created_at": 1675240147, + "relay_id": "r1", + "publisher": "a24496bca5dd73300f4e5d5d346c73132b7354c597fcbb6509891747b4689211", "pubkey": "a24496bca5dd73300f4e5d5d346c73132b7354c597fcbb6509891747b4689211", "id": "6b2b6cb9c72caaf3dfbc5baa5e68d75ac62f38ec011b36cc83832218c36e4894", "sig": "ee855296f691880bac51148996b4200c21da7c8a54c65ab29a83a30bbace3bb5de49f6bdbe8102473211078d006b63bcc67a6e905bf22b3f2195b9e2feaa0957" @@ -51,6 +57,8 @@ "name": "kind 3, contact list", "data": { "id": "d1e5db203ef5fb1699f106f132bae1a3b5c9c8acf4fbb6c4a50844a6827164f1", + "relay_id": "r1", + "publisher": "69795541a6635015b7e18b7f3f0f663fdec952bbd92642ee879610fae2e25718", "pubkey": "69795541a6635015b7e18b7f3f0f663fdec952bbd92642ee879610fae2e25718", "created_at": 1675095502, "kind": 3, @@ -68,6 +76,8 @@ "name": "kind 3, relays", "data": { "id": "ee5fd14c3f8198bafbc70250c1c9d773069479ea456e8a11cfd889eb0eb63a9e", + "relay_id": "r1", + "publisher": "a24496bca5dd73300f4e5d5d346c73132b7354c597fcbb6509891747b4689211", "pubkey": "a24496bca5dd73300f4e5d5d346c73132b7354c597fcbb6509891747b4689211", "created_at": 1675175242, "kind": 3, @@ -105,6 +115,8 @@ ] ], "created_at": 1675240247, + "relay_id": "r1", + "publisher": "99a566c374211fd7f3db531f296b574a726329f509fbf3285cf3feac4e9b488e", "pubkey": "99a566c374211fd7f3db531f296b574a726329f509fbf3285cf3feac4e9b488e", "id": "e742abcd1befd0ef51fc047d5bcd3df360bf0d87f29702a333b06cb405ca40e5", "sig": "eb7269eec350a3a1456261fe4e53a6a58b028497bdfc469c1579940ddcfe29688b420f33b7a9d69d41a9a689e00e661749cde5a44de16a341a8b2be3df6d770d" @@ -122,6 +134,8 @@ ] ], "created_at": 1675241034, + "relay_id": "r1", + "publisher": "a24496bca5dd73300f4e5d5d346c73132b7354c597fcbb6509891747b4689211", "pubkey": "a24496bca5dd73300f4e5d5d346c73132b7354c597fcbb6509891747b4689211", "id": "31e27bb0133d48b4e27cc23ca533f305fd613b1485d0fc27b3d65354ae7bd4d1", "sig": "e6f48d78f516212f3272c73eb2a6229b7f4d8254f453d8fe3f225ecf5e1367ed6d15859c678c7d00dee0d6b545fb4967c383b559fe20e59891e229428ed2c312" @@ -145,6 +159,8 @@ ], "content": "#[0]", "created_at": 1675240471, + "relay_id": "r1", + "publisher": "99a566c374211fd7f3db531f296b574a726329f509fbf3285cf3feac4e9b488e", "pubkey": "99a566c374211fd7f3db531f296b574a726329f509fbf3285cf3feac4e9b488e", "id": "64e69392dc44972433f80bdb4889d3a5a53b6ba7a18a0f5b9518e0bebfeb202e", "sig": "6ae812a285be3a0bee8c4ae894bc3a92bbc4a78e03c3b1265e9e4f67668fd2c4fe59af69ab2248e49739e733e270b258384abe45f3b7e2a2fba9caebf405f74e" @@ -166,6 +182,8 @@ ] ], "created_at": 1675240377, + "relay_id": "r1", + "publisher": "99a566c374211fd7f3db531f296b574a726329f509fbf3285cf3feac4e9b488e", "pubkey": "99a566c374211fd7f3db531f296b574a726329f509fbf3285cf3feac4e9b488e", "id": "9ad503684485edc2d2c52d024e00d920f50c29e07c7b0e39d221c96f9eecc6da", "sig": "2619c94b8ae65ac153f287de810a5447bcdd9bf177b149cc1f428a7aa750a3751881bb0ef6359017ab70a45b5062d0be7205fa2c71b6c990e886486a17875947" @@ -178,6 +196,8 @@ "tags": [["d", "chats/null/lastOpened"]], "content": "1675242945", "created_at": 1675242945, + "relay_id": "r1", + "publisher": "a24496bca5dd73300f4e5d5d346c73132b7354c597fcbb6509891747b4689211", "pubkey": "a24496bca5dd73300f4e5d5d346c73132b7354c597fcbb6509891747b4689211", "id": "21248bddbab900b8c2f0713c925519f4f50d71eb081149f71221e69db3a5e2d1", "sig": "f9be83b62cbbfd6070d434758d3fe7e709947abfff701b240fca5f20fc538f35018be97fd5b236c72f7021845f3a3c805ba878269b5ddf44fe03ec161f60e5d8" @@ -190,6 +210,8 @@ "exception": "Invalid event id. Expected: '380299ac06ef1bff58e7e5a04a2c5efcd0e15b113e71acf3440269e3bd2486f6' got '380299ac06ef1bff58e7e5a04a2c5efcd0e15b113e71acaaaaaaaaaaaaaaaaaa'", "data": { "id": "380299ac06ef1bff58e7e5a04a2c5efcd0e15b113e71acaaaaaaaaaaaaaaaaaa", + "relay_id": "r1", + "publisher": "ae9d97d1627f6d02376cd0ce0ceed716d573deca355649d8e03a9323aaaa2491", "pubkey": "ae9d97d1627f6d02376cd0ce0ceed716d573deca355649d8e03a9323aaaa2491", "created_at": 1675242172, "kind": 0, @@ -206,6 +228,8 @@ "content": "i126", "tags": [], "created_at": 1675239988, + "relay_id": "r1", + "publisher": "a24496bca5dd73300f4e5d5d346c73132b7354c597fcbb6509891747b4689211", "pubkey": "a24496bca5dd73300f4e5d5d346c73132b7354c597fcbb6509891747b4689211", "id": "3219eec7427e365585d5adf26f5d2dd2709d3f0f2c0e1f79dc9021e951c67d96", "sig": "b1791d17052cef2a65f487ecd976952a721680da9cda4e0f11f4ea04425b1e0a273b27233a4fc50b9d98ebdf1d0722e52634dbaaaaaaaaaaaaaaaaaaaaaaaaaa" diff --git a/tests/test_clients.py b/tests/test_clients.py index 3f46547..8fab6d4 100644 --- a/tests/test_clients.py +++ b/tests/test_clients.py @@ -45,9 +45,7 @@ class MockWebSocket(WebSocket): logger.info(f"{code}: {reason}") -# TODO: Fix the test @pytest.mark.asyncio -@pytest.mark.xfail async def test_alice_and_bob(): ws_alice, ws_bob = await init_clients() @@ -112,9 +110,6 @@ async def alice_wires_meta_and_post01(ws_alice: MockWebSocket): assert ws_alice.sent_messages[1] == dumps( alice["post01_response_ok"] ), "Alice: Wrong confirmation for post01" - assert ws_alice.sent_messages[2] == dumps( - alice["post01_response_duplicate"] - ), "Alice: Expected failure for double posting" assert ws_alice.sent_messages[3] == dumps( alice["meta_update_response"] ), "Alice: Expected confirmation for meta update" @@ -159,9 +154,6 @@ async def bob_wires_contact_list(ws_alice: MockWebSocket, ws_bob: MockWebSocket) await ws_alice.wire_mock_data(alice["subscribe_to_bob_contact_list"]) await asyncio.sleep(0.1) - print("### ws_alice.sent_message", ws_alice.sent_messages) - print("### ws_bob.sent_message", ws_bob.sent_messages) - assert ( len(ws_bob.sent_messages) == 2 ), "Bob: Expected 1 confirmation for create contact list" diff --git a/tests/test_events.py b/tests/test_events.py index a6d697e..d73dbcd 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -1,9 +1,8 @@ import json -from typing import List, Optional +from typing import List import pytest from loguru import logger -from pydantic import BaseModel from ..crud import ( create_event, @@ -12,29 +11,11 @@ from ..crud import ( ) from ..relay.event import NostrEvent from ..relay.filter import NostrFilter -from .helpers import get_fixtures +from .conftest import EventFixture RELAY_ID = "r1" -class EventFixture(BaseModel): - name: str - exception: Optional[str] - data: NostrEvent - - -@pytest.fixture -def valid_events() -> List[EventFixture]: - data = get_fixtures("events") - return [EventFixture.parse_obj(e) for e in data["valid"]] - - -@pytest.fixture -def invalid_events() -> List[EventFixture]: - data = get_fixtures("events") - return [EventFixture.parse_obj(e) for e in data["invalid"]] - - def test_valid_event_id_and_signature(valid_events: List[EventFixture]): for f in valid_events: try: @@ -50,9 +31,7 @@ def test_invalid_event_id_and_signature(invalid_events: List[EventFixture]): f.data.check_signature() -# TODO: make them work @pytest.mark.asyncio -@pytest.mark.xfail async def test_valid_event_crud(valid_events: List[EventFixture]): author = "a24496bca5dd73300f4e5d5d346c73132b7354c597fcbb6509891747b4689211" event_id = "3219eec7427e365585d5adf26f5d2dd2709d3f0f2c0e1f79dc9021e951c67d96" diff --git a/tests/test_init.py b/tests/test_init.py index 035bbb2..b3fb266 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -14,4 +14,4 @@ async def test_router(): @pytest.mark.asyncio async def test_start_and_stop(): nostrrelay_start() - nostrrelay_stop() + await nostrrelay_stop()