Cuts release 0.20.0. ## Highlights - X-Request-ID header on every response + sanitized to [A-Za-z0-9_-] (CRLF log-forging mitigation) - Error pages (HTML + JSON 500) surface request_id for support tickets - Dev debug toolbar gated by DEBUG=1 — fastapi-debug-toolbar with custom DuckDBPanel - Centralized app.logging_config.setup_logging() replaces 23 scattered basicConfig calls - Telegram bot drops bot.log file — stdout only (BREAKING) ## Devin findings addressed - BUG_0001: .env.template no longer claims FastAPI debug=True - BUG_0002: subprocess extractor logs INFO to stderr again - ANALYSIS_0003: _wants_html no longer matches Accept: */* (curl gets JSON as before) - BUG on b1c6ee9: HTML 500 page no longer leaks str(exc) in production - BUG on b13d2fe: 2 CLAUDE.md compliance flags (transform.py + ws_gateway) accepted as scope-limited logging refactor — follow-up to update CLAUDE.md if needed See CHANGELOG [0.20.0] for full notes.
65 lines
2.3 KiB
Python
65 lines
2.3 KiB
Python
"""
|
|
Script runner for Telegram bot - executes user notification scripts on demand.
|
|
|
|
Used by callback query handler when user clicks "Run" button in /status.
|
|
Runs the script as the owning user via the notify-scripts helper.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import subprocess
|
|
|
|
from . import config
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
NOTIFY_SCRIPTS_BIN = "/usr/local/bin/notify-scripts"
|
|
|
|
|
|
def run_user_script(username: str, script_name: str) -> dict | None:
|
|
"""Run a notification script as the specified user and return parsed JSON output.
|
|
|
|
Returns None on error, or the parsed JSON dict on success.
|
|
"""
|
|
if not script_name.endswith(".py"):
|
|
logger.warning(f"Not a Python script: {script_name}")
|
|
return None
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
["/usr/bin/sudo", "-u", username, NOTIFY_SCRIPTS_BIN, "run", script_name],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=config.SCRIPT_TIMEOUT_SECONDS + 10, # extra margin over inner timeout
|
|
)
|
|
|
|
if result.returncode != 0:
|
|
# notify-scripts prints JSON error to stdout on failure
|
|
try:
|
|
error_info = json.loads(result.stdout)
|
|
logger.warning(f"Script {script_name} (user={username}) failed: {error_info.get('error', 'unknown')}")
|
|
except (json.JSONDecodeError, Exception):
|
|
logger.warning(
|
|
f"Script {script_name} (user={username}) exited with code "
|
|
f"{result.returncode}: {result.stderr[:500]}"
|
|
)
|
|
return None
|
|
|
|
stdout = result.stdout.strip()
|
|
if not stdout:
|
|
logger.warning(f"Script {script_name} produced no stdout")
|
|
return None
|
|
|
|
parsed = json.loads(stdout)
|
|
logger.info(f"Script {script_name} output: image_path={parsed.get('image_path', 'MISSING')}")
|
|
return parsed
|
|
|
|
except subprocess.TimeoutExpired:
|
|
logger.error(f"Script {script_name} timed out after {config.SCRIPT_TIMEOUT_SECONDS}s")
|
|
return None
|
|
except json.JSONDecodeError as e:
|
|
logger.error(f"Script {script_name} returned invalid JSON: {e}")
|
|
return None
|
|
except Exception:
|
|
logger.exception(f"Error running script {script_name} for user {username}")
|
|
return None
|