From 6be0169ea914f2bbe17ab5b420ca7e3b9e07fe3e Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 17 Feb 2023 14:13:06 +0200 Subject: [PATCH] refactor: extract `NostrEvent` --- crud.py | 5 +- models.py | 101 +---------------------------------- relay/client_connection.py | 3 +- relay/client_manager.py | 3 +- relay/event.py | 105 +++++++++++++++++++++++++++++++++++++ relay/event_validator.py | 4 +- tests/test_events.py | 3 +- 7 files changed, 119 insertions(+), 105 deletions(-) create mode 100644 relay/event.py diff --git a/crud.py b/crud.py index 746010e..8eb7aa0 100644 --- a/crud.py +++ b/crud.py @@ -1,10 +1,11 @@ import json -from typing import Any, List, Optional, Tuple +from typing import List, Optional, Tuple + +from .relay.event import NostrEvent from . import db from .models import ( NostrAccount, - NostrEvent, NostrFilter, NostrRelay, RelayPublicSpec, diff --git a/models.py b/models.py index f59b500..1b3c953 100644 --- a/models.py +++ b/models.py @@ -1,11 +1,10 @@ -import hashlib import json -from enum import Enum from sqlite3 import Row from typing import Any, List, Optional, Tuple from pydantic import BaseModel, Field -from secp256k1 import PublicKey + +from .relay.event import NostrEvent class Spec(BaseModel): @@ -130,102 +129,6 @@ class NostrRelay(BaseModel): "version": "", } - -class NostrEventType(str, Enum): - EVENT = "EVENT" - REQ = "REQ" - CLOSE = "CLOSE" - AUTH = "AUTH" - - -class NostrEvent(BaseModel): - id: str - pubkey: str - created_at: int - kind: int - tags: List[List[str]] = [] - content: str = "" - sig: str - - def serialize(self) -> List: - return [0, self.pubkey, self.created_at, self.kind, self.tags, self.content] - - def serialize_json(self) -> str: - e = self.serialize() - return json.dumps(e, separators=(",", ":"), ensure_ascii=False) - - @property - def event_id(self) -> str: - data = self.serialize_json() - id = hashlib.sha256(data.encode()).hexdigest() - return id - - @property - def size_bytes(self) -> int: - s = json.dumps(dict(self), separators=(",", ":"), ensure_ascii=False) - return len(s.encode()) - - @property - def is_replaceable_event(self) -> bool: - return self.kind in [0, 3, 41] or (self.kind >= 10000 and self.kind < 20000) - - @property - def is_auth_response_event(self) -> bool: - return self.kind == 22242 - - @property - def is_direct_message(self) -> bool: - return self.kind == 4 - - @property - def is_delete_event(self) -> bool: - return self.kind == 5 - - @property - def is_regular_event(self) -> bool: - return self.kind >= 1000 and self.kind < 10000 - - @property - def is_ephemeral_event(self) -> bool: - return self.kind >= 20000 and self.kind < 30000 - - - def check_signature(self): - event_id = self.event_id - if self.id != event_id: - raise ValueError( - f"Invalid event id. Expected: '{event_id}' got '{self.id}'" - ) - try: - pub_key = PublicKey(bytes.fromhex("02" + self.pubkey), True) - except Exception: - raise ValueError( - f"Invalid public key: '{self.pubkey}' for event '{self.id}'" - ) - - valid_signature = pub_key.schnorr_verify( - bytes.fromhex(event_id), bytes.fromhex(self.sig), None, raw=True - ) - if not valid_signature: - raise ValueError(f"Invalid signature: '{self.sig}' for event '{self.id}'") - - def serialize_response(self, subscription_id): - return [NostrEventType.EVENT, subscription_id, dict(self)] - - def tag_values(self, tag_name: str) -> List[str]: - return [t[1] for t in self.tags if t[0] == tag_name] - - def has_tag_value(self, tag_name: str, tag_value: str) -> bool: - return tag_value in self.tag_values(tag_name) - - def is_direct_message_for_pubkey(self, pubkey: str) -> bool: - return self.is_direct_message and self.has_tag_value("p", pubkey) - - @classmethod - def from_row(cls, row: Row) -> "NostrEvent": - return cls(**dict(row)) - - class NostrFilter(BaseModel): subscription_id: Optional[str] diff --git a/relay/client_connection.py b/relay/client_connection.py index d7672a6..238873a 100644 --- a/relay/client_connection.py +++ b/relay/client_connection.py @@ -14,7 +14,8 @@ from ..crud import ( get_events, mark_events_deleted, ) -from ..models import NostrEvent, NostrEventType, NostrFilter, RelaySpec +from .event import NostrEvent, NostrEventType +from ..models import NostrFilter, RelaySpec from .event_validator import EventValidator diff --git a/relay/client_manager.py b/relay/client_manager.py index efdc9b4..7f41880 100644 --- a/relay/client_manager.py +++ b/relay/client_manager.py @@ -1,9 +1,10 @@ from typing import List +from .event import NostrEvent from .client_connection import NostrClientConnection from ..crud import get_config_for_all_active_relays -from ..models import NostrEvent, RelaySpec +from ..models import RelaySpec class NostrClientManager: diff --git a/relay/event.py b/relay/event.py new file mode 100644 index 0000000..f8e97af --- /dev/null +++ b/relay/event.py @@ -0,0 +1,105 @@ +import hashlib +import json +from enum import Enum +from sqlite3 import Row +from typing import List + +from pydantic import BaseModel +from secp256k1 import PublicKey + + + +class NostrEventType(str, Enum): + EVENT = "EVENT" + REQ = "REQ" + CLOSE = "CLOSE" + AUTH = "AUTH" + + +class NostrEvent(BaseModel): + id: str + pubkey: str + created_at: int + kind: int + tags: List[List[str]] = [] + content: str = "" + sig: str + + def serialize(self) -> List: + return [0, self.pubkey, self.created_at, self.kind, self.tags, self.content] + + def serialize_json(self) -> str: + e = self.serialize() + return json.dumps(e, separators=(",", ":"), ensure_ascii=False) + + @property + def event_id(self) -> str: + data = self.serialize_json() + id = hashlib.sha256(data.encode()).hexdigest() + return id + + @property + def size_bytes(self) -> int: + s = json.dumps(dict(self), separators=(",", ":"), ensure_ascii=False) + return len(s.encode()) + + @property + def is_replaceable_event(self) -> bool: + return self.kind in [0, 3, 41] or (self.kind >= 10000 and self.kind < 20000) + + @property + def is_auth_response_event(self) -> bool: + return self.kind == 22242 + + @property + def is_direct_message(self) -> bool: + return self.kind == 4 + + @property + def is_delete_event(self) -> bool: + return self.kind == 5 + + @property + def is_regular_event(self) -> bool: + return self.kind >= 1000 and self.kind < 10000 + + @property + def is_ephemeral_event(self) -> bool: + return self.kind >= 20000 and self.kind < 30000 + + + def check_signature(self): + event_id = self.event_id + if self.id != event_id: + raise ValueError( + f"Invalid event id. Expected: '{event_id}' got '{self.id}'" + ) + try: + pub_key = PublicKey(bytes.fromhex("02" + self.pubkey), True) + except Exception: + raise ValueError( + f"Invalid public key: '{self.pubkey}' for event '{self.id}'" + ) + + valid_signature = pub_key.schnorr_verify( + bytes.fromhex(event_id), bytes.fromhex(self.sig), None, raw=True + ) + if not valid_signature: + raise ValueError(f"Invalid signature: '{self.sig}' for event '{self.id}'") + + def serialize_response(self, subscription_id): + return [NostrEventType.EVENT, subscription_id, dict(self)] + + def tag_values(self, tag_name: str) -> List[str]: + return [t[1] for t in self.tags if t[0] == tag_name] + + def has_tag_value(self, tag_name: str, tag_value: str) -> bool: + return tag_value in self.tag_values(tag_name) + + def is_direct_message_for_pubkey(self, pubkey: str) -> bool: + return self.is_direct_message and self.has_tag_value("p", pubkey) + + @classmethod + def from_row(cls, row: Row) -> "NostrEvent": + return cls(**dict(row)) + diff --git a/relay/event_validator.py b/relay/event_validator.py index d6ec25e..aaa6cf2 100644 --- a/relay/event_validator.py +++ b/relay/event_validator.py @@ -1,9 +1,11 @@ import time from typing import Callable, Optional, Tuple +from .event import NostrEvent + from ..crud import get_account, get_storage_for_public_key, prune_old_events from ..helpers import extract_domain -from ..models import NostrAccount, NostrEvent, RelaySpec +from ..models import NostrAccount, RelaySpec class EventValidator: diff --git a/tests/test_events.py b/tests/test_events.py index 5b33c90..0a42ab2 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -10,7 +10,8 @@ from lnbits.extensions.nostrrelay.crud import ( # type: ignore get_event, get_events, ) -from lnbits.extensions.nostrrelay.models import NostrEvent, NostrFilter # type: ignore +from lnbits.extensions.nostrrelay.models import NostrFilter # type: ignore +from lnbits.extensions.nostrrelay.relay.event import NostrEvent # type: ignore from .helpers import get_fixtures