Wire relay publish + inbound subscription in nostr/service.py #2
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The Nostr app-event layer is sketched but not live.
nostr/service.py:_sign_and_publish()signs/encrypts correctly (viaresolve_signer) but the relay publish isTODO(relay)— events never reach a relay.subscribe_inbound()just logs; no relay connection, no dispatch.Work
nostrclientextension's relay manager if installed;nostr_transportrelay pool (lnbits #4);websocketsfan-out likelnurlp/tasks.py:send_to_relay.settings.relaysin_sign_and_publish; persist returned event ids (room.listing_event_id,booking.reservation_event_id).subscribe_inbound: subscribe on configured relays, NIP-44-decrypt via the operator signer, dispatch:kind:22000availability query → compute →respond_availability(kind:22001)api_request_bookingchatelet_start()(currently commented out).Acceptance
Publishing a room emits a real
kind:30402to relays; a guest availability query overkind:22000gets akind:22001reply; a booking request over a giftwrap DM creates aheldbooking.Reviewed
nostrclient— correcting the "reuse the relay manager" optionRead
nostrclient(router.py,nostr/, its CLAUDE.md). It is an always-on relay multiplexer, not an importable publish API. Architecture:NostrClientsingleton (router.py:14) owns aRelayManager; relay connections run on threads (RelayManager.open_connections()), client comms on asyncio, bridged via an executor intasks.py. Thenostr/impl is the old python-nostr lineage (mypy-excluded), not async-native./nostrclient/api/v1/{ws_id}(private, encrypted id) and/nostrclient/api/v1/relay(public, if enabled). It fans out to configured relays and aggregates/dedupes.Implication for this issue: "reuse the nostrclient relay manager in-process" (option 1 as originally written) is the wrong shape — it would couple Chatelet to nostrclient's threaded internals and require nostrclient always installed. The clean options are:
…/api/v1/relayWS endpoint. Loose coupling: Chatelet stays relay-agnostic (can point at any relay), nostrclient's internals stay encapsulated. This is the protocol-over-loopback sweet spot from the workspace protocol-vs-IPC rule.nostr_transportrelay pool (lnbits #4) — the same pool the RPC dispatcher in #1 uses. Preferred if we're doing #1 anyway, so app-event publish and RPC share one relay pool.websocketsfan-out (likelnurlp/tasks.py:send_to_relay) — simplest, no dependency, but we own reconnect/backoff.Recommendation: since #1 targets the core
nostr_transport, lean on option 2 (core pool) so publish + RPC share one relay connection, and keep the extension usable without nostrclient installed. Fall back to option 1 only if the core pool isn't exposed for arbitrary event publish.Updating the checklist here rather than the body; will finalize the transport choice when #1/#2 are picked up together.
padreug referenced this issue2026-07-18 23:30:34 +00:00
Blocked on a transport decision — core pool can't do this
While building #1 I read the core transport internals (
nostr_transport/relay_pool.py). Confirmed: the pool is a kind-21000 RPC bus only —NostrTransportPool._publish_eventhard-codeskind: 21000, and the relay subscription filterskinds: [21000]. There is no general publish-arbitrary-event API. So the "use the core pool for app-event publishing" recommendation from my earlier comment does not work — the core pool serves #1 (RPC) but not #2 (public NIP-99/78/52 discovery events).This reframes #2 substantially
With #1 merged, the entire functional booking flow runs over the kind-21000 RPC (
chatelet_room_list/_availability/_booking_request/_booking_get). So #2's public events are not needed for the app to work over Nostr — they're purely for open, third-party discoverability: letting someone browsing on a generic Nostr client (Amethyst, a NIP-99 market client) find and view a listing without speaking our RPC.Two consequences:
kind:22000/22001(availability query/response) is now redundant — availability is anAUTH_NONERPC. I'd drop the custom kinds and update ADR-0001 (and close #6, which registers them) unless we want non-RPC clients to query availability. Recommend dropping.kind:30402listing +kind:31923availability calendar (+ optionalkind:30078reservation) to public relays. That needs a relay path the core pool doesn't give us.Options for the public-event path (if we want open discovery at all)
websocketsfan-out (likelnurlp/tasks.py:send_zap), signing viaresolve_signer. Simple, no dependency; we own reconnect/backoff for a handful of publishes.…/api/v1/relayWS endpoint. Reuses its relay management, but adds a hard dependency on nostrclient being installed.Recommendation: A for now (the flow is complete over RPC after #1), and if/when open discovery is wanted, B — it keeps Chatelet self-contained and matches an existing pattern in the codebase. Reserve C only if we later want heavy relay fan-out managed centrally.
@padreug — how do you want to play #2: defer (A), or build the listing/calendar publish now (B)? And OK to drop
kind:22000/22001+ close #6?Resolved: the path is
nostrclient(in-process), and #2 is NOT deferredResearched the codebase (three passes). This supersedes my earlier "recommend A/defer, else B/direct-websockets" — B was wrong; the house pattern is nostrclient.
Findings
nostrmarketalready depends onnostrclient— connects to its loopback WS relay endpoint (ws://localhost:{port}/nostrclient/api/v1/{encrypted "relay"}) and speaks rawEVENT/REQ/CLOSEframes. nostrclient owns all real relay connections (its python-nostr-lineageRelayManager).spirekeeper(ours) uses nostrclient IN-PROCESS — imports the singleton and publishes arbitrary kinds directly: Subscribe =relay_manager.add_subscription(id, filters)+ pollNostrRouter.received_subscription_events[id]. nostrclient does not sign — caller pre-signs (we already do, viaresolve_signer).publish_messageis sync + thread-safe, callable straight from asyncio.restaurant/tasksbundle their own relay sockets — the anti-pattern to avoid (duplicated reconnect logic).Decision
Implement #2 via nostrclient, in-process, following the
spirekeepertemplate — chatelet already copies spirekeeper's signer pattern, so this is consistent and reuses its reconnect/backoff/dedup instead of reinventing it. Concretely wires the existingnostr/service.pystubs:_sign_and_publish→resolve_signersign, thennostr_client.relay_manager.publish_message(...)behind a lazy-import guard (no-op if nostrclient absent), publishingkind:30402listing,kind:31923calendar,kind:30078reservation.subscribe_inbound→add_subscriptionforkind:22000(availability query) + booking-request DMs, poll + dispatch to the sameservices.pyflow.#2 stays (not deferred), and #6 stays open
Per the now-recorded client-agnostic doctrine, public discovery events + the
kind:22000/22001availability proposal are the destination, not optional. So: do NOT drop the custom kinds, and #6 (register the allocation) remains valid — we're keeping them. Sequencing: land after the functional flow (#4) is solid, since the booking flow already works over RPC (#1).Adds a soft runtime dependency on the
nostrclientextension (same as nostrmarket) — worth noting inconfig.json/README so operators enable it.