From 1cb929a77649bbe23ac14f87d00ca249f9b95239 Mon Sep 17 00:00:00 2001 From: chelsea Date: Sun, 15 Feb 2026 02:14:46 -0600 Subject: [PATCH] Switch Discord notifications from webhook to user ID DMs Uses the existing bot token to send DMs to users by their Discord user ID instead of posting to a channel webhook. Co-Authored-By: Claude Opus 4.6 --- api/routes/notifications.py | 4 +-- config/schema.sql | 2 +- core/notifications.py | 36 +++++++++++++++---- .../src/app/dashboard/settings/page.tsx | 18 +++++----- synculous-client/src/lib/api.ts | 4 +-- 5 files changed, 43 insertions(+), 21 deletions(-) diff --git a/api/routes/notifications.py b/api/routes/notifications.py index ac88df3..9425428 100644 --- a/api/routes/notifications.py +++ b/api/routes/notifications.py @@ -89,14 +89,14 @@ def register(app): settings = notifications.getNotificationSettings(user_uuid) if not settings: return flask.jsonify({ - "discord_webhook": "", + "discord_user_id": "", "discord_enabled": False, "ntfy_topic": "", "ntfy_enabled": False, "web_push_enabled": False, }), 200 return flask.jsonify({ - "discord_webhook": settings.get("discord_webhook") or "", + "discord_user_id": settings.get("discord_user_id") or "", "discord_enabled": bool(settings.get("discord_enabled")), "ntfy_topic": settings.get("ntfy_topic") or "", "ntfy_enabled": bool(settings.get("ntfy_enabled")), diff --git a/config/schema.sql b/config/schema.sql index 816aae0..0c1940d 100644 --- a/config/schema.sql +++ b/config/schema.sql @@ -10,7 +10,7 @@ CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS notifications ( id UUID PRIMARY KEY, user_uuid UUID REFERENCES users(id) ON DELETE CASCADE UNIQUE, - discord_webhook VARCHAR(500), + discord_user_id VARCHAR(100), discord_enabled BOOLEAN DEFAULT FALSE, ntfy_topic VARCHAR(255), ntfy_enabled BOOLEAN DEFAULT FALSE, diff --git a/core/notifications.py b/core/notifications.py index 3940128..d07144e 100644 --- a/core/notifications.py +++ b/core/notifications.py @@ -19,8 +19,8 @@ def _sendToEnabledChannels(notif_settings, message, user_uuid=None): """Send message to all enabled channels. Returns True if at least one succeeded.""" sent = False - if notif_settings.get("discord_enabled") and notif_settings.get("discord_webhook"): - if discord.send(notif_settings["discord_webhook"], message): + if notif_settings.get("discord_enabled") and notif_settings.get("discord_user_id"): + if discord.send_dm(notif_settings["discord_user_id"], message): sent = True if notif_settings.get("ntfy_enabled") and notif_settings.get("ntfy_topic"): @@ -44,7 +44,7 @@ def getNotificationSettings(userUUID): def setNotificationSettings(userUUID, data_dict): existing = postgres.select_one("notifications", {"user_uuid": userUUID}) allowed = [ - "discord_webhook", + "discord_user_id", "discord_enabled", "ntfy_topic", "ntfy_enabled", @@ -64,11 +64,33 @@ def setNotificationSettings(userUUID, data_dict): class discord: @staticmethod - def send(webhook_url, message): + def send_dm(user_id, message): + """Send a DM to a Discord user via the bot.""" + bot_token = os.environ.get("DISCORD_BOT_TOKEN") + if not bot_token: + logger.warning("DISCORD_BOT_TOKEN not set, skipping Discord DM") + return False + headers = {"Authorization": f"Bot {bot_token}", "Content-Type": "application/json"} try: - response = requests.post(webhook_url, json={"content": message}) - return response.status_code == 204 - except: + # Open/get DM channel with the user + dm_resp = requests.post( + "https://discord.com/api/v10/users/@me/channels", + headers=headers, + json={"recipient_id": user_id}, + ) + if dm_resp.status_code != 200: + logger.error(f"Failed to open DM channel for user {user_id}: {dm_resp.status_code}") + return False + channel_id = dm_resp.json()["id"] + # Send the message + msg_resp = requests.post( + f"https://discord.com/api/v10/channels/{channel_id}/messages", + headers=headers, + json={"content": message}, + ) + return msg_resp.status_code == 200 + except Exception as e: + logger.error(f"Discord DM error: {e}") return False diff --git a/synculous-client/src/app/dashboard/settings/page.tsx b/synculous-client/src/app/dashboard/settings/page.tsx index d461408..4907be5 100644 --- a/synculous-client/src/app/dashboard/settings/page.tsx +++ b/synculous-client/src/app/dashboard/settings/page.tsx @@ -15,7 +15,7 @@ interface Preferences { } interface NotifSettings { - discord_webhook: string; + discord_user_id: string; discord_enabled: boolean; ntfy_topic: string; ntfy_enabled: boolean; @@ -29,7 +29,7 @@ export default function SettingsPage() { celebration_style: 'standard', }); const [notif, setNotif] = useState({ - discord_webhook: '', + discord_user_id: '', discord_enabled: false, ntfy_topic: '', ntfy_enabled: false, @@ -41,7 +41,7 @@ export default function SettingsPage() { Promise.all([ api.preferences.get().then((data: Preferences) => setPrefs(data)), api.notifications.getSettings().then((data) => setNotif({ - discord_webhook: data.discord_webhook, + discord_user_id: data.discord_user_id, discord_enabled: data.discord_enabled, ntfy_topic: data.ntfy_topic, ntfy_enabled: data.ntfy_enabled, @@ -214,7 +214,7 @@ export default function SettingsPage() {

Discord

-

Send notifications to a Discord channel

+

Get DMs from the Synculous bot

{notif.discord_enabled && ( setNotif({ ...notif, discord_webhook: e.target.value })} - onBlur={() => updateNotif({ discord_webhook: notif.discord_webhook })} + type="text" + placeholder="Your Discord user ID" + value={notif.discord_user_id} + onChange={(e) => setNotif({ ...notif, discord_user_id: e.target.value })} + onBlur={() => updateNotif({ discord_user_id: notif.discord_user_id })} className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-gray-900 placeholder-gray-400" /> )} diff --git a/synculous-client/src/lib/api.ts b/synculous-client/src/lib/api.ts index 2fec9b5..bd189d8 100644 --- a/synculous-client/src/lib/api.ts +++ b/synculous-client/src/lib/api.ts @@ -649,7 +649,7 @@ export const api = { getSettings: async () => { return request<{ - discord_webhook: string; + discord_user_id: string; discord_enabled: boolean; ntfy_topic: string; ntfy_enabled: boolean; @@ -658,7 +658,7 @@ export const api = { }, updateSettings: async (data: { - discord_webhook?: string; + discord_user_id?: string; discord_enabled?: boolean; ntfy_topic?: string; ntfy_enabled?: boolean;