diff --git a/.gitignore b/.gitignore index 161ec36..5fd242b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ nostr.egg-info/ dist/ nostr/_version.py .DS_Store +.python-version diff --git a/nostr/event.py b/nostr/event.py index 2ec5e28..28bcd19 100644 --- a/nostr/event.py +++ b/nostr/event.py @@ -1,11 +1,12 @@ import time import json from dataclasses import dataclass, field -from enum import IntEnum +from enum import Enum, IntEnum from typing import List -from secp256k1 import PrivateKey, PublicKey +from secp256k1 import PublicKey from hashlib import sha256 +from .exceptions import EventValidationException from .message_type import ClientMessageType @@ -17,6 +18,7 @@ class EventKind(IntEnum): CONTACTS = 3 ENCRYPTED_DIRECT_MESSAGE = 4 DELETE = 5 + REPORT = 1984 @@ -121,3 +123,39 @@ def id(self) -> str: if self.content is None: raise Exception("EncryptedDirectMessage `id` is undefined until its message is encrypted and stored in the `content` field") return super().id + + +class ReportType(Enum): + NUDITY = 'nudity' + PROFANITY = 'profanity' + ILLEGAL = 'illegal' + SPAM = 'spam' + IMPERSONATION = 'impersonation' + +@dataclass +class ReportEvent(Event): + """ + NIP-56 reporting event + """ + reported_pubkey: str = None + note_id: str = None + report_type: ReportType = None + victim_pubkey: str = None + + def __post_init__(self): + if self.reported_pubkey is None: + raise EventValidationException("Reports require the pubkey of the user being reported") + if self.report_type is None or not isinstance(self.report_type, ReportType): + raise EventValidationException("Reports require a valid report type") + self.kind = EventKind.REPORT + super().__post_init__() + self.tags = self.tags + self._build_tags() + + def _build_tags(self) -> List[List[str]]: + report_tags = [] + if self.note_id: + report_tags.append(["e", self.note_id, self.report_type]) + report_tags.append(["p", self.reported_pubkey, self.report_type]) + if self.victim_pubkey: + report_tags.append(["p", self.victim_pubkey]) + return report_tags diff --git a/nostr/exceptions.py b/nostr/exceptions.py new file mode 100644 index 0000000..86afc70 --- /dev/null +++ b/nostr/exceptions.py @@ -0,0 +1,4 @@ +class EventValidationException(Exception): + """ + Raised when a specific event does not meet NIP requirements + """ diff --git a/nostr/relay.py b/nostr/relay.py index 8876a8d..84fa73b 100644 --- a/nostr/relay.py +++ b/nostr/relay.py @@ -34,11 +34,15 @@ class RelayProxyConnectionConfig: class Relay: url: str message_pool: MessagePool - policy: RelayPolicy = RelayPolicy() - proxy_config: RelayProxyConnectionConfig = RelayProxyConnectionConfig() + policy: RelayPolicy = None + proxy_config: RelayProxyConnectionConfig = None ssl_options: Optional[dict] = None def __post_init__(self): + if self.policy is None: + self.policy = RelayPolicy() + if self.proxy_config is None: + self.proxy_config = RelayProxyConnectionConfig() self.subscriptions: dict[str, Subscription] = {} self.lock: Lock = Lock() self.ws: WebSocketApp = WebSocketApp( diff --git a/nostr/relay_manager.py b/nostr/relay_manager.py index bfaf254..143a479 100644 --- a/nostr/relay_manager.py +++ b/nostr/relay_manager.py @@ -28,10 +28,12 @@ def __post_init__(self): def add_relay( self, url: str, - policy: RelayPolicy = RelayPolicy(), + policy: RelayPolicy = None, ssl_options: dict = None, proxy_config: RelayProxyConnectionConfig = None): + if RelayPolicy is None: + policy = RelayPolicy() relay = Relay(url, self.message_pool, policy, ssl_options, proxy_config) with self.lock: diff --git a/test/test_event.py b/test/test_event.py index b968a98..49c8f97 100644 --- a/test/test_event.py +++ b/test/test_event.py @@ -1,6 +1,8 @@ +from collections import namedtuple import pytest import time -from nostr.event import Event, EncryptedDirectMessage +from nostr.event import Event, EncryptedDirectMessage, ReportEvent, ReportType +from nostr.exceptions import EventValidationException from nostr.key import PrivateKey @@ -78,7 +80,6 @@ def test_recipient_p_tag(self): """ Should generate recipient 'p' tag """ dm = EncryptedDirectMessage(cleartext_content="Secret message!", recipient_pubkey=self.recipient_pubkey) assert ['p', self.recipient_pubkey] in dm.tags - def test_unencrypted_dm_has_undefined_id(self): """ Should raise Exception if `id` is requested before DM is encrypted """ @@ -91,3 +92,29 @@ def test_unencrypted_dm_has_undefined_id(self): # But once we encrypt it, we can request its id self.sender_pk.encrypt_dm(dm) assert dm.id is not None + +class TestReportEvent: + def test_report_type(self): + """ Should not let users instantiate a new ReportEvent without valid data""" + pub_key = PrivateKey().public_key.hex() + reported_pubkey = PrivateKey().public_key.hex() + with pytest.raises(EventValidationException) as invalid_type_exception: + ReportEvent(pub_key, "this was a bad note!", report_type="invalidtype", reported_pubkey=reported_pubkey) + with pytest.raises(EventValidationException) as no_reported_pubkey: + ReportEvent(pub_key, report_type=ReportType.NUDITY) + assert "valid report type" in str(invalid_type_exception) + assert "user being reported" in str(no_reported_pubkey) + + def test_report_tags(self): + """ Should generate report-specific tags """ + report = ReportEvent( + public_key="pubkey", + reported_pubkey=PrivateKey().public_key.hex(), + note_id="fakenoteid", + report_type=ReportType.ILLEGAL, + victim_pubkey="thevictim" + ) + assert len(report.tags) == 3 + tag_types = [tag[0] for tag in report.tags] + print(tag_types) + assert tag_types == ["e", "p", "p"]