From 9ea01b78d86d32915d9c17b655a79b4ffe949072 Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Thu, 26 Oct 2023 19:46:10 +0300
Subject: [PATCH 01/30] refactor: clean-up
---
__init__.py | 2 +
crud.py | 8 +-
migrations.py | 2 +-
models.py | 9 +-
nostr/bech32.py | 32 +++--
nostr/client/client.py | 9 +-
nostr/delegation.py | 8 +-
nostr/key.py | 2 +-
nostr/message_pool.py | 14 +-
nostr/relay.py | 160 ++++-----------------
nostr/relay_manager.py | 43 +++---
nostr/subscription.py | 7 +-
router.py | 93 ++++++------
tasks.py | 32 +++--
templates/nostrclient/index.html | 238 +++++++++++++++++++++++--------
views_api.py | 17 ++-
16 files changed, 363 insertions(+), 313 deletions(-)
diff --git a/__init__.py b/__init__.py
index 7f573e7..84750dc 100644
--- a/__init__.py
+++ b/__init__.py
@@ -22,6 +22,8 @@
scheduled_tasks: List[asyncio.Task] = []
+
+# remove!
class NostrClient:
def __init__(self):
self.client: NostrClientLib = NostrClientLib(connect=False)
diff --git a/crud.py b/crud.py
index 780642d..c064a9a 100644
--- a/crud.py
+++ b/crud.py
@@ -1,9 +1,3 @@
-from typing import List, Optional, Union
-
-import shortuuid
-
-from lnbits.helpers import urlsafe_short_hash
-
from . import db
from .models import Relay, RelayList
@@ -15,7 +9,7 @@ async def get_relays() -> RelayList:
async def add_relay(relay: Relay) -> None:
await db.execute(
- f"""
+ """
INSERT INTO nostrclient.relays (
id,
url,
diff --git a/migrations.py b/migrations.py
index 5a30e45..73b9ed8 100644
--- a/migrations.py
+++ b/migrations.py
@@ -3,7 +3,7 @@ async def m001_initial(db):
Initial nostrclient table.
"""
await db.execute(
- f"""
+ """
CREATE TABLE nostrclient.relays (
id TEXT NOT NULL PRIMARY KEY,
url TEXT NOT NULL,
diff --git a/models.py b/models.py
index 88651fc..24730bc 100644
--- a/models.py
+++ b/models.py
@@ -1,8 +1,5 @@
-from dataclasses import dataclass
-from typing import Dict, List, Optional
+from typing import List, Optional
-from fastapi import Request
-from fastapi.param_functions import Query
from pydantic import BaseModel, Field
from lnbits.helpers import urlsafe_short_hash
@@ -14,7 +11,8 @@ class RelayStatus(BaseModel):
error_counter: Optional[int] = 0
error_list: Optional[List] = []
notice_list: Optional[List] = []
-
+
+
class Relay(BaseModel):
id: Optional[str] = None
url: Optional[str] = None
@@ -62,6 +60,7 @@ class TestMessage(BaseModel):
reciever_public_key: str
message: str
+
class TestMessageResponse(BaseModel):
private_key: str
public_key: str
diff --git a/nostr/bech32.py b/nostr/bech32.py
index 61a92c4..0ae6c80 100644
--- a/nostr/bech32.py
+++ b/nostr/bech32.py
@@ -26,19 +26,22 @@
class Encoding(Enum):
"""Enumeration type to list the various supported encodings."""
+
BECH32 = 1
BECH32M = 2
+
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
-BECH32M_CONST = 0x2bc830a3
+BECH32M_CONST = 0x2BC830A3
+
def bech32_polymod(values):
"""Internal function that computes the Bech32 checksum."""
- generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
+ generator = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3]
chk = 1
for value in values:
top = chk >> 25
- chk = (chk & 0x1ffffff) << 5 ^ value
+ chk = (chk & 0x1FFFFFF) << 5 ^ value
for i in range(5):
chk ^= generator[i] if ((top >> i) & 1) else 0
return chk
@@ -58,6 +61,7 @@ def bech32_verify_checksum(hrp, data):
return Encoding.BECH32M
return None
+
def bech32_create_checksum(hrp, data, spec):
"""Compute the checksum values given HRP and data."""
values = bech32_hrp_expand(hrp) + data
@@ -69,26 +73,29 @@ def bech32_create_checksum(hrp, data, spec):
def bech32_encode(hrp, data, spec):
"""Compute a Bech32 string given HRP and data values."""
combined = data + bech32_create_checksum(hrp, data, spec)
- return hrp + '1' + ''.join([CHARSET[d] for d in combined])
+ return hrp + "1" + "".join([CHARSET[d] for d in combined])
+
def bech32_decode(bech):
"""Validate a Bech32/Bech32m string, and determine HRP and data."""
- if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or
- (bech.lower() != bech and bech.upper() != bech)):
+ if (any(ord(x) < 33 or ord(x) > 126 for x in bech)) or (
+ bech.lower() != bech and bech.upper() != bech
+ ):
return (None, None, None)
bech = bech.lower()
- pos = bech.rfind('1')
+ pos = bech.rfind("1")
if pos < 1 or pos + 7 > len(bech) or len(bech) > 90:
return (None, None, None)
- if not all(x in CHARSET for x in bech[pos+1:]):
+ if not all(x in CHARSET for x in bech[pos + 1 :]):
return (None, None, None)
hrp = bech[:pos]
- data = [CHARSET.find(x) for x in bech[pos+1:]]
+ data = [CHARSET.find(x) for x in bech[pos + 1 :]]
spec = bech32_verify_checksum(hrp, data)
if spec is None:
return (None, None, None)
return (hrp, data[:-6], spec)
+
def convertbits(data, frombits, tobits, pad=True):
"""General power-of-2 base conversion."""
acc = 0
@@ -124,7 +131,12 @@ def decode(hrp, addr):
return (None, None)
if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32:
return (None, None)
- if data[0] == 0 and spec != Encoding.BECH32 or data[0] != 0 and spec != Encoding.BECH32M:
+ if (
+ data[0] == 0
+ and spec != Encoding.BECH32
+ or data[0] != 0
+ and spec != Encoding.BECH32M
+ ):
return (None, None)
return (data[0], decoded)
diff --git a/nostr/client/client.py b/nostr/client/client.py
index db07a06..40231a1 100644
--- a/nostr/client/client.py
+++ b/nostr/client/client.py
@@ -1,11 +1,13 @@
import asyncio
from typing import List
+from loguru import logger
+
from ..relay_manager import RelayManager
class NostrClient:
- relays = [ ]
+ relays = []
relay_manager = RelayManager()
def __init__(self, relays: List[str] = [], connect=True):
@@ -16,7 +18,10 @@ def __init__(self, relays: List[str] = [], connect=True):
async def connect(self):
for relay in self.relays:
- self.relay_manager.add_relay(relay)
+ try:
+ self.relay_manager.add_relay(relay)
+ except Exception as e:
+ logger.debug(e)
def close(self):
self.relay_manager.close_connections()
diff --git a/nostr/delegation.py b/nostr/delegation.py
index 94801f5..8b1c311 100644
--- a/nostr/delegation.py
+++ b/nostr/delegation.py
@@ -7,23 +7,23 @@ class Delegation:
delegator_pubkey: str
delegatee_pubkey: str
event_kind: int
- duration_secs: int = 30*24*60 # default to 30 days
+ duration_secs: int = 30 * 24 * 60 # default to 30 days
signature: str = None # set in PrivateKey.sign_delegation
@property
def expires(self) -> int:
return int(time.time()) + self.duration_secs
-
+
@property
def conditions(self) -> str:
return f"kind={self.event_kind}&created_at<{self.expires}"
-
+
@property
def delegation_token(self) -> str:
return f"nostr:delegation:{self.delegatee_pubkey}:{self.conditions}"
def get_tag(self) -> list[str]:
- """ Called by Event """
+ """Called by Event"""
return [
"delegation",
self.delegator_pubkey,
diff --git a/nostr/key.py b/nostr/key.py
index 8089e11..3c81e94 100644
--- a/nostr/key.py
+++ b/nostr/key.py
@@ -37,7 +37,7 @@ def from_npub(cls, npub: str):
class PrivateKey:
def __init__(self, raw_secret: bytes = None) -> None:
- if not raw_secret is None:
+ if raw_secret is not None:
self.raw_secret = raw_secret
else:
self.raw_secret = secrets.token_bytes(32)
diff --git a/nostr/message_pool.py b/nostr/message_pool.py
index 02f7fd4..e38e66e 100644
--- a/nostr/message_pool.py
+++ b/nostr/message_pool.py
@@ -69,7 +69,7 @@ def _process_message(self, message: str, url: str):
e["sig"],
)
with self.lock:
- if not f"{subscription_id}_{event.id}" in self._unique_events:
+ if f"{subscription_id}_{event.id}" not in self._unique_events:
self._accept_event(EventMessage(event, subscription_id, url))
elif message_type == RelayMessageType.NOTICE:
self.notices.put(NoticeMessage(message_json[1], url))
@@ -78,10 +78,12 @@ def _process_message(self, message: str, url: str):
def _accept_event(self, event_message: EventMessage):
"""
- Event uniqueness is considered per `subscription_id`.
- The `subscription_id` is rewritten to be unique and it is the same accross relays.
- The same event can come from different subscriptions (from the same client or from different ones).
- Clients that have joined later should receive older events.
+ Event uniqueness is considered per `subscription_id`.
+ The `subscription_id` is rewritten to be unique and it is the same accross relays.
+ The same event can come from different subscriptions (from the same client or from different ones).
+ Clients that have joined later should receive older events.
"""
self.events.put(event_message)
- self._unique_events.add(f"{event_message.subscription_id}_{event_message.event.id}")
\ No newline at end of file
+ self._unique_events.add(
+ f"{event_message.subscription_id}_{event_message.event.id}"
+ )
diff --git a/nostr/relay.py b/nostr/relay.py
index caacba0..0630ae5 100644
--- a/nostr/relay.py
+++ b/nostr/relay.py
@@ -2,43 +2,23 @@
import json
import time
from queue import Queue
-from threading import Lock
from typing import List
from loguru import logger
from websocket import WebSocketApp
-from .event import Event
-from .filter import Filters
from .message_pool import MessagePool
-from .message_type import RelayMessageType
from .subscription import Subscription
-class RelayPolicy:
- def __init__(self, should_read: bool = True, should_write: bool = True) -> None:
- self.should_read = should_read
- self.should_write = should_write
-
- def to_json_object(self) -> dict[str, bool]:
- return {"read": self.should_read, "write": self.should_write}
-
-
class Relay:
- def __init__(
- self,
- url: str,
- policy: RelayPolicy,
- message_pool: MessagePool,
- subscriptions: dict[str, Subscription] = {},
- ) -> None:
+ def __init__(self, url: str, message_pool: MessagePool) -> None:
self.url = url
- self.policy = policy
self.message_pool = message_pool
- self.subscriptions = subscriptions
self.connected: bool = False
self.reconnect: bool = True
self.shutdown: bool = False
+ # todo: extract stats
self.error_counter: int = 0
self.error_threshold: int = 100
self.error_list: List[str] = []
@@ -47,12 +27,10 @@ def __init__(
self.num_received_events: int = 0
self.num_sent_events: int = 0
self.num_subscriptions: int = 0
- self.ssl_options: dict = {}
- self.proxy: dict = {}
- self.lock = Lock()
+
self.queue = Queue()
- def connect(self, ssl_options: dict = None, proxy: dict = None):
+ def connect(self):
self.ws = WebSocketApp(
self.url,
on_open=self._on_open,
@@ -62,19 +40,14 @@ def connect(self, ssl_options: dict = None, proxy: dict = None):
on_ping=self._on_ping,
on_pong=self._on_pong,
)
- self.ssl_options = ssl_options
- self.proxy = proxy
if not self.connected:
- self.ws.run_forever(
- sslopt=ssl_options,
- http_proxy_host=None if proxy is None else proxy.get("host"),
- http_proxy_port=None if proxy is None else proxy.get("port"),
- proxy_type=None if proxy is None else proxy.get("type"),
- ping_interval=5,
- )
+ self.ws.run_forever(ping_interval=10)
def close(self):
- self.ws.close()
+ try:
+ self.ws.close()
+ except Exception as e:
+ logger.warning(f"[Relay: {self.url}] Failed to close websocket: {e}")
self.connected = False
self.shutdown = True
@@ -90,10 +63,10 @@ def ping(self):
def publish(self, message: str):
self.queue.put(message)
- def publish_subscriptions(self):
- for _, subscription in self.subscriptions.items():
+ def publish_subscriptions(self, subscriptions: List[Subscription] = []):
+ for subscription in subscriptions:
s = subscription.to_json_object()
- json_str = json.dumps(["REQ", s["id"], s["filters"][0]])
+ json_str = json.dumps(["REQ", s["id"], s["filters"]])
self.publish(json_str)
async def queue_worker(self):
@@ -107,51 +80,36 @@ async def queue_worker(self):
pass
else:
await asyncio.sleep(1)
-
- if self.shutdown:
- logger.warning(f"Closing queue worker for '{self.url}'.")
- break
- def add_subscription(self, id, filters: Filters):
- with self.lock:
- self.subscriptions[id] = Subscription(id, filters)
+ if self.shutdown:
+ logger.warning(f"[Relay: {self.url}] Closing queue worker.")
+ return
def close_subscription(self, id: str) -> None:
- with self.lock:
- self.subscriptions.pop(id)
- self.publish(json.dumps(["CLOSE", id]))
-
- def to_json_object(self) -> dict:
- return {
- "url": self.url,
- "policy": self.policy.to_json_object(),
- "subscriptions": [
- subscription.to_json_object()
- for subscription in self.subscriptions.values()
- ],
- }
+ self.publish(json.dumps(["CLOSE", id]))
def add_notice(self, notice: str):
- self.notice_list = ([notice] + self.notice_list)[:20]
+ self.notice_list = [notice] + self.notice_list
def _on_open(self, _):
- logger.info(f"Connected to relay: '{self.url}'.")
+ logger.info(f"[Relay: {self.url}] Connected.")
self.connected = True
-
+ self.shutdown = False
+
def _on_close(self, _, status_code, message):
- logger.warning(f"Connection to relay {self.url} closed. Status: '{status_code}'. Message: '{message}'.")
+ logger.warning(
+ f"[Relay: {self.url}] Connection closed. Status: '{status_code}'. Message: '{message}'."
+ )
self.close()
def _on_message(self, _, message: str):
- if self._is_valid_message(message):
- self.num_received_events += 1
- self.message_pool.add_message(message, self.url)
+ self.num_received_events += 1
+ self.message_pool.add_message(message, self.url)
def _on_error(self, _, error):
- logger.warning(f"Relay error: '{str(error)}'")
+ logger.warning(f"[Relay: {self.url}] Error: '{str(error)}'")
self._append_error_message(str(error))
- self.connected = False
- self.error_counter += 1
+ self.close()
def _on_ping(self, *_):
return
@@ -159,65 +117,7 @@ def _on_ping(self, *_):
def _on_pong(self, *_):
return
- def _is_valid_message(self, message: str) -> bool:
- message = message.strip("\n")
- if not message or message[0] != "[" or message[-1] != "]":
- return False
-
- message_json = json.loads(message)
- message_type = message_json[0]
-
- if not RelayMessageType.is_valid(message_type):
- return False
-
- if message_type == RelayMessageType.EVENT:
- return self._is_valid_event_message(message_json)
-
- if message_type == RelayMessageType.COMMAND_RESULT:
- return self._is_valid_command_result_message(message, message_json)
-
- return True
-
- def _is_valid_event_message(self, message_json):
- if not len(message_json) == 3:
- return False
-
- subscription_id = message_json[1]
- with self.lock:
- if subscription_id not in self.subscriptions:
- return False
-
- e = message_json[2]
- event = Event(
- e["content"],
- e["pubkey"],
- e["created_at"],
- e["kind"],
- e["tags"],
- e["sig"],
- )
- if not event.verify():
- return False
-
- with self.lock:
- subscription = self.subscriptions[subscription_id]
-
- if subscription.filters and not subscription.filters.match(event):
- return False
-
- return True
-
- def _is_valid_command_result_message(self, message, message_json):
- if not len(message_json) < 3:
- return False
-
- if message_json[2] != True:
- logger.warning(f"Relay '{self.url}' negative command result: '{message}'")
- self._append_error_message(message)
- return False
-
- return True
-
def _append_error_message(self, message):
- self.error_list = ([message] + self.error_list)[:20]
- self.last_error_date = int(time.time())
\ No newline at end of file
+ self.error_counter += 1
+ self.error_list = [message] + self.error_list
+ self.last_error_date = int(time.time())
diff --git a/nostr/relay_manager.py b/nostr/relay_manager.py
index f639fb0..9ec2850 100644
--- a/nostr/relay_manager.py
+++ b/nostr/relay_manager.py
@@ -1,4 +1,3 @@
-
import asyncio
import ssl
import threading
@@ -8,7 +7,7 @@
from .filter import Filters
from .message_pool import MessagePool, NoticeMessage
-from .relay import Relay, RelayPolicy
+from .relay import Relay
from .subscription import Subscription
@@ -25,40 +24,36 @@ def __init__(self) -> None:
self._cached_subscriptions: dict[str, Subscription] = {}
self._subscriptions_lock = threading.Lock()
- def add_relay(self, url: str, read: bool = True, write: bool = True) -> Relay:
+ def add_relay(self, url: str) -> Relay:
if url in list(self.relays.keys()):
return
-
- with self._subscriptions_lock:
- subscriptions = self._cached_subscriptions.copy()
- policy = RelayPolicy(read, write)
- relay = Relay(url, policy, self.message_pool, subscriptions)
+ relay = Relay(url, self.message_pool)
self.relays[url] = relay
self._open_connection(
- relay,
- {"cert_reqs": ssl.CERT_NONE}
+ relay, {"cert_reqs": ssl.CERT_NONE}
) # NOTE: This disables ssl certificate verification
- relay.publish_subscriptions()
+ relay.publish_subscriptions(self._cached_subscriptions.values())
return relay
def remove_relay(self, url: str):
+ # try-catch?
self.relays[url].close()
self.relays.pop(url)
self.threads[url].join(timeout=5)
self.threads.pop(url)
self.queue_threads[url].join(timeout=5)
self.queue_threads.pop(url)
-
def add_subscription(self, id: str, filters: Filters):
+ s = Subscription(id, filters)
with self._subscriptions_lock:
- self._cached_subscriptions[id] = Subscription(id, filters)
+ self._cached_subscriptions[id] = s
for relay in self.relays.values():
- relay.add_subscription(id, filters)
+ relay.publish_subscriptions([s])
def close_subscription(self, id: str):
with self._subscriptions_lock:
@@ -72,22 +67,22 @@ def check_and_restart_relays(self):
for relay in stopped_relays:
self._restart_relay(relay)
-
def close_connections(self):
for relay in self.relays.values():
relay.close()
def publish_message(self, message: str):
for relay in self.relays.values():
- if relay.policy.should_write:
- relay.publish(message)
+ relay.publish(message)
def handle_notice(self, notice: NoticeMessage):
relay = next((r for r in self.relays.values() if r.url == notice.url))
if relay:
relay.add_notice(notice.content)
- def _open_connection(self, relay: Relay, ssl_options: dict = None, proxy: dict = None):
+ def _open_connection(
+ self, relay: Relay, ssl_options: dict = None, proxy: dict = None
+ ):
self.threads[relay.url] = threading.Thread(
target=relay.connect,
args=(ssl_options, proxy),
@@ -98,7 +93,7 @@ def _open_connection(self, relay: Relay, ssl_options: dict = None, proxy: dict =
def wrap_async_queue_worker():
asyncio.run(relay.queue_worker())
-
+
self.queue_threads[relay.url] = threading.Thread(
target=wrap_async_queue_worker,
name=f"{relay.url}-queue",
@@ -108,14 +103,16 @@ def wrap_async_queue_worker():
def _restart_relay(self, relay: Relay):
time_since_last_error = time.time() - relay.last_error_date
-
- min_wait_time = min(60 * relay.error_counter, 60 * 60 * 24) # try at least once a day
+
+ min_wait_time = min(
+ 60 * relay.error_counter, 60 * 60 * 24
+ ) # try at least once a day
if time_since_last_error < min_wait_time:
return
-
+
logger.info(f"Restarting connection to relay '{relay.url}'")
self.remove_relay(relay.url)
new_relay = self.add_relay(relay.url)
new_relay.error_counter = relay.error_counter
- new_relay.error_list = relay.error_list
\ No newline at end of file
+ new_relay.error_list = relay.error_list
diff --git a/nostr/subscription.py b/nostr/subscription.py
index 76da0af..10b5363 100644
--- a/nostr/subscription.py
+++ b/nostr/subscription.py
@@ -2,12 +2,9 @@
class Subscription:
- def __init__(self, id: str, filters: Filters=None) -> None:
+ def __init__(self, id: str, filters: Filters = None) -> None:
self.id = id
self.filters = filters
def to_json_object(self):
- return {
- "id": self.id,
- "filters": self.filters.to_json_array()
- }
+ return {"id": self.id, "filters": self.filters.to_json_array()}
diff --git a/router.py b/router.py
index cc0a380..c6a0a91 100644
--- a/router.py
+++ b/router.py
@@ -2,7 +2,7 @@
import json
from typing import List, Union
-from fastapi import WebSocketDisconnect
+from fastapi import WebSocket, WebSocketDisconnect
from loguru import logger
from lnbits.helpers import urlsafe_short_hash
@@ -15,28 +15,29 @@
class NostrRouter:
-
received_subscription_events: dict[str, list[Event]] = {}
received_subscription_notices: list[NoticeMessage] = []
received_subscription_eosenotices: dict[str, EndOfStoredEventsMessage] = {}
- def __init__(self, websocket):
+ def __init__(self, websocket: WebSocket):
self.subscriptions: List[str] = []
self.connected: bool = True
- self.websocket = websocket
- self.tasks: List[asyncio.Task] = []
- self.original_subscription_ids = {}
+ self.websocket: WebSocket = websocket
+ self.tasks: List[asyncio.Task] = [] # chek why state is needed
+ self.original_subscription_ids = {} # here
async def client_to_nostr(self):
- """Receives requests / data from the client and forwards it to relays. If the
+ """
+ Receives requests / data from the client and forwards it to relays. If the
request was a subscription/filter, registers it with the nostr client lib.
Remembers the subscription id so we can send back responses from the relay to this
- client in `nostr_to_client`"""
- while True:
+ client in `nostr_to_client`
+ """
+ while self.connected:
try:
json_str = await self.websocket.receive_text()
except WebSocketDisconnect:
- self.connected = False
+ self.stop()
break
try:
@@ -44,15 +45,15 @@ async def client_to_nostr(self):
except Exception as e:
logger.debug(f"Failed to handle client message: '{str(e)}'.")
-
async def nostr_to_client(self):
- """Sends responses from relays back to the client. Polls the subscriptions of this client
+ """
+ Sends responses from relays back to the client. Polls the subscriptions of this client
stored in `my_subscriptions`. Then gets all responses for this subscription id from `received_subscription_events` which
is filled in tasks.py. Takes one response after the other and relays it back to the client. Reconstructs
the reponse manually because the nostr client lib we're using can't do it. Reconstructs the original subscription id
that we had previously rewritten in order to avoid collisions when multiple clients use the same id.
"""
- while True and self.connected:
+ while self.connected:
try:
await self._handle_subscriptions()
self._handle_notices()
@@ -60,12 +61,12 @@ async def nostr_to_client(self):
logger.debug(f"Failed to handle response for client: '{str(e)}'.")
await asyncio.sleep(0.1)
-
async def start(self):
+ self.connected = True
self.tasks.append(asyncio.create_task(self.client_to_nostr()))
self.tasks.append(asyncio.create_task(self.nostr_to_client()))
- async def stop(self):
+ def stop(self):
for t in self.tasks:
try:
t.cancel()
@@ -77,6 +78,11 @@ async def stop(self):
nostr.client.relay_manager.close_subscription(s)
except:
pass
+
+ try:
+ self.websocket.close()
+ except:
+ pass
self.connected = False
async def _handle_subscriptions(self):
@@ -86,8 +92,6 @@ async def _handle_subscriptions(self):
if s in NostrRouter.received_subscription_eosenotices:
await self._handle_received_subscription_eosenotices(s)
-
-
async def _handle_received_subscription_eosenotices(self, s):
try:
if s not in self.original_subscription_ids:
@@ -95,7 +99,7 @@ async def _handle_received_subscription_eosenotices(self, s):
s_original = self.original_subscription_ids[s]
event_to_forward = ["EOSE", s_original]
del NostrRouter.received_subscription_eosenotices[s]
-
+
await self.websocket.send_text(json.dumps(event_to_forward))
except Exception as e:
logger.debug(e)
@@ -104,18 +108,18 @@ async def _handle_received_subscription_events(self, s):
try:
if s not in NostrRouter.received_subscription_events:
return
+
while len(NostrRouter.received_subscription_events[s]):
my_event = NostrRouter.received_subscription_events[s].pop(0)
- # event.to_message() does not include the subscription ID, we have to add it manually
event_json = {
- "id": my_event.id,
- "pubkey": my_event.public_key,
- "created_at": my_event.created_at,
- "kind": my_event.kind,
- "tags": my_event.tags,
- "content": my_event.content,
- "sig": my_event.signature,
- }
+ "id": my_event.id,
+ "pubkey": my_event.public_key,
+ "created_at": my_event.created_at,
+ "kind": my_event.kind,
+ "tags": my_event.tags,
+ "content": my_event.content,
+ "sig": my_event.signature,
+ }
# this reconstructs the original response from the relay
# reconstruct original subscription id
@@ -123,18 +127,17 @@ async def _handle_received_subscription_events(self, s):
event_to_forward = ["EVENT", s_original, event_json]
await self.websocket.send_text(json.dumps(event_to_forward))
except Exception as e:
- logger.debug(e)
+ logger.debug(e) # there are 2900 errors here
def _handle_notices(self):
while len(NostrRouter.received_subscription_notices):
my_event = NostrRouter.received_subscription_notices.pop(0)
# note: we don't send it to the user because we don't know who should receive it
- logger.info(f"Relay ('{my_event.url}') notice: '{my_event.content}']")
+ logger.info(f"[Relay '{my_event.url}'] Notice: '{my_event.content}']")
nostr.client.relay_manager.handle_notice(my_event)
-
-
def _marshall_nostr_filters(self, data: Union[dict, list]):
+ # todo: get rid of this
filters = data if isinstance(data, list) else [data]
filters = [Filter.parse_obj(f) for f in filters]
filter_list: list[NostrFilter] = []
@@ -161,13 +164,12 @@ async def _handle_client_to_nostr(self, json_str):
"""
json_data = json.loads(json_str)
- assert len(json_data)
-
+ assert len(json_data), "Bad JSON array"
if json_data[0] == "REQ":
self._handle_client_req(json_data)
return
-
+
if json_data[0] == "CLOSE":
self._handle_client_close(json_data[1])
return
@@ -181,18 +183,25 @@ def _handle_client_req(self, json_data):
subscription_id_rewritten = urlsafe_short_hash()
self.original_subscription_ids[subscription_id_rewritten] = subscription_id
fltr = json_data[2:]
- filters = self._marshall_nostr_filters(fltr)
+ filters = self._marshall_nostr_filters(fltr) # revisit
- nostr.client.relay_manager.add_subscription(
- subscription_id_rewritten, filters
- )
+ nostr.client.relay_manager.add_subscription(subscription_id_rewritten, filters)
request_rewritten = json.dumps([json_data[0], subscription_id_rewritten] + fltr)
-
- self.subscriptions.append(subscription_id_rewritten)
- nostr.client.relay_manager.publish_message(request_rewritten)
+
+ self.subscriptions.append(subscription_id_rewritten) # why here also?
+ nostr.client.relay_manager.publish_message(
+ request_rewritten
+ ) # both `add_subscription` and `publish_message`?
def _handle_client_close(self, subscription_id):
- subscription_id_rewritten = next((k for k, v in self.original_subscription_ids.items() if v == subscription_id), None)
+ subscription_id_rewritten = next(
+ (
+ k
+ for k, v in self.original_subscription_ids.items()
+ if v == subscription_id
+ ),
+ None,
+ )
if subscription_id_rewritten:
self.original_subscription_ids.pop(subscription_id_rewritten)
nostr.client.relay_manager.close_subscription(subscription_id_rewritten)
diff --git a/tasks.py b/tasks.py
index 4c316bc..f025b0f 100644
--- a/tasks.py
+++ b/tasks.py
@@ -9,6 +9,7 @@
from .router import NostrRouter, nostr
+#### revisit
async def init_relays():
# reinitialize the entire client
nostr.__init__()
@@ -20,14 +21,14 @@ async def init_relays():
async def check_relays():
- """ Check relays that have been disconnected """
+ """Check relays that have been disconnected"""
while True:
try:
await asyncio.sleep(20)
nostr.client.relay_manager.check_and_restart_relays()
except Exception as e:
logger.warning(f"Cannot restart relays: '{str(e)}'.")
-
+
async def subscribe_events():
while not any([r.connected for r in nostr.client.relay_manager.relays.values()]):
@@ -39,14 +40,16 @@ def callback_events(eventMessage: EventMessage):
if eventMessage.event.id in set(
[
e.id
- for e in NostrRouter.received_subscription_events[eventMessage.subscription_id]
+ for e in NostrRouter.received_subscription_events[
+ eventMessage.subscription_id
+ ]
]
):
return
- NostrRouter.received_subscription_events[eventMessage.subscription_id].append(
- eventMessage.event
- )
+ NostrRouter.received_subscription_events[
+ eventMessage.subscription_id
+ ].append(eventMessage.event)
else:
NostrRouter.received_subscription_events[eventMessage.subscription_id] = [
eventMessage.event
@@ -59,7 +62,10 @@ def callback_notices(noticeMessage: NoticeMessage):
return
def callback_eose_notices(eventMessage: EndOfStoredEventsMessage):
- if eventMessage.subscription_id not in NostrRouter.received_subscription_eosenotices:
+ if (
+ eventMessage.subscription_id
+ not in NostrRouter.received_subscription_eosenotices
+ ):
NostrRouter.received_subscription_eosenotices[
eventMessage.subscription_id
] = eventMessage
@@ -67,11 +73,13 @@ def callback_eose_notices(eventMessage: EndOfStoredEventsMessage):
return
def wrap_async_subscribe():
- asyncio.run(nostr.client.subscribe(
- callback_events,
- callback_notices,
- callback_eose_notices,
- ))
+ asyncio.run(
+ nostr.client.subscribe(
+ callback_events,
+ callback_notices,
+ callback_eose_notices,
+ )
+ )
t = threading.Thread(
target=wrap_async_subscribe,
diff --git a/templates/nostrclient/index.html b/templates/nostrclient/index.html
index a0c5999..9e0eb31 100644
--- a/templates/nostrclient/index.html
+++ b/templates/nostrclient/index.html
@@ -6,13 +6,30 @@
-
+
-
-
-
+
+
@@ -29,18 +46,36 @@
Nostrclient
-
+
-
+
-
+
{{ col.label }}
@@ -49,29 +84,43 @@ Nostrclient
-
+
- ⬆️
- ⬇️
-
- ⚠️
-
+ ⬆️ ⬇️
+
+
+ ⚠️
-
+
ⓘ
-
-
-
+
{{ col.value }}
@@ -87,15 +136,32 @@
Nostrclient
- Copy address
+ Copy address
Your endpoint:
-
+
-
+
@@ -103,8 +169,13 @@
Nostrclient
Sender Private Key:
-
+
@@ -113,7 +184,8 @@
Nostrclient
No not use your real private key! Leave empty for a randomly
- generated key.
+ generated key.
@@ -122,7 +194,13 @@ Nostrclient
Sender Public Key:
-
+
@@ -130,8 +208,15 @@
Nostrclient
Test Message:
-
+
@@ -139,22 +224,35 @@
Nostrclient
Receiver Public Key:
-
+
- This is the recipient of the message. Field required.
+ This is the recipient of the message. Field required.
- Send Message
+ Send Message
@@ -166,7 +264,14 @@ Nostrclient
Sent Data:
-
+
@@ -174,7 +279,14 @@
Nostrclient
Received Data:
-
+
@@ -193,8 +305,12 @@ Nostrclient Extension
-
+
Only Admin users can manage this extension.
@@ -204,14 +320,21 @@ Nostrclient Extension
-
+
Close
-
{% endraw %} {% endblock %} {% block scripts %} {{ window_vars(user) }}
@@ -292,8 +415,7 @@ Nostrclient Extension
align: 'center',
label: 'Ping',
field: 'ping'
- }
- ,
+ },
{
name: 'delete',
align: 'center',
@@ -306,13 +428,13 @@ Nostrclient Extension
}
},
predefinedRelays: [
- "wss://relay.damus.io",
- "wss://nostr-pub.wellorder.net",
- "wss://nostr.zebedee.cloud",
- "wss://nodestr.fmt.wiz.biz",
- "wss://nostr.oxtr.dev",
- "wss://nostr.wine"
- ],
+ 'wss://relay.damus.io',
+ 'wss://nostr-pub.wellorder.net',
+ 'wss://nostr.zebedee.cloud',
+ 'wss://nodestr.fmt.wiz.biz',
+ 'wss://nostr.oxtr.dev',
+ 'wss://nostr.wine'
+ ]
}
},
methods: {
@@ -355,7 +477,7 @@ Nostrclient Extension
'POST',
'/nostrclient/api/v1/relay?usr=' + this.g.user.id,
this.g.user.wallets[0].adminkey,
- { url: this.relayToAdd }
+ {url: this.relayToAdd}
)
.then(function (response) {
console.log('response:', response)
@@ -387,15 +509,15 @@ Nostrclient Extension
'DELETE',
'/nostrclient/api/v1/relay?usr=' + this.g.user.id,
this.g.user.wallets[0].adminkey,
- { url: url }
+ {url: url}
)
- .then((response) => {
+ .then(response => {
const relayIndex = this.nostrrelayLinks.indexOf(r => r.url === url)
if (relayIndex !== -1) {
this.nostrrelayLinks.splice(relayIndex, 1)
}
})
- .catch((error) => {
+ .catch(error => {
console.error(error)
LNbits.utils.notifyApiError(error)
})
@@ -437,7 +559,7 @@ Nostrclient Extension
},
sendTestMessage: async function () {
try {
- const { data } = await LNbits.api.request(
+ const {data} = await LNbits.api.request(
'PUT',
'/nostrclient/api/v1/relay/test?usr=' + this.g.user.id,
this.g.user.wallets[0].adminkey,
@@ -458,7 +580,7 @@ Nostrclient Extension
const subscription = JSON.stringify([
'REQ',
'test-dms',
- { kinds: [4], '#p': [event.pubkey] }
+ {kinds: [4], '#p': [event.pubkey]}
])
this.testData.wsConnection.send(subscription)
} catch (error) {
@@ -527,4 +649,4 @@ Nostrclient Extension
}
})
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/views_api.py b/views_api.py
index b6b4527..4fe939c 100644
--- a/views_api.py
+++ b/views_api.py
@@ -50,7 +50,7 @@ async def api_get_relays() -> RelayList:
async def api_add_relay(relay: Relay) -> Optional[RelayList]:
if not relay.url:
raise HTTPException(
- status_code=HTTPStatus.BAD_REQUEST, detail=f"Relay url not provided."
+ status_code=HTTPStatus.BAD_REQUEST, detail="Relay url not provided."
)
if relay.url in nostr.client.relay_manager.relays:
raise HTTPException(
@@ -62,7 +62,6 @@ async def api_add_relay(relay: Relay) -> Optional[RelayList]:
nostr.client.relays.append(relay.url)
nostr.client.relay_manager.add_relay(relay.url)
-
return await get_relays()
@@ -73,7 +72,7 @@ async def api_add_relay(relay: Relay) -> Optional[RelayList]:
async def api_delete_relay(relay: Relay) -> None:
if not relay.url:
raise HTTPException(
- status_code=HTTPStatus.BAD_REQUEST, detail=f"Relay url not provided."
+ status_code=HTTPStatus.BAD_REQUEST, detail="Relay url not provided."
)
# we can remove relays during runtime
nostr.client.relay_manager.remove_relay(relay.url)
@@ -95,7 +94,11 @@ async def api_test_endpoint(data: TestMessage) -> TestMessageResponse:
)
private_key.sign_event(dm)
- return TestMessageResponse(private_key=private_key.hex(), public_key=to_public_key, event_json=dm.to_message())
+ return TestMessageResponse(
+ private_key=private_key.hex(),
+ public_key=to_public_key,
+ event_json=dm.to_message(),
+ )
except (ValueError, AssertionError) as ex:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
@@ -109,7 +112,6 @@ async def api_test_endpoint(data: TestMessage) -> TestMessageResponse:
)
-
@nostrclient_ext.delete(
"/api/v1", status_code=HTTPStatus.OK, dependencies=[Depends(check_admin)]
)
@@ -118,7 +120,8 @@ async def api_stop():
try:
for s in router.subscriptions:
nostr.client.relay_manager.close_subscription(s)
- await router.stop()
+
+ router.stop()
all_routers.remove(router)
except Exception as e:
logger.error(e)
@@ -148,6 +151,6 @@ async def ws_relay(websocket: WebSocket) -> None:
while True:
await asyncio.sleep(10)
if not router.connected:
- await router.stop()
+ router.stop()
all_routers.remove(router)
break
From b15be7ef389643fe92b62ada1a13b222c882da39 Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Thu, 26 Oct 2023 20:05:44 +0300
Subject: [PATCH 02/30] refactor: extra logs plus try-catch
---
nostr/relay.py | 5 ++++-
nostr/relay_manager.py | 47 +++++++++++++++++++++++++++---------------
2 files changed, 34 insertions(+), 18 deletions(-)
diff --git a/nostr/relay.py b/nostr/relay.py
index 0630ae5..4fa303a 100644
--- a/nostr/relay.py
+++ b/nostr/relay.py
@@ -86,7 +86,10 @@ async def queue_worker(self):
return
def close_subscription(self, id: str) -> None:
- self.publish(json.dumps(["CLOSE", id]))
+ try:
+ self.publish(json.dumps(["CLOSE", id]))
+ except Exception as e:
+ logger.debug(f"[Relay: {self.url}] Failed to close subscription: {e}")
def add_notice(self, notice: str):
self.notice_list = [notice] + self.notice_list
diff --git a/nostr/relay_manager.py b/nostr/relay_manager.py
index 9ec2850..050d9f7 100644
--- a/nostr/relay_manager.py
+++ b/nostr/relay_manager.py
@@ -26,26 +26,41 @@ def __init__(self) -> None:
def add_relay(self, url: str) -> Relay:
if url in list(self.relays.keys()):
+ logger.debug(f"Relay '{url}' already present.")
return
relay = Relay(url, self.message_pool)
self.relays[url] = relay
- self._open_connection(
- relay, {"cert_reqs": ssl.CERT_NONE}
- ) # NOTE: This disables ssl certificate verification
+ self._open_connection()
relay.publish_subscriptions(self._cached_subscriptions.values())
return relay
def remove_relay(self, url: str):
- # try-catch?
- self.relays[url].close()
- self.relays.pop(url)
- self.threads[url].join(timeout=5)
- self.threads.pop(url)
- self.queue_threads[url].join(timeout=5)
- self.queue_threads.pop(url)
+ try:
+ self.relays[url].close()
+ except Exception as e:
+ logger.debug(e)
+
+ if url in self.relays:
+ self.relays.pop(url)
+
+ try:
+ self.threads[url].join(timeout=5)
+ except Exception as e:
+ logger.debug(e)
+
+ if url in self.threads:
+ self.threads.pop(url)
+
+ try:
+ self.queue_threads[url].join(timeout=5)
+ except Exception as e:
+ logger.debug(e)
+
+ if url in self.queue_threads:
+ self.queue_threads.pop(url)
def add_subscription(self, id: str, filters: Filters):
s = Subscription(id, filters)
@@ -57,7 +72,8 @@ def add_subscription(self, id: str, filters: Filters):
def close_subscription(self, id: str):
with self._subscriptions_lock:
- self._cached_subscriptions.pop(id)
+ if id in self._cached_subscriptions:
+ self._cached_subscriptions.pop(id)
for relay in self.relays.values():
relay.close_subscription(id)
@@ -80,12 +96,9 @@ def handle_notice(self, notice: NoticeMessage):
if relay:
relay.add_notice(notice.content)
- def _open_connection(
- self, relay: Relay, ssl_options: dict = None, proxy: dict = None
- ):
+ def _open_connection( self, relay: Relay ):
self.threads[relay.url] = threading.Thread(
target=relay.connect,
- args=(ssl_options, proxy),
name=f"{relay.url}-thread",
daemon=True,
)
@@ -105,8 +118,8 @@ def _restart_relay(self, relay: Relay):
time_since_last_error = time.time() - relay.last_error_date
min_wait_time = min(
- 60 * relay.error_counter, 60 * 60 * 24
- ) # try at least once a day
+ 60 * relay.error_counter, 60 * 60
+ ) # try at least once an hour
if time_since_last_error < min_wait_time:
return
From de744f3a38bdac8c3bffe5875f0b99e0ffaba938 Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Thu, 26 Oct 2023 20:07:01 +0300
Subject: [PATCH 03/30] refactor: do not use bare `except`
---
nostr/relay.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/nostr/relay.py b/nostr/relay.py
index 4fa303a..b898d16 100644
--- a/nostr/relay.py
+++ b/nostr/relay.py
@@ -76,7 +76,7 @@ async def queue_worker(self):
message = self.queue.get(timeout=1)
self.num_sent_events += 1
self.ws.send(message)
- except:
+ except Exception as _:
pass
else:
await asyncio.sleep(1)
From 9ef62e10c35a9f4a7370d268130af43ff8906f3d Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Thu, 26 Oct 2023 20:13:57 +0300
Subject: [PATCH 04/30] refactor: clean-up redundant fields
---
nostr/client/client.py | 11 ++---------
tasks.py | 4 ++--
views_api.py | 1 -
3 files changed, 4 insertions(+), 12 deletions(-)
diff --git a/nostr/client/client.py b/nostr/client/client.py
index 40231a1..21d8690 100644
--- a/nostr/client/client.py
+++ b/nostr/client/client.py
@@ -7,17 +7,10 @@
class NostrClient:
- relays = []
relay_manager = RelayManager()
- def __init__(self, relays: List[str] = [], connect=True):
- if len(relays):
- self.relays = relays
- if connect:
- self.connect()
-
- async def connect(self):
- for relay in self.relays:
+ async def connect(self, relays):
+ for relay in relays:
try:
self.relay_manager.add_relay(relay)
except Exception as e:
diff --git a/tasks.py b/tasks.py
index f025b0f..cc5dfcb 100644
--- a/tasks.py
+++ b/tasks.py
@@ -16,8 +16,8 @@ async def init_relays():
# get relays from db
relays = await get_relays()
# set relays and connect to them
- nostr.client.relays = list(set([r.url for r in relays.__root__ if r.url]))
- await nostr.client.connect()
+ valid_relays = list(set([r.url for r in relays.__root__ if r.url]))
+ await nostr.client.connect(valid_relays)
async def check_relays():
diff --git a/views_api.py b/views_api.py
index 4fe939c..fe3d62f 100644
--- a/views_api.py
+++ b/views_api.py
@@ -60,7 +60,6 @@ async def api_add_relay(relay: Relay) -> Optional[RelayList]:
relay.id = urlsafe_short_hash()
await add_relay(relay)
- nostr.client.relays.append(relay.url)
nostr.client.relay_manager.add_relay(relay.url)
return await get_relays()
From 91fac031b89dcd639d8a8b1f32c101a956336f5d Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Thu, 26 Oct 2023 20:15:11 +0300
Subject: [PATCH 05/30] chore: pass code checks
---
router.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/router.py b/router.py
index c6a0a91..374a567 100644
--- a/router.py
+++ b/router.py
@@ -70,18 +70,18 @@ def stop(self):
for t in self.tasks:
try:
t.cancel()
- except:
+ except Exception as _:
pass
for s in self.subscriptions:
try:
nostr.client.relay_manager.close_subscription(s)
- except:
+ except Exception as _:
pass
try:
self.websocket.close()
- except:
+ except Exception as _:
pass
self.connected = False
From 636d4aaa9fac1a4fa390e4b0a62d6b08313c50c7 Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Thu, 26 Oct 2023 20:18:16 +0300
Subject: [PATCH 06/30] chore: code format
---
nostr/client/client.py | 1 -
nostr/relay_manager.py | 3 +--
2 files changed, 1 insertion(+), 3 deletions(-)
diff --git a/nostr/client/client.py b/nostr/client/client.py
index 21d8690..c8e1ffa 100644
--- a/nostr/client/client.py
+++ b/nostr/client/client.py
@@ -1,5 +1,4 @@
import asyncio
-from typing import List
from loguru import logger
diff --git a/nostr/relay_manager.py b/nostr/relay_manager.py
index 050d9f7..6d0c09f 100644
--- a/nostr/relay_manager.py
+++ b/nostr/relay_manager.py
@@ -1,5 +1,4 @@
import asyncio
-import ssl
import threading
import time
@@ -96,7 +95,7 @@ def handle_notice(self, notice: NoticeMessage):
if relay:
relay.add_notice(notice.content)
- def _open_connection( self, relay: Relay ):
+ def _open_connection(self, relay: Relay):
self.threads[relay.url] = threading.Thread(
target=relay.connect,
name=f"{relay.url}-thread",
From 8c7c9a6235877e7cae8822e9a9a143a7edbbb082 Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Thu, 26 Oct 2023 20:23:53 +0300
Subject: [PATCH 07/30] refactor: code clean-up
---
nostr/relay_manager.py | 5 -----
1 file changed, 5 deletions(-)
diff --git a/nostr/relay_manager.py b/nostr/relay_manager.py
index 6d0c09f..5853ee8 100644
--- a/nostr/relay_manager.py
+++ b/nostr/relay_manager.py
@@ -9,11 +9,6 @@
from .relay import Relay
from .subscription import Subscription
-
-class RelayException(Exception):
- pass
-
-
class RelayManager:
def __init__(self) -> None:
self.relays: dict[str, Relay] = {}
From 55a71c40893409af636468c281779f745ae1f062 Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Thu, 26 Oct 2023 20:34:14 +0300
Subject: [PATCH 08/30] fix: refactoring stuff
---
__init__.py | 2 +-
nostr/relay.py | 2 +-
nostr/relay_manager.py | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/__init__.py b/__init__.py
index 84750dc..897c34f 100644
--- a/__init__.py
+++ b/__init__.py
@@ -26,7 +26,7 @@
# remove!
class NostrClient:
def __init__(self):
- self.client: NostrClientLib = NostrClientLib(connect=False)
+ self.client: NostrClientLib = NostrClientLib()
nostr = NostrClient()
diff --git a/nostr/relay.py b/nostr/relay.py
index b898d16..500bdf7 100644
--- a/nostr/relay.py
+++ b/nostr/relay.py
@@ -66,7 +66,7 @@ def publish(self, message: str):
def publish_subscriptions(self, subscriptions: List[Subscription] = []):
for subscription in subscriptions:
s = subscription.to_json_object()
- json_str = json.dumps(["REQ", s["id"], s["filters"]])
+ json_str = json.dumps(["REQ", s["id"], s["filters"][0]])
self.publish(json_str)
async def queue_worker(self):
diff --git a/nostr/relay_manager.py b/nostr/relay_manager.py
index 5853ee8..9a28082 100644
--- a/nostr/relay_manager.py
+++ b/nostr/relay_manager.py
@@ -26,7 +26,7 @@ def add_relay(self, url: str) -> Relay:
relay = Relay(url, self.message_pool)
self.relays[url] = relay
- self._open_connection()
+ self._open_connection(relay)
relay.publish_subscriptions(self._cached_subscriptions.values())
return relay
From 1d6f0b9bbef6540059699840dfbb1be8147e48e6 Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Thu, 26 Oct 2023 20:36:07 +0300
Subject: [PATCH 09/30] refactor: remove un-used file
---
cbc.py | 26 --------------------------
1 file changed, 26 deletions(-)
delete mode 100644 cbc.py
diff --git a/cbc.py b/cbc.py
deleted file mode 100644
index 0d9e04f..0000000
--- a/cbc.py
+++ /dev/null
@@ -1,26 +0,0 @@
-from Cryptodome.Cipher import AES
-
-BLOCK_SIZE = 16
-
-
-class AESCipher(object):
- """This class is compatible with crypto.createCipheriv('aes-256-cbc')"""
-
- def __init__(self, key=None):
- self.key = key
-
- def pad(self, data):
- length = BLOCK_SIZE - (len(data) % BLOCK_SIZE)
- return data + (chr(length) * length).encode()
-
- def unpad(self, data):
- return data[: -(data[-1] if type(data[-1]) == int else ord(data[-1]))]
-
- def encrypt(self, plain_text):
- cipher = AES.new(self.key, AES.MODE_CBC)
- b = plain_text.encode("UTF-8")
- return cipher.iv, cipher.encrypt(self.pad(b))
-
- def decrypt(self, iv, enc_text):
- cipher = AES.new(self.key, AES.MODE_CBC, iv=iv)
- return self.unpad(cipher.decrypt(enc_text).decode("UTF-8"))
From ce265f80576a5eda8b2395eb16f5a6e2f80c2dc5 Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Thu, 26 Oct 2023 20:39:52 +0300
Subject: [PATCH 10/30] chore: code clean-up
---
tasks.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tasks.py b/tasks.py
index cc5dfcb..32d255c 100644
--- a/tasks.py
+++ b/tasks.py
@@ -6,7 +6,7 @@
from . import nostr
from .crud import get_relays
from .nostr.message_pool import EndOfStoredEventsMessage, EventMessage, NoticeMessage
-from .router import NostrRouter, nostr
+from .router import NostrRouter
#### revisit
From e776ec997f9d2bd24ab10f2733c461e1039e427f Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Thu, 26 Oct 2023 20:42:45 +0300
Subject: [PATCH 11/30] chore: code clean-up
---
nostr/relay_manager.py | 1 +
views_api.py | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/nostr/relay_manager.py b/nostr/relay_manager.py
index 9a28082..e712f35 100644
--- a/nostr/relay_manager.py
+++ b/nostr/relay_manager.py
@@ -9,6 +9,7 @@
from .relay import Relay
from .subscription import Subscription
+
class RelayManager:
def __init__(self) -> None:
self.relays: dict[str, Relay] = {}
diff --git a/views_api.py b/views_api.py
index fe3d62f..fef3179 100644
--- a/views_api.py
+++ b/views_api.py
@@ -14,7 +14,7 @@
from .helpers import normalize_public_key
from .models import Relay, RelayList, TestMessage, TestMessageResponse
from .nostr.key import EncryptedDirectMessage, PrivateKey
-from .router import NostrRouter, nostr
+from .router import NostrRouter
# we keep this in
all_routers: list[NostrRouter] = []
From 1c4dd6111a0170b6d85d4ac989bd862c8d3ad9fc Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Thu, 26 Oct 2023 21:45:58 +0300
Subject: [PATCH 12/30] chore: code-format fix
---
__init__.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/__init__.py b/__init__.py
index 897c34f..662409e 100644
--- a/__init__.py
+++ b/__init__.py
@@ -36,7 +36,7 @@ def nostr_renderer():
return template_renderer(["nostrclient/templates"])
-from .tasks import check_relays, init_relays, subscribe_events
+from .tasks import check_relays, init_relays, subscribe_events # noqa
from .views import * # noqa
from .views_api import * # noqa
From 0f6659a7e04dfc969e0dfdb2d0992dc6df80424a Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Thu, 26 Oct 2023 22:08:50 +0300
Subject: [PATCH 13/30] refactor: remove nostr.client wrapper
---
__init__.py | 12 ++----------
nostr/client/client.py | 6 +++++-
nostr/relay_manager.py | 5 +++++
router.py | 14 +++++++-------
tasks.py | 14 ++++++--------
views_api.py | 14 +++++++-------
6 files changed, 32 insertions(+), 33 deletions(-)
diff --git a/__init__.py b/__init__.py
index 662409e..919fd26 100644
--- a/__init__.py
+++ b/__init__.py
@@ -7,7 +7,7 @@
from lnbits.helpers import template_renderer
from lnbits.tasks import catch_everything_and_restart
-from .nostr.client.client import NostrClient as NostrClientLib
+from .nostr.client.client import NostrClient
db = Database("ext_nostrclient")
@@ -22,15 +22,7 @@
scheduled_tasks: List[asyncio.Task] = []
-
-# remove!
-class NostrClient:
- def __init__(self):
- self.client: NostrClientLib = NostrClientLib()
-
-
-nostr = NostrClient()
-
+nostr_client = NostrClient()
def nostr_renderer():
return template_renderer(["nostrclient/templates"])
diff --git a/nostr/client/client.py b/nostr/client/client.py
index c8e1ffa..a29eb05 100644
--- a/nostr/client/client.py
+++ b/nostr/client/client.py
@@ -8,13 +8,17 @@
class NostrClient:
relay_manager = RelayManager()
- async def connect(self, relays):
+ def connect(self, relays):
for relay in relays:
try:
self.relay_manager.add_relay(relay)
except Exception as e:
logger.debug(e)
+ def reconnect(self, relays):
+ self.relay_manager.remove_relays()
+ self.connect(relays)
+
def close(self):
self.relay_manager.close_connections()
diff --git a/nostr/relay_manager.py b/nostr/relay_manager.py
index e712f35..a2002c4 100644
--- a/nostr/relay_manager.py
+++ b/nostr/relay_manager.py
@@ -57,6 +57,11 @@ def remove_relay(self, url: str):
if url in self.queue_threads:
self.queue_threads.pop(url)
+ def remove_relays(self):
+ relay_urls = list( self.relays.keys())
+ for url in relay_urls:
+ self.remove_relay(url)
+
def add_subscription(self, id: str, filters: Filters):
s = Subscription(id, filters)
with self._subscriptions_lock:
diff --git a/router.py b/router.py
index 374a567..32906ff 100644
--- a/router.py
+++ b/router.py
@@ -7,7 +7,7 @@
from lnbits.helpers import urlsafe_short_hash
-from . import nostr
+from . import nostr_client
from .models import Event, Filter
from .nostr.filter import Filter as NostrFilter
from .nostr.filter import Filters as NostrFilters
@@ -75,7 +75,7 @@ def stop(self):
for s in self.subscriptions:
try:
- nostr.client.relay_manager.close_subscription(s)
+ nostr_client.relay_manager.close_subscription(s)
except Exception as _:
pass
@@ -134,7 +134,7 @@ def _handle_notices(self):
my_event = NostrRouter.received_subscription_notices.pop(0)
# note: we don't send it to the user because we don't know who should receive it
logger.info(f"[Relay '{my_event.url}'] Notice: '{my_event.content}']")
- nostr.client.relay_manager.handle_notice(my_event)
+ nostr_client.relay_manager.handle_notice(my_event)
def _marshall_nostr_filters(self, data: Union[dict, list]):
# todo: get rid of this
@@ -175,7 +175,7 @@ async def _handle_client_to_nostr(self, json_str):
return
if json_data[0] == "EVENT":
- nostr.client.relay_manager.publish_message(json_str)
+ nostr_client.relay_manager.publish_message(json_str)
return
def _handle_client_req(self, json_data):
@@ -185,11 +185,11 @@ def _handle_client_req(self, json_data):
fltr = json_data[2:]
filters = self._marshall_nostr_filters(fltr) # revisit
- nostr.client.relay_manager.add_subscription(subscription_id_rewritten, filters)
+ nostr_client.relay_manager.add_subscription(subscription_id_rewritten, filters)
request_rewritten = json.dumps([json_data[0], subscription_id_rewritten] + fltr)
self.subscriptions.append(subscription_id_rewritten) # why here also?
- nostr.client.relay_manager.publish_message(
+ nostr_client.relay_manager.publish_message(
request_rewritten
) # both `add_subscription` and `publish_message`?
@@ -204,6 +204,6 @@ def _handle_client_close(self, subscription_id):
)
if subscription_id_rewritten:
self.original_subscription_ids.pop(subscription_id_rewritten)
- nostr.client.relay_manager.close_subscription(subscription_id_rewritten)
+ nostr_client.relay_manager.close_subscription(subscription_id_rewritten)
else:
logger.debug(f"Failed to unsubscribe from '{subscription_id}.'")
diff --git a/tasks.py b/tasks.py
index 32d255c..e7468cd 100644
--- a/tasks.py
+++ b/tasks.py
@@ -3,21 +3,19 @@
from loguru import logger
-from . import nostr
+from . import nostr_client
from .crud import get_relays
from .nostr.message_pool import EndOfStoredEventsMessage, EventMessage, NoticeMessage
from .router import NostrRouter
-#### revisit
async def init_relays():
- # reinitialize the entire client
- nostr.__init__()
# get relays from db
relays = await get_relays()
# set relays and connect to them
valid_relays = list(set([r.url for r in relays.__root__ if r.url]))
- await nostr.client.connect(valid_relays)
+
+ nostr_client.reconnect(valid_relays)
async def check_relays():
@@ -25,13 +23,13 @@ async def check_relays():
while True:
try:
await asyncio.sleep(20)
- nostr.client.relay_manager.check_and_restart_relays()
+ nostr_client.relay_manager.check_and_restart_relays()
except Exception as e:
logger.warning(f"Cannot restart relays: '{str(e)}'.")
async def subscribe_events():
- while not any([r.connected for r in nostr.client.relay_manager.relays.values()]):
+ while not any([r.connected for r in nostr_client.relay_manager.relays.values()]):
await asyncio.sleep(2)
def callback_events(eventMessage: EventMessage):
@@ -74,7 +72,7 @@ def callback_eose_notices(eventMessage: EndOfStoredEventsMessage):
def wrap_async_subscribe():
asyncio.run(
- nostr.client.subscribe(
+ nostr_client.subscribe(
callback_events,
callback_notices,
callback_eose_notices,
diff --git a/views_api.py b/views_api.py
index fef3179..3a40529 100644
--- a/views_api.py
+++ b/views_api.py
@@ -9,7 +9,7 @@
from lnbits.decorators import check_admin
from lnbits.helpers import urlsafe_short_hash
-from . import nostr, nostrclient_ext, scheduled_tasks
+from . import nostr_client, nostrclient_ext, scheduled_tasks
from .crud import add_relay, delete_relay, get_relays
from .helpers import normalize_public_key
from .models import Relay, RelayList, TestMessage, TestMessageResponse
@@ -23,7 +23,7 @@
@nostrclient_ext.get("/api/v1/relays")
async def api_get_relays() -> RelayList:
relays = RelayList(__root__=[])
- for url, r in nostr.client.relay_manager.relays.items():
+ for url, r in nostr_client.relay_manager.relays.items():
relay_id = urlsafe_short_hash()
relays.__root__.append(
Relay(
@@ -52,7 +52,7 @@ async def api_add_relay(relay: Relay) -> Optional[RelayList]:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Relay url not provided."
)
- if relay.url in nostr.client.relay_manager.relays:
+ if relay.url in nostr_client.relay_manager.relays:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"Relay: {relay.url} already exists.",
@@ -60,7 +60,7 @@ async def api_add_relay(relay: Relay) -> Optional[RelayList]:
relay.id = urlsafe_short_hash()
await add_relay(relay)
- nostr.client.relay_manager.add_relay(relay.url)
+ nostr_client.relay_manager.add_relay(relay.url)
return await get_relays()
@@ -74,7 +74,7 @@ async def api_delete_relay(relay: Relay) -> None:
status_code=HTTPStatus.BAD_REQUEST, detail="Relay url not provided."
)
# we can remove relays during runtime
- nostr.client.relay_manager.remove_relay(relay.url)
+ nostr_client.relay_manager.remove_relay(relay.url)
await delete_relay(relay)
@@ -118,14 +118,14 @@ async def api_stop():
for router in all_routers:
try:
for s in router.subscriptions:
- nostr.client.relay_manager.close_subscription(s)
+ nostr_client.relay_manager.close_subscription(s)
router.stop()
all_routers.remove(router)
except Exception as e:
logger.error(e)
try:
- nostr.client.relay_manager.close_connections()
+ nostr_client.relay_manager.close_connections()
except Exception as e:
logger.error(e)
From c02317c9f319b3d1399f7364ea4c1a82b716419e Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Thu, 26 Oct 2023 22:12:03 +0300
Subject: [PATCH 14/30] refactor: code clean-up
---
nostr/client/client.py | 2 +-
nostr/delegation.py | 32 --------------------------------
nostr/key.py | 5 -----
3 files changed, 1 insertion(+), 38 deletions(-)
delete mode 100644 nostr/delegation.py
diff --git a/nostr/client/client.py b/nostr/client/client.py
index a29eb05..090d413 100644
--- a/nostr/client/client.py
+++ b/nostr/client/client.py
@@ -4,7 +4,7 @@
from ..relay_manager import RelayManager
-
+# todo: why a module
class NostrClient:
relay_manager = RelayManager()
diff --git a/nostr/delegation.py b/nostr/delegation.py
deleted file mode 100644
index 8b1c311..0000000
--- a/nostr/delegation.py
+++ /dev/null
@@ -1,32 +0,0 @@
-import time
-from dataclasses import dataclass
-
-
-@dataclass
-class Delegation:
- delegator_pubkey: str
- delegatee_pubkey: str
- event_kind: int
- duration_secs: int = 30 * 24 * 60 # default to 30 days
- signature: str = None # set in PrivateKey.sign_delegation
-
- @property
- def expires(self) -> int:
- return int(time.time()) + self.duration_secs
-
- @property
- def conditions(self) -> str:
- return f"kind={self.event_kind}&created_at<{self.expires}"
-
- @property
- def delegation_token(self) -> str:
- return f"nostr:delegation:{self.delegatee_pubkey}:{self.conditions}"
-
- def get_tag(self) -> list[str]:
- """Called by Event"""
- return [
- "delegation",
- self.delegator_pubkey,
- self.conditions,
- self.signature,
- ]
diff --git a/nostr/key.py b/nostr/key.py
index 3c81e94..1cea686 100644
--- a/nostr/key.py
+++ b/nostr/key.py
@@ -8,7 +8,6 @@
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from . import bech32
-from .delegation import Delegation
from .event import EncryptedDirectMessage, Event, EventKind
@@ -116,10 +115,6 @@ def sign_event(self, event: Event) -> None:
event.public_key = self.public_key.hex()
event.signature = self.sign_message_hash(bytes.fromhex(event.id))
- def sign_delegation(self, delegation: Delegation) -> None:
- delegation.signature = self.sign_message_hash(
- sha256(delegation.delegation_token.encode()).digest()
- )
def __eq__(self, other):
return self.raw_secret == other.raw_secret
From a7f3448241730cfad4ff158b90722b7c5cba8de2 Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Thu, 26 Oct 2023 22:19:35 +0300
Subject: [PATCH 15/30] chore: code format
---
__init__.py | 1 +
nostr/client/client.py | 1 +
nostr/event.py | 3 ++-
nostr/key.py | 2 --
nostr/relay.py | 3 ++-
nostr/relay_manager.py | 2 +-
router.py | 19 +++++--------------
views_api.py | 3 ++-
8 files changed, 14 insertions(+), 20 deletions(-)
diff --git a/__init__.py b/__init__.py
index 919fd26..d50998b 100644
--- a/__init__.py
+++ b/__init__.py
@@ -24,6 +24,7 @@
nostr_client = NostrClient()
+
def nostr_renderer():
return template_renderer(["nostrclient/templates"])
diff --git a/nostr/client/client.py b/nostr/client/client.py
index 090d413..7342b1b 100644
--- a/nostr/client/client.py
+++ b/nostr/client/client.py
@@ -4,6 +4,7 @@
from ..relay_manager import RelayManager
+
# todo: why a module
class NostrClient:
relay_manager = RelayManager()
diff --git a/nostr/event.py b/nostr/event.py
index 65b187d..a7d4f1d 100644
--- a/nostr/event.py
+++ b/nostr/event.py
@@ -122,6 +122,7 @@ def __post_init__(self):
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"
+ "EncryptedDirectMessage `id` is undefined until its"
+ + " message is encrypted and stored in the `content` field"
)
return super().id
diff --git a/nostr/key.py b/nostr/key.py
index 1cea686..70d6bab 100644
--- a/nostr/key.py
+++ b/nostr/key.py
@@ -1,6 +1,5 @@
import base64
import secrets
-from hashlib import sha256
import secp256k1
from cffi import FFI
@@ -115,7 +114,6 @@ def sign_event(self, event: Event) -> None:
event.public_key = self.public_key.hex()
event.signature = self.sign_message_hash(bytes.fromhex(event.id))
-
def __eq__(self, other):
return self.raw_secret == other.raw_secret
diff --git a/nostr/relay.py b/nostr/relay.py
index 500bdf7..50cca98 100644
--- a/nostr/relay.py
+++ b/nostr/relay.py
@@ -101,7 +101,8 @@ def _on_open(self, _):
def _on_close(self, _, status_code, message):
logger.warning(
- f"[Relay: {self.url}] Connection closed. Status: '{status_code}'. Message: '{message}'."
+ f"[Relay: {self.url}] Connection closed."
+ + f" Status: '{status_code}'. Message: '{message}'."
)
self.close()
diff --git a/nostr/relay_manager.py b/nostr/relay_manager.py
index a2002c4..ff89de2 100644
--- a/nostr/relay_manager.py
+++ b/nostr/relay_manager.py
@@ -58,7 +58,7 @@ def remove_relay(self, url: str):
self.queue_threads.pop(url)
def remove_relays(self):
- relay_urls = list( self.relays.keys())
+ relay_urls = list(self.relays.keys())
for url in relay_urls:
self.remove_relay(url)
diff --git a/router.py b/router.py
index 32906ff..aeafabe 100644
--- a/router.py
+++ b/router.py
@@ -30,8 +30,8 @@ async def client_to_nostr(self):
"""
Receives requests / data from the client and forwards it to relays. If the
request was a subscription/filter, registers it with the nostr client lib.
- Remembers the subscription id so we can send back responses from the relay to this
- client in `nostr_to_client`
+ Remembers the subscription id so we can send back responses from the relay
+ to this client in `nostr_to_client`.
"""
while self.connected:
try:
@@ -47,11 +47,7 @@ async def client_to_nostr(self):
async def nostr_to_client(self):
"""
- Sends responses from relays back to the client. Polls the subscriptions of this client
- stored in `my_subscriptions`. Then gets all responses for this subscription id from `received_subscription_events` which
- is filled in tasks.py. Takes one response after the other and relays it back to the client. Reconstructs
- the reponse manually because the nostr client lib we're using can't do it. Reconstructs the original subscription id
- that we had previously rewritten in order to avoid collisions when multiple clients use the same id.
+ Sends responses from relays back to the client.
"""
while self.connected:
try:
@@ -132,8 +128,9 @@ async def _handle_received_subscription_events(self, s):
def _handle_notices(self):
while len(NostrRouter.received_subscription_notices):
my_event = NostrRouter.received_subscription_notices.pop(0)
- # note: we don't send it to the user because we don't know who should receive it
logger.info(f"[Relay '{my_event.url}'] Notice: '{my_event.content}']")
+ # Note: we don't send it to the user because
+ # we don't know who should receive it
nostr_client.relay_manager.handle_notice(my_event)
def _marshall_nostr_filters(self, data: Union[dict, list]):
@@ -157,12 +154,6 @@ def _marshall_nostr_filters(self, data: Union[dict, list]):
return NostrFilters(filter_list)
async def _handle_client_to_nostr(self, json_str):
- """Parses a (string) request from a client. If it is a subscription (REQ) or a CLOSE, it will
- register the subscription in the nostr client library that we're using so we can
- receive the callbacks on it later. Will rewrite the subscription id since we expect
- multiple clients to use the router and want to avoid subscription id collisions
- """
-
json_data = json.loads(json_str)
assert len(json_data), "Bad JSON array"
diff --git a/views_api.py b/views_api.py
index 3a40529..8b3643b 100644
--- a/views_api.py
+++ b/views_api.py
@@ -146,7 +146,8 @@ async def ws_relay(websocket: WebSocket) -> None:
await router.start()
all_routers.append(router)
- # we kill this websocket and the subscriptions if the user disconnects and thus `connected==False`
+ # we kill this websocket and the subscriptions
+ # if the user disconnects and thus `connected==False`
while True:
await asyncio.sleep(10)
if not router.connected:
From 879e244def7e68d13bdc8de92d667f368233153c Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Fri, 27 Oct 2023 14:07:35 +0300
Subject: [PATCH 16/30] refactor: remove `RelayList` class
---
crud.py | 10 ++++++----
models.py | 7 ++++---
tasks.py | 2 +-
views_api.py | 12 ++++++------
4 files changed, 17 insertions(+), 14 deletions(-)
diff --git a/crud.py b/crud.py
index c064a9a..05ca907 100644
--- a/crud.py
+++ b/crud.py
@@ -1,10 +1,12 @@
+from typing import List
+
from . import db
-from .models import Relay, RelayList
+from .models import Relay
-async def get_relays() -> RelayList:
- row = await db.fetchall("SELECT * FROM nostrclient.relays")
- return RelayList(__root__=row)
+async def get_relays() -> List[Relay]:
+ rows = await db.fetchall("SELECT * FROM nostrclient.relays")
+ return [Relay.from_row(r) for r in rows]
async def add_relay(relay: Relay) -> None:
diff --git a/models.py b/models.py
index 24730bc..d73d6a3 100644
--- a/models.py
+++ b/models.py
@@ -1,3 +1,4 @@
+from sqlite3 import Row
from typing import List, Optional
from pydantic import BaseModel, Field
@@ -26,9 +27,9 @@ def _init__(self):
if not self.id:
self.id = urlsafe_short_hash()
-
-class RelayList(BaseModel):
- __root__: List[Relay]
+ @classmethod
+ def from_row(cls, row: Row) -> "Relay":
+ return cls(**dict(row))
class Event(BaseModel):
diff --git a/tasks.py b/tasks.py
index e7468cd..3ac8da9 100644
--- a/tasks.py
+++ b/tasks.py
@@ -13,7 +13,7 @@ async def init_relays():
# get relays from db
relays = await get_relays()
# set relays and connect to them
- valid_relays = list(set([r.url for r in relays.__root__ if r.url]))
+ valid_relays = list(set([r.url for r in relays if r.url]))
nostr_client.reconnect(valid_relays)
diff --git a/views_api.py b/views_api.py
index 8b3643b..c120a97 100644
--- a/views_api.py
+++ b/views_api.py
@@ -1,6 +1,6 @@
import asyncio
from http import HTTPStatus
-from typing import Optional
+from typing import List
from fastapi import Depends, WebSocket
from loguru import logger
@@ -12,7 +12,7 @@
from . import nostr_client, nostrclient_ext, scheduled_tasks
from .crud import add_relay, delete_relay, get_relays
from .helpers import normalize_public_key
-from .models import Relay, RelayList, TestMessage, TestMessageResponse
+from .models import Relay, TestMessage, TestMessageResponse
from .nostr.key import EncryptedDirectMessage, PrivateKey
from .router import NostrRouter
@@ -21,11 +21,11 @@
@nostrclient_ext.get("/api/v1/relays")
-async def api_get_relays() -> RelayList:
- relays = RelayList(__root__=[])
+async def api_get_relays() -> List[Relay]:
+ relays = []
for url, r in nostr_client.relay_manager.relays.items():
relay_id = urlsafe_short_hash()
- relays.__root__.append(
+ relays.append(
Relay(
id=relay_id,
url=url,
@@ -47,7 +47,7 @@ async def api_get_relays() -> RelayList:
@nostrclient_ext.post(
"/api/v1/relay", status_code=HTTPStatus.OK, dependencies=[Depends(check_admin)]
)
-async def api_add_relay(relay: Relay) -> Optional[RelayList]:
+async def api_add_relay(relay: Relay) -> List[Relay]:
if not relay.url:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Relay url not provided."
From 23b1ef62e7903e9f72c8f282de31757388514b5d Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Fri, 27 Oct 2023 14:31:41 +0300
Subject: [PATCH 17/30] refactor: extract smaller methods with try-catch
---
nostr/client/client.py | 22 ++++++++++++++++++++--
1 file changed, 20 insertions(+), 2 deletions(-)
diff --git a/nostr/client/client.py b/nostr/client/client.py
index 7342b1b..fff3610 100644
--- a/nostr/client/client.py
+++ b/nostr/client/client.py
@@ -30,17 +30,35 @@ async def subscribe(
callback_eosenotices_func=None,
):
while True:
+ self._check_events(callback_events_func)
+ self._check_notices(callback_notices_func)
+ self._check_eos_notices(callback_eosenotices_func)
+
+ await asyncio.sleep(0.5)
+
+ def _check_events(self, callback_events_func = None):
+ try:
while self.relay_manager.message_pool.has_events():
event_msg = self.relay_manager.message_pool.get_event()
if callback_events_func:
callback_events_func(event_msg)
+ except Exception as e:
+ logger.debug(e)
+
+ def _check_notices(self, callback_notices_func = None):
+ try:
while self.relay_manager.message_pool.has_notices():
event_msg = self.relay_manager.message_pool.get_notice()
if callback_notices_func:
callback_notices_func(event_msg)
+ except Exception as e:
+ logger.debug(e)
+
+ def _check_eos_notices(self, callback_eosenotices_func = None):
+ try:
while self.relay_manager.message_pool.has_eose_notices():
event_msg = self.relay_manager.message_pool.get_eose_notice()
if callback_eosenotices_func:
callback_eosenotices_func(event_msg)
-
- await asyncio.sleep(0.5)
+ except Exception as e:
+ logger.debug(e)
From 311c170e1746e7b6862fde6ed7bdea3d28ee2372 Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Tue, 31 Oct 2023 13:45:47 +0200
Subject: [PATCH 18/30] fix: better exception handling
---
nostr/__init__.py | 0
nostr/client/client.py | 16 ++++++++++++---
nostr/relay_manager.py | 18 ++++++++++++-----
router.py | 7 +++----
tasks.py | 46 +++++++++++++++---------------------------
views_api.py | 12 ++++-------
6 files changed, 49 insertions(+), 50 deletions(-)
delete mode 100644 nostr/__init__.py
diff --git a/nostr/__init__.py b/nostr/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/nostr/client/client.py b/nostr/client/client.py
index fff3610..e74913f 100644
--- a/nostr/client/client.py
+++ b/nostr/client/client.py
@@ -9,19 +9,29 @@
class NostrClient:
relay_manager = RelayManager()
+ def __init__(self):
+ self.running = True
+
def connect(self, relays):
for relay in relays:
try:
self.relay_manager.add_relay(relay)
except Exception as e:
logger.debug(e)
+ self.running = True
def reconnect(self, relays):
self.relay_manager.remove_relays()
self.connect(relays)
def close(self):
- self.relay_manager.close_connections()
+ try:
+ self.relay_manager.close_subscriptions()
+ self.relay_manager.close_connections()
+
+ self.running = False
+ except Exception as e:
+ logger.error(e)
async def subscribe(
self,
@@ -29,12 +39,12 @@ async def subscribe(
callback_notices_func=None,
callback_eosenotices_func=None,
):
- while True:
+ while self.running:
self._check_events(callback_events_func)
self._check_notices(callback_notices_func)
self._check_eos_notices(callback_eosenotices_func)
- await asyncio.sleep(0.5)
+ await asyncio.sleep(0.2)
def _check_events(self, callback_events_func = None):
try:
diff --git a/nostr/relay_manager.py b/nostr/relay_manager.py
index ff89de2..8440f39 100644
--- a/nostr/relay_manager.py
+++ b/nostr/relay_manager.py
@@ -71,12 +71,20 @@ def add_subscription(self, id: str, filters: Filters):
relay.publish_subscriptions([s])
def close_subscription(self, id: str):
- with self._subscriptions_lock:
- if id in self._cached_subscriptions:
- self._cached_subscriptions.pop(id)
+ try:
+ with self._subscriptions_lock:
+ if id in self._cached_subscriptions:
+ self._cached_subscriptions.pop(id)
- for relay in self.relays.values():
- relay.close_subscription(id)
+ for relay in self.relays.values():
+ relay.close_subscription(id)
+ except Exception as e:
+ logger.debug(e)
+
+ def close_subscriptions(self):
+ all_subscriptions = list(self._cached_subscriptions.keys())
+ for id in all_subscriptions:
+ self.close_subscription(id)
def check_and_restart_relays(self):
stopped_relays = [r for r in self.relays.values() if r.shutdown]
diff --git a/router.py b/router.py
index aeafabe..2d4c299 100644
--- a/router.py
+++ b/router.py
@@ -46,9 +46,7 @@ async def client_to_nostr(self):
logger.debug(f"Failed to handle client message: '{str(e)}'.")
async def nostr_to_client(self):
- """
- Sends responses from relays back to the client.
- """
+ """ Sends responses from relays back to the client. """
while self.connected:
try:
await self._handle_subscriptions()
@@ -57,7 +55,7 @@ async def nostr_to_client(self):
logger.debug(f"Failed to handle response for client: '{str(e)}'.")
await asyncio.sleep(0.1)
- async def start(self):
+ def start(self):
self.connected = True
self.tasks.append(asyncio.create_task(self.client_to_nostr()))
self.tasks.append(asyncio.create_task(self.nostr_to_client()))
@@ -83,6 +81,7 @@ def stop(self):
async def _handle_subscriptions(self):
for s in self.subscriptions:
+ # print("### _handle_subscriptions for each")
if s in NostrRouter.received_subscription_events:
await self._handle_received_subscription_events(s)
if s in NostrRouter.received_subscription_eosenotices:
diff --git a/tasks.py b/tasks.py
index 3ac8da9..249a2ff 100644
--- a/tasks.py
+++ b/tasks.py
@@ -33,42 +33,28 @@ async def subscribe_events():
await asyncio.sleep(2)
def callback_events(eventMessage: EventMessage):
- if eventMessage.subscription_id in NostrRouter.received_subscription_events:
- # do not add duplicate events (by event id)
- if eventMessage.event.id in set(
- [
- e.id
- for e in NostrRouter.received_subscription_events[
- eventMessage.subscription_id
- ]
- ]
- ):
- return
-
- NostrRouter.received_subscription_events[
- eventMessage.subscription_id
- ].append(eventMessage.event)
- else:
- NostrRouter.received_subscription_events[eventMessage.subscription_id] = [
- eventMessage.event
- ]
- return
+ sub_id = eventMessage.subscription_id
+ if sub_id not in NostrRouter.received_subscription_events:
+ NostrRouter.received_subscription_events[sub_id] = [eventMessage.event]
+ return
+
+ # do not add duplicate events (by event id)
+ ids = set([e.id for e in NostrRouter.received_subscription_events[sub_id]])
+ if eventMessage.event.id in ids:
+ return
+
+ NostrRouter.received_subscription_events[sub_id].append(eventMessage.event)
def callback_notices(noticeMessage: NoticeMessage):
if noticeMessage not in NostrRouter.received_subscription_notices:
NostrRouter.received_subscription_notices.append(noticeMessage)
- return
def callback_eose_notices(eventMessage: EndOfStoredEventsMessage):
- if (
- eventMessage.subscription_id
- not in NostrRouter.received_subscription_eosenotices
- ):
- NostrRouter.received_subscription_eosenotices[
- eventMessage.subscription_id
- ] = eventMessage
-
- return
+ sub_id = eventMessage.subscription_id
+ if sub_id in NostrRouter.received_subscription_eosenotices:
+ return
+
+ NostrRouter.received_subscription_eosenotices[sub_id] = eventMessage
def wrap_async_subscribe():
asyncio.run(
diff --git a/views_api.py b/views_api.py
index c120a97..ff58a54 100644
--- a/views_api.py
+++ b/views_api.py
@@ -117,17 +117,13 @@ async def api_test_endpoint(data: TestMessage) -> TestMessageResponse:
async def api_stop():
for router in all_routers:
try:
- for s in router.subscriptions:
- nostr_client.relay_manager.close_subscription(s)
-
router.stop()
all_routers.remove(router)
except Exception as e:
logger.error(e)
- try:
- nostr_client.relay_manager.close_connections()
- except Exception as e:
- logger.error(e)
+
+ nostr_client.close()
+
for scheduled_task in scheduled_tasks:
try:
@@ -143,7 +139,7 @@ async def ws_relay(websocket: WebSocket) -> None:
"""Relay multiplexer: one client (per endpoint) <-> multiple relays"""
await websocket.accept()
router = NostrRouter(websocket)
- await router.start()
+ router.start()
all_routers.append(router)
# we kill this websocket and the subscriptions
From 672990a37c616a7f6239597c7be16275d5e78b4a Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Tue, 31 Oct 2023 14:04:21 +0200
Subject: [PATCH 19/30] fix: remove redundant filters
---
models.py | 17 +-----
nostr/client/client.py | 6 +-
nostr/filter.py | 134 -----------------------------------------
nostr/relay_manager.py | 4 +-
nostr/subscription.py | 6 +-
router.py | 35 +++--------
views_api.py | 1 -
7 files changed, 16 insertions(+), 187 deletions(-)
delete mode 100644 nostr/filter.py
diff --git a/models.py b/models.py
index d73d6a3..f2ca2a4 100644
--- a/models.py
+++ b/models.py
@@ -1,7 +1,7 @@
from sqlite3 import Row
from typing import List, Optional
-from pydantic import BaseModel, Field
+from pydantic import BaseModel
from lnbits.helpers import urlsafe_short_hash
@@ -41,21 +41,6 @@ class Event(BaseModel):
sig: str
-class Filter(BaseModel):
- ids: Optional[List[str]]
- kinds: Optional[List[int]]
- authors: Optional[List[str]]
- since: Optional[int]
- until: Optional[int]
- e: Optional[List[str]] = Field(alias="#e")
- p: Optional[List[str]] = Field(alias="#p")
- limit: Optional[int]
-
-
-class Filters(BaseModel):
- __root__: List[Filter]
-
-
class TestMessage(BaseModel):
sender_private_key: Optional[str]
reciever_public_key: str
diff --git a/nostr/client/client.py b/nostr/client/client.py
index e74913f..7366289 100644
--- a/nostr/client/client.py
+++ b/nostr/client/client.py
@@ -46,7 +46,7 @@ async def subscribe(
await asyncio.sleep(0.2)
- def _check_events(self, callback_events_func = None):
+ def _check_events(self, callback_events_func=None):
try:
while self.relay_manager.message_pool.has_events():
event_msg = self.relay_manager.message_pool.get_event()
@@ -55,7 +55,7 @@ def _check_events(self, callback_events_func = None):
except Exception as e:
logger.debug(e)
- def _check_notices(self, callback_notices_func = None):
+ def _check_notices(self, callback_notices_func=None):
try:
while self.relay_manager.message_pool.has_notices():
event_msg = self.relay_manager.message_pool.get_notice()
@@ -64,7 +64,7 @@ def _check_notices(self, callback_notices_func = None):
except Exception as e:
logger.debug(e)
- def _check_eos_notices(self, callback_eosenotices_func = None):
+ def _check_eos_notices(self, callback_eosenotices_func=None):
try:
while self.relay_manager.message_pool.has_eose_notices():
event_msg = self.relay_manager.message_pool.get_eose_notice()
diff --git a/nostr/filter.py b/nostr/filter.py
deleted file mode 100644
index f119079..0000000
--- a/nostr/filter.py
+++ /dev/null
@@ -1,134 +0,0 @@
-from collections import UserList
-from typing import List
-
-from .event import Event, EventKind
-
-
-class Filter:
- """
- NIP-01 filtering.
-
- Explicitly supports "#e" and "#p" tag filters via `event_refs` and `pubkey_refs`.
-
- Arbitrary NIP-12 single-letter tag filters are also supported via `add_arbitrary_tag`.
- If a particular single-letter tag gains prominence, explicit support should be
- added. For example:
- # arbitrary tag
- filter.add_arbitrary_tag('t', [hashtags])
-
- # promoted to explicit support
- Filter(hashtag_refs=[hashtags])
- """
-
- def __init__(
- 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.kinds = kinds
- self.authors = authors
- self.since = since
- self.until = until
- self.event_refs = event_refs
- self.pubkey_refs = pubkey_refs
- self.limit = limit
-
- self.tags = {}
- if self.event_refs:
- self.add_arbitrary_tag("e", self.event_refs)
- if self.pubkey_refs:
- self.add_arbitrary_tag("p", self.pubkey_refs)
-
- def add_arbitrary_tag(self, tag: str, values: list):
- """
- Filter on any arbitrary tag with explicit handling for NIP-01 and NIP-12
- single-letter tags.
- """
- # 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}"
- self.tags[tag_key] = values
-
- def matches(self, event: Event) -> bool:
- if self.event_ids is not None and event.id not in self.event_ids:
- return False
- if self.kinds is not None and event.kind not in self.kinds:
- return False
- if self.authors is not None and event.public_key not in self.authors:
- return False
- if self.since is not None and event.created_at < self.since:
- return False
- if self.until is not None and event.created_at > self.until:
- return False
- if (self.event_refs is not None or self.pubkey_refs is not None) and len(
- event.tags
- ) == 0:
- return False
-
- if self.tags:
- e_tag_identifiers = set([e_tag[0] for e_tag in event.tags])
- for f_tag, f_tag_values in self.tags.items():
- # Omit any NIP-01 or NIP-12 "#" chars on single-letter tags
- f_tag = f_tag.replace("#", "")
-
- if f_tag not in e_tag_identifiers:
- # Event is missing a tag type that we're looking for
- return False
-
- # Multiple values within f_tag_values are treated as OR search; an Event
- # needs to match only one.
- # Note: an Event could have multiple entries of the same tag type
- # (e.g. a reply to multiple people) so we have to check all of them.
- match_found = False
- for e_tag in event.tags:
- if e_tag[0] == f_tag and e_tag[1] in f_tag_values:
- match_found = True
- break
- if not match_found:
- return False
-
- return True
-
- def to_json_object(self) -> dict:
- res = {}
- if self.event_ids is not None:
- res["ids"] = self.event_ids
- if self.kinds is not None:
- res["kinds"] = self.kinds
- if self.authors is not None:
- res["authors"] = self.authors
- if self.since is not None:
- res["since"] = self.since
- if self.until is not None:
- res["until"] = self.until
- if self.limit is not None:
- res["limit"] = self.limit
- if self.tags:
- res.update(self.tags)
-
- return res
-
-
-class Filters(UserList):
- def __init__(self, initlist: "list[Filter]" = []) -> None:
- super().__init__(initlist)
- self.data: "list[Filter]"
-
- def match(self, event: Event):
- for filter in self.data:
- if filter.matches(event):
- return True
- return False
-
- def to_json_array(self) -> list:
- return [filter.to_json_object() for filter in self.data]
diff --git a/nostr/relay_manager.py b/nostr/relay_manager.py
index 8440f39..35726c6 100644
--- a/nostr/relay_manager.py
+++ b/nostr/relay_manager.py
@@ -1,10 +1,10 @@
import asyncio
import threading
import time
+from typing import List
from loguru import logger
-from .filter import Filters
from .message_pool import MessagePool, NoticeMessage
from .relay import Relay
from .subscription import Subscription
@@ -62,7 +62,7 @@ def remove_relays(self):
for url in relay_urls:
self.remove_relay(url)
- def add_subscription(self, id: str, filters: Filters):
+ def add_subscription(self, id: str, filters: List[str]):
s = Subscription(id, filters)
with self._subscriptions_lock:
self._cached_subscriptions[id] = s
diff --git a/nostr/subscription.py b/nostr/subscription.py
index 10b5363..56f049d 100644
--- a/nostr/subscription.py
+++ b/nostr/subscription.py
@@ -1,10 +1,10 @@
-from .filter import Filters
+from typing import List
class Subscription:
- def __init__(self, id: str, filters: Filters = None) -> None:
+ def __init__(self, id: str, filters: List[str] = None) -> None:
self.id = id
self.filters = filters
def to_json_object(self):
- return {"id": self.id, "filters": self.filters.to_json_array()}
+ return {"id": self.id, "filters": self.filters}
diff --git a/router.py b/router.py
index 2d4c299..a18a495 100644
--- a/router.py
+++ b/router.py
@@ -1,6 +1,6 @@
import asyncio
import json
-from typing import List, Union
+from typing import List
from fastapi import WebSocket, WebSocketDisconnect
from loguru import logger
@@ -8,9 +8,7 @@
from lnbits.helpers import urlsafe_short_hash
from . import nostr_client
-from .models import Event, Filter
-from .nostr.filter import Filter as NostrFilter
-from .nostr.filter import Filters as NostrFilters
+from .models import Event
from .nostr.message_pool import EndOfStoredEventsMessage, NoticeMessage
@@ -46,7 +44,7 @@ async def client_to_nostr(self):
logger.debug(f"Failed to handle client message: '{str(e)}'.")
async def nostr_to_client(self):
- """ Sends responses from relays back to the client. """
+ """Sends responses from relays back to the client."""
while self.connected:
try:
await self._handle_subscriptions()
@@ -132,26 +130,6 @@ def _handle_notices(self):
# we don't know who should receive it
nostr_client.relay_manager.handle_notice(my_event)
- def _marshall_nostr_filters(self, data: Union[dict, list]):
- # todo: get rid of this
- filters = data if isinstance(data, list) else [data]
- filters = [Filter.parse_obj(f) for f in filters]
- filter_list: list[NostrFilter] = []
- for filter in filters:
- filter_list.append(
- NostrFilter(
- event_ids=filter.ids, # type: ignore
- kinds=filter.kinds, # type: ignore
- authors=filter.authors, # type: ignore
- since=filter.since, # type: ignore
- until=filter.until, # type: ignore
- event_refs=filter.e, # type: ignore
- pubkey_refs=filter.p, # type: ignore
- limit=filter.limit, # type: ignore
- )
- )
- return NostrFilters(filter_list)
-
async def _handle_client_to_nostr(self, json_str):
json_data = json.loads(json_str)
assert len(json_data), "Bad JSON array"
@@ -172,11 +150,12 @@ def _handle_client_req(self, json_data):
subscription_id = json_data[1]
subscription_id_rewritten = urlsafe_short_hash()
self.original_subscription_ids[subscription_id_rewritten] = subscription_id
- fltr = json_data[2:]
- filters = self._marshall_nostr_filters(fltr) # revisit
+ filters = json_data[2:]
nostr_client.relay_manager.add_subscription(subscription_id_rewritten, filters)
- request_rewritten = json.dumps([json_data[0], subscription_id_rewritten] + fltr)
+ request_rewritten = json.dumps(
+ [json_data[0], subscription_id_rewritten] + filters
+ )
self.subscriptions.append(subscription_id_rewritten) # why here also?
nostr_client.relay_manager.publish_message(
diff --git a/views_api.py b/views_api.py
index ff58a54..bc44e1f 100644
--- a/views_api.py
+++ b/views_api.py
@@ -124,7 +124,6 @@ async def api_stop():
nostr_client.close()
-
for scheduled_task in scheduled_tasks:
try:
scheduled_task.cancel()
From 0b8e73bb8049746e65c420d7428092edea800e12 Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Tue, 31 Oct 2023 14:51:27 +0200
Subject: [PATCH 20/30] fix: simplify event
---
models.py | 9 ---------
nostr/message_pool.py | 24 ++++++++++--------------
router.py | 21 ++++++---------------
tasks.py | 8 ++++----
4 files changed, 20 insertions(+), 42 deletions(-)
diff --git a/models.py b/models.py
index f2ca2a4..e08ade3 100644
--- a/models.py
+++ b/models.py
@@ -32,15 +32,6 @@ def from_row(cls, row: Row) -> "Relay":
return cls(**dict(row))
-class Event(BaseModel):
- content: str
- pubkey: str
- created_at: Optional[int]
- kind: int
- tags: Optional[List[List[str]]]
- sig: str
-
-
class TestMessage(BaseModel):
sender_private_key: Optional[str]
reciever_public_key: str
diff --git a/nostr/message_pool.py b/nostr/message_pool.py
index e38e66e..5a7211b 100644
--- a/nostr/message_pool.py
+++ b/nostr/message_pool.py
@@ -2,13 +2,13 @@
from queue import Queue
from threading import Lock
-from .event import Event
from .message_type import RelayMessageType
class EventMessage:
- def __init__(self, event: Event, subscription_id: str, url: str) -> None:
+ def __init__(self, event: str, event_id: str, subscription_id: str, url: str) -> None:
self.event = event
+ self.event_id = event_id
self.subscription_id = subscription_id
self.url = url
@@ -59,18 +59,14 @@ def _process_message(self, message: str, url: str):
message_type = message_json[0]
if message_type == RelayMessageType.EVENT:
subscription_id = message_json[1]
- e = message_json[2]
- event = Event(
- e["content"],
- e["pubkey"],
- e["created_at"],
- e["kind"],
- e["tags"],
- e["sig"],
- )
+ event = message_json[2]
+ if "id" not in event:
+ return
+ event_id = event["id"]
+
with self.lock:
- if f"{subscription_id}_{event.id}" not in self._unique_events:
- self._accept_event(EventMessage(event, subscription_id, url))
+ if f"{subscription_id}_{event_id}" not in self._unique_events:
+ self._accept_event(EventMessage(json.dumps(event), event_id, subscription_id, url))
elif message_type == RelayMessageType.NOTICE:
self.notices.put(NoticeMessage(message_json[1], url))
elif message_type == RelayMessageType.END_OF_STORED_EVENTS:
@@ -85,5 +81,5 @@ def _accept_event(self, event_message: EventMessage):
"""
self.events.put(event_message)
self._unique_events.add(
- f"{event_message.subscription_id}_{event_message.event.id}"
+ f"{event_message.subscription_id}_{event_message.event_id}"
)
diff --git a/router.py b/router.py
index a18a495..2d4f0f4 100644
--- a/router.py
+++ b/router.py
@@ -8,12 +8,11 @@
from lnbits.helpers import urlsafe_short_hash
from . import nostr_client
-from .models import Event
-from .nostr.message_pool import EndOfStoredEventsMessage, NoticeMessage
+from .nostr.message_pool import EndOfStoredEventsMessage, EventMessage, NoticeMessage
class NostrRouter:
- received_subscription_events: dict[str, list[Event]] = {}
+ received_subscription_events: dict[str, List[EventMessage]] = {}
received_subscription_notices: list[NoticeMessage] = []
received_subscription_eosenotices: dict[str, EndOfStoredEventsMessage] = {}
@@ -103,22 +102,14 @@ async def _handle_received_subscription_events(self, s):
return
while len(NostrRouter.received_subscription_events[s]):
- my_event = NostrRouter.received_subscription_events[s].pop(0)
- event_json = {
- "id": my_event.id,
- "pubkey": my_event.public_key,
- "created_at": my_event.created_at,
- "kind": my_event.kind,
- "tags": my_event.tags,
- "content": my_event.content,
- "sig": my_event.signature,
- }
+ event_message = NostrRouter.received_subscription_events[s].pop(0)
+ event_json = event_message.event
# this reconstructs the original response from the relay
# reconstruct original subscription id
s_original = self.original_subscription_ids[s]
- event_to_forward = ["EVENT", s_original, event_json]
- await self.websocket.send_text(json.dumps(event_to_forward))
+ event_to_forward = f"""["EVENT", "{s_original}", {event_json}]"""
+ await self.websocket.send_text(event_to_forward)
except Exception as e:
logger.debug(e) # there are 2900 errors here
diff --git a/tasks.py b/tasks.py
index 249a2ff..c1831d4 100644
--- a/tasks.py
+++ b/tasks.py
@@ -35,15 +35,15 @@ async def subscribe_events():
def callback_events(eventMessage: EventMessage):
sub_id = eventMessage.subscription_id
if sub_id not in NostrRouter.received_subscription_events:
- NostrRouter.received_subscription_events[sub_id] = [eventMessage.event]
+ NostrRouter.received_subscription_events[sub_id] = [eventMessage]
return
# do not add duplicate events (by event id)
- ids = set([e.id for e in NostrRouter.received_subscription_events[sub_id]])
- if eventMessage.event.id in ids:
+ ids = set([e.event_id for e in NostrRouter.received_subscription_events[sub_id]])
+ if eventMessage.event_id in ids:
return
- NostrRouter.received_subscription_events[sub_id].append(eventMessage.event)
+ NostrRouter.received_subscription_events[sub_id].append(eventMessage)
def callback_notices(noticeMessage: NoticeMessage):
if noticeMessage not in NostrRouter.received_subscription_notices:
From f14ca6e6457a60e3822b36be29af8fad95048777 Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Tue, 31 Oct 2023 14:59:48 +0200
Subject: [PATCH 21/30] chore: code format
---
nostr/key.py | 5 ++++-
nostr/message_pool.py | 14 +++++++++-----
tasks.py | 4 +++-
3 files changed, 16 insertions(+), 7 deletions(-)
diff --git a/nostr/key.py b/nostr/key.py
index 70d6bab..3803650 100644
--- a/nostr/key.py
+++ b/nostr/key.py
@@ -77,7 +77,10 @@ def encrypt_message(self, message: str, public_key_hex: str) -> str:
encryptor = cipher.encryptor()
encrypted_message = encryptor.update(padded_data) + encryptor.finalize()
- return f"{base64.b64encode(encrypted_message).decode()}?iv={base64.b64encode(iv).decode()}"
+ return (
+ f"{base64.b64encode(encrypted_message).decode()}"
+ + f"?iv={base64.b64encode(iv).decode()}"
+ )
def encrypt_dm(self, dm: EncryptedDirectMessage) -> None:
dm.content = self.encrypt_message(
diff --git a/nostr/message_pool.py b/nostr/message_pool.py
index 5a7211b..a3e6c5f 100644
--- a/nostr/message_pool.py
+++ b/nostr/message_pool.py
@@ -6,7 +6,9 @@
class EventMessage:
- def __init__(self, event: str, event_id: str, subscription_id: str, url: str) -> None:
+ def __init__(
+ self, event: str, event_id: str, subscription_id: str, url: str
+ ) -> None:
self.event = event
self.event_id = event_id
self.subscription_id = subscription_id
@@ -66,7 +68,9 @@ def _process_message(self, message: str, url: str):
with self.lock:
if f"{subscription_id}_{event_id}" not in self._unique_events:
- self._accept_event(EventMessage(json.dumps(event), event_id, subscription_id, url))
+ self._accept_event(
+ EventMessage(json.dumps(event), event_id, subscription_id, url)
+ )
elif message_type == RelayMessageType.NOTICE:
self.notices.put(NoticeMessage(message_json[1], url))
elif message_type == RelayMessageType.END_OF_STORED_EVENTS:
@@ -74,9 +78,9 @@ def _process_message(self, message: str, url: str):
def _accept_event(self, event_message: EventMessage):
"""
- Event uniqueness is considered per `subscription_id`.
- The `subscription_id` is rewritten to be unique and it is the same accross relays.
- The same event can come from different subscriptions (from the same client or from different ones).
+ Event uniqueness is considered per `subscription_id`. The `subscription_id` is
+ rewritten to be unique and it is the same accross relays. The same event can
+ come from different subscriptions (from the same client or from different ones).
Clients that have joined later should receive older events.
"""
self.events.put(event_message)
diff --git a/tasks.py b/tasks.py
index c1831d4..69aa33c 100644
--- a/tasks.py
+++ b/tasks.py
@@ -39,7 +39,9 @@ def callback_events(eventMessage: EventMessage):
return
# do not add duplicate events (by event id)
- ids = set([e.event_id for e in NostrRouter.received_subscription_events[sub_id]])
+ ids = set(
+ [e.event_id for e in NostrRouter.received_subscription_events[sub_id]]
+ )
if eventMessage.event_id in ids:
return
From 95b584551fb201ca1dfc88e23243935318619219 Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Tue, 31 Oct 2023 15:16:48 +0200
Subject: [PATCH 22/30] fix: code check
---
nostr/relay_manager.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/nostr/relay_manager.py b/nostr/relay_manager.py
index 35726c6..b32a1ac 100644
--- a/nostr/relay_manager.py
+++ b/nostr/relay_manager.py
@@ -22,14 +22,14 @@ def __init__(self) -> None:
def add_relay(self, url: str) -> Relay:
if url in list(self.relays.keys()):
logger.debug(f"Relay '{url}' already present.")
- return
+ return self.relays[url]
relay = Relay(url, self.message_pool)
self.relays[url] = relay
self._open_connection(relay)
- relay.publish_subscriptions(self._cached_subscriptions.values())
+ relay.publish_subscriptions(list(self._cached_subscriptions.values()))
return relay
def remove_relay(self, url: str):
From 0d948ea95696a32d1d27635f8464ba1649a4f78f Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Tue, 31 Oct 2023 15:17:13 +0200
Subject: [PATCH 23/30] fix: code check
---
router.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/router.py b/router.py
index 2d4f0f4..775c583 100644
--- a/router.py
+++ b/router.py
@@ -1,6 +1,6 @@
import asyncio
import json
-from typing import List
+from typing import Dict, List
from fastapi import WebSocket, WebSocketDisconnect
from loguru import logger
@@ -21,7 +21,7 @@ def __init__(self, websocket: WebSocket):
self.connected: bool = True
self.websocket: WebSocket = websocket
self.tasks: List[asyncio.Task] = [] # chek why state is needed
- self.original_subscription_ids = {} # here
+ self.original_subscription_ids: Dict[str, str] = {} # here
async def client_to_nostr(self):
"""
From e3d2843389a9d664d752d0c2d53345697eab48b9 Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Tue, 31 Oct 2023 15:52:05 +0200
Subject: [PATCH 24/30] fix: simplify `REQ`
---
nostr/client/client.py | 2 --
nostr/relay.py | 7 +++----
nostr/subscription.py | 5 +----
router.py | 13 +++++--------
4 files changed, 9 insertions(+), 18 deletions(-)
diff --git a/nostr/client/client.py b/nostr/client/client.py
index 7366289..ebe55bd 100644
--- a/nostr/client/client.py
+++ b/nostr/client/client.py
@@ -4,8 +4,6 @@
from ..relay_manager import RelayManager
-
-# todo: why a module
class NostrClient:
relay_manager = RelayManager()
diff --git a/nostr/relay.py b/nostr/relay.py
index 50cca98..b576cfa 100644
--- a/nostr/relay.py
+++ b/nostr/relay.py
@@ -18,7 +18,7 @@ def __init__(self, url: str, message_pool: MessagePool) -> None:
self.connected: bool = False
self.reconnect: bool = True
self.shutdown: bool = False
- # todo: extract stats
+
self.error_counter: int = 0
self.error_threshold: int = 100
self.error_list: List[str] = []
@@ -64,9 +64,8 @@ def publish(self, message: str):
self.queue.put(message)
def publish_subscriptions(self, subscriptions: List[Subscription] = []):
- for subscription in subscriptions:
- s = subscription.to_json_object()
- json_str = json.dumps(["REQ", s["id"], s["filters"][0]])
+ for s in subscriptions:
+ json_str = json.dumps(["REQ", s.id] + s.filters)
self.publish(json_str)
async def queue_worker(self):
diff --git a/nostr/subscription.py b/nostr/subscription.py
index 56f049d..3825002 100644
--- a/nostr/subscription.py
+++ b/nostr/subscription.py
@@ -4,7 +4,4 @@
class Subscription:
def __init__(self, id: str, filters: List[str] = None) -> None:
self.id = id
- self.filters = filters
-
- def to_json_object(self):
- return {"id": self.id, "filters": self.filters}
+ self.filters = filters
\ No newline at end of file
diff --git a/router.py b/router.py
index 775c583..f62d899 100644
--- a/router.py
+++ b/router.py
@@ -17,11 +17,14 @@ class NostrRouter:
received_subscription_eosenotices: dict[str, EndOfStoredEventsMessage] = {}
def __init__(self, websocket: WebSocket):
- self.subscriptions: List[str] = []
self.connected: bool = True
self.websocket: WebSocket = websocket
self.tasks: List[asyncio.Task] = [] # chek why state is needed
- self.original_subscription_ids: Dict[str, str] = {} # here
+ self.original_subscription_ids: Dict[str, str] = {}
+
+ @property
+ def subscriptions(self) -> List[str]:
+ return self.original_subscription_ids.keys()
async def client_to_nostr(self):
"""
@@ -78,7 +81,6 @@ def stop(self):
async def _handle_subscriptions(self):
for s in self.subscriptions:
- # print("### _handle_subscriptions for each")
if s in NostrRouter.received_subscription_events:
await self._handle_received_subscription_events(s)
if s in NostrRouter.received_subscription_eosenotices:
@@ -148,11 +150,6 @@ def _handle_client_req(self, json_data):
[json_data[0], subscription_id_rewritten] + filters
)
- self.subscriptions.append(subscription_id_rewritten) # why here also?
- nostr_client.relay_manager.publish_message(
- request_rewritten
- ) # both `add_subscription` and `publish_message`?
-
def _handle_client_close(self, subscription_id):
subscription_id_rewritten = next(
(
From 44fbb3582bfdaf16a478a6dec8240e48b06f9f71 Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Tue, 31 Oct 2023 16:49:15 +0200
Subject: [PATCH 25/30] fix: more clean-ups
---
nostr/client/client.py | 1 +
nostr/subscription.py | 2 +-
router.py | 5 +----
views_api.py | 2 +-
4 files changed, 4 insertions(+), 6 deletions(-)
diff --git a/nostr/client/client.py b/nostr/client/client.py
index ebe55bd..5fa3a00 100644
--- a/nostr/client/client.py
+++ b/nostr/client/client.py
@@ -4,6 +4,7 @@
from ..relay_manager import RelayManager
+
class NostrClient:
relay_manager = RelayManager()
diff --git a/nostr/subscription.py b/nostr/subscription.py
index 3825002..a75c1a1 100644
--- a/nostr/subscription.py
+++ b/nostr/subscription.py
@@ -4,4 +4,4 @@
class Subscription:
def __init__(self, id: str, filters: List[str] = None) -> None:
self.id = id
- self.filters = filters
\ No newline at end of file
+ self.filters = filters
diff --git a/router.py b/router.py
index f62d899..fa2961b 100644
--- a/router.py
+++ b/router.py
@@ -24,7 +24,7 @@ def __init__(self, websocket: WebSocket):
@property
def subscriptions(self) -> List[str]:
- return self.original_subscription_ids.keys()
+ return list(self.original_subscription_ids.keys())
async def client_to_nostr(self):
"""
@@ -146,9 +146,6 @@ def _handle_client_req(self, json_data):
filters = json_data[2:]
nostr_client.relay_manager.add_subscription(subscription_id_rewritten, filters)
- request_rewritten = json.dumps(
- [json_data[0], subscription_id_rewritten] + filters
- )
def _handle_client_close(self, subscription_id):
subscription_id_rewritten = next(
diff --git a/views_api.py b/views_api.py
index bc44e1f..daaaca5 100644
--- a/views_api.py
+++ b/views_api.py
@@ -86,7 +86,7 @@ async def api_test_endpoint(data: TestMessage) -> TestMessageResponse:
to_public_key = normalize_public_key(data.reciever_public_key)
pk = bytes.fromhex(data.sender_private_key) if data.sender_private_key else None
- private_key = PrivateKey(pk)
+ private_key = PrivateKey(pk) if pk else PrivateKey()
dm = EncryptedDirectMessage(
recipient_pubkey=to_public_key, cleartext_content=data.message
From e6b6ed91cf728736c04d2e6f11909136be62692f Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Tue, 31 Oct 2023 16:55:22 +0200
Subject: [PATCH 26/30] refactor: use simpler method
---
router.py | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/router.py b/router.py
index fa2961b..8cde361 100644
--- a/router.py
+++ b/router.py
@@ -67,11 +67,7 @@ def stop(self):
except Exception as _:
pass
- for s in self.subscriptions:
- try:
- nostr_client.relay_manager.close_subscription(s)
- except Exception as _:
- pass
+ nostr_client.relay_manager.close_subscriptions(self.subscriptions)
try:
self.websocket.close()
From 3bc10a7322e59ea089b29f510f12c725d8c66598 Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Tue, 31 Oct 2023 17:13:50 +0200
Subject: [PATCH 27/30] refactor: re-order and rename
---
router.py | 47 +++++++++++++++++++++++------------------------
1 file changed, 23 insertions(+), 24 deletions(-)
diff --git a/router.py b/router.py
index 8cde361..a6db765 100644
--- a/router.py
+++ b/router.py
@@ -25,13 +25,30 @@ def __init__(self, websocket: WebSocket):
@property
def subscriptions(self) -> List[str]:
return list(self.original_subscription_ids.keys())
+
+ def start(self):
+ self.connected = True
+ self.tasks.append(asyncio.create_task(self._client_to_nostr()))
+ self.tasks.append(asyncio.create_task(self._nostr_to_client()))
+
+ def stop(self):
+ for t in self.tasks:
+ try:
+ t.cancel()
+ except Exception as _:
+ pass
+
+ nostr_client.relay_manager.close_subscriptions(self.subscriptions)
+
+ try:
+ self.websocket.close()
+ except Exception as _:
+ pass
+ self.connected = False
- async def client_to_nostr(self):
+ async def _client_to_nostr(self):
"""
- Receives requests / data from the client and forwards it to relays. If the
- request was a subscription/filter, registers it with the nostr client lib.
- Remembers the subscription id so we can send back responses from the relay
- to this client in `nostr_to_client`.
+ Receives requests / data from the client and forwards it to relays.
"""
while self.connected:
try:
@@ -45,7 +62,7 @@ async def client_to_nostr(self):
except Exception as e:
logger.debug(f"Failed to handle client message: '{str(e)}'.")
- async def nostr_to_client(self):
+ async def _nostr_to_client(self):
"""Sends responses from relays back to the client."""
while self.connected:
try:
@@ -55,25 +72,7 @@ async def nostr_to_client(self):
logger.debug(f"Failed to handle response for client: '{str(e)}'.")
await asyncio.sleep(0.1)
- def start(self):
- self.connected = True
- self.tasks.append(asyncio.create_task(self.client_to_nostr()))
- self.tasks.append(asyncio.create_task(self.nostr_to_client()))
-
- def stop(self):
- for t in self.tasks:
- try:
- t.cancel()
- except Exception as _:
- pass
-
- nostr_client.relay_manager.close_subscriptions(self.subscriptions)
- try:
- self.websocket.close()
- except Exception as _:
- pass
- self.connected = False
async def _handle_subscriptions(self):
for s in self.subscriptions:
From fb1df37c11e9cecad4baec359b32d816c81704c8 Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Tue, 31 Oct 2023 17:50:03 +0200
Subject: [PATCH 28/30] fix: stop logic
---
router.py | 11 +++++------
views_api.py | 10 +++++-----
2 files changed, 10 insertions(+), 11 deletions(-)
diff --git a/router.py b/router.py
index a6db765..cef82e2 100644
--- a/router.py
+++ b/router.py
@@ -19,32 +19,32 @@ class NostrRouter:
def __init__(self, websocket: WebSocket):
self.connected: bool = True
self.websocket: WebSocket = websocket
- self.tasks: List[asyncio.Task] = [] # chek why state is needed
+ self.tasks: List[asyncio.Task] = []
self.original_subscription_ids: Dict[str, str] = {}
@property
def subscriptions(self) -> List[str]:
return list(self.original_subscription_ids.keys())
-
+
def start(self):
self.connected = True
self.tasks.append(asyncio.create_task(self._client_to_nostr()))
self.tasks.append(asyncio.create_task(self._nostr_to_client()))
def stop(self):
+ nostr_client.relay_manager.close_subscriptions(self.subscriptions)
+ self.connected = False
+
for t in self.tasks:
try:
t.cancel()
except Exception as _:
pass
- nostr_client.relay_manager.close_subscriptions(self.subscriptions)
-
try:
self.websocket.close()
except Exception as _:
pass
- self.connected = False
async def _client_to_nostr(self):
"""
@@ -73,7 +73,6 @@ async def _nostr_to_client(self):
await asyncio.sleep(0.1)
-
async def _handle_subscriptions(self):
for s in self.subscriptions:
if s in NostrRouter.received_subscription_events:
diff --git a/views_api.py b/views_api.py
index daaaca5..b0ebee6 100644
--- a/views_api.py
+++ b/views_api.py
@@ -143,9 +143,9 @@ async def ws_relay(websocket: WebSocket) -> None:
# we kill this websocket and the subscriptions
# if the user disconnects and thus `connected==False`
- while True:
+ while router.connected:
await asyncio.sleep(10)
- if not router.connected:
- router.stop()
- all_routers.remove(router)
- break
+
+ router.stop()
+ all_routers.remove(router)
+
From 09d42f1df748a02a8929a1002b2f698ed9b03007 Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Tue, 31 Oct 2023 21:23:18 +0200
Subject: [PATCH 29/30] fix: subscription close before disconnect
---
nostr/client/client.py | 2 +-
nostr/relay_manager.py | 9 ++++++---
router.py | 9 +++++----
views_api.py | 4 ++--
4 files changed, 14 insertions(+), 10 deletions(-)
diff --git a/nostr/client/client.py b/nostr/client/client.py
index 5fa3a00..4624ff3 100644
--- a/nostr/client/client.py
+++ b/nostr/client/client.py
@@ -25,7 +25,7 @@ def reconnect(self, relays):
def close(self):
try:
- self.relay_manager.close_subscriptions()
+ self.relay_manager.close_all_subscriptions()
self.relay_manager.close_connections()
self.running = False
diff --git a/nostr/relay_manager.py b/nostr/relay_manager.py
index b32a1ac..ff7ca9c 100644
--- a/nostr/relay_manager.py
+++ b/nostr/relay_manager.py
@@ -81,11 +81,14 @@ def close_subscription(self, id: str):
except Exception as e:
logger.debug(e)
- def close_subscriptions(self):
- all_subscriptions = list(self._cached_subscriptions.keys())
- for id in all_subscriptions:
+ def close_subscriptions(self, subscriptions: List[str]):
+ for id in subscriptions:
self.close_subscription(id)
+ def close_all_subscriptions(self):
+ all_subscriptions = list(self._cached_subscriptions.keys())
+ self.close_subscriptions(all_subscriptions)
+
def check_and_restart_relays(self):
stopped_relays = [r for r in self.relays.values() if r.shutdown]
for relay in stopped_relays:
diff --git a/router.py b/router.py
index cef82e2..e6ccdef 100644
--- a/router.py
+++ b/router.py
@@ -31,7 +31,7 @@ def start(self):
self.tasks.append(asyncio.create_task(self._client_to_nostr()))
self.tasks.append(asyncio.create_task(self._nostr_to_client()))
- def stop(self):
+ async def stop(self):
nostr_client.relay_manager.close_subscriptions(self.subscriptions)
self.connected = False
@@ -42,7 +42,7 @@ def stop(self):
pass
try:
- self.websocket.close()
+ await self.websocket.close()
except Exception as _:
pass
@@ -53,8 +53,9 @@ async def _client_to_nostr(self):
while self.connected:
try:
json_str = await self.websocket.receive_text()
- except WebSocketDisconnect:
- self.stop()
+ except WebSocketDisconnect as e:
+ logger.debug(e)
+ await self.stop()
break
try:
diff --git a/views_api.py b/views_api.py
index b0ebee6..14642d0 100644
--- a/views_api.py
+++ b/views_api.py
@@ -117,7 +117,7 @@ async def api_test_endpoint(data: TestMessage) -> TestMessageResponse:
async def api_stop():
for router in all_routers:
try:
- router.stop()
+ await router.stop()
all_routers.remove(router)
except Exception as e:
logger.error(e)
@@ -146,6 +146,6 @@ async def ws_relay(websocket: WebSocket) -> None:
while router.connected:
await asyncio.sleep(10)
- router.stop()
+ await router.stop()
all_routers.remove(router)
From 73e520f7a954df6efd690e72a2e1f69a8246bb13 Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Wed, 1 Nov 2023 13:35:47 +0200
Subject: [PATCH 30/30] chore: play commit
---
templates/nostrclient/index.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/templates/nostrclient/index.html b/templates/nostrclient/index.html
index 9e0eb31..db0f98e 100644
--- a/templates/nostrclient/index.html
+++ b/templates/nostrclient/index.html
@@ -21,7 +21,7 @@
color="primary"
class="float-right"
type="submit"
- label="Add Relay"
+ label="Add Relay X"
>