diff --git a/__init__.py b/__init__.py index 974c20c..14c1590 100644 --- a/__init__.py +++ b/__init__.py @@ -21,9 +21,6 @@ events_static_files = [ scheduled_tasks: list[asyncio.Task] = [] -# Module-level NostrClient — None when nostrclient is unavailable -nostr_client = None - def events_stop(): for task in scheduled_tasks: @@ -32,32 +29,12 @@ def events_stop(): except Exception as ex: logger.warning(ex) - global nostr_client - if nostr_client: - asyncio.get_event_loop().create_task(nostr_client.stop()) - def events_start(): from lnbits.tasks import create_permanent_unique_task - task1 = create_permanent_unique_task("ext_events", wait_for_paid_invoices) - scheduled_tasks.append(task1) - - async def _start_nostr_client(): - global nostr_client - await asyncio.sleep(10) # Wait for nostrclient to be ready - try: - from .nostr.nostr_client import NostrClient - - nostr_client = NostrClient() - logger.info("[EVENTS] Starting NostrClient for NIP-52 publishing") - await nostr_client.run_forever() - except Exception as e: - logger.warning(f"[EVENTS] NostrClient failed to start: {e}") - logger.info("[EVENTS] Events will work without Nostr publishing") - - task2 = create_permanent_unique_task("ext_events_nostr", _start_nostr_client) - scheduled_tasks.append(task2) + task = create_permanent_unique_task("ext_events", wait_for_paid_invoices) + scheduled_tasks.append(task) __all__ = ["db", "events_ext", "events_start", "events_static_files", "events_stop"] diff --git a/migrations.py b/migrations.py index 22fe495..357c3ed 100644 --- a/migrations.py +++ b/migrations.py @@ -202,15 +202,3 @@ async def m008_add_event_status(db): await db.execute( "ALTER TABLE events.events ADD COLUMN status TEXT NOT NULL DEFAULT 'approved';" ) - - -async def m009_add_nostr_columns(db): - """ - Add columns to track published NIP-52 Nostr calendar events. - """ - await db.execute( - "ALTER TABLE events.events ADD COLUMN nostr_event_id TEXT;" - ) - await db.execute( - "ALTER TABLE events.events ADD COLUMN nostr_event_created_at INTEGER;" - ) diff --git a/models.py b/models.py index 56f6dff..0b92e15 100644 --- a/models.py +++ b/models.py @@ -80,8 +80,6 @@ class Event(BaseModel): banner: str | None = None extra: EventExtra = Field(default_factory=EventExtra) status: str = "approved" # proposed, approved, rejected - nostr_event_id: str | None = None - nostr_event_created_at: int | None = None class TicketExtra(BaseModel): diff --git a/nostr/__init__.py b/nostr/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/nostr/event.py b/nostr/event.py deleted file mode 100644 index 7da8288..0000000 --- a/nostr/event.py +++ /dev/null @@ -1,27 +0,0 @@ -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() diff --git a/nostr/nostr_client.py b/nostr/nostr_client.py deleted file mode 100644 index 09f57d4..0000000 --- a/nostr/nostr_client.py +++ /dev/null @@ -1,100 +0,0 @@ -""" -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 diff --git a/nostr_publisher.py b/nostr_publisher.py deleted file mode 100644 index fe0431b..0000000 --- a/nostr_publisher.py +++ /dev/null @@ -1,115 +0,0 @@ -""" -NIP-52 calendar event publishing for the events extension. - -Builds kind 31922 (date-based) calendar events from the Event model, -signs them with the event creator's Account keypair, and publishes -via the NostrClient to nostrclient relays. - -Reference: https://github.com/nostr-protocol/nips/blob/master/52.md -""" - -import time -from typing import Optional - -import coincurve -from loguru import logger - -from .models import Event -from .nostr.event import NostrEvent - - -def build_nip52_event(event: Event, pubkey: str) -> NostrEvent: - """ - Convert an Event model to a NIP-52 kind 31922 (date-based) calendar event. - - Tags: - d - event.id (addressable identifier) - title - event.name - start - event.event_start_date (ISO date string) - end - event.event_end_date (optional) - image - event.banner (optional) - Content: event.info (description) - """ - tags = [ - ["d", event.id], - ["title", event.name], - ["start", event.event_start_date], - ] - - if event.event_end_date: - tags.append(["end", event.event_end_date]) - if event.banner: - tags.append(["image", event.banner]) - - nostr_event = NostrEvent( - pubkey=pubkey, - created_at=int(time.time()), - kind=31922, - tags=tags, - content=event.info, - ) - nostr_event.id = nostr_event.event_id - return nostr_event - - -def build_nip52_delete_event(event: Event, pubkey: str) -> NostrEvent: - """ - Build a kind 5 delete event for a published NIP-52 calendar event. - - Uses an 'a' tag to reference the parameterized replaceable event - (kind 31922) per NIP-09. - """ - nostr_event = NostrEvent( - pubkey=pubkey, - created_at=int(time.time()), - kind=5, - tags=[ - ["a", f"31922:{pubkey}:{event.id}"], - ], - content="Event canceled", - ) - nostr_event.id = nostr_event.event_id - return nostr_event - - -def sign_nostr_event(nostr_event: NostrEvent, private_key_hex: str) -> None: - """Sign a NostrEvent in-place using Schnorr signature.""" - privkey = coincurve.PrivateKey(bytes.fromhex(private_key_hex)) - sig = privkey.sign_schnorr(bytes.fromhex(nostr_event.id)) - nostr_event.sig = sig.hex() - - -async def publish_event_to_nostr( - nostr_client, - event: Event, - account_pubkey: str, - account_prvkey: str, - delete: bool = False, -) -> Optional[NostrEvent]: - """ - Build, sign, and publish a NIP-52 calendar event (or delete event). - - Returns the published NostrEvent for metadata storage, or None on failure. - """ - if not nostr_client: - logger.debug("[EVENTS] No NostrClient available, skipping publish") - return None - - try: - if delete: - nostr_event = build_nip52_delete_event(event, account_pubkey) - else: - nostr_event = build_nip52_event(event, account_pubkey) - - sign_nostr_event(nostr_event, account_prvkey) - await nostr_client.publish_nostr_event(nostr_event) - - logger.info( - f"[EVENTS] Published NIP-52 {'delete' if delete else 'calendar'} " - f"event: {nostr_event.id[:16]}... (kind {nostr_event.kind})" - ) - return nostr_event - - except Exception as e: - logger.warning(f"[EVENTS] Failed to publish to Nostr: {e}") - return None diff --git a/views_api.py b/views_api.py index f69d3c0..73c91c9 100644 --- a/views_api.py +++ b/views_api.py @@ -35,38 +35,11 @@ from .crud import ( update_ticket, ) from .models import CreateEvent, CreateTicket, Ticket -from .nostr_publisher import publish_event_to_nostr from .services import refund_tickets, set_ticket_paid events_api_router = APIRouter() -async def _publish_or_delete_nostr_event(event, delete=False): - """Publish (or delete) a NIP-52 calendar event using the creator's keypair.""" - try: - from lnbits.core.crud.wallets import get_wallet - from lnbits.core.crud.users import get_account - - from . import nostr_client - - wallet_obj = await get_wallet(event.wallet) - if not wallet_obj: - return - account = await get_account(wallet_obj.user) - if not account or not account.pubkey or not account.prvkey: - return - - nostr_event = await publish_event_to_nostr( - nostr_client, event, account.pubkey, account.prvkey, delete=delete - ) - if nostr_event and not delete: - event.nostr_event_id = nostr_event.id - event.nostr_event_created_at = nostr_event.created_at - await update_event(event) - except Exception as e: - logger.warning(f"[EVENTS] Nostr publish failed: {e}") - - @events_api_router.get("/api/v1/events") async def api_events( all_wallets: bool = Query(False), @@ -123,10 +96,6 @@ async def api_event_create( for k, v in data.dict().items(): setattr(event, k, v) event = await update_event(event) - - # Republish to Nostr if event is approved (kind 31922 is replaceable) - if event.status == "approved" and event.nostr_event_id: - await _publish_or_delete_nostr_event(event) else: if not data.wallet: data.wallet = wallet.wallet.id @@ -142,10 +111,6 @@ async def api_event_create( data.status = "proposed" event = await create_event(data) - # Publish to Nostr if auto-approved (admin-created) - if event.status == "approved": - await _publish_or_delete_nostr_event(event) - return event.dict() @@ -166,10 +131,6 @@ async def api_event_cancel( event = await update_event(event) await refund_tickets(event.id) - # Delete NIP-52 event from Nostr if it was published - if event.nostr_event_id: - await _publish_or_delete_nostr_event(event, delete=True) - return event.dict() @@ -186,10 +147,6 @@ async def api_form_delete( if event.wallet != wallet.wallet.id: raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your event.") - # Delete NIP-52 event from Nostr if it was published - if event.nostr_event_id: - await _publish_or_delete_nostr_event(event, delete=True) - await delete_event(event_id) await delete_event_tickets(event_id) return "", HTTPStatus.NO_CONTENT @@ -240,10 +197,6 @@ async def api_event_approve( ) event.status = "approved" event = await update_event(event) - - # Publish NIP-52 calendar event to Nostr - await _publish_or_delete_nostr_event(event) - return event.dict()