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 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 02:14:46 -06:00
parent c29ec8e210
commit 1cb929a776
5 changed files with 43 additions and 21 deletions

View File

@@ -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")),

View File

@@ -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,

View File

@@ -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

View File

@@ -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<NotifSettings>({
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() {
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-gray-900">Discord</p>
<p className="text-sm text-gray-500">Send notifications to a Discord channel</p>
<p className="text-sm text-gray-500">Get DMs from the Synculous bot</p>
</div>
<button
onClick={() => updateNotif({ discord_enabled: !notif.discord_enabled })}
@@ -229,11 +229,11 @@ export default function SettingsPage() {
</div>
{notif.discord_enabled && (
<input
type="url"
placeholder="Discord webhook URL"
value={notif.discord_webhook}
onChange={(e) => 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"
/>
)}

View File

@@ -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;