feat: emailed iCalendar invites on confirm / reschedule / cancel (#189) #190
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/189-email-invites"
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?
Fixes #189. The replacement for #107 Phase B (Google Calendar OAuth push, which was dropped as poor value for a self-hosted tool).
Why
The #107 iCal feed gives subscription sync, but Google refreshes external feeds only every 8–24h. Emailed iCalendar invites are the near-instant, provider-agnostic, no-OAuth alternative: an invite lands in Gmail/Apple/Outlook and auto-adds the session; a reschedule updates it in place; a cancel removes it — via standard iTIP
METHOD:REQUEST/CANCEL.What changed
users.calendar_invite_opt_in(migrationd1e2f3a4b5c6,down_revision c0d1e2f3a4b5), mirroringrecap_email_opt_in, with a Profile toggle. Invites go only to opted-in members who have an email; a silent no-op if SMTP is unconfigured.calendar_service.build_itip_invite(): a single-VEVENT iTIPVCALENDARwithMETHOD:REQUEST/CANCEL,ORGANIZER, and oneATTENDEE. Reuses #107's stableUID+SEQUENCE, so a client updates the same event across reschedules rather than duplicating. Feed/download builders untouched.email.send_calendar_invite():multipart/mixed→alternative[html, text/calendar; method=REQUEST]+ anapplication/icsattachment — iMIP-correct so mail clients render "add to calendar".send_session_confirmed(confirm and reschedule share this task) andsend_session_cancelled. Best-effort — a calendar failure is logged and swallowed, never turning a good Discord/bot notification into a Celery retry.Two notification-path changes — verified NOT to regress Discord
These were necessary (else invites wouldn't fire without Discord), and I reviewed each:
_send_confirmation_notice;cancel_sessionpreviously only queued whenguild_idwas set) so invites also fire for campaigns with no Discord integration.returnafter a successful bot publish — abot_notifiedflag preserves the original "skip the webhook when the bot was notified" behaviour (no double-post), while letting the calendar step always run afterward. I confirmed the webhook still fires only when the bot wasn't notified.Acceptance criteria
METHOD:REQUEST(correctUID/ATTENDEE/DTSTART).SEQUENCE(same UID → updates in place).METHOD:CANCEL/STATUS:CANCELLED.Verification
Backend (dev-host harness): migration applies from base and
downgrade -1 → upgrade headround-trips (opt-in column drops/recreates); 434 passed (4 new);ruff check/formatclean. Frontend (node:20): lint clean, 170 tests, build succeeds.I also verified the iMIP wire format (calendar part
Content-Type: text/calendar; method=REQUEST) and read the restructuredsend_session_confirmed/send_session_cancelledto confirm no webhook double-post.Deploy note: migration
d1e2f3a4b5c6ships with the next deploy (backup-first), after v3.6.0's three.🤖 Generated with Claude Code
Pushed a security fix (
5969b6b): the automated review flagged stored XSS via manual HTML building — the invite email body interpolated the GM-controlled campaign name and session title into HTML without escaping. Nowhtml.escape()d in_send_calendar_invites_impl, and I fixed the same pre-existing pattern inEmailNotificationBackend.send_confirmation/send_reminderwhile there. The ICS builder is unaffected (RFC 5545 escaping, not an HTML context). Added a test asserting<script>/<img onerror>in a campaign/session name is escaped in the email'stext/htmlpart. Backend 435 passed, ruff clean.