Skip to content

feat: NIP-52 Nostr publishing, approval workflow, and event proposals#46

Draft
PatMulligan wants to merge 5 commits intolnbits:mainfrom
PatMulligan:nostr-events
Draft

feat: NIP-52 Nostr publishing, approval workflow, and event proposals#46
PatMulligan wants to merge 5 commits intolnbits:mainfrom
PatMulligan:nostr-events

Conversation

@PatMulligan
Copy link
Copy Markdown
Contributor

@PatMulligan PatMulligan commented Apr 28, 2026

Summary

This PR adds three main features to the events extension, plus several smaller improvements that emerged along the way:

1. Event proposal & approval workflow

  • New status field on Event (proposed / approved / rejected)
  • *Non-admin users' events default to proposed and require LNbits admin approval
  • Admin-toggleable auto_approve setting (e.g. for trusted/demo deployments)
  • Admin UI: pending approvals card, "all users' events" view, settings toggle

2. NIP-52 calendar event publishing

  • Publishes kind 31922 calendar events to Nostr relays via the nostrclient extension on event approval/creation
  • Map fields: title, start, end, image, location, t (categories), d (event id)
  • Republishes on update (replaceable events)
  • Publishes kind 5 delete on cancel/delete
  • Events signed with the creator's LNbits Account nostr keypair

3. Bidirectional Nostr sync

  • *Subscribes to kind 31922/31923 from relays and upserts into the local DB
  • *Enables federated event discovery

Misc improvements

  • Make event_end_date, closing_date, info optional with sensible defaults
  • New optional fields: location, categories
  • Admin-only /events/all and /events/pending endpoints (uses check_admin)
  • Consolidated POST /events and POST /events/propose into a single endpoint

Dependencies

  • nostrclient extension must be installed for Nostr sync; if not, the extension still functions normally without Nostr publishing.
  • The Account keypair signing assumes accounts have a prvkey field (we have this in our LNbits fork). (The Nostr publishing in nostr_hooks.py reads account.prvkey to sign NIP-52 calendar events with the wallet owner's keypair.)

* Footnote:

There is a bit of a misleading feature included here... In one respect I have an admin approval feature, however there is nothing stopping someone from just publishing a NIP-52 to the relay. In the context of a community, running this as a service and/or reducing spam, I wonder if I might also sign published events as the admin to indicate "approved" events versus any old nip-52.

Other note

Currently there's no nostr component of purchasing tickets like you can do with nostrmarket, but it's planned

@dni
Copy link
Copy Markdown
Member

dni commented May 5, 2026

@PatMulligan hey! sorry that your PR got totally screwed, but we had this big refactor changing this extension to be loaded dynamically which was done before your pr and took a bit to finish. also the register page, changed, so it now only stores scanned tickets locally and does not fetch them. #43

@PatMulligan
Copy link
Copy Markdown
Contributor Author

@PatMulligan hey! sorry that your PR got totally screwed, but we had this big refactor changing this extension to be loaded dynamically which was done before your pr and took a bit to finish. also the register page, changed, so it now only stores scanned tickets locally and does not fetch them. #43

hey no problem at all 😄 I will take a look at your guys work soon to check it out! 🦾

@arcbtc
Copy link
Copy Markdown
Member

arcbtc commented May 5, 2026

Thats very understanding of you @PatMulligan. Good luck. Let us know if we can help

PatMulligan and others added 5 commits May 5, 2026 20:08
Earlier downstream forks added some of these columns under different
migration names. A duplicate-column-tolerant ALTER ADD COLUMN keeps
the migration log monotonic for both fresh installs and forks that
upgrade in-place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add an alternative ticket identifier scheme: instead of (name, email),
external integrations can issue tickets bound to an LNbits user_id.

- m007 adds the user_id column on events.ticket
- CreateTicket validator enforces exactly one identifier scheme per ticket
- Ticket / PublicTicket: name, email, user_id all Optional
- _parse_ticket_row reverses the empty-string sentinel used to keep the
  NOT NULL name/email columns satisfied when user_id is the identifier
- POST /tickets/{event_id} dispatches to _create_user_id_ticket vs
  _create_named_ticket based on the supplied identifier
- New GET /tickets/user/{user_id} returns tickets for a given user

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Non-admin event submissions now land in a "proposed" queue that LNbits
admins review before the event becomes ticketable and publicly listed.

- m008 adds events.events.status (proposed/approved/rejected); m010 seeds
  an events.settings singleton row with the auto_approve toggle.
- Models: Event/CreateEvent.status, EventsSettings, optional date fields
  with sensible defaults (closing_date defaults to event_end_date which
  defaults to event_start_date), PublicEvent.status surfaces the workflow
  state on the public endpoint.
- crud: get_all/public/pending_events for the admin views; get/update_settings
  for the auto_approve toggle; create_event auto-fills missing date defaults.
- views_api:
  * POST /api/v1/events accepts wallet invoice keys so anyone can submit;
    handler stamps status="proposed" for non-admins when auto_approve is off
  * /public, /all, /pending, /settings (GET+PUT), /{id}/{approve,reject},
    /{id}/tickets endpoints; literal-prefix routes declared before /{event_id}
    so FastAPI matches them correctly
  * Public GET /{event_id} bypasses sold-out / closing-window gates for
    proposed/rejected events and returns the trimmed PublicEvent so the SFC
    can render a "pending approval" banner
  * POST /tickets/{event_id} rejects when event.status != "approved"
- Frontend: index.vue gains an admin Settings card, Pending Approvals list,
  status badge column and approve/reject row actions, plus an All Users'
  Events admin table; index.js gains the data + methods + an isAdmin probe
  via GET /events/all; display.vue shows pending/rejected banners and
  hides the Buy Ticket form unless status === "approved".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
- black/prettier reformatting across new aio code
- type annotations on db.fetchone/fetchall callsites in crud.py
- explicit dict[str, list[str]] for tag_lists in nostr_sync.py
- type:ignore[attr-defined] on Account.prvkey access — the field is
  added by the aio-fork lnbits.core.models.Account; upstream lnbits
  does not yet have it, so consumers without the fork must add a
  prvkey column to accounts before the Nostr publisher can sign.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@PatMulligan
Copy link
Copy Markdown
Contributor Author

it has been le rebased 👌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants