Stabilize (#24)

* refactor: clean-up

* refactor: extra logs plus try-catch

* refactor: do not use bare `except`

* refactor: clean-up redundant fields

* chore: pass code checks

* chore: code format

* refactor: code clean-up

* fix: refactoring stuff

* refactor: remove un-used file

* chore: code clean-up

* chore: code clean-up

* chore: code-format fix

* refactor: remove nostr.client wrapper

* refactor: code clean-up

* chore: code format

* refactor: remove `RelayList` class

* refactor: extract smaller methods with try-catch

* fix: better exception handling

* fix: remove redundant filters

* fix: simplify event

* chore: code format

* fix: code check

* fix: code check

* fix: simplify `REQ`

* fix: more clean-ups

* refactor: use simpler method

* refactor: re-order and rename

* fix: stop logic

* fix: subscription close before disconnect

* chore: play commit
This commit is contained in:
Vlad Stan 2023-11-01 17:46:42 +02:00 committed by GitHub
parent ab185bd2c4
commit 16ae9d15a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 522 additions and 717 deletions

View file

@ -2,43 +2,23 @@ import asyncio
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
self.error_counter: int = 0
self.error_threshold: int = 100
self.error_list: List[str] = []
@ -47,12 +27,10 @@ class Relay:
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 @@ class Relay:
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,9 @@ class Relay:
def publish(self, message: str):
self.queue.put(message)
def publish_subscriptions(self):
for _, subscription in self.subscriptions.items():
s = subscription.to_json_object()
json_str = json.dumps(["REQ", s["id"], s["filters"][0]])
def publish_subscriptions(self, subscriptions: List[Subscription] = []):
for s in subscriptions:
json_str = json.dumps(["REQ", s.id] + s.filters)
self.publish(json_str)
async def queue_worker(self):
@ -103,55 +75,44 @@ class Relay:
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)
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)
try:
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()
],
}
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)[: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."
+ f" 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 +120,7 @@ class Relay:
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())
self.error_counter += 1
self.error_list = [message] + self.error_list
self.last_error_date = int(time.time())