Build reusable bot framework
This commit is contained in:
105
core/notifications.py
Normal file
105
core/notifications.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""Optional Discord-webhook and ntfy notification helpers."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from urllib.parse import quote, urlparse
|
||||
import uuid
|
||||
|
||||
import requests
|
||||
|
||||
import core.postgres as postgres
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
REQUEST_TIMEOUT = float(os.getenv("NOTIFICATION_TIMEOUT", 10))
|
||||
|
||||
|
||||
def _sendToEnabledChannels(notif_settings, message):
|
||||
"""Send to enabled channels and return True when at least one succeeds."""
|
||||
if not isinstance(notif_settings, dict):
|
||||
return False
|
||||
sent = False
|
||||
if notif_settings.get("discord_enabled") and notif_settings.get(
|
||||
"discord_webhook"
|
||||
):
|
||||
sent = discord.send(notif_settings["discord_webhook"], message) or sent
|
||||
if notif_settings.get("ntfy_enabled") and notif_settings.get("ntfy_topic"):
|
||||
sent = ntfy.send(notif_settings["ntfy_topic"], message) or sent
|
||||
return sent
|
||||
|
||||
|
||||
def getNotificationSettings(userUUID):
|
||||
return postgres.select_one("notifications", {"user_uuid": userUUID}) or False
|
||||
|
||||
|
||||
def setNotificationSettings(userUUID, data_dict):
|
||||
if not isinstance(data_dict, dict):
|
||||
return False
|
||||
allowed = {
|
||||
"discord_webhook",
|
||||
"discord_enabled",
|
||||
"ntfy_topic",
|
||||
"ntfy_enabled",
|
||||
}
|
||||
updates = {key: value for key, value in data_dict.items() if key in allowed}
|
||||
if not updates:
|
||||
return False
|
||||
if updates.get("discord_webhook"):
|
||||
_validateDiscordWebhook(updates["discord_webhook"])
|
||||
|
||||
existing = postgres.select_one("notifications", {"user_uuid": userUUID})
|
||||
if existing:
|
||||
postgres.update("notifications", updates, {"user_uuid": userUUID})
|
||||
else:
|
||||
updates["id"] = str(uuid.uuid4())
|
||||
updates["user_uuid"] = userUUID
|
||||
postgres.insert("notifications", updates)
|
||||
return True
|
||||
|
||||
|
||||
def _validateDiscordWebhook(webhookURL):
|
||||
parsed = urlparse(webhookURL)
|
||||
allowedHosts = {"discord.com", "canary.discord.com", "ptb.discord.com"}
|
||||
if parsed.scheme != "https" or parsed.hostname not in allowedHosts:
|
||||
raise ValueError("Discord webhook must use an official HTTPS Discord host")
|
||||
if not parsed.path.startswith("/api/webhooks/"):
|
||||
raise ValueError("Invalid Discord webhook path")
|
||||
return webhookURL
|
||||
|
||||
|
||||
class discord:
|
||||
@staticmethod
|
||||
def send(webhook_url, message):
|
||||
try:
|
||||
_validateDiscordWebhook(webhook_url)
|
||||
response = requests.post(
|
||||
webhook_url,
|
||||
json={"content": str(message)},
|
||||
timeout=REQUEST_TIMEOUT,
|
||||
)
|
||||
return response.status_code in {200, 204}
|
||||
except (ValueError, requests.RequestException) as error:
|
||||
logger.warning("Discord webhook delivery failed: %s", error)
|
||||
return False
|
||||
|
||||
|
||||
class ntfy:
|
||||
@staticmethod
|
||||
def send(topic, message):
|
||||
if not isinstance(topic, str) or not topic.strip():
|
||||
return False
|
||||
baseURL = os.getenv("NTFY_BASE_URL", "https://ntfy.sh").rstrip("/")
|
||||
headers = {}
|
||||
if os.getenv("NTFY_TOKEN"):
|
||||
headers["Authorization"] = f"Bearer {os.environ['NTFY_TOKEN']}"
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{baseURL}/{quote(topic.strip(), safe='')}",
|
||||
data=str(message).encode("utf-8"),
|
||||
headers=headers,
|
||||
timeout=REQUEST_TIMEOUT,
|
||||
)
|
||||
return 200 <= response.status_code < 300
|
||||
except requests.RequestException as error:
|
||||
logger.warning("ntfy delivery failed: %s", error)
|
||||
return False
|
||||
Reference in New Issue
Block a user