2026-04-24 13:32:55 +03:00
|
|
|
"""Activities API routes — CRUD for mood-based song/game activity lists."""
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Request
|
|
|
|
|
from fastapi.responses import JSONResponse
|
|
|
|
|
from utils.logger import get_logger
|
|
|
|
|
|
|
|
|
|
logger = get_logger('api')
|
|
|
|
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/activities")
|
|
|
|
|
def get_all_activities():
|
|
|
|
|
"""Return the full activities data (normal + evil sections, all moods)."""
|
|
|
|
|
from utils.activities import get_all_activities
|
|
|
|
|
return get_all_activities()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/activities/{section}/{mood}")
|
|
|
|
|
def get_mood_activities(section: str, mood: str):
|
|
|
|
|
"""Return activities for a specific mood.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
section: "normal" or "evil"
|
|
|
|
|
mood: mood name (e.g. "bubbly", "aggressive")
|
|
|
|
|
"""
|
|
|
|
|
if section not in ("normal", "evil"):
|
|
|
|
|
return JSONResponse(status_code=400, content={"error": "Section must be 'normal' or 'evil'"})
|
|
|
|
|
|
|
|
|
|
from utils.activities import get_activities_for_mood
|
|
|
|
|
activities = get_activities_for_mood(mood, is_evil=(section == "evil"))
|
|
|
|
|
return {"section": section, "mood": mood, "activities": activities}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/activities/{section}/{mood}")
|
|
|
|
|
async def set_mood_activities(section: str, mood: str, request: Request):
|
|
|
|
|
"""Update activities for a specific mood.
|
|
|
|
|
|
|
|
|
|
Body: {"activities": [{"type": "listening"|"playing", "name": "...", "weight": 1}]}
|
|
|
|
|
"""
|
|
|
|
|
if section not in ("normal", "evil"):
|
|
|
|
|
return JSONResponse(status_code=400, content={"error": "Section must be 'normal' or 'evil'"})
|
|
|
|
|
|
2026-04-28 00:18:25 +03:00
|
|
|
try:
|
|
|
|
|
data = await request.json()
|
|
|
|
|
except Exception:
|
|
|
|
|
return JSONResponse(status_code=400, content={"error": "Invalid JSON body"})
|
|
|
|
|
|
2026-04-24 13:32:55 +03:00
|
|
|
activities = data.get("activities")
|
|
|
|
|
|
|
|
|
|
if activities is None:
|
|
|
|
|
return JSONResponse(status_code=400, content={"error": "Request body must include 'activities' list"})
|
|
|
|
|
|
|
|
|
|
if not isinstance(activities, list):
|
|
|
|
|
return JSONResponse(status_code=400, content={"error": "'activities' must be a list"})
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
from utils.activities import set_activities_for_mood
|
|
|
|
|
set_activities_for_mood(mood, is_evil=(section == "evil"), activities=activities)
|
|
|
|
|
logger.info(f"Updated activities for {section}/{mood}: {len(activities)} entries")
|
|
|
|
|
return {"status": "ok", "section": section, "mood": mood, "count": len(activities)}
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
return JSONResponse(status_code=400, content={"error": str(e)})
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Failed to save activities for {section}/{mood}: {e}")
|
|
|
|
|
return JSONResponse(status_code=500, content={"error": "Internal server error"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/activities/reload")
|
|
|
|
|
def reload_activities():
|
|
|
|
|
"""Force reload activities from disk (useful after hand-editing the YAML)."""
|
|
|
|
|
from utils.activities import _load_activities
|
|
|
|
|
data = _load_activities(force=True)
|
|
|
|
|
normal_count = sum(len(v) for v in data.get("normal", {}).values())
|
|
|
|
|
evil_count = sum(len(v) for v in data.get("evil", {}).values())
|
|
|
|
|
logger.info(f"Force-reloaded activities: {normal_count} normal entries, {evil_count} evil entries")
|
|
|
|
|
return {"status": "ok", "normal_entries": normal_count, "evil_entries": evil_count}
|
Refactor activity system: energy-based probability, manual override, all 5 activity types
- Rewrite utils/activities.py with mood energy-driven activity probability
(high-energy moods like excited/bubbly show activity ~80-85% of the time,
low-energy moods like sleepy/melancholy only ~15-25%)
- Add manual override system with 30-min auto-expiry for Web UI control
- Support all 5 Discord activity types: listening, playing, watching,
competing, streaming (with purple LIVE badge via discord.Streaming)
- Add current activity tracking (get_current_activity)
- Add force=True param to update_bot_presence for on_ready (bot.py)
- Add 4 new API routes for manual override:
GET/POST/DELETE /activities/current, POST /activities/current/auto
- Expand activities.yaml from 139 to 157 entries, adding watching,
competing, and streaming entries across 11 moods
- Update Web UI: activity type dropdown with all 5 types, conditional
URL field for streaming, 'Current Activity' override panel with
set/clear/auto controls, type-aware icons and labels
2026-04-27 23:39:18 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
# Manual Override — set / clear / release current activity
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
@router.get("/activities/current")
|
|
|
|
|
def get_current_activity():
|
|
|
|
|
"""Return the bot's current activity and override status."""
|
|
|
|
|
from utils.activities import get_current_activity, is_manual_override_active
|
|
|
|
|
activity = get_current_activity()
|
|
|
|
|
override = is_manual_override_active()
|
|
|
|
|
result = {
|
|
|
|
|
"activity": activity, # dict or null
|
|
|
|
|
"manual_override": override,
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/activities/current")
|
|
|
|
|
async def set_current_activity(request: Request):
|
|
|
|
|
"""Manually set the bot's activity (bypasses mood system for 30 min).
|
|
|
|
|
|
|
|
|
|
Body: {"type": "listening"|"playing"|"watching"|"competing"|"streaming",
|
|
|
|
|
"name": "...", "state": "..." (optional), "url": "..." (required for streaming)}
|
|
|
|
|
"""
|
2026-04-28 00:18:25 +03:00
|
|
|
try:
|
|
|
|
|
data = await request.json()
|
|
|
|
|
except Exception:
|
|
|
|
|
return JSONResponse(status_code=400, content={"error": "Invalid JSON body"})
|
|
|
|
|
|
Refactor activity system: energy-based probability, manual override, all 5 activity types
- Rewrite utils/activities.py with mood energy-driven activity probability
(high-energy moods like excited/bubbly show activity ~80-85% of the time,
low-energy moods like sleepy/melancholy only ~15-25%)
- Add manual override system with 30-min auto-expiry for Web UI control
- Support all 5 Discord activity types: listening, playing, watching,
competing, streaming (with purple LIVE badge via discord.Streaming)
- Add current activity tracking (get_current_activity)
- Add force=True param to update_bot_presence for on_ready (bot.py)
- Add 4 new API routes for manual override:
GET/POST/DELETE /activities/current, POST /activities/current/auto
- Expand activities.yaml from 139 to 157 entries, adding watching,
competing, and streaming entries across 11 moods
- Update Web UI: activity type dropdown with all 5 types, conditional
URL field for streaming, 'Current Activity' override panel with
set/clear/auto controls, type-aware icons and labels
2026-04-27 23:39:18 +03:00
|
|
|
activity_type = data.get("type", "").lower().strip()
|
|
|
|
|
name = data.get("name", "").strip()
|
|
|
|
|
state = data.get("state") or None
|
|
|
|
|
url = data.get("url") or None
|
|
|
|
|
|
2026-04-28 00:18:25 +03:00
|
|
|
# Pre-validate before passing to activity module
|
|
|
|
|
if not activity_type:
|
|
|
|
|
return JSONResponse(status_code=400, content={"error": "'type' is required"})
|
|
|
|
|
if not name:
|
|
|
|
|
return JSONResponse(status_code=400, content={"error": "'name' is required"})
|
|
|
|
|
if len(name) > 128:
|
|
|
|
|
return JSONResponse(status_code=400, content={"error": f"'name' exceeds 128 characters ({len(name)})"})
|
|
|
|
|
|
Refactor activity system: energy-based probability, manual override, all 5 activity types
- Rewrite utils/activities.py with mood energy-driven activity probability
(high-energy moods like excited/bubbly show activity ~80-85% of the time,
low-energy moods like sleepy/melancholy only ~15-25%)
- Add manual override system with 30-min auto-expiry for Web UI control
- Support all 5 Discord activity types: listening, playing, watching,
competing, streaming (with purple LIVE badge via discord.Streaming)
- Add current activity tracking (get_current_activity)
- Add force=True param to update_bot_presence for on_ready (bot.py)
- Add 4 new API routes for manual override:
GET/POST/DELETE /activities/current, POST /activities/current/auto
- Expand activities.yaml from 139 to 157 entries, adding watching,
competing, and streaming entries across 11 moods
- Update Web UI: activity type dropdown with all 5 types, conditional
URL field for streaming, 'Current Activity' override panel with
set/clear/auto controls, type-aware icons and labels
2026-04-27 23:39:18 +03:00
|
|
|
try:
|
|
|
|
|
from utils.activities import set_activity_manual
|
|
|
|
|
await set_activity_manual(activity_type, name, state=state, url=url)
|
|
|
|
|
return {"status": "ok", "activity": {"type": activity_type, "name": name, "state": state, "url": url}}
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
return JSONResponse(status_code=400, content={"error": str(e)})
|
|
|
|
|
except RuntimeError as e:
|
|
|
|
|
return JSONResponse(status_code=503, content={"error": str(e)})
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Failed to set manual activity: {e}")
|
|
|
|
|
return JSONResponse(status_code=500, content={"error": "Internal server error"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/activities/current")
|
|
|
|
|
async def clear_current_activity():
|
|
|
|
|
"""Manually clear the bot's activity (stays idle, override stays active)."""
|
|
|
|
|
try:
|
|
|
|
|
from utils.activities import clear_activity_manual
|
|
|
|
|
await clear_activity_manual()
|
|
|
|
|
return {"status": "ok", "activity": None, "manual_override": True}
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Failed to clear manual activity: {e}")
|
|
|
|
|
return JSONResponse(status_code=500, content={"error": "Internal server error"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/activities/current/auto")
|
|
|
|
|
async def release_to_auto():
|
|
|
|
|
"""Release manual override and return to automatic mood-based activity."""
|
|
|
|
|
try:
|
|
|
|
|
from utils.activities import release_manual_override
|
|
|
|
|
await release_manual_override()
|
|
|
|
|
return {"status": "ok", "manual_override": False}
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Failed to release manual override: {e}")
|
|
|
|
|
return JSONResponse(status_code=500, content={"error": "Internal server error"})
|