11import time
22import json
3+ from dataclasses import dataclass , field
34from enum import IntEnum
5+ from typing import List
46from secp256k1 import PrivateKey , PublicKey
57from hashlib import sha256
68
79from nostr .message_type import ClientMessageType
810
911
12+
1013class EventKind (IntEnum ):
1114 SET_METADATA = 0
1215 TEXT_NOTE = 1
@@ -16,41 +19,58 @@ class EventKind(IntEnum):
1619 DELETE = 5
1720
1821
19- class Event ():
20- def __init__ (
21- self ,
22- public_key : str ,
23- content : str ,
24- created_at : int = None ,
25- kind : int = EventKind .TEXT_NOTE ,
26- tags : "list[list[str]]" = [],
27- id : str = None ,
28- signature : str = None ) -> None :
29- if not isinstance (content , str ):
22+
23+ @dataclass
24+ class Event :
25+ content : str = None
26+ public_key : str = None
27+ created_at : int = None
28+ kind : int = EventKind .TEXT_NOTE
29+ tags : List [List [str ]] = field (default_factory = list ) # Dataclasses require special handling when the default value is a mutable type
30+ signature : str = None
31+
32+
33+ def __post_init__ (self ):
34+ if self .content is not None and not isinstance (self .content , str ):
35+ # DMs initialize content to None but all other kinds should pass in a str
3036 raise TypeError ("Argument 'content' must be of type str" )
3137
32- self .public_key = public_key
33- self .content = content
34- self .created_at = created_at or int (time .time ())
35- self .kind = kind
36- self .tags = tags
37- self .signature = signature
38- self .id = id or Event .compute_id (self .public_key , self .created_at , self .kind , self .tags , self .content )
38+ if self .created_at is None :
39+ self .created_at = int (time .time ())
40+
3941
4042 @staticmethod
41- def serialize (public_key : str , created_at : int , kind : int , tags : "list[list [str]]" , content : str ) -> bytes :
43+ def serialize (public_key : str , created_at : int , kind : int , tags : List [ List [str ]], content : str ) -> bytes :
4244 data = [0 , public_key , created_at , kind , tags , content ]
4345 data_str = json .dumps (data , separators = (',' , ':' ), ensure_ascii = False )
4446 return data_str .encode ()
4547
48+
4649 @staticmethod
47- def compute_id (public_key : str , created_at : int , kind : int , tags : "list[list [str]]" , content : str ) -> str :
50+ def compute_id (public_key : str , created_at : int , kind : int , tags : List [ List [str ]], content : str ):
4851 return sha256 (Event .serialize (public_key , created_at , kind , tags , content )).hexdigest ()
4952
53+
54+ @property
55+ def id (self ) -> str :
56+ # Always recompute the id to reflect the up-to-date state of the Event
57+ return Event .compute_id (self .public_key , self .created_at , self .kind , self .tags , self .content )
58+
59+
60+ def add_pubkey_ref (self , pubkey :str ):
61+ """ Adds a reference to a pubkey as a 'p' tag """
62+ self .tags .append (['p' , pubkey ])
63+
64+
65+ def add_event_ref (self , event_id :str ):
66+ """ Adds a reference to an event_id as an 'e' tag """
67+ self .tags .append (['e' , event_id ])
68+
69+
5070 def verify (self ) -> bool :
51- pub_key = PublicKey (bytes .fromhex ("02" + self .public_key ), True ) # add 02 for schnorr (bip340)
52- event_id = Event . compute_id ( self . public_key , self .created_at , self .kind , self . tags , self . content )
53- return pub_key . schnorr_verify ( bytes . fromhex ( event_id ), bytes . fromhex ( self . signature ), None , raw = True )
71+ pub_key = PublicKey (bytes .fromhex ("02" + self .public_key ), True ) # add 02 for schnorr (bip340)
72+ return pub_key . schnorr_verify ( bytes . fromhex ( self .id ), bytes . fromhex ( self .signature ), None , raw = True )
73+
5474
5575 def to_message (self ) -> str :
5676 return json .dumps (
@@ -67,3 +87,37 @@ def to_message(self) -> str:
6787 }
6888 ]
6989 )
90+
91+
92+
93+ @dataclass
94+ class EncryptedDirectMessage (Event ):
95+ recipient_pubkey : str = None
96+ cleartext_content : str = None
97+ reference_event_id : str = None
98+
99+
100+ def __post_init__ (self ):
101+ if self .content is not None :
102+ self .cleartext_content = self .content
103+ self .content = None
104+
105+ if self .recipient_pubkey is None :
106+ raise Exception ("Must specify a recipient_pubkey." )
107+
108+ self .kind = EventKind .ENCRYPTED_DIRECT_MESSAGE
109+ super ().__post_init__ ()
110+
111+ # Must specify the DM recipient's pubkey in a 'p' tag
112+ self .add_pubkey_ref (self .recipient_pubkey )
113+
114+ # Optionally specify a reference event (DM) this is a reply to
115+ if self .reference_event_id is not None :
116+ self .add_event_ref (self .reference_event_id )
117+
118+
119+ @property
120+ def id (self ) -> str :
121+ if self .content is None :
122+ raise Exception ("EncryptedDirectMessage `id` is undefined until its message is encrypted and stored in the `content` field" )
123+ return super ().id
0 commit comments