mirror of
https://github.com/aljazceru/python-nostr.git
synced 2025-12-18 23:04:20 +01:00
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
100
nostr/event.py
100
nostr/event.py
@@ -1,12 +1,15 @@
|
||||
import time
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from enum import IntEnum
|
||||
from typing import List
|
||||
from secp256k1 import PrivateKey, PublicKey
|
||||
from hashlib import sha256
|
||||
|
||||
from nostr.message_type import ClientMessageType
|
||||
|
||||
|
||||
|
||||
class EventKind(IntEnum):
|
||||
SET_METADATA = 0
|
||||
TEXT_NOTE = 1
|
||||
@@ -16,41 +19,58 @@ class EventKind(IntEnum):
|
||||
DELETE = 5
|
||||
|
||||
|
||||
class Event():
|
||||
def __init__(
|
||||
self,
|
||||
public_key: str,
|
||||
content: str,
|
||||
created_at: int = None,
|
||||
kind: int=EventKind.TEXT_NOTE,
|
||||
tags: "list[list[str]]"=[],
|
||||
id: str=None,
|
||||
signature: str=None) -> None:
|
||||
if not isinstance(content, str):
|
||||
|
||||
@dataclass
|
||||
class Event:
|
||||
content: str = None
|
||||
public_key: str = None
|
||||
created_at: int = None
|
||||
kind: int = EventKind.TEXT_NOTE
|
||||
tags: List[List[str]] = field(default_factory=list) # Dataclasses require special handling when the default value is a mutable type
|
||||
signature: str = None
|
||||
|
||||
|
||||
def __post_init__(self):
|
||||
if self.content is not None and not isinstance(self.content, str):
|
||||
# DMs initialize content to None but all other kinds should pass in a str
|
||||
raise TypeError("Argument 'content' must be of type str")
|
||||
|
||||
self.public_key = public_key
|
||||
self.content = content
|
||||
self.created_at = created_at or int(time.time())
|
||||
self.kind = kind
|
||||
self.tags = tags
|
||||
self.signature = signature
|
||||
self.id = id or Event.compute_id(self.public_key, self.created_at, self.kind, self.tags, self.content)
|
||||
if self.created_at is None:
|
||||
self.created_at = int(time.time())
|
||||
|
||||
|
||||
@staticmethod
|
||||
def serialize(public_key: str, created_at: int, kind: int, tags: "list[list[str]]", content: str) -> bytes:
|
||||
def serialize(public_key: str, created_at: int, kind: int, tags: List[List[str]], content: str) -> bytes:
|
||||
data = [0, public_key, created_at, kind, tags, content]
|
||||
data_str = json.dumps(data, separators=(',', ':'), ensure_ascii=False)
|
||||
return data_str.encode()
|
||||
|
||||
|
||||
@staticmethod
|
||||
def compute_id(public_key: str, created_at: int, kind: int, tags: "list[list[str]]", content: str) -> str:
|
||||
def compute_id(public_key: str, created_at: int, kind: int, tags: List[List[str]], content: str):
|
||||
return sha256(Event.serialize(public_key, created_at, kind, tags, content)).hexdigest()
|
||||
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
# Always recompute the id to reflect the up-to-date state of the Event
|
||||
return Event.compute_id(self.public_key, self.created_at, self.kind, self.tags, self.content)
|
||||
|
||||
|
||||
def add_pubkey_ref(self, pubkey:str):
|
||||
""" Adds a reference to a pubkey as a 'p' tag """
|
||||
self.tags.append(['p', pubkey])
|
||||
|
||||
|
||||
def add_event_ref(self, event_id:str):
|
||||
""" Adds a reference to an event_id as an 'e' tag """
|
||||
self.tags.append(['e', event_id])
|
||||
|
||||
|
||||
def verify(self) -> bool:
|
||||
pub_key = PublicKey(bytes.fromhex("02" + self.public_key), True) # add 02 for schnorr (bip340)
|
||||
event_id = Event.compute_id(self.public_key, self.created_at, self.kind, self.tags, self.content)
|
||||
return pub_key.schnorr_verify(bytes.fromhex(event_id), bytes.fromhex(self.signature), None, raw=True)
|
||||
pub_key = PublicKey(bytes.fromhex("02" + self.public_key), True) # add 02 for schnorr (bip340)
|
||||
return pub_key.schnorr_verify(bytes.fromhex(self.id), bytes.fromhex(self.signature), None, raw=True)
|
||||
|
||||
|
||||
def to_message(self) -> str:
|
||||
return json.dumps(
|
||||
@@ -67,3 +87,37 @@ class Event():
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class EncryptedDirectMessage(Event):
|
||||
recipient_pubkey: str = None
|
||||
cleartext_content: str = None
|
||||
reference_event_id: str = None
|
||||
|
||||
|
||||
def __post_init__(self):
|
||||
if self.content is not None:
|
||||
self.cleartext_content = self.content
|
||||
self.content = None
|
||||
|
||||
if self.recipient_pubkey is None:
|
||||
raise Exception("Must specify a recipient_pubkey.")
|
||||
|
||||
self.kind = EventKind.ENCRYPTED_DIRECT_MESSAGE
|
||||
super().__post_init__()
|
||||
|
||||
# Must specify the DM recipient's pubkey in a 'p' tag
|
||||
self.add_pubkey_ref(self.recipient_pubkey)
|
||||
|
||||
# Optionally specify a reference event (DM) this is a reply to
|
||||
if self.reference_event_id is not None:
|
||||
self.add_event_ref(self.reference_event_id)
|
||||
|
||||
|
||||
@property
|
||||
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
|
||||
|
||||
11
nostr/key.py
11
nostr/key.py
@@ -7,7 +7,7 @@ from cryptography.hazmat.primitives import padding
|
||||
from hashlib import sha256
|
||||
|
||||
from nostr.delegation import Delegation
|
||||
from nostr.event import Event
|
||||
from nostr.event import EncryptedDirectMessage, Event, EventKind
|
||||
from . import bech32
|
||||
|
||||
|
||||
@@ -77,6 +77,9 @@ class PrivateKey:
|
||||
encrypted_message = encryptor.update(padded_data) + encryptor.finalize()
|
||||
|
||||
return f"{base64.b64encode(encrypted_message).decode()}?iv={base64.b64encode(iv).decode()}"
|
||||
|
||||
def encrypt_dm(self, dm: EncryptedDirectMessage) -> None:
|
||||
dm.content = self.encrypt_message(message=dm.cleartext_content, public_key_hex=dm.recipient_pubkey)
|
||||
|
||||
def decrypt_message(self, encoded_message: str, public_key_hex: str) -> str:
|
||||
encoded_data = encoded_message.split('?iv=')
|
||||
@@ -100,6 +103,10 @@ class PrivateKey:
|
||||
return sig.hex()
|
||||
|
||||
def sign_event(self, event: Event) -> None:
|
||||
if event.kind == EventKind.ENCRYPTED_DIRECT_MESSAGE and event.content is None:
|
||||
self.encrypt_dm(event)
|
||||
if event.public_key is None:
|
||||
event.public_key = self.public_key.hex()
|
||||
event.signature = self.sign_message_hash(bytes.fromhex(event.id))
|
||||
|
||||
def sign_delegation(self, delegation: Delegation) -> None:
|
||||
@@ -108,6 +115,7 @@ class PrivateKey:
|
||||
def __eq__(self, other):
|
||||
return self.raw_secret == other.raw_secret
|
||||
|
||||
|
||||
def mine_vanity_key(prefix: str = None, suffix: str = None) -> PrivateKey:
|
||||
if prefix is None and suffix is None:
|
||||
raise ValueError("Expected at least one of 'prefix' or 'suffix' arguments")
|
||||
@@ -122,6 +130,7 @@ def mine_vanity_key(prefix: str = None, suffix: str = None) -> PrivateKey:
|
||||
|
||||
return sk
|
||||
|
||||
|
||||
ffi = FFI()
|
||||
@ffi.callback("int (unsigned char *, const unsigned char *, const unsigned char *, void *)")
|
||||
def copy_x(output, x32, y32, data):
|
||||
|
||||
Reference in New Issue
Block a user