mirror of
https://github.com/aljazceru/python-nostr.git
synced 2026-02-23 15:14:19 +01:00
Merge branch 'main' into feat/count_events
This commit is contained in:
37
README.md
37
README.md
@@ -11,6 +11,7 @@ public_key = private_key.public_key
|
|||||||
print(f"Private key: {private_key.bech32()}")
|
print(f"Private key: {private_key.bech32()}")
|
||||||
print(f"Public key: {public_key.bech32()}")
|
print(f"Public key: {public_key.bech32()}")
|
||||||
```
|
```
|
||||||
|
|
||||||
**Connect to relays**
|
**Connect to relays**
|
||||||
```python
|
```python
|
||||||
import json
|
import json
|
||||||
@@ -30,6 +31,7 @@ while relay_manager.message_pool.has_notices():
|
|||||||
|
|
||||||
relay_manager.close_connections()
|
relay_manager.close_connections()
|
||||||
```
|
```
|
||||||
|
|
||||||
**Publish to relays**
|
**Publish to relays**
|
||||||
```python
|
```python
|
||||||
import json
|
import json
|
||||||
@@ -48,7 +50,7 @@ time.sleep(1.25) # allow the connections to open
|
|||||||
|
|
||||||
private_key = PrivateKey()
|
private_key = PrivateKey()
|
||||||
|
|
||||||
event = Event(private_key.public_key.hex(), "Hello Nostr")
|
event = Event("Hello Nostr")
|
||||||
private_key.sign_event(event)
|
private_key.sign_event(event)
|
||||||
|
|
||||||
relay_manager.publish_event(event)
|
relay_manager.publish_event(event)
|
||||||
@@ -56,6 +58,38 @@ time.sleep(1) # allow the messages to send
|
|||||||
|
|
||||||
relay_manager.close_connections()
|
relay_manager.close_connections()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Reply to a note**
|
||||||
|
```python
|
||||||
|
from nostr.event import Event
|
||||||
|
|
||||||
|
reply = Event(
|
||||||
|
content="Hey, that's a great point!",
|
||||||
|
)
|
||||||
|
|
||||||
|
# create 'e' tag reference to the note you're replying to
|
||||||
|
reply.add_event_ref(original_note_id)
|
||||||
|
|
||||||
|
# create 'p' tag reference to the pubkey you're replying to
|
||||||
|
reply.add_pubkey_ref(original_note_author_pubkey)
|
||||||
|
|
||||||
|
private_key.sign_event(reply)
|
||||||
|
relay_manager.publish_event(reply)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Send a DM**
|
||||||
|
```python
|
||||||
|
from nostr.event import EncryptedDirectMessage
|
||||||
|
|
||||||
|
dm = EncryptedDirectMessage(
|
||||||
|
recipient_pubkey=recipient_pubkey,
|
||||||
|
cleartext_content="Secret message!"
|
||||||
|
)
|
||||||
|
private_key.sign_event(dm)
|
||||||
|
relay_manager.publish_event(dm)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
**Receive events from relays**
|
**Receive events from relays**
|
||||||
```python
|
```python
|
||||||
import json
|
import json
|
||||||
@@ -112,7 +146,6 @@ delegation = Delegation(
|
|||||||
identity_pk.sign_delegation(delegation)
|
identity_pk.sign_delegation(delegation)
|
||||||
|
|
||||||
event = Event(
|
event = Event(
|
||||||
delegatee_pk.public_key.hex(),
|
|
||||||
"Hello, NIP-26!",
|
"Hello, NIP-26!",
|
||||||
tags=[delegation.get_tag()],
|
tags=[delegation.get_tag()],
|
||||||
)
|
)
|
||||||
|
|||||||
11
main.py
11
main.py
@@ -96,8 +96,9 @@ async def post():
|
|||||||
sender_client.post(msg)
|
sender_client.post(msg)
|
||||||
|
|
||||||
|
|
||||||
# write a DM and receive DMs
|
if input("Enter 1 for DM, 2 for Posts (Default: 1)") or 1 == 1:
|
||||||
asyncio.run(dm())
|
# write a DM and receive DMs
|
||||||
|
asyncio.run(dm())
|
||||||
# make a post and subscribe to posts
|
else:
|
||||||
# asyncio.run(post())
|
# make a post and subscribe to posts
|
||||||
|
asyncio.run(post())
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from ..message_type import ClientMessageType
|
|||||||
from ..key import PrivateKey, PublicKey
|
from ..key import PrivateKey, PublicKey
|
||||||
|
|
||||||
from ..filter import Filter, Filters
|
from ..filter import Filter, Filters
|
||||||
from ..event import Event, EventKind
|
from ..event import Event, EventKind, EncryptedDirectMessage
|
||||||
from ..relay_manager import RelayManager
|
from ..relay_manager import RelayManager
|
||||||
from ..message_type import ClientMessageType
|
from ..message_type import ClientMessageType
|
||||||
|
|
||||||
@@ -37,13 +37,15 @@ class NostrClient:
|
|||||||
|
|
||||||
if len(relays):
|
if len(relays):
|
||||||
self.relays = relays
|
self.relays = relays
|
||||||
|
|
||||||
if connect:
|
if connect:
|
||||||
for relay in self.relays:
|
self.connect()
|
||||||
self.relay_manager.add_relay(relay)
|
|
||||||
self.relay_manager.open_connections(
|
def connect(self):
|
||||||
{"cert_reqs": ssl.CERT_NONE}
|
for relay in self.relays:
|
||||||
) # NOTE: This disables ssl certificate verification
|
self.relay_manager.add_relay(relay)
|
||||||
|
self.relay_manager.open_connections(
|
||||||
|
{"cert_reqs": ssl.CERT_NONE}
|
||||||
|
) # NOTE: This disables ssl certificate verification
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
self.relay_manager.close_connections()
|
self.relay_manager.close_connections()
|
||||||
@@ -54,10 +56,12 @@ class NostrClient:
|
|||||||
self.public_key = self.private_key.public_key
|
self.public_key = self.private_key.public_key
|
||||||
|
|
||||||
def post(self, message: str):
|
def post(self, message: str):
|
||||||
event = Event(self.public_key.hex(), message, kind=EventKind.TEXT_NOTE)
|
event = Event(message, self.public_key.hex(), kind=EventKind.TEXT_NOTE)
|
||||||
event.sign(self.private_key.hex())
|
self.private_key.sign_event(event)
|
||||||
message = json.dumps([ClientMessageType.EVENT, event.to_json_object()])
|
event_json = event.to_message()
|
||||||
self.relay_manager.publish_message(message)
|
# print("Publishing message:")
|
||||||
|
# print(event_json)
|
||||||
|
self.relay_manager.publish_message(event_json)
|
||||||
|
|
||||||
def get_post(
|
def get_post(
|
||||||
self, sender_publickey: PublicKey = None, callback_func=None, filter_kwargs={}
|
self, sender_publickey: PublicKey = None, callback_func=None, filter_kwargs={}
|
||||||
@@ -79,32 +83,25 @@ class NostrClient:
|
|||||||
while True:
|
while True:
|
||||||
while self.relay_manager.message_pool.has_events():
|
while self.relay_manager.message_pool.has_events():
|
||||||
event_msg = self.relay_manager.message_pool.get_event()
|
event_msg = self.relay_manager.message_pool.get_event()
|
||||||
print(event_msg.event.content)
|
|
||||||
if callback_func:
|
if callback_func:
|
||||||
callback_func(event_msg.event)
|
callback_func(event_msg.event)
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
def dm(self, message: str, to_pubkey: PublicKey):
|
def dm(self, message: str, to_pubkey: PublicKey):
|
||||||
shared_secret = self.private_key.compute_shared_secret(to_pubkey.hex())
|
dm = EncryptedDirectMessage(
|
||||||
aes = cbc.AESCipher(key=shared_secret)
|
recipient_pubkey=to_pubkey.hex(), cleartext_content=message
|
||||||
iv, enc_text = aes.encrypt(message)
|
|
||||||
content = f"{base64.b64encode(enc_text).decode('utf-8')}?iv={base64.b64encode(iv).decode('utf-8')}"
|
|
||||||
|
|
||||||
event = Event(
|
|
||||||
self.public_key.hex(),
|
|
||||||
content,
|
|
||||||
tags=[["p", to_pubkey.hex()]],
|
|
||||||
kind=EventKind.ENCRYPTED_DIRECT_MESSAGE,
|
|
||||||
)
|
)
|
||||||
event.sign(self.private_key.hex())
|
self.private_key.sign_event(dm)
|
||||||
event_message = json.dumps([ClientMessageType.EVENT, event.to_json_object()])
|
self.relay_manager.publish_event(dm)
|
||||||
self.relay_manager.publish_message(event_message)
|
|
||||||
|
|
||||||
def get_dm(self, sender_publickey: PublicKey, callback_func=None, filter_kwargs={}):
|
def get_dm(self, sender_publickey: PublicKey, callback_func=None):
|
||||||
filter = Filter(
|
filters = Filters(
|
||||||
kinds=[EventKind.ENCRYPTED_DIRECT_MESSAGE],
|
[
|
||||||
tags={"#p": [sender_publickey.hex()]},
|
Filter(
|
||||||
**filter_kwargs,
|
kinds=[EventKind.ENCRYPTED_DIRECT_MESSAGE],
|
||||||
|
pubkey_refs=[sender_publickey.hex()],
|
||||||
|
)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
filters = Filters([filter])
|
filters = Filters([filter])
|
||||||
subscription_id = os.urandom(4).hex()
|
subscription_id = os.urandom(4).hex()
|
||||||
@@ -139,6 +136,5 @@ class NostrClient:
|
|||||||
while True:
|
while True:
|
||||||
while self.relay_manager.message_pool.has_events():
|
while self.relay_manager.message_pool.has_events():
|
||||||
event_msg = self.relay_manager.message_pool.get_event()
|
event_msg = self.relay_manager.message_pool.get_event()
|
||||||
print(event_msg.event.content)
|
|
||||||
break
|
break
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|||||||
100
nostr/event.py
100
nostr/event.py
@@ -1,12 +1,15 @@
|
|||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
|
from dataclasses import dataclass, field
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
from typing import List
|
||||||
from secp256k1 import PrivateKey, PublicKey
|
from secp256k1 import PrivateKey, PublicKey
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
|
|
||||||
from nostr.message_type import ClientMessageType
|
from nostr.message_type import ClientMessageType
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class EventKind(IntEnum):
|
class EventKind(IntEnum):
|
||||||
SET_METADATA = 0
|
SET_METADATA = 0
|
||||||
TEXT_NOTE = 1
|
TEXT_NOTE = 1
|
||||||
@@ -16,41 +19,58 @@ class EventKind(IntEnum):
|
|||||||
DELETE = 5
|
DELETE = 5
|
||||||
|
|
||||||
|
|
||||||
class Event():
|
|
||||||
def __init__(
|
@dataclass
|
||||||
self,
|
class Event:
|
||||||
public_key: str,
|
content: str = None
|
||||||
content: str,
|
public_key: str = None
|
||||||
created_at: int = None,
|
created_at: int = None
|
||||||
kind: int=EventKind.TEXT_NOTE,
|
kind: int = EventKind.TEXT_NOTE
|
||||||
tags: "list[list[str]]"=[],
|
tags: List[List[str]] = field(default_factory=list) # Dataclasses require special handling when the default value is a mutable type
|
||||||
id: str=None,
|
signature: str = None
|
||||||
signature: str=None) -> None:
|
|
||||||
if not isinstance(content, str):
|
|
||||||
|
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")
|
raise TypeError("Argument 'content' must be of type str")
|
||||||
|
|
||||||
self.public_key = public_key
|
if self.created_at is None:
|
||||||
self.content = content
|
self.created_at = int(time.time())
|
||||||
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)
|
|
||||||
|
|
||||||
@staticmethod
|
@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 = [0, public_key, created_at, kind, tags, content]
|
||||||
data_str = json.dumps(data, separators=(',', ':'), ensure_ascii=False)
|
data_str = json.dumps(data, separators=(',', ':'), ensure_ascii=False)
|
||||||
return data_str.encode()
|
return data_str.encode()
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@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()
|
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:
|
def verify(self) -> bool:
|
||||||
pub_key = PublicKey(bytes.fromhex("02" + self.public_key), True) # add 02 for schnorr (bip340)
|
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(self.id), bytes.fromhex(self.signature), None, raw=True)
|
||||||
return pub_key.schnorr_verify(bytes.fromhex(event_id), bytes.fromhex(self.signature), None, raw=True)
|
|
||||||
|
|
||||||
def to_message(self) -> str:
|
def to_message(self) -> str:
|
||||||
return json.dumps(
|
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
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ from typing import List
|
|||||||
from .event import Event, EventKind
|
from .event import Event, EventKind
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Filter:
|
class Filter:
|
||||||
"""
|
"""
|
||||||
NIP-01 filtering.
|
NIP-01 filtering.
|
||||||
@@ -21,30 +19,23 @@ class Filter:
|
|||||||
# promoted to explicit support
|
# promoted to explicit support
|
||||||
Filter(hashtag_refs=[hashtags])
|
Filter(hashtag_refs=[hashtags])
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
<<<<<<< HEAD
|
|
||||||
self,
|
self,
|
||||||
ids: "list[str]" = None,
|
event_ids: List[str] = None,
|
||||||
kinds: "list[int]" = None,
|
kinds: List[EventKind] = None,
|
||||||
authors: "list[str]" = None,
|
authors: List[str] = None,
|
||||||
since: int = None,
|
since: int = None,
|
||||||
until: int = None,
|
until: int = None,
|
||||||
tags: "dict[str, list[str]]" = None,
|
event_refs: List[
|
||||||
|
str
|
||||||
|
] = None, # the "#e" attr; list of event ids referenced in an "e" tag
|
||||||
|
pubkey_refs: List[
|
||||||
|
str
|
||||||
|
] = None, # The "#p" attr; list of pubkeys referenced in a "p" tag
|
||||||
limit: int = None,
|
limit: int = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.IDs = ids
|
|
||||||
=======
|
|
||||||
self,
|
|
||||||
event_ids: List[str] = None,
|
|
||||||
kinds: List[EventKind] = None,
|
|
||||||
authors: List[str] = None,
|
|
||||||
since: int = None,
|
|
||||||
until: int = None,
|
|
||||||
event_refs: List[str] = None, # the "#e" attr; list of event ids referenced in an "e" tag
|
|
||||||
pubkey_refs: List[str] = None, # The "#p" attr; list of pubkeys referenced in a "p" tag
|
|
||||||
limit: int = None) -> None:
|
|
||||||
self.event_ids = event_ids
|
self.event_ids = event_ids
|
||||||
>>>>>>> bda320f6d6d5087fe1afecd122831afe025f7633
|
|
||||||
self.kinds = kinds
|
self.kinds = kinds
|
||||||
self.authors = authors
|
self.authors = authors
|
||||||
self.since = since
|
self.since = since
|
||||||
@@ -55,21 +46,19 @@ class Filter:
|
|||||||
|
|
||||||
self.tags = {}
|
self.tags = {}
|
||||||
if self.event_refs:
|
if self.event_refs:
|
||||||
self.add_arbitrary_tag('e', self.event_refs)
|
self.add_arbitrary_tag("e", self.event_refs)
|
||||||
if self.pubkey_refs:
|
if self.pubkey_refs:
|
||||||
self.add_arbitrary_tag('p', self.pubkey_refs)
|
self.add_arbitrary_tag("p", self.pubkey_refs)
|
||||||
|
|
||||||
|
|
||||||
def add_arbitrary_tag(self, tag: str, values: list):
|
def add_arbitrary_tag(self, tag: str, values: list):
|
||||||
"""
|
"""
|
||||||
Filter on any arbitrary tag with explicit handling for NIP-01 and NIP-12
|
Filter on any arbitrary tag with explicit handling for NIP-01 and NIP-12
|
||||||
single-letter tags.
|
single-letter tags.
|
||||||
"""
|
"""
|
||||||
# NIP-01 'e' and 'p' tags and any NIP-12 single-letter tags must be prefixed with "#"
|
# NIP-01 'e' and 'p' tags and any NIP-12 single-letter tags must be prefixed with "#"
|
||||||
tag_key = tag if len(tag) > 1 else f"#{tag}"
|
tag_key = tag if len(tag) > 1 else f"#{tag}"
|
||||||
self.tags[tag_key] = values
|
self.tags[tag_key] = values
|
||||||
|
|
||||||
|
|
||||||
def matches(self, event: Event) -> bool:
|
def matches(self, event: Event) -> bool:
|
||||||
if self.event_ids is not None and event.id not in self.event_ids:
|
if self.event_ids is not None and event.id not in self.event_ids:
|
||||||
return False
|
return False
|
||||||
@@ -81,16 +70,13 @@ class Filter:
|
|||||||
return False
|
return False
|
||||||
if self.until is not None and event.created_at > self.until:
|
if self.until is not None and event.created_at > self.until:
|
||||||
return False
|
return False
|
||||||
if (self.event_refs is not None or self.pubkey_refs is not None) and len(event.tags) == 0:
|
if (self.event_refs is not None or self.pubkey_refs is not None) and len(
|
||||||
|
event.tags
|
||||||
|
) == 0:
|
||||||
return False
|
return False
|
||||||
<<<<<<< HEAD
|
|
||||||
if self.tags != None:
|
|
||||||
e_tag_identifiers = [e_tag[0] for e_tag in event.tags]
|
|
||||||
=======
|
|
||||||
|
|
||||||
if self.tags:
|
if self.tags:
|
||||||
e_tag_identifiers = set([e_tag[0] for e_tag in event.tags])
|
e_tag_identifiers = set([e_tag[0] for e_tag in event.tags])
|
||||||
>>>>>>> bda320f6d6d5087fe1afecd122831afe025f7633
|
|
||||||
for f_tag, f_tag_values in self.tags.items():
|
for f_tag, f_tag_values in self.tags.items():
|
||||||
# Omit any NIP-01 or NIP-12 "#" chars on single-letter tags
|
# Omit any NIP-01 or NIP-12 "#" chars on single-letter tags
|
||||||
f_tag = f_tag.replace("#", "")
|
f_tag = f_tag.replace("#", "")
|
||||||
@@ -105,31 +91,19 @@ class Filter:
|
|||||||
# (e.g. a reply to multiple people) so we have to check all of them.
|
# (e.g. a reply to multiple people) so we have to check all of them.
|
||||||
match_found = False
|
match_found = False
|
||||||
for e_tag in event.tags:
|
for e_tag in event.tags:
|
||||||
<<<<<<< HEAD
|
|
||||||
if e_tag[1] not in f_tag_values:
|
|
||||||
return False
|
|
||||||
=======
|
|
||||||
if e_tag[0] == f_tag and e_tag[1] in f_tag_values:
|
if e_tag[0] == f_tag and e_tag[1] in f_tag_values:
|
||||||
match_found = True
|
match_found = True
|
||||||
break
|
break
|
||||||
if not match_found:
|
if not match_found:
|
||||||
return False
|
return False
|
||||||
>>>>>>> bda320f6d6d5087fe1afecd122831afe025f7633
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def to_json_object(self) -> dict:
|
def to_json_object(self) -> dict:
|
||||||
res = {}
|
res = {}
|
||||||
<<<<<<< HEAD
|
|
||||||
if self.IDs != None:
|
|
||||||
res["ids"] = self.IDs
|
|
||||||
if self.kinds != None:
|
|
||||||
=======
|
|
||||||
if self.event_ids is not None:
|
if self.event_ids is not None:
|
||||||
res["ids"] = self.event_ids
|
res["ids"] = self.event_ids
|
||||||
if self.kinds is not None:
|
if self.kinds is not None:
|
||||||
>>>>>>> bda320f6d6d5087fe1afecd122831afe025f7633
|
|
||||||
res["kinds"] = self.kinds
|
res["kinds"] = self.kinds
|
||||||
if self.authors is not None:
|
if self.authors is not None:
|
||||||
res["authors"] = self.authors
|
res["authors"] = self.authors
|
||||||
@@ -145,10 +119,6 @@ class Filter:
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
|
|
||||||
>>>>>>> bda320f6d6d5087fe1afecd122831afe025f7633
|
|
||||||
class Filters(UserList):
|
class Filters(UserList):
|
||||||
def __init__(self, initlist: "list[Filter]" = []) -> None:
|
def __init__(self, initlist: "list[Filter]" = []) -> None:
|
||||||
super().__init__(initlist)
|
super().__init__(initlist)
|
||||||
@@ -161,8 +131,4 @@ class Filters(UserList):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def to_json_array(self) -> list:
|
def to_json_array(self) -> list:
|
||||||
<<<<<<< HEAD
|
|
||||||
return [filter.to_json_object() for filter in self.data]
|
return [filter.to_json_object() for filter in self.data]
|
||||||
=======
|
|
||||||
return [filter.to_json_object() for filter in self.data]
|
|
||||||
>>>>>>> bda320f6d6d5087fe1afecd122831afe025f7633
|
|
||||||
|
|||||||
11
nostr/key.py
11
nostr/key.py
@@ -7,7 +7,7 @@ from cryptography.hazmat.primitives import padding
|
|||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
|
|
||||||
from nostr.delegation import Delegation
|
from nostr.delegation import Delegation
|
||||||
from nostr.event import Event
|
from nostr.event import EncryptedDirectMessage, Event, EventKind
|
||||||
from . import bech32
|
from . import bech32
|
||||||
|
|
||||||
|
|
||||||
@@ -78,6 +78,9 @@ class PrivateKey:
|
|||||||
|
|
||||||
return f"{base64.b64encode(encrypted_message).decode()}?iv={base64.b64encode(iv).decode()}"
|
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:
|
def decrypt_message(self, encoded_message: str, public_key_hex: str) -> str:
|
||||||
encoded_data = encoded_message.split('?iv=')
|
encoded_data = encoded_message.split('?iv=')
|
||||||
encoded_content, encoded_iv = encoded_data[0], encoded_data[1]
|
encoded_content, encoded_iv = encoded_data[0], encoded_data[1]
|
||||||
@@ -100,6 +103,10 @@ class PrivateKey:
|
|||||||
return sig.hex()
|
return sig.hex()
|
||||||
|
|
||||||
def sign_event(self, event: Event) -> None:
|
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))
|
event.signature = self.sign_message_hash(bytes.fromhex(event.id))
|
||||||
|
|
||||||
def sign_delegation(self, delegation: Delegation) -> None:
|
def sign_delegation(self, delegation: Delegation) -> None:
|
||||||
@@ -108,6 +115,7 @@ class PrivateKey:
|
|||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return self.raw_secret == other.raw_secret
|
return self.raw_secret == other.raw_secret
|
||||||
|
|
||||||
|
|
||||||
def mine_vanity_key(prefix: str = None, suffix: str = None) -> PrivateKey:
|
def mine_vanity_key(prefix: str = None, suffix: str = None) -> PrivateKey:
|
||||||
if prefix is None and suffix is None:
|
if prefix is None and suffix is None:
|
||||||
raise ValueError("Expected at least one of 'prefix' or 'suffix' arguments")
|
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
|
return sk
|
||||||
|
|
||||||
|
|
||||||
ffi = FFI()
|
ffi = FFI()
|
||||||
@ffi.callback("int (unsigned char *, const unsigned char *, const unsigned char *, void *)")
|
@ffi.callback("int (unsigned char *, const unsigned char *, const unsigned char *, void *)")
|
||||||
def copy_x(output, x32, y32, data):
|
def copy_x(output, x32, y32, data):
|
||||||
|
|||||||
@@ -4,22 +4,26 @@ from threading import Lock
|
|||||||
from .message_type import RelayMessageType
|
from .message_type import RelayMessageType
|
||||||
from .event import Event
|
from .event import Event
|
||||||
|
|
||||||
|
|
||||||
class EventMessage:
|
class EventMessage:
|
||||||
def __init__(self, event: Event, subscription_id: str, url: str) -> None:
|
def __init__(self, event: Event, subscription_id: str, url: str) -> None:
|
||||||
self.event = event
|
self.event = event
|
||||||
self.subscription_id = subscription_id
|
self.subscription_id = subscription_id
|
||||||
self.url = url
|
self.url = url
|
||||||
|
|
||||||
|
|
||||||
class NoticeMessage:
|
class NoticeMessage:
|
||||||
def __init__(self, content: str, url: str) -> None:
|
def __init__(self, content: str, url: str) -> None:
|
||||||
self.content = content
|
self.content = content
|
||||||
self.url = url
|
self.url = url
|
||||||
|
|
||||||
|
|
||||||
class EndOfStoredEventsMessage:
|
class EndOfStoredEventsMessage:
|
||||||
def __init__(self, subscription_id: str, url: str) -> None:
|
def __init__(self, subscription_id: str, url: str) -> None:
|
||||||
self.subscription_id = subscription_id
|
self.subscription_id = subscription_id
|
||||||
self.url = url
|
self.url = url
|
||||||
|
|
||||||
|
|
||||||
class MessagePool:
|
class MessagePool:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.events: Queue[EventMessage] = Queue()
|
self.events: Queue[EventMessage] = Queue()
|
||||||
@@ -55,7 +59,14 @@ class MessagePool:
|
|||||||
if message_type == RelayMessageType.EVENT:
|
if message_type == RelayMessageType.EVENT:
|
||||||
subscription_id = message_json[1]
|
subscription_id = message_json[1]
|
||||||
e = message_json[2]
|
e = message_json[2]
|
||||||
event = Event(e['pubkey'], e['content'], e['created_at'], e['kind'], e['tags'], e['id'], e['sig'])
|
event = Event(
|
||||||
|
e["content"],
|
||||||
|
e["pubkey"],
|
||||||
|
e["created_at"],
|
||||||
|
e["kind"],
|
||||||
|
e["tags"],
|
||||||
|
e["sig"],
|
||||||
|
)
|
||||||
with self.lock:
|
with self.lock:
|
||||||
if not event.id in self._unique_events:
|
if not event.id in self._unique_events:
|
||||||
self.events.put(EventMessage(event, subscription_id, url))
|
self.events.put(EventMessage(event, subscription_id, url))
|
||||||
@@ -64,5 +75,3 @@ class MessagePool:
|
|||||||
self.notices.put(NoticeMessage(message_json[1], url))
|
self.notices.put(NoticeMessage(message_json[1], url))
|
||||||
elif message_type == RelayMessageType.END_OF_STORED_EVENTS:
|
elif message_type == RelayMessageType.END_OF_STORED_EVENTS:
|
||||||
self.eose_notices.put(EndOfStoredEventsMessage(message_json[1], url))
|
self.eose_notices.put(EndOfStoredEventsMessage(message_json[1], url))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class Relay:
|
|||||||
self.num_sent_events: int = 0
|
self.num_sent_events: int = 0
|
||||||
self.num_subscriptions: int = 0
|
self.num_subscriptions: int = 0
|
||||||
self.ssl_options: dict = {}
|
self.ssl_options: dict = {}
|
||||||
|
self.proxy: dict = {}
|
||||||
self.lock = Lock()
|
self.lock = Lock()
|
||||||
self.ws = WebSocketApp(
|
self.ws = WebSocketApp(
|
||||||
url,
|
url,
|
||||||
@@ -49,19 +50,16 @@ class Relay:
|
|||||||
on_pong=self._on_pong,
|
on_pong=self._on_pong,
|
||||||
)
|
)
|
||||||
|
|
||||||
def connect(self, ssl_options: dict=None, proxy: dict=None):
|
def connect(self, ssl_options: dict = None, proxy: dict = None):
|
||||||
self.ssl_options = ssl_options
|
self.ssl_options = ssl_options
|
||||||
<<<<<<< HEAD
|
|
||||||
print(self.url, "🟢")
|
print(self.url, "🟢")
|
||||||
self.ws.run_forever(sslopt=self.ssl_options)
|
self.proxy = proxy
|
||||||
=======
|
|
||||||
self.ws.run_forever(
|
self.ws.run_forever(
|
||||||
sslopt=ssl_options,
|
sslopt=ssl_options,
|
||||||
http_proxy_host=None if proxy is None else proxy.get('host'),
|
http_proxy_host=None if proxy is None else proxy.get("host"),
|
||||||
http_proxy_port=None if proxy is None else proxy.get('port'),
|
http_proxy_port=None if proxy is None else proxy.get("port"),
|
||||||
proxy_type=None if proxy is None else proxy.get('type')
|
proxy_type=None if proxy is None else proxy.get("type"),
|
||||||
)
|
)
|
||||||
>>>>>>> main
|
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
print(self.url, "🔴")
|
print(self.url, "🔴")
|
||||||
@@ -75,7 +73,7 @@ class Relay:
|
|||||||
self.connected = False
|
self.connected = False
|
||||||
if self.reconnect:
|
if self.reconnect:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
self.connect(self.ssl_options)
|
self.connect(self.ssl_options, self.proxy)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ping(self):
|
def ping(self):
|
||||||
@@ -164,12 +162,11 @@ class Relay:
|
|||||||
|
|
||||||
e = message_json[2]
|
e = message_json[2]
|
||||||
event = Event(
|
event = Event(
|
||||||
e["pubkey"],
|
|
||||||
e["content"],
|
e["content"],
|
||||||
|
e["pubkey"],
|
||||||
e["created_at"],
|
e["created_at"],
|
||||||
e["kind"],
|
e["kind"],
|
||||||
e["tags"],
|
e["tags"],
|
||||||
e["id"],
|
|
||||||
e["sig"],
|
e["sig"],
|
||||||
)
|
)
|
||||||
if not event.verify():
|
if not event.verify():
|
||||||
|
|||||||
@@ -8,18 +8,18 @@ from .message_type import ClientMessageType
|
|||||||
from .relay import Relay, RelayPolicy
|
from .relay import Relay, RelayPolicy
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class RelayException(Exception):
|
class RelayException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class RelayManager:
|
class RelayManager:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.relays: dict[str, Relay] = {}
|
self.relays: dict[str, Relay] = {}
|
||||||
self.message_pool = MessagePool()
|
self.message_pool = MessagePool()
|
||||||
|
|
||||||
def add_relay(self, url: str, read: bool=True, write: bool=True, subscriptions={}):
|
def add_relay(
|
||||||
|
self, url: str, read: bool = True, write: bool = True, subscriptions={}
|
||||||
|
):
|
||||||
policy = RelayPolicy(read, write)
|
policy = RelayPolicy(read, write)
|
||||||
relay = Relay(url, policy, self.message_pool, subscriptions)
|
relay = Relay(url, policy, self.message_pool, subscriptions)
|
||||||
self.relays[url] = relay
|
self.relays[url] = relay
|
||||||
@@ -35,12 +35,12 @@ class RelayManager:
|
|||||||
for relay in self.relays.values():
|
for relay in self.relays.values():
|
||||||
relay.close_subscription(id)
|
relay.close_subscription(id)
|
||||||
|
|
||||||
def open_connections(self, ssl_options: dict=None, proxy: dict=None):
|
def open_connections(self, ssl_options: dict = None, proxy: dict = None):
|
||||||
for relay in self.relays.values():
|
for relay in self.relays.values():
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=relay.connect,
|
target=relay.connect,
|
||||||
args=(ssl_options, proxy),
|
args=(ssl_options, proxy),
|
||||||
name=f"{relay.url}-thread"
|
name=f"{relay.url}-thread",
|
||||||
).start()
|
).start()
|
||||||
|
|
||||||
def close_connections(self):
|
def close_connections(self):
|
||||||
@@ -53,11 +53,12 @@ class RelayManager:
|
|||||||
relay.publish(message)
|
relay.publish(message)
|
||||||
|
|
||||||
def publish_event(self, event: Event):
|
def publish_event(self, event: Event):
|
||||||
""" Verifies that the Event is publishable before submitting it to relays """
|
"""Verifies that the Event is publishable before submitting it to relays"""
|
||||||
if event.signature is None:
|
if event.signature is None:
|
||||||
raise RelayException(f"Could not publish {event.id}: must be signed")
|
raise RelayException(f"Could not publish {event.id}: must be signed")
|
||||||
|
|
||||||
if not event.verify():
|
if not event.verify():
|
||||||
raise RelayException(f"Could not publish {event.id}: failed to verify signature {event.signature}")
|
raise RelayException(
|
||||||
|
f"Could not publish {event.id}: failed to verify signature {event.signature}"
|
||||||
|
)
|
||||||
self.publish_message(event.to_message())
|
self.publish_message(event.to_message())
|
||||||
|
|||||||
262
poetry.lock
generated
262
poetry.lock
generated
@@ -1,259 +1,7 @@
|
|||||||
[[package]]
|
# This file is automatically @generated by Poetry and should not be changed by hand.
|
||||||
name = "black"
|
package = []
|
||||||
version = "22.12.0"
|
|
||||||
description = "The uncompromising code formatter."
|
|
||||||
category = "dev"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
click = ">=8.0.0"
|
|
||||||
mypy-extensions = ">=0.4.3"
|
|
||||||
pathspec = ">=0.9.0"
|
|
||||||
platformdirs = ">=2"
|
|
||||||
tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
|
|
||||||
typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""}
|
|
||||||
typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
colorama = ["colorama (>=0.4.3)"]
|
|
||||||
d = ["aiohttp (>=3.7.4)"]
|
|
||||||
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
|
||||||
uvloop = ["uvloop (>=0.15.2)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "click"
|
|
||||||
version = "8.1.3"
|
|
||||||
description = "Composable command line interface toolkit"
|
|
||||||
category = "dev"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
|
||||||
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "colorama"
|
|
||||||
version = "0.4.6"
|
|
||||||
description = "Cross-platform colored terminal text."
|
|
||||||
category = "dev"
|
|
||||||
optional = false
|
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "importlib-metadata"
|
|
||||||
version = "5.2.0"
|
|
||||||
description = "Read metadata from Python packages"
|
|
||||||
category = "dev"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
|
|
||||||
zipp = ">=0.5"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
|
||||||
perf = ["ipython"]
|
|
||||||
testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mypy-extensions"
|
|
||||||
version = "0.4.3"
|
|
||||||
description = "Experimental type system extensions for programs checked with the mypy typechecker."
|
|
||||||
category = "dev"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pathspec"
|
|
||||||
version = "0.10.3"
|
|
||||||
description = "Utility library for gitignore style pattern matching of file paths."
|
|
||||||
category = "dev"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "platformdirs"
|
|
||||||
version = "2.6.0"
|
|
||||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
|
||||||
category = "dev"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.4)"]
|
|
||||||
test = ["appdirs (==1.4.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pycryptodomex"
|
|
||||||
version = "3.16.0"
|
|
||||||
description = "Cryptographic library for Python"
|
|
||||||
category = "main"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tomli"
|
|
||||||
version = "2.0.1"
|
|
||||||
description = "A lil' TOML parser"
|
|
||||||
category = "dev"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "typed-ast"
|
|
||||||
version = "1.5.4"
|
|
||||||
description = "a fork of Python 2 and 3 ast modules with type comment support"
|
|
||||||
category = "dev"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.6"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "typing-extensions"
|
|
||||||
version = "4.4.0"
|
|
||||||
description = "Backported and Experimental Type Hints for Python 3.7+"
|
|
||||||
category = "dev"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "websocket-client"
|
|
||||||
version = "1.3.3"
|
|
||||||
description = "WebSocket client for Python with low level API options"
|
|
||||||
category = "main"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"]
|
|
||||||
optional = ["python-socks", "wsaccel"]
|
|
||||||
test = ["websockets"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zipp"
|
|
||||||
version = "3.11.0"
|
|
||||||
description = "Backport of pathlib-compatible object wrapper for zip files"
|
|
||||||
category = "dev"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"]
|
|
||||||
testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
|
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.7"
|
python-versions = "*"
|
||||||
content-hash = "b4819f5f4403092e6aafe15d38b9a0850eff2c7b5f9417b2cd0ad5b91c913bcd"
|
content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8"
|
||||||
|
|
||||||
[metadata.files]
|
|
||||||
black = [
|
|
||||||
{file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"},
|
|
||||||
{file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"},
|
|
||||||
{file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"},
|
|
||||||
{file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"},
|
|
||||||
{file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"},
|
|
||||||
{file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"},
|
|
||||||
{file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"},
|
|
||||||
{file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"},
|
|
||||||
{file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"},
|
|
||||||
{file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"},
|
|
||||||
{file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"},
|
|
||||||
{file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"},
|
|
||||||
]
|
|
||||||
click = [
|
|
||||||
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
|
|
||||||
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
|
|
||||||
]
|
|
||||||
colorama = [
|
|
||||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
|
||||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
|
||||||
]
|
|
||||||
importlib-metadata = [
|
|
||||||
{file = "importlib_metadata-5.2.0-py3-none-any.whl", hash = "sha256:0eafa39ba42bf225fc00e67f701d71f85aead9f878569caf13c3724f704b970f"},
|
|
||||||
{file = "importlib_metadata-5.2.0.tar.gz", hash = "sha256:404d48d62bba0b7a77ff9d405efd91501bef2e67ff4ace0bed40a0cf28c3c7cd"},
|
|
||||||
]
|
|
||||||
mypy-extensions = [
|
|
||||||
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
|
|
||||||
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
|
|
||||||
]
|
|
||||||
pathspec = [
|
|
||||||
{file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"},
|
|
||||||
{file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"},
|
|
||||||
]
|
|
||||||
platformdirs = [
|
|
||||||
{file = "platformdirs-2.6.0-py3-none-any.whl", hash = "sha256:1a89a12377800c81983db6be069ec068eee989748799b946cce2a6e80dcc54ca"},
|
|
||||||
{file = "platformdirs-2.6.0.tar.gz", hash = "sha256:b46ffafa316e6b83b47489d240ce17173f123a9b9c83282141c3daf26ad9ac2e"},
|
|
||||||
]
|
|
||||||
pycryptodomex = [
|
|
||||||
{file = "pycryptodomex-3.16.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b3d04c00d777c36972b539fb79958790126847d84ec0129fce1efef250bfe3ce"},
|
|
||||||
{file = "pycryptodomex-3.16.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e5a670919076b71522c7d567a9043f66f14b202414a63c3a078b5831ae342c03"},
|
|
||||||
{file = "pycryptodomex-3.16.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:ce338a9703f54b2305a408fc9890eb966b727ce72b69f225898bb4e9d9ed3f1f"},
|
|
||||||
{file = "pycryptodomex-3.16.0-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:a1c0ae7123448ecb034c75c713189cb00ebe2d415b11682865b6c54d200d9c93"},
|
|
||||||
{file = "pycryptodomex-3.16.0-cp27-cp27m-win32.whl", hash = "sha256:8851585ff19871e5d69e1790f4ca5f6fd1699d6b8b14413b472a4c0dbc7ea780"},
|
|
||||||
{file = "pycryptodomex-3.16.0-cp27-cp27m-win_amd64.whl", hash = "sha256:8dd2d9e3c617d0712ed781a77efd84ea579e76c5f9b2a4bc0b684ebeddf868b2"},
|
|
||||||
{file = "pycryptodomex-3.16.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:2ad9bb86b355b6104796567dd44c215b3dc953ef2fae5e0bdfb8516731df92cf"},
|
|
||||||
{file = "pycryptodomex-3.16.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:e25a2f5667d91795f9417cb856f6df724ccdb0cdd5cbadb212ee9bf43946e9f8"},
|
|
||||||
{file = "pycryptodomex-3.16.0-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:b0789a8490114a2936ed77c87792cfe77582c829cb43a6d86ede0f9624ba8aa3"},
|
|
||||||
{file = "pycryptodomex-3.16.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:0da835af786fdd1c9930994c78b23e88d816dc3f99aa977284a21bbc26d19735"},
|
|
||||||
{file = "pycryptodomex-3.16.0-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:22aed0868622d95179217c298e37ed7410025c7b29dac236d3230617d1e4ed56"},
|
|
||||||
{file = "pycryptodomex-3.16.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1619087fb5b31510b0b0b058a54f001a5ffd91e6ffee220d9913064519c6a69d"},
|
|
||||||
{file = "pycryptodomex-3.16.0-cp35-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:70288d9bfe16b2fd0d20b6c365db614428f1bcde7b20d56e74cf88ade905d9eb"},
|
|
||||||
{file = "pycryptodomex-3.16.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7993d26dae4d83b8f4ce605bb0aecb8bee330bb3c95475ef06f3694403621e71"},
|
|
||||||
{file = "pycryptodomex-3.16.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:1cda60207be8c1cf0b84b9138f9e3ca29335013d2b690774a5e94678ff29659a"},
|
|
||||||
{file = "pycryptodomex-3.16.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:04610536921c1ec7adba158ef570348550c9f3a40bc24be9f8da2ef7ab387981"},
|
|
||||||
{file = "pycryptodomex-3.16.0-cp35-abi3-win32.whl", hash = "sha256:daa67f5ebb6fbf1ee9c90decaa06ca7fc88a548864e5e484d52b0920a57fe8a5"},
|
|
||||||
{file = "pycryptodomex-3.16.0-cp35-abi3-win_amd64.whl", hash = "sha256:231dc8008cbdd1ae0e34645d4523da2dbc7a88c325f0d4a59635a86ee25b41dd"},
|
|
||||||
{file = "pycryptodomex-3.16.0-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:4dbbe18cc232b5980c7633972ae5417d0df76fe89e7db246eefd17ef4d8e6d7a"},
|
|
||||||
{file = "pycryptodomex-3.16.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:893f8a97d533c66cc3a56e60dd3ed40a3494ddb4aafa7e026429a08772f8a849"},
|
|
||||||
{file = "pycryptodomex-3.16.0-pp27-pypy_73-win32.whl", hash = "sha256:6a465e4f856d2a4f2a311807030c89166529ccf7ccc65bef398de045d49144b6"},
|
|
||||||
{file = "pycryptodomex-3.16.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ba57ac7861fd2c837cdb33daf822f2a052ff57dd769a2107807f52a36d0e8d38"},
|
|
||||||
{file = "pycryptodomex-3.16.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f2b971a7b877348a27dcfd0e772a0343fb818df00b74078e91c008632284137d"},
|
|
||||||
{file = "pycryptodomex-3.16.0-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e2453162f473c1eae4826eb10cd7bce19b5facac86d17fb5f29a570fde145abd"},
|
|
||||||
{file = "pycryptodomex-3.16.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:0ba28aa97cdd3ff5ed1a4f2b7f5cd04e721166bd75bd2b929e2734433882b583"},
|
|
||||||
{file = "pycryptodomex-3.16.0.tar.gz", hash = "sha256:e9ba9d8ed638733c9e95664470b71d624a6def149e2db6cc52c1aca5a6a2df1d"},
|
|
||||||
]
|
|
||||||
tomli = [
|
|
||||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
|
||||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
|
||||||
]
|
|
||||||
typed-ast = [
|
|
||||||
{file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"},
|
|
||||||
{file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"},
|
|
||||||
{file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"},
|
|
||||||
{file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"},
|
|
||||||
{file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"},
|
|
||||||
{file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"},
|
|
||||||
{file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"},
|
|
||||||
{file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"},
|
|
||||||
{file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"},
|
|
||||||
{file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"},
|
|
||||||
{file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"},
|
|
||||||
{file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"},
|
|
||||||
{file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"},
|
|
||||||
{file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"},
|
|
||||||
{file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"},
|
|
||||||
{file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"},
|
|
||||||
{file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"},
|
|
||||||
{file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"},
|
|
||||||
{file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"},
|
|
||||||
{file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"},
|
|
||||||
{file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"},
|
|
||||||
{file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"},
|
|
||||||
{file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"},
|
|
||||||
{file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"},
|
|
||||||
]
|
|
||||||
typing-extensions = [
|
|
||||||
{file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
|
|
||||||
{file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
|
|
||||||
]
|
|
||||||
websocket-client = [
|
|
||||||
{file = "websocket-client-1.3.3.tar.gz", hash = "sha256:d58c5f284d6a9bf8379dab423259fe8f85b70d5fa5d2916d5791a84594b122b1"},
|
|
||||||
{file = "websocket_client-1.3.3-py3-none-any.whl", hash = "sha256:5d55652dc1d0b3c734f044337d929aaf83f4f9138816ec680c1aefefb4dc4877"},
|
|
||||||
]
|
|
||||||
zipp = [
|
|
||||||
{file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"},
|
|
||||||
{file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"},
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
[tool.poetry]
|
||||||
|
name = "python-nostr"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = ""
|
||||||
|
authors = ["Your Name <you@example.com>"]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools", "setuptools-scm"]
|
requires = ["setuptools", "setuptools-scm"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|||||||
@@ -1,14 +1,93 @@
|
|||||||
from nostr.event import Event
|
import pytest
|
||||||
from nostr.key import PrivateKey
|
|
||||||
import time
|
import time
|
||||||
|
from nostr.event import Event, EncryptedDirectMessage
|
||||||
|
from nostr.key import PrivateKey
|
||||||
|
|
||||||
def test_event_default_time():
|
|
||||||
"""
|
|
||||||
ensure created_at default value reflects the time at Event object instantiation
|
class TestEvent:
|
||||||
see: https://github.com/jeffthibault/python-nostr/issues/23
|
def test_event_default_time(self):
|
||||||
"""
|
"""
|
||||||
public_key = PrivateKey().public_key.hex()
|
ensure created_at default value reflects the time at Event object instantiation
|
||||||
event1 = Event(public_key=public_key, content='test event')
|
see: https://github.com/jeffthibault/python-nostr/issues/23
|
||||||
time.sleep(1.5)
|
"""
|
||||||
event2 = Event(public_key=public_key, content='test event')
|
event1 = Event(content='test event')
|
||||||
assert event1.created_at < event2.created_at
|
time.sleep(1.5)
|
||||||
|
event2 = Event(content='test event')
|
||||||
|
assert event1.created_at < event2.created_at
|
||||||
|
|
||||||
|
|
||||||
|
def test_content_only_instantiation(self):
|
||||||
|
""" should be able to create an Event by only specifying content without kwarg """
|
||||||
|
event = Event("Hello, world!")
|
||||||
|
assert event.content is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_id_recomputes(self):
|
||||||
|
""" should recompute the Event.id to reflect the current Event attrs """
|
||||||
|
event = Event(content="some event")
|
||||||
|
|
||||||
|
# id should be computed on the fly
|
||||||
|
event_id = event.id
|
||||||
|
|
||||||
|
event.created_at += 10
|
||||||
|
|
||||||
|
# Recomputed id should now be different
|
||||||
|
assert event.id != event_id
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_event_ref(self):
|
||||||
|
""" should add an 'e' tag for each event_ref added """
|
||||||
|
some_event_id = "some_event_id"
|
||||||
|
event = Event(content="Adding an 'e' tag")
|
||||||
|
event.add_event_ref(some_event_id)
|
||||||
|
assert ['e', some_event_id] in event.tags
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_pubkey_ref(self):
|
||||||
|
""" should add a 'p' tag for each pubkey_ref added """
|
||||||
|
some_pubkey = "some_pubkey"
|
||||||
|
event = Event(content="Adding a 'p' tag")
|
||||||
|
event.add_pubkey_ref(some_pubkey)
|
||||||
|
assert ['p', some_pubkey] in event.tags
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TestEncryptedDirectMessage:
|
||||||
|
def setup_class(self):
|
||||||
|
self.sender_pk = PrivateKey()
|
||||||
|
self.sender_pubkey = self.sender_pk.public_key.hex()
|
||||||
|
self.recipient_pk = PrivateKey()
|
||||||
|
self.recipient_pubkey = self.recipient_pk.public_key.hex()
|
||||||
|
|
||||||
|
|
||||||
|
def test_content_field_moved_to_cleartext_content(self):
|
||||||
|
""" Should transfer `content` field data to `cleartext_content` """
|
||||||
|
dm = EncryptedDirectMessage(content="My message!", recipient_pubkey=self.recipient_pubkey)
|
||||||
|
assert dm.content is None
|
||||||
|
assert dm.cleartext_content is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_nokwarg_content_allowed(self):
|
||||||
|
""" Should allow creating a new DM w/no `content` nor `cleartext_content` kwarg """
|
||||||
|
dm = EncryptedDirectMessage("My message!", recipient_pubkey=self.recipient_pubkey)
|
||||||
|
assert dm.cleartext_content is not None
|
||||||
|
|
||||||
|
|
||||||
|
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 """
|
||||||
|
dm = EncryptedDirectMessage(cleartext_content="My message!", recipient_pubkey=self.recipient_pubkey)
|
||||||
|
|
||||||
|
with pytest.raises(Exception) as e:
|
||||||
|
dm.id
|
||||||
|
assert "undefined" in str(e)
|
||||||
|
|
||||||
|
# But once we encrypt it, we can request its id
|
||||||
|
self.sender_pk.encrypt_dm(dm)
|
||||||
|
assert dm.id is not None
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from nostr.event import Event, EncryptedDirectMessage
|
||||||
from nostr.key import PrivateKey
|
from nostr.key import PrivateKey
|
||||||
|
|
||||||
|
|
||||||
@@ -21,3 +22,78 @@ def test_from_nsec():
|
|||||||
pk1 = PrivateKey()
|
pk1 = PrivateKey()
|
||||||
pk2 = PrivateKey.from_nsec(pk1.bech32())
|
pk2 = PrivateKey.from_nsec(pk1.bech32())
|
||||||
assert pk1.raw_secret == pk2.raw_secret
|
assert pk1.raw_secret == pk2.raw_secret
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TestEvent:
|
||||||
|
def setup_class(self):
|
||||||
|
self.sender_pk = PrivateKey()
|
||||||
|
self.sender_pubkey = self.sender_pk.public_key.hex()
|
||||||
|
|
||||||
|
|
||||||
|
def test_sign_event_is_valid(self):
|
||||||
|
""" sign should create a signature that can be verified against Event.id """
|
||||||
|
event = Event(content="Hello, world!")
|
||||||
|
self.sender_pk.sign_event(event)
|
||||||
|
assert event.verify()
|
||||||
|
|
||||||
|
|
||||||
|
def test_sign_event_adds_pubkey(self):
|
||||||
|
""" sign should add the sender's pubkey if not already specified """
|
||||||
|
event = Event(content="Hello, world!")
|
||||||
|
|
||||||
|
# The event's public_key hasn't been specified yet
|
||||||
|
assert event.public_key is None
|
||||||
|
|
||||||
|
self.sender_pk.sign_event(event)
|
||||||
|
|
||||||
|
# PrivateKey.sign() should have populated public_key
|
||||||
|
assert event.public_key == self.sender_pubkey
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TestEncryptedDirectMessage:
|
||||||
|
def setup_class(self):
|
||||||
|
self.sender_pk = PrivateKey()
|
||||||
|
self.sender_pubkey = self.sender_pk.public_key.hex()
|
||||||
|
self.recipient_pk = PrivateKey()
|
||||||
|
self.recipient_pubkey = self.recipient_pk.public_key.hex()
|
||||||
|
|
||||||
|
|
||||||
|
def test_encrypt_dm(self):
|
||||||
|
""" Should encrypt a DM and populate its `content` field with ciphertext that either party can decrypt """
|
||||||
|
message = "My secret message!"
|
||||||
|
|
||||||
|
dm = EncryptedDirectMessage(
|
||||||
|
recipient_pubkey=self.recipient_pubkey,
|
||||||
|
cleartext_content=message,
|
||||||
|
)
|
||||||
|
|
||||||
|
# DM's content field should be initially blank
|
||||||
|
assert dm.content is None
|
||||||
|
self.sender_pk.encrypt_dm(dm)
|
||||||
|
|
||||||
|
# After encrypting, the content field should now be populated
|
||||||
|
assert dm.content is not None
|
||||||
|
|
||||||
|
# Sender should be able to decrypt
|
||||||
|
decrypted_message = self.sender_pk.decrypt_message(encoded_message=dm.content, public_key_hex=self.recipient_pubkey)
|
||||||
|
assert decrypted_message == message
|
||||||
|
|
||||||
|
# Recipient should be able to decrypt by referencing the sender's pubkey
|
||||||
|
decrypted_message = self.recipient_pk.decrypt_message(encoded_message=dm.content, public_key_hex=self.sender_pubkey)
|
||||||
|
assert decrypted_message == message
|
||||||
|
|
||||||
|
|
||||||
|
def test_sign_encrypts_dm(self):
|
||||||
|
""" `sign` should encrypt a DM that hasn't been encrypted yet """
|
||||||
|
dm = EncryptedDirectMessage(
|
||||||
|
recipient_pubkey=self.recipient_pubkey,
|
||||||
|
cleartext_content="Some DM message",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert dm.content is None
|
||||||
|
|
||||||
|
self.sender_pk.sign_event(dm)
|
||||||
|
|
||||||
|
assert dm.content is not None
|
||||||
|
|||||||
Reference in New Issue
Block a user