feat: add NIP-52 Nostr publish + sync of calendar events
Approved events are mirrored to Nostr as NIP-52 calendar events (kind
31922) signed by the wallet owner's pubkey, and incoming kind 31922/31923
events from subscribed relays are synced into the local DB so events
created on other LNbits instances or Nostr clients show up locally.
- m009 stores nostr_event_id + nostr_event_created_at on each event
(used for replaceable updates and NIP-09 deletes); m011 adds location
+ JSON-encoded categories list (NIP-52 location/`t` tags).
- models: Event/PublicEvent/CreateEvent gain location, categories,
nostr_event_id, nostr_event_created_at; parse_categories validator
decodes the JSON column on read.
- nostr/{event,nostr_client}.py: Schnorr signing, websocket relay client,
and a NostrEvent model (publish-only and subscribe variants).
- nostr_publisher.py: build/sign NIP-52 kind 31922 events and NIP-09
delete events; publish via the relay client.
- nostr_sync.py: subscribe to kinds 31922/31923, dedupe by nostr_event_id
/ d-tag, upsert Events; auto-approves discovered Nostr events since
they're already public.
- nostr_hooks.py: thin bridge that views_api handlers call to publish
or delete a NIP-52 event for a given local event. Lives in its own
module to keep `from . import nostr_client` out of the view layer
and avoid the views_api -> publisher import cycle.
- views_api: hooks publish_or_delete_nostr_event into create-on-approved,
update-when-already-published, cancel (delete), delete (delete), and
approve (publish).
- __init__.py: 3-task lifespan — wait_for_paid_invoices (upstream),
NostrClient bootstrap, and the NIP-52 sync loop. Module-level
nostr_client global is set by the bootstrap and read dynamically by
publish_or_delete_nostr_event so the import order works regardless of
whether nostrclient is up at startup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c7e95c5452
commit
6aa280680e
9 changed files with 575 additions and 5 deletions
47
__init__.py
47
__init__.py
|
|
@ -22,6 +22,11 @@ events_static_files = [
|
|||
|
||||
scheduled_tasks: list[asyncio.Task] = []
|
||||
|
||||
# Module-level NostrClient — None when nostrclient is unavailable. Set by the
|
||||
# bootstrap task in events_start() and read via dynamic attribute lookup
|
||||
# from nostr_hooks.publish_or_delete_nostr_event.
|
||||
nostr_client = None
|
||||
|
||||
|
||||
def events_stop():
|
||||
for task in scheduled_tasks:
|
||||
|
|
@ -30,12 +35,50 @@ 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
|
||||
|
||||
task = create_permanent_unique_task("ext_events", wait_for_paid_invoices)
|
||||
scheduled_tasks.append(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 sync")
|
||||
await nostr_client.run_forever()
|
||||
except Exception as exc:
|
||||
logger.warning(f"[EVENTS] NostrClient failed to start: {exc}")
|
||||
logger.info("[EVENTS] Events will work without Nostr sync")
|
||||
|
||||
task2 = create_permanent_unique_task("ext_events_nostr", _start_nostr_client)
|
||||
scheduled_tasks.append(task2)
|
||||
|
||||
async def _sync_nostr_events():
|
||||
global nostr_client
|
||||
await asyncio.sleep(15) # Wait for NostrClient to connect
|
||||
if not nostr_client:
|
||||
logger.info("[EVENTS] No NostrClient, skipping Nostr sync")
|
||||
return
|
||||
try:
|
||||
from .nostr_sync import wait_for_nostr_events
|
||||
|
||||
await wait_for_nostr_events(nostr_client)
|
||||
except Exception as exc:
|
||||
logger.error(f"[EVENTS] Nostr sync task failed: {exc}")
|
||||
|
||||
task3 = create_permanent_unique_task(
|
||||
"ext_events_nostr_sync", _sync_nostr_events
|
||||
)
|
||||
scheduled_tasks.append(task3)
|
||||
|
||||
|
||||
__all__ = ["db", "events_ext", "events_start", "events_static_files", "events_stop"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue