patching voting system

This commit is contained in:
Subinacls
2026-06-09 07:22:08 -04:00
parent a789fb131f
commit 154735e133
4 changed files with 116 additions and 16 deletions
+48 -2
View File
@@ -1,5 +1,6 @@
"""Configuration and persistence for the Excommunicado bot."""
import json
import logging
import os
import tempfile
import threading
@@ -7,6 +8,8 @@ from datetime import datetime, timezone, timedelta
from pathlib import Path
from typing import Any, Dict, Optional, Tuple
logger = logging.getLogger("excommunicado.config")
DATA_DIR = Path(__file__).parent.parent / "data"
CONFIG_FILE = DATA_DIR / "xcom_config.json"
@@ -20,6 +23,27 @@ def _ensure_data_dir() -> None:
DATA_DIR.mkdir(parents=True, exist_ok=True)
def check_writable() -> Tuple[bool, str]:
"""Verify the data directory is writable. Returns (ok, message).
Useful at startup to fail loudly when a Docker volume is owned by the
wrong user (the classic non-root + bind-mount permission problem).
"""
try:
_ensure_data_dir()
probe = DATA_DIR / ".write_test"
with open(probe, "w", encoding="utf-8") as f:
f.write("ok")
probe.unlink(missing_ok=True)
return True, f"data dir writable: {DATA_DIR} (uid={os.getuid() if hasattr(os, 'getuid') else 'n/a'})"
except OSError as err:
uid = os.getuid() if hasattr(os, "getuid") else "n/a"
return False, (
f"DATA DIR NOT WRITABLE: {DATA_DIR} (process uid={uid}). "
f"Fix volume ownership or run: chown -R 1000:1000 ./data ({err})"
)
def load_config() -> Dict[str, Any]:
"""Load the entire config dict (cached after first read)."""
global _cache
@@ -53,13 +77,26 @@ def save_config(data: Dict[str, Any]) -> None:
with _lock:
_ensure_data_dir()
# Write to a temp file in the same dir, then atomically replace.
fd, tmp_path = tempfile.mkstemp(dir=str(DATA_DIR), suffix=".tmp")
# NOTE: temp file must be on the SAME filesystem as CONFIG_FILE for
# os.replace to be atomic, so it lives in DATA_DIR (not /tmp).
try:
fd, tmp_path = tempfile.mkstemp(dir=str(DATA_DIR), suffix=".tmp")
except OSError as err:
uid = os.getuid() if hasattr(os, "getuid") else "n/a"
logger.error(
"Cannot create temp file in %s (uid=%s): %s. "
"The data volume is likely owned by another user. "
"Run: chown -R 1000:1000 ./data (or switch to a named volume).",
DATA_DIR, uid, err,
)
raise
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
f.flush()
os.fsync(f.fileno())
os.replace(tmp_path, CONFIG_FILE)
logger.debug("Config saved (%d guild entries) to %s", len(data), CONFIG_FILE)
except BaseException:
if os.path.exists(tmp_path):
os.remove(tmp_path)
@@ -231,10 +268,15 @@ def cast_vote(
"timestamp": now.isoformat(),
}
save_config(config)
score = _record_score(rec)
logger.info(
"Vote recorded: guild=%s target=%s voter=%s dir=%+d weight=%d -> score=%d voters=%d",
gid, tid, vid, direction, int(weight), score, len(voters),
)
return {
"ok": True,
"reason": None,
"score": _record_score(rec),
"score": score,
"unique_voters": len(voters),
"retry_hours": None,
}
@@ -298,6 +340,10 @@ def add_keyword_points(
rec["keyword_points"] = int(rec.get("keyword_points", 0)) + int(amount)
save_config(config)
logger.info(
"Keyword points added: guild=%s target=%s amount=%+d -> keyword_points=%d score=%d",
gid, tid, int(amount), rec["keyword_points"], _record_score(rec),
)
return {
"score": _record_score(rec),
"unique_voters": len(rec.get("voters", {})),