patching voting system
This commit is contained in:
+48
-2
@@ -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", {})),
|
||||
|
||||
Reference in New Issue
Block a user