From 57681c674fc37a8ed602268282f8c59859a3a128 Mon Sep 17 00:00:00 2001 From: orion Date: Sat, 4 Apr 2026 21:03:48 +0000 Subject: [PATCH] feat: add discord wakeup test endpoint --- app/api/routers/monitor.py | 12 +++++ app/services/discord_wakeup.py | 72 ++++++++++++++++++++++++++++++ app/services/harborforge_config.py | 26 +++++++++++ 3 files changed, 110 insertions(+) create mode 100644 app/services/discord_wakeup.py create mode 100644 app/services/harborforge_config.py diff --git a/app/api/routers/monitor.py b/app/api/routers/monitor.py index f731bb0..4384f8f 100644 --- a/app/api/routers/monitor.py +++ b/app/api/routers/monitor.py @@ -22,6 +22,7 @@ from app.services.monitoring import ( get_server_states_view, test_provider_connection, ) +from app.services.discord_wakeup import create_private_wakeup_channel router = APIRouter(prefix='/monitor', tags=['Monitor']) SUPPORTED_PROVIDERS = {'anthropic', 'openai', 'minimax', 'kimi', 'qwen'} @@ -42,6 +43,12 @@ class MonitoredServerCreate(BaseModel): display_name: str | None = None +class DiscordWakeupTestRequest(BaseModel): + discord_user_id: str + title: str = "HarborForge Wakeup" + message: str = "A HarborForge slot is ready to start." + + def require_admin(current_user: models.User = Depends(get_current_user_or_apikey)): if not current_user.is_admin: raise HTTPException(status_code=403, detail='Admin required') @@ -175,6 +182,11 @@ def revoke_api_key(server_id: int, db: Session = Depends(get_db), _: models.User return None +@router.post('/admin/discord-wakeup/test') +def discord_wakeup_test(payload: DiscordWakeupTestRequest, _: models.User = Depends(require_admin)): + return create_private_wakeup_channel(payload.discord_user_id, payload.title, payload.message) + + class TelemetryPayload(BaseModel): identifier: str openclaw_version: str | None = None diff --git a/app/services/discord_wakeup.py b/app/services/discord_wakeup.py new file mode 100644 index 0000000..7503c7a --- /dev/null +++ b/app/services/discord_wakeup.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from typing import Any + +import requests +from fastapi import HTTPException + +from app.services.harborforge_config import get_discord_wakeup_config + +DISCORD_API_BASE = "https://discord.com/api/v10" +WAKEUP_CATEGORY_NAME = "HarborForge Wakeup" + + +def _headers(bot_token: str) -> dict[str, str]: + return { + "Authorization": f"Bot {bot_token}", + "Content-Type": "application/json", + } + + +def _ensure_category(guild_id: str, bot_token: str) -> str | None: + resp = requests.get(f"{DISCORD_API_BASE}/guilds/{guild_id}/channels", headers=_headers(bot_token), timeout=15) + if not resp.ok: + raise HTTPException(status_code=502, detail=f"Discord list channels failed: {resp.text}") + for ch in resp.json(): + if ch.get("type") == 4 and ch.get("name") == WAKEUP_CATEGORY_NAME: + return ch.get("id") + payload = {"name": WAKEUP_CATEGORY_NAME, "type": 4} + created = requests.post(f"{DISCORD_API_BASE}/guilds/{guild_id}/channels", headers=_headers(bot_token), json=payload, timeout=15) + if not created.ok: + raise HTTPException(status_code=502, detail=f"Discord create category failed: {created.text}") + return created.json().get("id") + + +def create_private_wakeup_channel(discord_user_id: str, title: str, message: str) -> dict[str, Any]: + cfg = get_discord_wakeup_config() + guild_id = cfg.get("guild_id") + bot_token = cfg.get("bot_token") + if not guild_id or not bot_token: + raise HTTPException(status_code=400, detail="Discord wakeup config is incomplete") + + category_id = _ensure_category(guild_id, bot_token) + channel_name = f"wake-{discord_user_id[-6:]}-{int(datetime.now(timezone.utc).timestamp())}" + payload = { + "name": channel_name, + "type": 0, + "parent_id": category_id, + "permission_overwrites": [ + {"id": guild_id, "type": 0, "deny": "1024"}, + {"id": discord_user_id, "type": 1, "allow": "1024"}, + ], + "topic": title, + } + created = requests.post(f"{DISCORD_API_BASE}/guilds/{guild_id}/channels", headers=_headers(bot_token), json=payload, timeout=15) + if not created.ok: + raise HTTPException(status_code=502, detail=f"Discord create channel failed: {created.text}") + channel = created.json() + sent = requests.post( + f"{DISCORD_API_BASE}/channels/{channel['id']}/messages", + headers=_headers(bot_token), + json={"content": message}, + timeout=15, + ) + if not sent.ok: + raise HTTPException(status_code=502, detail=f"Discord send message failed: {sent.text}") + return { + "guild_id": guild_id, + "channel_id": channel.get("id"), + "channel_name": channel.get("name"), + "message_id": sent.json().get("id"), + } diff --git a/app/services/harborforge_config.py b/app/services/harborforge_config.py new file mode 100644 index 0000000..42e84d7 --- /dev/null +++ b/app/services/harborforge_config.py @@ -0,0 +1,26 @@ +import json +import os +from typing import Any + +CONFIG_DIR = os.getenv("CONFIG_DIR", "/config") +CONFIG_FILE = os.getenv("CONFIG_FILE", "harborforge.json") + + +def load_runtime_config() -> dict[str, Any]: + config_path = os.path.join(CONFIG_DIR, CONFIG_FILE) + if not os.path.exists(config_path): + return {} + try: + with open(config_path, "r") as f: + return json.load(f) + except Exception: + return {} + + +def get_discord_wakeup_config() -> dict[str, str | None]: + cfg = load_runtime_config() + discord_cfg = cfg.get("discord") or {} + return { + "guild_id": discord_cfg.get("guild_id"), + "bot_token": discord_cfg.get("bot_token"), + }