Chatelet is multi-tenant: any LNbits user can host rooms. What an operator decides for all their rooms now lives in chatelet.operator_settings, keyed by user id and created lazily (m003, which also indexes bookings by guest): check-in/out times, cancellation policy, and accept_fiat. Guests see it: the public room view (both doors) gains house_rules and payment_methods, and the kind:30402 listing carries payment_methods, checkin_time and checkout_time tags so a generic Nostr client can render the right pay buttons and rules without our RPC. The check-in DM reads the room owner's rules instead of the instance row. Card is offered only when the operator opted in, the room is fiat-priced, and LNbits core has a fiat provider for that user — resolved through settings.get_fiat_providers_for_user(owner), the one seam lnbits#67's per-user Stripe credentials will plug into; chatelet never sees creds. Operator endpoints: GET/PUT /api/v1/operator (admin key → wallet user) and RPC twins chatelet_operator_get/update (AUTH_WALLET); saving re-publishes the owner's active listings. Admin UI moves the house-rule inputs into a per-operator card with the card toggle and a provider hint. The old house-rule columns on settings stay for old rows but are no longer read. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
137 lines
4.8 KiB
Python
137 lines
4.8 KiB
Python
"""Builders that turn Chatelet rows into unsigned Nostr events.
|
|
|
|
These are pure functions: they return unsigned event dicts (kind, tags,
|
|
content) with no pubkey/id/sig. Signing + NIP-44 encryption happen in
|
|
service.py via lnbits.core.signers.resolve_signer, so no nsec is handled
|
|
here and the same code works with a LocalSigner today or a NIP-46 bunker
|
|
later. See docs/event-flow.md for who publishes what, when.
|
|
"""
|
|
|
|
import json
|
|
|
|
from ..models import AvailabilityResult, Booking, Room
|
|
from .kinds import (
|
|
KIND_AVAILABILITY_RESPONSE,
|
|
KIND_CALENDAR_EVENT,
|
|
KIND_LISTING,
|
|
KIND_RESERVATION,
|
|
)
|
|
|
|
|
|
def build_listing_event(
|
|
room: Room,
|
|
*,
|
|
payment_methods: list[str] | None = None,
|
|
house_rules: dict | None = None,
|
|
) -> dict:
|
|
"""NIP-99 kind:30402 classified listing for a room. Public, signed by
|
|
the operator's identity. `d` == room.id so re-publishing replaces.
|
|
|
|
`payment_methods` (comma-joined, like events' tickets_payment_methods) and
|
|
the check-in/out times ride as tags so a generic Nostr client can render
|
|
the right pay buttons and house rules without speaking our RPC."""
|
|
tags = [
|
|
["d", room.id],
|
|
["title", room.title],
|
|
["price", str(room.price_amount), room.price_currency, room.price_frequency],
|
|
["status", "active"],
|
|
]
|
|
if payment_methods:
|
|
tags.append(["payment_methods", ",".join(payment_methods)])
|
|
if house_rules:
|
|
for key in ("checkin_time", "checkout_time"):
|
|
if house_rules.get(key):
|
|
tags.append([key, str(house_rules[key])])
|
|
if room.location:
|
|
tags.append(["location", room.location])
|
|
if room.geohash:
|
|
tags.append(["g", room.geohash])
|
|
for url in room.images:
|
|
tags.append(["image", url])
|
|
for amenity in room.amenities:
|
|
tags.append(["t", amenity])
|
|
return {
|
|
"kind": KIND_LISTING,
|
|
"content": room.description,
|
|
"tags": tags,
|
|
}
|
|
|
|
|
|
def build_reservation_event(booking: Booking, guest_pubkey: str) -> dict:
|
|
"""NIP-78 kind:30078 reservation object — the guest's durable, private
|
|
copy of their booking. `content` MUST be NIP-44 encrypted to the guest
|
|
by the caller (service.py) before signing; here we return the plaintext
|
|
payload so the signer can seal it. `d` == booking.id, addressable so
|
|
status transitions replace in place.
|
|
|
|
amount_sat is copied verbatim from the booking (canonical value) — do
|
|
not recompute it here.
|
|
"""
|
|
payload = {
|
|
"booking_id": booking.id,
|
|
"room_id": booking.room_id,
|
|
"check_in": booking.check_in,
|
|
"check_out": booking.check_out,
|
|
"nights": booking.nights,
|
|
"num_guests": booking.num_guests,
|
|
"status": booking.status.value,
|
|
"amount_sat": booking.amount_sat,
|
|
"deposit_sat": booking.deposit_sat,
|
|
"currency": booking.currency,
|
|
"price_fiat": booking.price_fiat,
|
|
}
|
|
return {
|
|
"kind": KIND_RESERVATION,
|
|
# plaintext JSON; service.py NIP-44-encrypts this to the guest before
|
|
# signing (publish_reservation passes encrypt_to=guest_pubkey).
|
|
"content": json.dumps(payload),
|
|
"tags": [
|
|
["d", booking.id],
|
|
["p", guest_pubkey],
|
|
],
|
|
}
|
|
|
|
|
|
def build_block_calendar_event(
|
|
room: Room, start_date: str, end_date: str, block_id: str
|
|
) -> dict:
|
|
"""NIP-52 kind:31923 marking a range as unavailable on the room's public
|
|
calendar. Deliberately carries NO guest PII — just 'these dates are
|
|
taken'. Only published when settings.publish_availability is true."""
|
|
return {
|
|
"kind": KIND_CALENDAR_EVENT,
|
|
"content": "",
|
|
"tags": [
|
|
["d", f"{room.id}:{block_id}"],
|
|
["title", f"{room.title} — unavailable"],
|
|
["start", start_date],
|
|
["end", end_date],
|
|
],
|
|
}
|
|
|
|
|
|
def build_availability_response(
|
|
result: AvailabilityResult, requester_pubkey: str
|
|
) -> dict:
|
|
"""Aiolabs kind:22001 (ephemeral) reply to an availability query.
|
|
|
|
Plaintext by design: room availability for a date range is public
|
|
information (the same facts published in the NIP-52 calendar), so this
|
|
needs no encryption — which also means it works today without a bunker/
|
|
server-signing signer (only sign_event is required, not nip44_encrypt).
|
|
p-tagged to the requester so their client can match the reply."""
|
|
payload = {
|
|
"room_id": result.room_id,
|
|
"check_in": result.check_in,
|
|
"check_out": result.check_out,
|
|
"available": result.available,
|
|
"nights": result.nights,
|
|
"quote_sat": result.quote_sat,
|
|
"quote_fiat": result.quote_fiat,
|
|
"currency": result.currency,
|
|
}
|
|
return {
|
|
"kind": KIND_AVAILABILITY_RESPONSE,
|
|
"content": json.dumps(payload),
|
|
"tags": [["p", requester_pubkey]],
|
|
}
|