Inventory reaches clients only through the republished calendar event, and until now a publish that failed or was skipped left no durable trace — only a log line, if that. Twice the drift was caught by a human reading a wrong number on a public page (#35 on aio-demo, #51 on cfaun, where an event's relay copy sat 14 days behind the DB). Adds `events.nostr_publish_pending`, set before every attempt and cleared only on a confirmed success. Ordering it that way is what makes "the attempt was never made" — no signer resolved, no NostrClient, the process died mid-flight — as discoverable as "the attempt raised". Both shapes have now been observed in production; only the second one was ever visible. `set_ticket_paid` raises the flag inside its own update so the counters and "the relay doesn't know about them yet" commit atomically, and the sale path pays no extra write. `publish_or_delete_nostr_event` now returns a bool so callers can branch. The flag, not the return value, is the durable record — the existing call sites stay correct ignoring it. Publish failures move from WARNING to ERROR: the published ticket count has stopped tracking reality, which is not routine journal noise. Refs #35
81 lines
3.6 KiB
Python
81 lines
3.6 KiB
Python
"""Helpers that bridge event-mutation handlers to the Nostr publisher.
|
|
|
|
Lives in its own module so both `events_api_router` and any future router
|
|
can call it without importing through `views_api`, which would create an
|
|
import cycle (views_api -> nostr_hooks -> nostr_publisher -> models).
|
|
"""
|
|
|
|
from loguru import logger
|
|
|
|
from .crud import update_event
|
|
from .models import Event
|
|
from .nostr_publisher import publish_event_to_nostr
|
|
|
|
|
|
async def publish_or_delete_nostr_event(event: Event, *, delete: bool = False) -> bool:
|
|
"""Publish or delete the NIP-52 calendar event for `event`.
|
|
|
|
Resolves a `NostrSigner` for the wallet owner — backend-agnostic
|
|
(LocalSigner / RemoteBunkerSigner / ClientSideOnlySigner). The
|
|
signer abstraction handles the actual key material; this hook
|
|
only needs `signer.pubkey` for event construction and
|
|
`await signer.sign_event(...)` for signing. Failures are logged
|
|
and swallowed so a Nostr outage doesn't break the HTTP flow that
|
|
triggered the publish.
|
|
|
|
Returns True when the event was signed and handed to the client,
|
|
False on any skip or failure. Callers are free to ignore it — the
|
|
`nostr_publish_pending` flag is the durable record, and the sweep
|
|
retries from that rather than from a return value.
|
|
"""
|
|
# Mark before attempting, clear only on confirmed success. Doing it
|
|
# in this order is what makes "the attempt was never made" — no
|
|
# signer, no NostrClient, process died mid-flight — as visible as
|
|
# "the attempt raised". Cheap guard so a re-publish of an already
|
|
# pending row doesn't write twice; `set_ticket_paid` sets the flag
|
|
# inside its own update so the sale path adds no extra write.
|
|
if not event.nostr_publish_pending:
|
|
event.nostr_publish_pending = True
|
|
await update_event(event)
|
|
|
|
try:
|
|
from lnbits.core.signers import resolve_for_wallet
|
|
|
|
from . import nostr_client
|
|
|
|
signer = await resolve_for_wallet(event.wallet)
|
|
if signer is None:
|
|
# Wallet missing, account missing, unclassified row, or
|
|
# ClientSideOnlySigner account (server can't sign for them).
|
|
# Soft-fail: the HTTP / payment flow that triggered this must
|
|
# not break. The user can still publish kind-31922/31923
|
|
# events client-side once we have that path.
|
|
#
|
|
# Logged at WARNING, not debug: skipping the publish means the
|
|
# relay keeps serving whatever inventory it last saw, so the
|
|
# public ticket count silently stops tracking the DB. That has
|
|
# twice been discovered only by a human noticing a wrong number
|
|
# on a public page (aiolabs/events#35, #51).
|
|
logger.warning(
|
|
f"[EVENTS] No signer for wallet {event.wallet}, skipping "
|
|
f"NIP-52 {'delete' if delete else 'publish'} for event {event.id}"
|
|
)
|
|
return False
|
|
|
|
nostr_event = await publish_event_to_nostr(
|
|
nostr_client, event, signer, delete=delete
|
|
)
|
|
if nostr_event is None:
|
|
return False
|
|
|
|
event.nostr_publish_pending = False
|
|
if not delete:
|
|
event.nostr_event_id = nostr_event.id
|
|
event.nostr_event_created_at = nostr_event.created_at
|
|
await update_event(event)
|
|
return True
|
|
except Exception as exc:
|
|
# ERROR, not warning: the row stays flagged and its published
|
|
# counts stay behind until the sweep or a later edit succeeds.
|
|
logger.error(f"[EVENTS] Nostr publish failed for event {event.id}: {exc}")
|
|
return False
|