feat: NIP-17 gift-wrapped check-in DM on confirmation (#5)
On settlement, send the guest their private check-in details as a NIP-59 gift-wrapped DM (nostr/giftwrap.py, built from lnbits core primitives — no vendored crypto): - rumor (kind 14) -> seal (kind 13, operator-encrypted + operator-signed via the signer abstraction) -> gift wrap (kind 1059, ephemeral-key encrypted + signed locally via core nip44_encrypt + sign_event). created_at randomised into the past per NIP-59. - service.send_checkin_dm builds the message (room.checkin_instructions + settings times/policy) and publishes via nostrclient (_publish_signed, extracted from _sign_and_publish). - tasks.on_invoice_paid calls it best-effort — a DM failure never undoes a confirmed, paid booking. Encrypted layer (seal) soft-fails on a LocalSigner until bunker/server- signing (lnbits#18), same as the reservation event; the ephemeral wrap layer always works. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019VUQCfdqiLSsFS2jcGnaFD
This commit is contained in:
parent
df95fd37dc
commit
b6ca1b0e02
3 changed files with 166 additions and 3 deletions
|
|
@ -23,7 +23,7 @@ from loguru import logger
|
|||
|
||||
from .. import crud, services
|
||||
from ..models import Booking, Room
|
||||
from . import events
|
||||
from . import events, giftwrap
|
||||
from .kinds import KIND_AVAILABILITY_QUERY
|
||||
|
||||
try:
|
||||
|
|
@ -104,7 +104,12 @@ async def _sign_and_publish(unsigned: dict, *, encrypt_to: str | None = None) ->
|
|||
return None
|
||||
if not signed:
|
||||
return None
|
||||
return _publish_signed(signed)
|
||||
|
||||
|
||||
def _publish_signed(signed: dict) -> str | None:
|
||||
"""Publish an already-signed event via nostrclient. Returns the event id,
|
||||
or None if nostrclient isn't installed."""
|
||||
try:
|
||||
_, nostr_client = _nostrclient()
|
||||
except _NostrclientUnavailable:
|
||||
|
|
@ -120,6 +125,40 @@ async def _sign_and_publish(unsigned: dict, *, encrypt_to: str | None = None) ->
|
|||
return signed.get("id")
|
||||
|
||||
|
||||
async def send_checkin_dm(booking, room, settings) -> str | None:
|
||||
"""On confirmation, send the guest a NIP-17 gift-wrapped DM with the
|
||||
private check-in details. Encrypted end-to-end to the guest; soft-fails
|
||||
(returns None) if the operator signer can't encrypt (LocalSigner pre-
|
||||
bunker) or nostrclient isn't installed."""
|
||||
account, signer = await _operator_signer()
|
||||
if not signer:
|
||||
return None
|
||||
wrap = await giftwrap.build_dm(
|
||||
signer=signer,
|
||||
sender_pubkey=account.pubkey,
|
||||
recipient_pubkey=booking.guest_pubkey,
|
||||
content=_checkin_message(booking, room, settings),
|
||||
)
|
||||
if not wrap:
|
||||
return None
|
||||
return _publish_signed(wrap)
|
||||
|
||||
|
||||
def _checkin_message(booking, room, settings) -> str:
|
||||
lines = [
|
||||
f"Your booking at {room.title} is confirmed! 🏰",
|
||||
"",
|
||||
f"Check-in: {booking.check_in} from {settings.checkin_time}",
|
||||
f"Check-out: {booking.check_out} by {settings.checkout_time}",
|
||||
f"Guests: {booking.num_guests}",
|
||||
]
|
||||
if room.checkin_instructions:
|
||||
lines += ["", room.checkin_instructions]
|
||||
if settings.cancellation_policy:
|
||||
lines += ["", f"Cancellation policy: {settings.cancellation_policy}"]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
async def publish_listing(room: Room) -> str | None:
|
||||
"""Publish/refresh a room's NIP-99 kind:30402 listing (public)."""
|
||||
return await _sign_and_publish(events.build_listing_event(room))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue