v2.5.0: improve admin domain language logs

This commit is contained in:
Виталий Литвинов
2026-04-24 22:59:21 +03:00
parent 9c74a0d00f
commit 3b075a7ed7
7 changed files with 235 additions and 20 deletions

View File

@@ -36,11 +36,13 @@ HISTORY_FILE = Path(os.getenv("GOTELEGRAM_STATS_HISTORY", "/opt/gotelegram/stats
CURRENT_STATS = Path(os.getenv("GOTELEGRAM_STATS_CURRENT", "/run/gotelegram/stats_current.json"))
BACKUP_DIR = Path(os.getenv("GOTELEGRAM_BACKUP_DIR", "/opt/gotelegram/backups"))
INSTALL_DIR = Path(os.getenv("GOTELEGRAM_DIR", "/opt/gotelegram"))
BOT_DIR = Path(os.getenv("GOTELEGRAM_BOT_DIR", "/opt/gotelegram-bot"))
HOST = os.getenv("GOTELEGRAM_ADMIN_HOST", "127.0.0.1")
PORT = int(os.getenv("GOTELEGRAM_ADMIN_PORT", "1984"))
VERSION = "2.5.0"
USER_RE = re.compile(r"^[A-Za-z0-9_.-]{1,48}$")
LANG_RE = re.compile(r"^(en|ru)$")
def utc_now() -> str:
@@ -69,6 +71,14 @@ def load_json(path: Path, fallback: Any = None) -> Any:
return fallback
def save_json(path: Path, data: Any, mode: int = 0o600) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(path.suffix + ".tmp")
tmp.write_text(json.dumps(data, ensure_ascii=False, indent=4) + "\n", encoding="utf-8")
os.chmod(tmp, mode)
tmp.replace(path)
def read_language(config: dict[str, Any] | None = None) -> str:
config = config or load_json(GOTELEGRAM_CONFIG, {}) or {}
lang = str(config.get("language") or config.get("lang") or "").strip().lower()
@@ -81,6 +91,36 @@ def read_language(config: dict[str, Any] | None = None) -> str:
return lang if lang in {"en", "ru"} else "en"
def write_language(lang: str) -> dict[str, Any]:
lang = str(lang or "").strip().lower()
if not LANG_RE.match(lang):
raise ValueError("unsupported language")
config = load_json(GOTELEGRAM_CONFIG, {}) or {}
if not isinstance(config, dict):
config = {}
config["language"] = lang
config["updated_at"] = utc_now()
save_json(GOTELEGRAM_CONFIG, config)
INSTALL_DIR.mkdir(parents=True, exist_ok=True)
(INSTALL_DIR / ".language").write_text(lang + "\n", encoding="utf-8")
bot_env = BOT_DIR / ".env"
if bot_env.exists():
lines = bot_env.read_text(encoding="utf-8", errors="ignore").splitlines()
found = False
out = []
for line in lines:
if line.startswith("BOT_LANG="):
out.append(f"BOT_LANG={lang}")
found = True
else:
out.append(line)
if not found:
out.append(f"BOT_LANG={lang}")
bot_env.write_text("\n".join(out).rstrip() + "\n", encoding="utf-8")
os.chmod(bot_env, 0o600)
return {"language": lang, "config": config}
def read_telemt_users() -> dict[str, str]:
if not TELEMT_CONFIG.exists():
return {}
@@ -236,6 +276,31 @@ def telemt_api(path: str) -> Any:
return None
def site_status(config: dict[str, Any] | None = None) -> dict[str, Any]:
config = config or load_json(GOTELEGRAM_CONFIG, {}) or {}
host = str(config.get("domain") or "").strip()
if not host:
return {"host": "", "url": "", "http_code": 0, "ok": False, "checked": False, "error": "domain_missing"}
if not re.match(r"^[A-Za-z0-9.-]{1,253}$", host) or ".." in host or host.startswith(".") or host.endswith("."):
return {"host": host, "url": "", "http_code": 0, "ok": False, "checked": False, "error": "invalid_domain"}
url = f"https://{host}/"
code, stdout, stderr = run(["curl", "-k", "-L", "-sS", "-o", "/dev/null", "-w", "%{http_code}", "--max-time", "8", url], timeout=10)
raw_code = stdout.strip()
try:
http_code = int(raw_code)
except ValueError:
http_code = 0
return {
"host": host,
"url": url,
"http_code": http_code,
"ok": code == 0 and http_code == 200,
"checked": True,
"error": "" if code == 0 else (stderr.strip() or f"curl exit {code}"),
"checked_at": int(time.time()),
}
def load_stats_history(limit: int = 240) -> list[dict[str, int]]:
if not HISTORY_FILE.exists():
return []
@@ -373,6 +438,25 @@ def create_backup() -> tuple[bool, str]:
return code == 0, text
def read_log_payload(service: str) -> dict[str, Any]:
allowed = {"telemt", "nginx", "gotelegram-bot", "gotelegram-stats", "gotelegram-admin"}
if service not in allowed:
raise ValueError("unsupported service")
code, stdout, stderr = run(["journalctl", "-u", service, "-n", "180", "--no-pager", "-o", "short-iso"], timeout=10)
text = stdout if code == 0 else stderr
lines = text.splitlines()
if code == 0 and not lines:
text = f"No journal entries for {service}."
lines = [text]
return {
"service": service,
"ok": code == 0,
"exit_code": code,
"line_count": len(lines),
"text": text,
}
def user_payload(name: str, secret: str, include_runtime: bool = False) -> dict[str, Any]:
item: dict[str, Any] = {
"name": name,
@@ -405,6 +489,7 @@ def overview_payload() -> dict[str, Any]:
"language": language,
"admin_bind": {"host": HOST, "port": PORT},
"config": config,
"site_status": site_status(config),
"users_count": len(users),
"services": services,
"stats_current": current,
@@ -467,15 +552,17 @@ class AdminHandler(BaseHTTPRequestHandler):
current = load_json(CURRENT_STATS, {}) or {}
history = load_stats_history()
self.send_json({"ok": True, "data": {"current": current, "history": history, "status": stats_status(current, history)}})
elif path == "/api/site/check":
self.send_json({"ok": True, "data": site_status()})
elif path == "/api/logs":
qs = urllib.parse.parse_qs(parsed.query)
service = qs.get("service", ["telemt"])[0]
allowed = {"telemt", "nginx", "gotelegram-bot", "gotelegram-stats", "gotelegram-admin"}
if service not in allowed:
try:
payload = read_log_payload(service)
except ValueError:
self.send_error_json(400, "unsupported service")
return
code, stdout, stderr = run(["journalctl", "-u", service, "-n", "120", "--no-pager"], timeout=8)
self.send_json({"ok": code == 0, "data": stdout if code == 0 else stderr})
self.send_json({"ok": True, "data": payload})
else:
self.send_error_json(404, "not found")
@@ -519,6 +606,13 @@ class AdminHandler(BaseHTTPRequestHandler):
ok, message, payload = run_stats_action("repair")
payload["message"] = message
self.send_json({"ok": ok, "data": payload}, 200 if ok else 500)
elif path == "/api/settings/language":
try:
lang_payload = write_language(str(body.get("language", "")))
except Exception as exc:
self.send_error_json(400, str(exc))
return
self.send_json({"ok": True, "data": lang_payload})
elif path.startswith("/api/services/") and path.endswith("/restart"):
service = path[len("/api/services/"):-len("/restart")]
allowed = {"telemt", "nginx", "gotelegram-bot", "gotelegram-stats"}