WHAT WAS DONE: - Migrated Arbiter (discord-oauth-arbiter) code to services/arbiter/ - Migrated Modpack Version Checker code to services/modpack-version-checker/ - Created .env.example for Arbiter with all required environment variables - Moved systemd service file to services/arbiter/deploy/ - Organized directory structure per Gemini monorepo recommendations WHY: - Consolidate all service code in one repository - Prepare for Gemini code review (Panel v1.12 compatibility check) - Enable service-prefixed Git tagging (arbiter-v2.1.0, modpack-v1.0.0) - Support npm workspaces for shared dependencies SERVICES MIGRATED: 1. Arbiter (Discord OAuth bot) - Originally written by Gemini + Claude - Full source code from ops-manual docs/implementation/ - Created comprehensive .env.example - Ready for Panel v1.12 compatibility verification 2. Modpack Version Checker (Python CLI tool) - Full source code from ops-manual docs/tasks/ - Written for Panel v1.11, needs Gemini review for v1.12 - Never had code review before STILL TODO: - Whitelist Manager - Pull from Billing VPS (38.68.14.188) - Currently deployed and running - Needs Panel v1.12 API compatibility fix (Task #86) - Requires SSH access to pull code NEXT STEPS: - Gemini code review for Panel v1.12 API compatibility - Create package.json for each service - Test npm workspaces integration - Deploy after verification FILES: - services/arbiter/ (25 new files, full application) - services/modpack-version-checker/ (21 new files, full application) Signed-off-by: The Golden Chronicler <claude@firefrostgaming.com>
123 lines
4.0 KiB
Python
123 lines
4.0 KiB
Python
"""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
|