chore: auto-commit from dashboard
This commit is contained in:
21
src/main.py
21
src/main.py
@@ -112,6 +112,27 @@ def main():
|
||||
interval_min, hb_channel,
|
||||
)
|
||||
|
||||
# Newsletter Cercetași checker (optional)
|
||||
newsletter_config = config.get("newsletter_cercetasi", {})
|
||||
if newsletter_config.get("enabled"):
|
||||
from src.newsletter_cercetasi import check_and_send as check_newsletter
|
||||
from apscheduler.triggers.cron import CronTrigger as _CronTrigger
|
||||
|
||||
async def _newsletter_tick() -> None:
|
||||
try:
|
||||
await check_newsletter(config, _send_to_channel)
|
||||
except Exception as exc:
|
||||
logger.error("Newsletter checker failed: %s", exc)
|
||||
|
||||
nl_cron = newsletter_config.get("cron", "0 9 * * *")
|
||||
scheduler._scheduler.add_job(
|
||||
_newsletter_tick,
|
||||
trigger=_CronTrigger.from_crontab(nl_cron),
|
||||
id="__newsletter_cercetasi__",
|
||||
max_instances=1,
|
||||
)
|
||||
logger.info("Newsletter Cercetasi checker registered (cron: %s)", nl_cron)
|
||||
|
||||
# Telegram bot (optional — only if telegram_token exists)
|
||||
telegram_token = get_secret("telegram_token")
|
||||
telegram_app = None
|
||||
|
||||
134
src/newsletter_cercetasi.py
Normal file
134
src/newsletter_cercetasi.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user