"""Notification delivery — Discord webhooks and generic HTTP webhooks.""" from __future__ import annotations from typing import Optional import requests class NotificationError(Exception): """Raised when a notification cannot be delivered.""" class DiscordNotifier: """Send modpack update alerts to a Discord channel via webhook. Embeds are used so the messages look polished without additional configuration on the user's side. """ _EMBED_COLOR_UPDATE = 0xF5A623 # amber — update available _EMBED_COLOR_TEST = 0x43B581 # green — test / OK def __init__(self, webhook_url: str, timeout: int = 10) -> None: self.webhook_url = webhook_url self.timeout = timeout # ------------------------------------------------------------------ # Public helpers # ------------------------------------------------------------------ def send_update( self, modpack_name: str, curseforge_id: int, old_version: Optional[str], new_version: str, ) -> None: """Post an update-available embed to Discord. Raises: NotificationError: if the webhook request fails. """ embed = { "title": "Modpack Update Available", "color": self._EMBED_COLOR_UPDATE, "url": f"https://www.curseforge.com/minecraft/modpacks/{curseforge_id}", "fields": [ { "name": "Modpack", "value": modpack_name, "inline": True, }, { "name": "CurseForge ID", "value": str(curseforge_id), "inline": True, }, { "name": "Previous Version", "value": old_version or "Unknown", "inline": False, }, { "name": "New Version", "value": new_version, "inline": False, }, ], "footer": { "text": "Modpack Version Checker \u2022 Firefrost Gaming", }, } self._post({"embeds": [embed]}) def test(self) -> None: """Send a simple test message to verify the webhook URL works. Raises: NotificationError: if the request fails. """ self._post( { "embeds": [ { "title": "Webhook Connected", "description": ( "Modpack Version Checker notifications are working correctly." ), "color": self._EMBED_COLOR_TEST, "footer": {"text": "Modpack Version Checker \u2022 Firefrost Gaming"}, } ] } ) # ------------------------------------------------------------------ # Internal # ------------------------------------------------------------------ def _post(self, payload: dict) -> None: try: response = requests.post( self.webhook_url, json=payload, timeout=self.timeout, ) # Discord returns 204 No Content on success if response.status_code not in (200, 204): raise NotificationError( f"Discord returned HTTP {response.status_code}: {response.text[:200]}" ) except requests.ConnectionError as exc: raise NotificationError( "Could not reach Discord. Check your internet connection." ) from exc except requests.Timeout: raise NotificationError( f"Discord webhook timed out after {self.timeout}s." ) except requests.RequestException as exc: raise NotificationError(f"Webhook request failed: {exc}") from exc