feat: add publish-only NostrClient and NostrEvent model
Stripped-down Nostr client that connects to nostrclient's internal WebSocket for publishing NIP-52 calendar events. No subscription capabilities — publish queue only. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1ad99aa3d6
commit
f965cf07c9
3 changed files with 127 additions and 0 deletions
0
nostr/__init__.py
Normal file
0
nostr/__init__.py
Normal file
27
nostr/event.py
Normal file
27
nostr/event.py
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class NostrEvent(BaseModel):
|
||||||
|
id: str = ""
|
||||||
|
pubkey: str
|
||||||
|
created_at: int
|
||||||
|
kind: int
|
||||||
|
tags: List[List[str]] = []
|
||||||
|
content: str = ""
|
||||||
|
sig: Optional[str] = None
|
||||||
|
|
||||||
|
def serialize(self) -> List:
|
||||||
|
return [0, self.pubkey, self.created_at, self.kind, self.tags, self.content]
|
||||||
|
|
||||||
|
def serialize_json(self) -> str:
|
||||||
|
e = self.serialize()
|
||||||
|
return json.dumps(e, separators=(",", ":"), ensure_ascii=False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def event_id(self) -> str:
|
||||||
|
data = self.serialize_json()
|
||||||
|
return hashlib.sha256(data.encode()).hexdigest()
|
||||||
100
nostr/nostr_client.py
Normal file
100
nostr/nostr_client.py
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
"""
|
||||||
|
Publish-only Nostr client for the events extension.
|
||||||
|
|
||||||
|
Connects to the nostrclient extension's internal WebSocket to publish
|
||||||
|
NIP-52 calendar events. No subscription/receive capabilities — this
|
||||||
|
is a stripped-down version of nostrmarket's NostrClient.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from asyncio import Queue
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
from websocket import WebSocketApp
|
||||||
|
|
||||||
|
from lnbits.helpers import encrypt_internal_message
|
||||||
|
from lnbits.settings import settings
|
||||||
|
|
||||||
|
from .event import NostrEvent
|
||||||
|
|
||||||
|
|
||||||
|
class NostrClient:
|
||||||
|
def __init__(self):
|
||||||
|
self.send_req_queue: Queue = Queue()
|
||||||
|
self.ws: Optional[WebSocketApp] = None
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_websocket_connected(self):
|
||||||
|
if not self.ws:
|
||||||
|
return False
|
||||||
|
return self.ws.keep_running
|
||||||
|
|
||||||
|
async def connect(self) -> WebSocketApp:
|
||||||
|
relay_endpoint = encrypt_internal_message("relay", urlsafe=True)
|
||||||
|
ws_url = (
|
||||||
|
f"ws://localhost:{settings.port}"
|
||||||
|
f"/nostrclient/api/v1/{relay_endpoint}"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("[EVENTS] Connecting to nostrclient WebSocket...")
|
||||||
|
|
||||||
|
def on_open(_):
|
||||||
|
logger.info("[EVENTS] Connected to nostrclient WebSocket")
|
||||||
|
|
||||||
|
def on_message(_, message):
|
||||||
|
# Log relay responses (OK, NOTICE) but don't process
|
||||||
|
logger.debug(f"[EVENTS] Relay response: {message[:200]}")
|
||||||
|
|
||||||
|
def on_error(_, error):
|
||||||
|
logger.warning(f"[EVENTS] WebSocket error: {error}")
|
||||||
|
|
||||||
|
def on_close(_, status_code, message):
|
||||||
|
logger.warning(
|
||||||
|
f"[EVENTS] WebSocket closed: {status_code} {message}"
|
||||||
|
)
|
||||||
|
|
||||||
|
ws = WebSocketApp(
|
||||||
|
ws_url,
|
||||||
|
on_message=on_message,
|
||||||
|
on_open=on_open,
|
||||||
|
on_close=on_close,
|
||||||
|
on_error=on_error,
|
||||||
|
)
|
||||||
|
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
wst = Thread(target=ws.run_forever)
|
||||||
|
wst.daemon = True
|
||||||
|
wst.start()
|
||||||
|
|
||||||
|
return ws
|
||||||
|
|
||||||
|
async def run_forever(self):
|
||||||
|
self.running = True
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
if not self.is_websocket_connected:
|
||||||
|
self.ws = await self.connect()
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
req = await self.send_req_queue.get()
|
||||||
|
assert self.ws
|
||||||
|
self.ws.send(json.dumps(req))
|
||||||
|
except Exception as ex:
|
||||||
|
logger.warning(f"[EVENTS] NostrClient error: {ex}")
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
|
||||||
|
async def publish_nostr_event(self, e: NostrEvent):
|
||||||
|
await self.send_req_queue.put(["EVENT", e.dict()])
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
self.running = False
|
||||||
|
if self.ws:
|
||||||
|
try:
|
||||||
|
self.ws.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.ws = None
|
||||||
Loading…
Add table
Add a link
Reference in a new issue