diff --git a/config.json b/config.json index 330240f..46be592 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,6 @@ { "id": "events", - "version": "1.6.1-aio.8", + "version": "1.6.1-aio.9", "name": "Events", "repo": "https://git.atitlan.io/aiolabs/events", "short_description": "Sell and register event tickets", diff --git a/docs/upstream-candidates.md b/docs/upstream-candidates.md index b3246c2..539fbef 100644 --- a/docs/upstream-candidates.md +++ b/docs/upstream-candidates.md @@ -4,14 +4,15 @@ Running log of fork features that are shaped so they could be offered to `lnbits/events` (or `lnbits/lnbits`). Add a row whenever a change lands here in an upstream-compatible form; strike it when the PR merges upstream. -| Feature | Where | Upstream target | Readiness | -| ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------- | -| `frontend_url` + origin allow-list + `?checkout=` return contract | `views_api.py` `_resolve_frontend_root`, `api_ticket_create` | lnbits/events | after the #33 rebase, as a small PR | -| Ticket ids minted before the invoice so `success_url` can carry them | `api_ticket_create` | lnbits/events | ships with the above | -| `extra.checkout` (success/cancel URL, `customer_email`, line item, metadata) on fiat purchases | `api_ticket_create` | lnbits/events (needs lnbits `StripeCheckoutOptions.cancel_url`/`customer_email`, PR'd from aiolabs/lnbits) | with the lnbits patch | -| `extra.payment_methods` per event + `tickets_payment_methods` NIP-52 tag | `models.py`, `nostr_publisher.py` | lnbits/events (v2 PR #64 introduces the same field) | offer as review input on #64 | -| `asyncio.to_thread` around the smtplib send | `services.py` `_send_ticket_email_notification` | lnbits/events | trivial, standalone | -| QR logo overlay in `make_qr_png` (instance `lnbits_qr_logo`) | `views_api.py` | lnbits/events | standalone | -| Multi-ticket purchase as N rows on one `payment_hash` | `api_ticket_create`, `crud.py` | lnbits/events | overlaps v2 baskets; review input on #64 | -| Free tickets without minting an invoice | `_issue_free_tickets` | lnbits/events | small, standalone | -| NIP-52 publishing + approval workflow | `nostr_*.py`, `views_api.py` | lnbits/events #46 | open; rebase onto v1.6.8 | +| Feature | Where | Upstream target | Readiness | +| --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | +| `frontend_url` + origin allow-list + `?checkout=` return contract | `views_api.py` `_resolve_frontend_root`, `api_ticket_create` | lnbits/events | after the #33 rebase, as a small PR | +| Ticket ids minted before the invoice so `success_url` can carry them | `api_ticket_create` | lnbits/events | ships with the above | +| `extra.checkout` (success/cancel URL, `customer_email`, line item, metadata) on fiat purchases | `api_ticket_create` | lnbits/events (needs lnbits `StripeCheckoutOptions.cancel_url`/`customer_email`, PR'd from aiolabs/lnbits) | with the lnbits patch | +| `extra.payment_methods` per event + `tickets_payment_methods` NIP-52 tag | `models.py`, `nostr_publisher.py` | lnbits/events (v2 PR #64 introduces the same field) | offer as review input on #64 | +| `asyncio.to_thread` around the smtplib send | `services.py` `_send_ticket_email_notification` | lnbits/events | trivial, standalone | +| QR logo overlay in `make_qr_png` (instance `lnbits_qr_logo`) | `views_api.py` | lnbits/events | standalone | +| Multi-ticket purchase as N rows on one `payment_hash` | `api_ticket_create`, `crud.py` | lnbits/events | overlaps v2 baskets; review input on #64 | +| Free tickets without minting an invoice | `_issue_free_tickets` | lnbits/events | small, standalone | +| NIP-52 publishing + approval workflow | `nostr_*.py`, `views_api.py` | lnbits/events #46 | open; rebase onto v1.6.8 | +| `Date` + `Message-ID` + From display name on the ticket email (`build_ticket_email`); event details in the body | `services.py` | lnbits/events (mailer) **and** lnbits/lnbits `send_email` (same omissions, hits password-reset/admin mails) | trivial, standalone — measured: SpamAssassin MISSING_DATE 1.4 + MISSING_MID 0.14 | diff --git a/services.py b/services.py index 83ac5c6..8476798 100644 --- a/services.py +++ b/services.py @@ -5,6 +5,7 @@ import smtplib from asyncio.tasks import create_task from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText +from email.utils import formataddr, formatdate, make_msgid from html import escape from lnbits.core.models.users import UserNotifications @@ -114,12 +115,34 @@ def _ticket_notification_message(ticket: Ticket, event: Event) -> tuple[str, str return subject, f"{body}\n\nOpen it here: {ticket_url}" -def _ticket_delivery_message(ticket: Ticket, base_message: str) -> str: - return f"{base_message}\n\nTicket image: {_ticket_image_url(ticket)}" +def _ticket_details(ticket: Ticket, event: Event) -> str: + """Human-readable ticket facts for the email body. Also what keeps the + mail from being an image with no words (SpamAssassin HTML_IMAGE_ONLY).""" + when = event.event_start_date + if event.event_end_date and event.event_end_date != event.event_start_date: + when = f"{when} to {event.event_end_date}" + lines = [f"Event: {event.name}", f"When: {when}"] + if event.location: + lines.append(f"Where: {event.location}") + if ticket.name: + lines.append(f"Name on ticket: {ticket.name}") + lines.append(f"Ticket ID: {ticket.id}") + lines.append( + "Show the QR code below (or open the link above on your phone) at the " + "door to be scanned in." + ) + return "\n".join(lines) -def _ticket_email_html_message(ticket: Ticket, base_message: str) -> str: - text_message = _ticket_delivery_message(ticket, base_message) +def _ticket_delivery_message(ticket: Ticket, event: Event, base_message: str) -> str: + return ( + f"{base_message}\n\n{_ticket_details(ticket, event)}" + f"\n\nTicket image: {_ticket_image_url(ticket)}" + ) + + +def _ticket_email_html_message(ticket: Ticket, event: Event, base_message: str) -> str: + text_message = _ticket_delivery_message(ticket, event, base_message) html_message = f"
{escape(text_message).replace(chr(10), '
')}
html
" + ) + assert msg["Date"] + assert msg["Message-ID"].endswith("@example.org>") + assert msg["From"] == "Oyez!