Merge branch 'main' into feat/count_events

This commit is contained in:
callebtc
2023-02-07 23:07:56 +01:00
13 changed files with 384 additions and 409 deletions

View File

@@ -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
View File

@@ -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())

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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):

View File

@@ -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))

View File

@@ -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():

View File

@@ -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
View File

@@ -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"},
]

View File

@@ -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"

View File

@@ -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

View File

@@ -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