Fix all activity system edge cases

Critical fixes:
- Add threading.Lock for all shared mutable state (override, cache, current activity)
- Atomic YAML writes (temp file + os.replace) to prevent corruption on crash
- Deep-copy cache on reads to prevent callers from mutating shared state

High-severity fixes:
- Validate entries in pick_activity_for_mood() — skip/log malformed instead of KeyError
- Log warning on unrecognized activity type fallback
- Normalize empty-string state to None (avoid 'None' display)
- release_manual_override() now uses force=True so bot always shows activity
- Add try/except in release_manual_override() to handle failures gracefully

Medium fixes:
- Remove dead 'test' mood from activities.yaml
- Validate name length (128 char Discord limit) in CRUD and manual set
- Validate streaming entries have URL in CRUD path
- Add JSON parse error handling in API routes
- on_ready preserves active manual override instead of overwriting
- Log override expiry timestamp (HH:MM:SS) for easier debugging
- exc_info=True on presence update errors for full stack traces

Low fixes:
- JS activitySetFromEntry() shows notification on parse error
This commit is contained in:
2026-04-28 00:18:25 +03:00
parent 2d7acd7850
commit 6080fe170f
5 changed files with 172 additions and 78 deletions

View File

@@ -41,7 +41,11 @@ async def set_mood_activities(section: str, mood: str, request: Request):
if section not in ("normal", "evil"):
return JSONResponse(status_code=400, content={"error": "Section must be 'normal' or 'evil'"})
data = await request.json()
try:
data = await request.json()
except Exception:
return JSONResponse(status_code=400, content={"error": "Invalid JSON body"})
activities = data.get("activities")
if activities is None:
@@ -97,12 +101,24 @@ async def set_current_activity(request: Request):
Body: {"type": "listening"|"playing"|"watching"|"competing"|"streaming",
"name": "...", "state": "..." (optional), "url": "..." (required for streaming)}
"""
data = await request.json()
try:
data = await request.json()
except Exception:
return JSONResponse(status_code=400, content={"error": "Invalid JSON body"})
activity_type = data.get("type", "").lower().strip()
name = data.get("name", "").strip()
state = data.get("state") or None
url = data.get("url") or None
# 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)})"})
try:
from utils.activities import set_activity_manual
await set_activity_manual(activity_type, name, state=state, url=url)