From 9a2764ae3342bcd7da2822c471932ebba3636922 Mon Sep 17 00:00:00 2001 From: Emi Griffith Date: Sun, 19 Jul 2026 21:04:45 -0700 Subject: [PATCH] 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 --- static/account.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/static/account.js b/static/account.js index a443ae7..6ca83fa 100644 --- a/static/account.js +++ b/static/account.js @@ -302,7 +302,8 @@ function renderHeader() { @@ -338,6 +339,15 @@ function renderHeader() { await refreshUser(); emitAuth(); // repaint the popover in its unlinked state }); + const dmBtn = el.querySelector(".acct-discord-dm"); + if (dmBtn) dmBtn.addEventListener("click", async () => { + dmBtn.disabled = true; + try { + await apiFetch("api/v2/discord/dm", { method: "POST", json: { enabled: !currentUser.discord_dm } }); + } catch (e) {} + await refreshUser(); + emitAuth(); // repaint with the new on/off label + }); paintBadge(); paintNotifList(); startNotifPolling();