Commit graph

2 commits

Author SHA1 Message Date
Emi Griffith
024613c06d Deliver unusual-weather alerts as Discord DMs for linked users (#210)
Adds Discord DM as a notification channel beside web push, for users who linked
their Discord account and opted in. It rides the same fan-out point as push
(_dispatch_discord next to _dispatch_push in the notifier pass), reusing the
transport-agnostic title/body/deep-link, and stays best-effort and isolated: the
in-app row is already committed, and push/email remain the fallback for anyone
Discord can't reach (its DM rule requires a shared server / user install).

- discord.py: send_dm() opens the DM channel then posts an embed via the bot token,
  with one capped 429 retry. Absolute deep links (a DM can't resolve a relative
  path the way the service worker can).
- notify.py: _dispatch_discord() delivers only when the subscriber has a linked
  discord_id and discord_dm on; no-ops entirely when no bot token is configured.
- models.py: User.discord_dm (opt-in flag) + migration 002. Linking sets it True
  (an active opt-in); a new POST /discord/dm mutes it without unlinking, and unlink
  clears it. Exposed on UserRead; account popover shows an on/off toggle.

Claude-Session: https://claude.ai/code/session_013dRZmX9D3JEntfMKWMTWZ8
2026-07-20 04:04:45 +00:00
Emi Griffith
41e84060ab Link a Thermograph account to Discord via OAuth2 (#208)
Lets a signed-in user connect their Discord account (OAuth2 identify), storing the
Discord user id that a later feature (DM alerts) will deliver to. Standard
authorization-code flow, all server-side:

- backend/discord_link.py: /discord/link/start redirects to Discord's consent
  screen; /discord/link/callback exchanges the code, reads the Discord user id, and
  stores it; /discord/unlink forgets it. The `state` is signed with the app auth
  secret (stdlib hmac, no new dependency) and carries the Thermograph user id, so a
  callback can't be replayed or bound to another account. Every route requires an
  active session, so linking acts on whoever is actually logged in.
- models.py: User.discord_id (unique, nullable). schemas.py exposes it on UserRead
  so the frontend can show link state.
- app.py: the router under /api/v2/discord.
- account.js: a "Link Discord" / "Unlink Discord" control in the account popover.
- deploy/migrations/001-user-discord-id.sql: the manual column add for the existing
  prod accounts DB. This project has no Alembic — create_all only makes missing
  tables, so an added column needs a hand-applied migration (documented in-file).
- env example: THERMOGRAPH_DISCORD_CLIENT_SECRET + the redirect to register.

Claude-Session: https://claude.ai/code/session_013dRZmX9D3JEntfMKWMTWZ8
2026-07-20 02:26:33 +00:00