"""Newsletter Cercetași checker — detects new editions and sends WhatsApp summaries.""" import asyncio import json import logging import subprocess from datetime import datetime, timezone from pathlib import Path from typing import Awaitable, Callable import httpx from src.claude_session import CLAUDE_BIN, PROJECT_ROOT, _safe_env, build_system_prompt log = logging.getLogger("echo-core.newsletter-cercetasi") NEWSLETTER_BASE_URL = "https://cercetaiis-newsletter.beehiiv.com/p/newsletter-{n}-din-{year}" STATE_FILE = PROJECT_ROOT / "cron" / "newsletter-cercetasi-state.json" KB_PROMPT_FILE = ( PROJECT_ROOT / "memory" / "kb" / "projects" / "grup-sprijin" / "prompt-newsletter-cercetasi.md" ) CLAUDE_TIMEOUT = 120 def _read_state() -> dict: try: return json.loads(STATE_FILE.read_text()) except (FileNotFoundError, json.JSONDecodeError): return {"last_sent": 0, "year": datetime.now(timezone.utc).year} def _write_state(state: dict) -> None: STATE_FILE.parent.mkdir(parents=True, exist_ok=True) STATE_FILE.write_text(json.dumps(state, indent=2, ensure_ascii=False) + "\n") async def _newsletter_exists(n: int, year: int) -> bool: """Return True if newsletter #{n}/{year} returns HTTP 200.""" url = NEWSLETTER_BASE_URL.format(n=n, year=year) try: async with httpx.AsyncClient(follow_redirects=True) as client: resp = await client.get(url, timeout=10) return resp.status_code == 200 except Exception as e: log.debug("Newsletter #%d/%d check failed: %s", n, year, e) return False def _generate_summary(n: int, year: int) -> str | None: """Run Claude CLI to generate summary for newsletter #{n}/{year}. Returns text or None.""" url = NEWSLETTER_BASE_URL.format(n=n, year=year) try: kb_prompt = KB_PROMPT_FILE.read_text() except FileNotFoundError: log.error("KB prompt file not found: %s", KB_PROMPT_FILE) return None prompt = ( f"Newsletter-ul Cercetașilor #{n}/{year} este disponibil la: {url}\n\n" f"Urmează instrucțiunile de mai jos pentru a genera rezumatul:\n\n" f"{kb_prompt}" ) cmd = [ CLAUDE_BIN, "-p", prompt, "--model", "sonnet", "--output-format", "json", "--allowedTools", "WebFetch", ] try: system_prompt = build_system_prompt() cmd += ["--system-prompt", system_prompt] except FileNotFoundError: pass try: proc = subprocess.run( cmd, capture_output=True, text=True, timeout=CLAUDE_TIMEOUT, env=_safe_env(), cwd=PROJECT_ROOT, ) if proc.returncode != 0: log.error("Claude CLI error (exit %d): %s", proc.returncode, proc.stderr[:300]) return None data = json.loads(proc.stdout) return data.get("result", "").strip() or None except subprocess.TimeoutExpired: log.error("Claude CLI timed out for newsletter #%d", n) return None except (json.JSONDecodeError, Exception) as e: log.error("Failed to generate newsletter summary: %s", e) return None async def check_and_send(config, send_callback) -> None: """Check for new newsletter; if found, generate summary and send via callback.""" state = _read_state() current_year = datetime.now(timezone.utc).year # New year → reset counter if state.get("year", current_year) != current_year: log.info("New year detected (%d → %d), resetting newsletter counter", state["year"], current_year) state = {"last_sent": 0, "year": current_year} next_n = state["last_sent"] + 1 log.info("Checking for Cercetasi newsletter #%d/%d...", next_n, current_year) if not await _newsletter_exists(next_n, current_year): log.info("Newsletter #%d/%d not yet available", next_n, current_year) return log.info("Newsletter #%d/%d found! Generating summary...", next_n, current_year) summary = await asyncio.to_thread(_generate_summary, next_n, current_year) if not summary: log.error("Failed to generate summary for newsletter #%d/%d", next_n, current_year) return channel = config.get("newsletter_cercetasi.channel", "echo-core") try: await send_callback(channel, summary) state["last_sent"] = next_n state["year"] = current_year state["last_sent_at"] = datetime.now(timezone.utc).isoformat() _write_state(state) log.info("Newsletter #%d/%d summary sent to channel '%s'", next_n, current_year, channel) except Exception as e: log.error("Send failed for newsletter #%d/%d — will retry next check: %s", next_n, current_year, e)