feat: publish ticket counts in NIP-52 tags + republish on sale
Some checks failed
lint.yml / feat: publish ticket counts in NIP-52 tags + republish on sale (pull_request) Failing after 0s
Some checks failed
lint.yml / feat: publish ticket counts in NIP-52 tags + republish on sale (pull_request) Failing after 0s
Inventory sync over Nostr, mirroring how nostrmarket republishes kind 30018 product events when stock changes. Connected webapp / other-client subscriptions pick up the new state via their existing relay subscription — no REST polling needed. build_nip52_event grows four AIO custom tags on every published kind 31922/31923 event: - tickets_available — current remaining (omitted when amount_tickets is 0, the schema's "unlimited" sentinel, so clients can tell the difference between unlimited and sold-out) - tickets_sold — running count, always emitted (clients derive original_capacity = available + sold for progress bars) - tickets_price — price_per_ticket (0 means free) - tickets_currency — the currency string Tags are AIO additions outside the NIP-52 spec; spec-compliant clients MUST ignore unknown tags so this stays backwards-compatible. set_ticket_paid calls publish_or_delete_nostr_event after the counter update so the new state lands on relays. The whole sequence (counter update + republish) is wrapped in a per-event-id asyncio lock to address the existing # todo: lock and to ensure two paid invoices for the same event can't reorder the published state. Failures inside the Nostr publish are logged + swallowed by the existing wrapper, so a relay outage can never break the payment flow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
814581f307
commit
edf1493e0c
2 changed files with 56 additions and 11 deletions
|
|
@ -44,7 +44,18 @@ def build_nip52_event(event: Event, pubkey: str) -> NostrEvent:
|
|||
start - unix timestamp (31923) or YYYY-MM-DD (31922)
|
||||
end - same encoding (optional)
|
||||
image, location, t (categories) - optional
|
||||
tickets_available - current remaining capacity (omitted when unlimited)
|
||||
tickets_sold - running paid-count (always emitted; clients can
|
||||
derive original_capacity = available + sold)
|
||||
tickets_price - price_per_ticket (always emitted; 0 means free)
|
||||
tickets_currency - the currency string
|
||||
Content: event.info
|
||||
|
||||
The four ticket_* tags are AIO custom additions outside the NIP-52
|
||||
spec; spec-compliant clients ignore unknown tags so this stays
|
||||
backwards-compatible. They let connected clients render the
|
||||
"X tickets remaining" badge and the Buy CTA without an extra REST hop,
|
||||
and pick up live inventory updates via the same relay subscription.
|
||||
"""
|
||||
time_based = _has_time(event.event_start_date)
|
||||
kind = 31923 if time_based else 31922
|
||||
|
|
@ -81,6 +92,15 @@ def build_nip52_event(event: Event, pubkey: str) -> NostrEvent:
|
|||
for cat in event.categories or []:
|
||||
tags.append(["t", cat])
|
||||
|
||||
# `amount_tickets == 0` means unlimited capacity in this extension's
|
||||
# schema. Omitting the tag is how clients distinguish unlimited from
|
||||
# "0 left" (sold out).
|
||||
if event.amount_tickets > 0:
|
||||
tags.append(["tickets_available", str(event.amount_tickets)])
|
||||
tags.append(["tickets_sold", str(event.sold)])
|
||||
tags.append(["tickets_price", str(event.price_per_ticket)])
|
||||
tags.append(["tickets_currency", event.currency])
|
||||
|
||||
nostr_event = NostrEvent(
|
||||
pubkey=pubkey,
|
||||
created_at=int(time.time()),
|
||||
|
|
|
|||
25
services.py
25
services.py
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from asyncio.tasks import create_task
|
||||
|
||||
from lnbits.core.models.users import UserNotifications
|
||||
|
|
@ -21,6 +22,7 @@ from .crud import (
|
|||
update_ticket,
|
||||
)
|
||||
from .models import Event, Ticket
|
||||
from .nostr_hooks import publish_or_delete_nostr_event
|
||||
|
||||
DEFAULT_NOSTR_RELAYS = [
|
||||
"wss://relay.damus.io",
|
||||
|
|
@ -28,11 +30,27 @@ DEFAULT_NOSTR_RELAYS = [
|
|||
"wss://relay.nostr.band",
|
||||
]
|
||||
|
||||
# Per-event lock: serializes the counter-update + Nostr republish for a
|
||||
# single event_id so two paid invoices landing on the listener queue back-
|
||||
# to-back can't reorder the published state. Lazy-populated; entries are
|
||||
# left in memory for the lifetime of the process (cheap — one asyncio.Lock
|
||||
# object per event ever sold).
|
||||
_event_paid_locks: dict[str, asyncio.Lock] = {}
|
||||
|
||||
|
||||
def _event_paid_lock(event_id: str) -> asyncio.Lock:
|
||||
lock = _event_paid_locks.get(event_id)
|
||||
if lock is None:
|
||||
lock = asyncio.Lock()
|
||||
_event_paid_locks[event_id] = lock
|
||||
return lock
|
||||
|
||||
|
||||
async def set_ticket_paid(ticket: Ticket) -> Ticket:
|
||||
if ticket.paid:
|
||||
return ticket
|
||||
|
||||
async with _event_paid_lock(ticket.event):
|
||||
ticket.paid = True
|
||||
await update_ticket(ticket)
|
||||
|
||||
|
|
@ -42,6 +60,13 @@ async def set_ticket_paid(ticket: Ticket) -> Ticket:
|
|||
event.amount_tickets -= 1
|
||||
await update_event(event)
|
||||
|
||||
# Republish the NIP-52 calendar event so connected clients see
|
||||
# the new tickets_available / tickets_sold counters via their
|
||||
# existing relay subscription. Failures are logged + swallowed
|
||||
# inside publish_or_delete_nostr_event so a Nostr outage doesn't
|
||||
# break the payment flow.
|
||||
await publish_or_delete_nostr_event(event)
|
||||
|
||||
return ticket
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue