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
This commit is contained in:
parent
169678e1bc
commit
9a2764ae33
1 changed files with 11 additions and 1 deletions
|
|
@ -302,7 +302,8 @@ function renderHeader() {
|
||||||
<div class="acct-pop" hidden>
|
<div class="acct-pop" hidden>
|
||||||
<a href="${APP_BASE}/alerts" class="acct-pop-link">My alerts</a>
|
<a href="${APP_BASE}/alerts" class="acct-pop-link">My alerts</a>
|
||||||
${currentUser.discord_id
|
${currentUser.discord_id
|
||||||
? '<button type="button" class="acct-pop-link acct-discord-unlink">Unlink Discord</button>'
|
? `<button type="button" class="acct-pop-link acct-discord-dm">${currentUser.discord_dm ? "Discord alerts: on" : "Discord alerts: off"}</button>`
|
||||||
|
+ '<button type="button" class="acct-pop-link acct-discord-unlink">Unlink Discord</button>'
|
||||||
: `<a href="${APP_BASE}/api/v2/discord/link/start" class="acct-pop-link">Link Discord</a>`}
|
: `<a href="${APP_BASE}/api/v2/discord/link/start" class="acct-pop-link">Link Discord</a>`}
|
||||||
<button type="button" class="acct-pop-link acct-signout">Sign out</button>
|
<button type="button" class="acct-pop-link acct-signout">Sign out</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -338,6 +339,15 @@ function renderHeader() {
|
||||||
await refreshUser();
|
await refreshUser();
|
||||||
emitAuth(); // repaint the popover in its unlinked state
|
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();
|
paintBadge();
|
||||||
paintNotifList();
|
paintNotifList();
|
||||||
startNotifPolling();
|
startNotifPolling();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue