feat(fuenfvoracht): Anti-Halluzination Auto-5vor8 API und Bot-Benachrichtigungen
- Draft-Status, review_reason, notify-failed Endpoint - Telegram bei Entwurf, erfolgreichem Auto-Approve und Generierungsfehler - Nachmittags-Briefing 15:00 statt 10:00, bessere Entwurf-vs-leer Logik
This commit is contained in:
parent
04f69dfd81
commit
668055d260
2 changed files with 391 additions and 80 deletions
|
|
@ -2,6 +2,7 @@ from flask import Flask, render_template, request, jsonify, redirect, url_for, R
|
|||
from functools import wraps
|
||||
from datetime import datetime, date, timedelta
|
||||
import os
|
||||
import html
|
||||
import asyncio
|
||||
import logging
|
||||
import requests as req_lib
|
||||
|
|
@ -76,8 +77,13 @@ def planning_days(count=7):
|
|||
return [(t + timedelta(days=i)).strftime('%Y-%m-%d') for i in range(count)]
|
||||
|
||||
|
||||
|
||||
def sanitize_telegram_html(text: str) -> str:
|
||||
"""Escape <>& so Telegram HTML parse_mode does not fail on stray tags from KI/Quellen."""
|
||||
return html.escape(text or "", quote=False)
|
||||
|
||||
def with_branding(content: str) -> str:
|
||||
text = (content or "").rstrip()
|
||||
text = sanitize_telegram_html((content or "").rstrip())
|
||||
if BRAND_MARKER in text:
|
||||
return text
|
||||
return f"{text}\n\n{BRAND_SIGNATURE}" if text else BRAND_SIGNATURE
|
||||
|
|
@ -105,6 +111,50 @@ def notify_all_reviewers(text, reply_markup=None):
|
|||
return results
|
||||
|
||||
|
||||
def _notify_auto5vor8_review(date_str, post_time, title="", review_reason="", has_draft=True):
|
||||
"""Telegram an Reviewer: Auto-5vor8 braucht manuelle Prüfung."""
|
||||
title_line = sanitize_telegram_html(title[:120]) if title else "Dr.-Bine-Artikel"
|
||||
reason_line = sanitize_telegram_html(review_reason) if review_reason else ""
|
||||
if has_draft:
|
||||
msg = (
|
||||
f"⚠️ <b>Auto-5vor8: Bitte manuell prüfen</b>\n\n"
|
||||
f"📅 Slot: <b>{date_str} {post_time}</b>\n"
|
||||
f"📰 {title_line}\n"
|
||||
)
|
||||
if reason_line:
|
||||
msg += f"\n❗ <b>Faktencheck:</b> {reason_line}\n"
|
||||
msg += (
|
||||
f"\nDer KI-Text hat die automatische Qualitätsprüfung nicht bestanden. "
|
||||
f"Ein <b>Entwurf</b> liegt im Dashboard — bitte lesen, ggf. anpassen und freigeben.\n\n"
|
||||
f"👉 https://fuenfvoracht.orbitalo.net"
|
||||
)
|
||||
else:
|
||||
msg = (
|
||||
f"🚨 <b>Auto-5vor8: Generierung fehlgeschlagen</b>\n\n"
|
||||
f"📰 {title_line}\n"
|
||||
)
|
||||
if reason_line:
|
||||
msg += f"\n❗ {reason_line}\n"
|
||||
msg += (
|
||||
f"\nEs wurde <b>kein</b> Telegram-Text erzeugt. Bitte im Dashboard manuell anlegen.\n\n"
|
||||
f"👉 https://fuenfvoracht.orbitalo.net"
|
||||
)
|
||||
notify_all_reviewers(msg)
|
||||
|
||||
|
||||
def _notify_auto5vor8_scheduled(date_str, post_time, title=""):
|
||||
"""Telegram: Auto-5vor8 hat erfolgreich eingeplant."""
|
||||
title_line = sanitize_telegram_html(title[:120]) if title else "Dr.-Bine-Artikel"
|
||||
msg = (
|
||||
f"✅ <b>Auto-5vor8: Artikel eingeplant</b>\n\n"
|
||||
f"📅 <b>{date_str} · {post_time} Uhr</b>\n"
|
||||
f"📰 {title_line}\n\n"
|
||||
f"Wird automatisch gepostet — im Dashboard noch editierbar.\n"
|
||||
f"👉 https://fuenfvoracht.orbitalo.net"
|
||||
)
|
||||
notify_all_reviewers(msg)
|
||||
|
||||
|
||||
# ── Main Dashboard ────────────────────────────────────────────────────────────
|
||||
|
||||
@app.route('/')
|
||||
|
|
@ -168,7 +218,7 @@ def index():
|
|||
|
||||
@app.route('/history')
|
||||
def history():
|
||||
articles = db.get_recent_posted_articles(30)
|
||||
articles = db.get_recent_articles(30)
|
||||
return render_template('history.html', articles=articles)
|
||||
|
||||
|
||||
|
|
@ -354,7 +404,19 @@ def api_save(date_str):
|
|||
if existing:
|
||||
db.update_article_content(date_str, content, post_time=post_time)
|
||||
flog.article_saved(date_str, post_time)
|
||||
return jsonify({'success': True})
|
||||
return jsonify({'success': True})
|
||||
all_today = db.get_articles_by_date(date_str)
|
||||
candidate = next((a for a in all_today if a['status'] in ('draft', 'scheduled', 'sent_to_bot', 'approved', 'pending_review')), None)
|
||||
if candidate:
|
||||
old_time = candidate['post_time']
|
||||
conn = db.get_conn()
|
||||
conn.execute("UPDATE articles SET post_time=? WHERE date=? AND post_time=?", (post_time, date_str, old_time))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
db.update_article_content(date_str, content, post_time=post_time)
|
||||
flog.article_saved(date_str, post_time)
|
||||
return jsonify({'success': True})
|
||||
return jsonify({'success': False, 'error': 'Kein Artikel für diesen Tag vorhanden'})
|
||||
|
||||
|
||||
@app.route('/api/article/<date_str>/schedule', methods=['POST'])
|
||||
|
|
@ -365,33 +427,38 @@ def api_schedule(date_str):
|
|||
notify_mode = data.get('notify_mode', 'auto') # sofort | auto | custom
|
||||
notify_at_custom = data.get('notify_at')
|
||||
|
||||
# Slot-Konflikt prüfen
|
||||
existing = db.get_article_by_date(date_str, post_time)
|
||||
if not existing:
|
||||
return jsonify({'success': False, 'error': 'Kein Artikel für diesen Slot'})
|
||||
all_today = db.get_articles_by_date(date_str)
|
||||
candidate = next((a for a in all_today if a['status'] in ('draft', 'scheduled', 'sent_to_bot', 'pending_review')), None)
|
||||
if not candidate:
|
||||
return jsonify({'success': False, 'error': 'Kein Artikel für diesen Tag gefunden'})
|
||||
old_time = candidate['post_time']
|
||||
conn = db.get_conn()
|
||||
conn.execute(
|
||||
"UPDATE articles SET post_time=? WHERE date=? AND post_time=?",
|
||||
(post_time, date_str, old_time)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# notify_at berechnen
|
||||
import pytz
|
||||
from datetime import datetime as dt
|
||||
berlin = pytz.timezone('Europe/Berlin')
|
||||
now_berlin = dt.now(berlin)
|
||||
article_dt = berlin.localize(dt.strptime(f"{date_str} {post_time}", '%Y-%m-%d %H:%M'))
|
||||
conn = db.get_conn()
|
||||
ts = datetime.utcnow().isoformat()
|
||||
conn.execute(
|
||||
"UPDATE articles SET status='approved', scheduled_at=?, notify_at=? WHERE date=? AND post_time=?",
|
||||
(ts, ts, date_str, post_time)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
if notify_mode == 'sofort':
|
||||
notify_at = dt.utcnow().isoformat()
|
||||
elif notify_mode == 'custom' and notify_at_custom:
|
||||
notify_at = notify_at_custom
|
||||
else:
|
||||
# Auto: wenn heute → sofort, sonst Vortag 17:00
|
||||
if article_dt.date() == now_berlin.date():
|
||||
notify_at = dt.utcnow().isoformat()
|
||||
else:
|
||||
day_before = (article_dt - timedelta(days=1)).replace(hour=17, minute=0, second=0)
|
||||
notify_at = day_before.astimezone(pytz.utc).strftime('%Y-%m-%dT%H:%M:%S')
|
||||
notify_all_reviewers(
|
||||
f"📅 <b>Artikel eingeplant</b>\n\n"
|
||||
f"📆 {date_str} um <b>{post_time} Uhr</b>\n"
|
||||
f"Wird automatisch gepostet."
|
||||
)
|
||||
|
||||
db.schedule_article(date_str, post_time, notify_at)
|
||||
flog.article_scheduled(date_str, post_time, notify_at)
|
||||
return jsonify({'success': True, 'notify_at': notify_at})
|
||||
flog.article_scheduled(date_str, post_time, ts)
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
@app.route('/api/article/<int:article_id>/reschedule', methods=['POST'])
|
||||
|
|
@ -533,13 +600,35 @@ def api_skip(date_str):
|
|||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# ── Auto-5vor8 ────────────────────────────────────────────────────────────────
|
||||
# == Auto-5vor8 (API + Dashboard) ===============================================
|
||||
|
||||
STYLE_ROTATION = [
|
||||
("Standard", "📌 Das Wesentliche"),
|
||||
("Leicht sarkastisch", "🎭 Worum es wirklich geht"),
|
||||
("Humorvoll-serioes", "☕ Kurz erklaert"),
|
||||
("Knapp & direkt", "⚡ In Kuerze"),
|
||||
("Storytelling", "📖 Die Story"),
|
||||
("Wissenschaftlich nuechtern", "🔬 Befund"),
|
||||
]
|
||||
|
||||
|
||||
def _style_for_date(day):
|
||||
from datetime import date as _date
|
||||
if isinstance(day, str):
|
||||
day = _date.fromisoformat(day)
|
||||
return STYLE_ROTATION[day.toordinal() % len(STYLE_ROTATION)]
|
||||
|
||||
|
||||
def _get_auto5vor8_settings():
|
||||
conn = db.get_conn()
|
||||
rows = conn.execute("SELECT key, value FROM auto_5vor8_settings").fetchall()
|
||||
conn.close()
|
||||
return {r["key"]: r["value"] for r in rows}
|
||||
settings = {r["key"]: r["value"] for r in rows}
|
||||
if "style_rotation" not in settings:
|
||||
settings["style_rotation"] = "1"
|
||||
if "ai_model" not in settings or not settings["ai_model"]:
|
||||
settings["ai_model"] = "deepseek/deepseek-v4-flash"
|
||||
return settings
|
||||
|
||||
|
||||
def _set_auto5vor8_setting(key, value):
|
||||
|
|
@ -553,30 +642,128 @@ def _set_auto5vor8_setting(key, value):
|
|||
conn.close()
|
||||
|
||||
|
||||
@app.route('/auto5vor8')
|
||||
def _auto5vor8_style_info():
|
||||
from datetime import date, timedelta
|
||||
today = date.today()
|
||||
tomorrow = today + timedelta(days=1)
|
||||
t_name, t_intro = _style_for_date(today)
|
||||
n_name, n_intro = _style_for_date(tomorrow)
|
||||
return {
|
||||
"styles": [{"name": n, "intro": i} for n, i in STYLE_ROTATION],
|
||||
"today_style": t_name,
|
||||
"today_intro": t_intro,
|
||||
"tomorrow_style": n_name,
|
||||
"tomorrow_intro": n_intro,
|
||||
}
|
||||
|
||||
|
||||
@app.route("/api/article/create", methods=["POST"])
|
||||
def api_article_create():
|
||||
"""Erstellt einen Artikel und plant ihn direkt ein (fuer Auto-5vor8)."""
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"success": False, "error": "Kein JSON-Body"}), 400
|
||||
content = data.get("content", "").strip()
|
||||
date_str = data.get("date", "").strip()
|
||||
post_time = data.get("post_time", POST_TIME)
|
||||
tag = data.get("tag", "allgemein")
|
||||
source = data.get("source", "")
|
||||
image_url = data.get("image_url", "")
|
||||
if not content or not date_str:
|
||||
return jsonify({"success": False, "error": "content und date erforderlich"}), 400
|
||||
if db.slot_is_taken(date_str, post_time):
|
||||
return jsonify({"success": False, "error": f"Slot {date_str} {post_time} belegt"}), 409
|
||||
article_status = (data.get("status") or "approved").strip().lower()
|
||||
if article_status not in ("approved", "draft"):
|
||||
article_status = "approved"
|
||||
db.create_article(date_str, source, content, None, tag, post_time)
|
||||
from datetime import datetime as _dt
|
||||
ts = _dt.utcnow().isoformat()
|
||||
conn = db.get_conn()
|
||||
if article_status == "approved":
|
||||
conn.execute(
|
||||
"UPDATE articles SET status='approved', scheduled_at=?, image_url=? WHERE date=? AND post_time=?",
|
||||
(ts, image_url, date_str, post_time)
|
||||
)
|
||||
else:
|
||||
conn.execute(
|
||||
"UPDATE articles SET status='draft', image_url=? WHERE date=? AND post_time=?",
|
||||
(image_url, date_str, post_time)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
if article_status == "draft":
|
||||
_notify_auto5vor8_review(
|
||||
date_str,
|
||||
post_time,
|
||||
title=(data.get("title") or "").strip(),
|
||||
review_reason=(data.get("review_reason") or "").strip(),
|
||||
has_draft=True,
|
||||
)
|
||||
elif (data.get("title") or "").strip():
|
||||
_notify_auto5vor8_scheduled(
|
||||
date_str,
|
||||
post_time,
|
||||
title=(data.get("title") or "").strip(),
|
||||
)
|
||||
return jsonify({"success": True, "date": date_str, "post_time": post_time, "status": article_status})
|
||||
|
||||
|
||||
@app.route("/api/auto5vor8/notify-failed", methods=["POST"])
|
||||
def api_auto5vor8_notify_failed():
|
||||
"""Benachrichtigung wenn Auto-5vor8 gar keinen Text erzeugen konnte."""
|
||||
data = request.get_json() or {}
|
||||
_notify_auto5vor8_review(
|
||||
date_str=(data.get("date") or today_str()).strip(),
|
||||
post_time=(data.get("post_time") or POST_TIME).strip(),
|
||||
title=(data.get("title") or "").strip(),
|
||||
review_reason=(data.get("review_reason") or "KI-Text konnte nicht generiert werden").strip(),
|
||||
has_draft=False,
|
||||
)
|
||||
return jsonify({"success": True})
|
||||
|
||||
|
||||
@app.route("/api/auto5vor8/settings", methods=["GET"])
|
||||
def api_auto5vor8_settings_get():
|
||||
settings = _get_auto5vor8_settings()
|
||||
settings.update(_auto5vor8_style_info())
|
||||
return jsonify(settings)
|
||||
|
||||
|
||||
@app.route("/api/auto5vor8/settings", methods=["POST"])
|
||||
def api_auto5vor8_settings_post():
|
||||
data = request.get_json() or {}
|
||||
for key, val in data.items():
|
||||
if val is not None:
|
||||
_set_auto5vor8_setting(key, str(val).strip())
|
||||
return jsonify({"success": True})
|
||||
|
||||
|
||||
@app.route("/auto5vor8")
|
||||
def auto5vor8():
|
||||
settings = _get_auto5vor8_settings()
|
||||
return render_template('auto5vor8.html', settings=settings)
|
||||
style_info = _auto5vor8_style_info()
|
||||
return render_template("auto5vor8.html", settings=settings, style_info=style_info)
|
||||
|
||||
|
||||
@app.route('/auto5vor8/toggle', methods=['POST'])
|
||||
@app.route("/auto5vor8/toggle", methods=["POST"])
|
||||
def auto5vor8_toggle():
|
||||
current = _get_auto5vor8_settings()
|
||||
new_val = '0' if current.get('enabled') == '1' else '1'
|
||||
_set_auto5vor8_setting('enabled', new_val)
|
||||
state = 'aktiviert' if new_val == '1' else 'deaktiviert'
|
||||
logger.info('Auto-5vor8 %s', state)
|
||||
return redirect(url_for('auto5vor8'))
|
||||
new_val = "0" if current.get("enabled") == "1" else "1"
|
||||
_set_auto5vor8_setting("enabled", new_val)
|
||||
state = "aktiviert" if new_val == "1" else "deaktiviert"
|
||||
logger.info("Auto-5vor8 %s", state)
|
||||
return redirect(url_for("auto5vor8"))
|
||||
|
||||
|
||||
@app.route('/auto5vor8/settings', methods=['POST'])
|
||||
@app.route("/auto5vor8/settings", methods=["POST"])
|
||||
def auto5vor8_save_settings():
|
||||
for field in ('prompt', 'post_time', 'ai_model', 'feed_id', 'tag'):
|
||||
for field in ("prompt", "post_time", "ai_model", "feed_id", "tag", "style_rotation"):
|
||||
val = request.form.get(field)
|
||||
if val is not None:
|
||||
_set_auto5vor8_setting(field, val.strip())
|
||||
logger.info('Auto-5vor8 Einstellungen gespeichert')
|
||||
return redirect(url_for('auto5vor8'))
|
||||
logger.info("Auto-5vor8 Einstellungen gespeichert")
|
||||
return redirect(url_for("auto5vor8"))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ FünfVorAcht Bot — Review, Scheduling, Briefing, Fehler-Alarm
|
|||
"""
|
||||
import asyncio
|
||||
import os
|
||||
import html
|
||||
import re
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pytz
|
||||
|
|
@ -16,6 +20,50 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|||
import database as db
|
||||
import logger as flog
|
||||
|
||||
|
||||
# Pre-Send-Check: jede arakavanews-URL muss vor dem Posten 200 liefern.
|
||||
# Faengt drei Klassen von Bugs ab:
|
||||
# 1. KI hat Slug halluziniert (auto_5vor8 sollte das schon repariert haben — Defense in Depth)
|
||||
# 2. WP-Post wurde nachtraeglich geloescht/depubliziert
|
||||
# 3. Cloudflare-Tunnel oder WP down zum Sendezeitpunkt
|
||||
_URL_RE = re.compile(r"https?://arakavanews\.com/\S+", re.IGNORECASE)
|
||||
|
||||
|
||||
def _check_arakava_links(text: str) -> tuple[bool, str]:
|
||||
"""Prueft alle arakavanews-URLs im Text per HEAD via stdlib. (ok, fehlertext).
|
||||
|
||||
"ok" ist True wenn entweder keine URL drin ist oder ALLE URLs HTTP 200 liefern.
|
||||
Bei mehreren URLs reicht ein einziger Fehlschlag fuer ok=False.
|
||||
Nutzt urllib (stdlib), damit der Bot-Container keine zusaetzliche Dependency
|
||||
(requests) braucht.
|
||||
"""
|
||||
if not text:
|
||||
return True, ""
|
||||
urls = []
|
||||
for raw in _URL_RE.findall(text):
|
||||
clean = raw.rstrip(').,;:!?"\'<>')
|
||||
if clean and clean not in urls:
|
||||
urls.append(clean)
|
||||
if not urls:
|
||||
return True, ""
|
||||
for url in urls:
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
method="HEAD",
|
||||
headers={"User-Agent": "fuenfvoracht-presend-check/1.0"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||
if resp.status != 200:
|
||||
return False, f"{url} -> HTTP {resp.status}"
|
||||
except urllib.error.HTTPError as e:
|
||||
return False, f"{url} -> HTTP {e.code}"
|
||||
except urllib.error.URLError as e:
|
||||
return False, f"{url} -> URLError: {str(e.reason)[:80]}"
|
||||
except Exception as e:
|
||||
return False, f"{url} -> {type(e).__name__}: {str(e)[:80]}"
|
||||
return True, ""
|
||||
|
||||
BOT_TOKEN = os.environ['TELEGRAM_BOT_TOKEN']
|
||||
CHANNEL_ID = os.environ.get('TELEGRAM_CHANNEL_ID', '')
|
||||
TZ = pytz.timezone(os.environ.get('TIMEZONE', 'Europe/Berlin'))
|
||||
|
|
@ -35,8 +83,13 @@ def today_str():
|
|||
return datetime.now(TZ).strftime('%Y-%m-%d')
|
||||
|
||||
|
||||
|
||||
def sanitize_telegram_html(text: str) -> str:
|
||||
"""Escape <>& so Telegram HTML parse_mode does not fail on stray tags from KI/Quellen."""
|
||||
return html.escape(text or "", quote=False)
|
||||
|
||||
def with_branding(content: str) -> str:
|
||||
text = (content or "").rstrip()
|
||||
text = sanitize_telegram_html((content or "").rstrip())
|
||||
if BRAND_MARKER in text:
|
||||
return text
|
||||
return f"{text}\n\n{BRAND_SIGNATURE}" if text else BRAND_SIGNATURE
|
||||
|
|
@ -232,10 +285,48 @@ async def job_post_articles(bot: Bot):
|
|||
return
|
||||
try:
|
||||
final_text = with_branding(article['content_final'])
|
||||
msg = await bot.send_message(CHANNEL_ID, final_text, parse_mode=ParseMode.HTML)
|
||||
|
||||
ok, link_err = await asyncio.to_thread(_check_arakava_links, final_text)
|
||||
if not ok:
|
||||
flog.posting_failed(d, current_slot, f"link_check_failed: {link_err}")
|
||||
await notify_reviewers(
|
||||
bot,
|
||||
f"⛔ <b>Posting abgebrochen — kaputter Link!</b>\n\n"
|
||||
f"📅 {d} · ⏰ {current_slot} Uhr\n\n"
|
||||
f"<b>Fehler:</b>\n<code>{link_err}</code>\n\n"
|
||||
f"Artikel bleibt auf <code>approved</code> (kein Auto-Retry).\n"
|
||||
f"👉 Dashboard: http://100.73.171.62:8080"
|
||||
)
|
||||
continue
|
||||
|
||||
image_url = article.get('image_url', '') or ''
|
||||
if image_url:
|
||||
try:
|
||||
msg = await bot.send_photo(CHANNEL_ID, photo=image_url, caption=final_text, parse_mode=ParseMode.HTML)
|
||||
except Exception:
|
||||
msg = await bot.send_message(CHANNEL_ID, final_text, parse_mode=ParseMode.HTML)
|
||||
else:
|
||||
msg = await bot.send_message(CHANNEL_ID, final_text, parse_mode=ParseMode.HTML)
|
||||
db.update_article_status(d, 'posted', post_time=current_slot)
|
||||
db.save_post_history(d, msg.message_id, post_time=current_slot)
|
||||
flog.article_posted(d, current_slot, CHANNEL_ID, msg.message_id)
|
||||
# memory_service_event
|
||||
try:
|
||||
import requests as _rq
|
||||
_rq.post("http://100.121.192.94:8400/events", json={
|
||||
"source": "fuenfvoracht",
|
||||
"event_type": "article_posted",
|
||||
"object_key": f"{d}_{current_slot}",
|
||||
"payload_json": __import__("json").dumps({
|
||||
"date": d,
|
||||
"slot": current_slot,
|
||||
"channel": str(CHANNEL_ID),
|
||||
"message_id": msg.message_id,
|
||||
"title": article.get("title", ""),
|
||||
}, ensure_ascii=False),
|
||||
}, headers={"Authorization": "Bearer Ai8eeQibV6Z1RWc7oNPim4PXB4vILU1nRW2-XgRcX2M"}, timeout=3)
|
||||
except Exception:
|
||||
pass
|
||||
await notify_reviewers(
|
||||
bot,
|
||||
f"📤 <b>Fünf vor Acht gepostet!</b>\n{d} · {current_slot} Uhr"
|
||||
|
|
@ -253,23 +344,19 @@ async def job_post_articles(bot: Bot):
|
|||
|
||||
|
||||
async def job_check_notify(bot: Bot):
|
||||
"""Prüft alle 5 Min ob scheduled-Artikel zur Bot-Benachrichtigung fällig sind."""
|
||||
"""Prüft alle 5 Min ob scheduled-Artikel fällig sind → direkt approved setzen."""
|
||||
due = db.get_due_notifications()
|
||||
for article in due:
|
||||
d = article['date']
|
||||
pt = article['post_time']
|
||||
text = (
|
||||
f"📋 <b>Review: {d} · {pt} Uhr</b>\n"
|
||||
f"Version {article['version']}\n"
|
||||
f"──────────────────────\n\n"
|
||||
f"{article['content_final']}\n\n"
|
||||
f"──────────────────────\n"
|
||||
f"Freigeben oder bearbeiten?"
|
||||
db.update_article_status(d, 'approved', post_time=pt)
|
||||
await notify_reviewers(
|
||||
bot,
|
||||
f"📅 <b>Artikel eingeplant</b>\n\n"
|
||||
f"📆 {d} um <b>{pt} Uhr</b>\n"
|
||||
f"Wird automatisch gepostet."
|
||||
)
|
||||
await notify_reviewers(bot, text,
|
||||
reply_markup=review_keyboard(d, pt))
|
||||
db.update_article_status(d, 'sent_to_bot', post_time=pt)
|
||||
flog.article_sent_to_bot(d, pt, db.get_reviewer_chat_ids())
|
||||
flog.info('article_auto_approved', date=d, post_time=pt)
|
||||
|
||||
|
||||
async def job_morning_briefing(bot: Bot):
|
||||
|
|
@ -300,53 +387,91 @@ async def job_morning_briefing(bot: Bot):
|
|||
lines.append(f"✅ Eingeplant: {len(approved)} Slot(s)")
|
||||
for a in approved:
|
||||
lines.append(f" • {a['post_time']} Uhr — {(a['content_final'] or '')[:50]}…")
|
||||
elif missing:
|
||||
lines.append("⚠️ <b>Entwurf wartet auf Freigabe</b> (Auto-5vor8 Faktencheck)")
|
||||
for a in missing:
|
||||
preview = (a.get('content_final') or a.get('content_raw') or '')[:50]
|
||||
lines.append(f" • {a['post_time']} Uhr — {preview}…")
|
||||
lines.append(" → Bitte im Dashboard prüfen und freigeben.")
|
||||
else:
|
||||
lines.append("⚠️ Noch kein Artikel für heute freigegeben!")
|
||||
lines.append(
|
||||
"⚠️ <b>Heute noch kein Artikel im Plan.</b>\n"
|
||||
" Der Dr.-Bine-RSS-Lauf ist um 14 Uhr — wenn dort nichts Neues kam, "
|
||||
"bitte manuell anlegen."
|
||||
)
|
||||
|
||||
if missing:
|
||||
lines.append(f"📝 Entwürfe (noch nicht freigegeben): {len(missing)}")
|
||||
|
||||
if not articles_today:
|
||||
lines.append("❌ Kein Artikel erstellt — bitte jetzt anlegen.")
|
||||
if missing and approved:
|
||||
lines.append(f"\n📝 Zusätzlich Entwürfe offen: {len(missing)}")
|
||||
for a in missing:
|
||||
preview = (a.get('content_final') or a.get('content_raw') or '')[:50]
|
||||
lines.append(f" • {a['post_time']} Uhr — {preview}…")
|
||||
|
||||
lines.append(f"\n📆 <b>Nächste 3 Tage:</b>")
|
||||
lines.extend(plan_lines)
|
||||
lines.append(f"\n👉 Dashboard: http://100.73.171.62:8080")
|
||||
lines.append(f"\n👉 https://fuenfvoracht.orbitalo.net")
|
||||
|
||||
await notify_reviewers(bot, '\n'.join(lines))
|
||||
flog.info('morning_briefing_sent', date=d)
|
||||
|
||||
|
||||
async def job_cleanup_db():
|
||||
"""Wöchentliche DB-Bereinigung: alte Einträge löschen."""
|
||||
"""Woechentliche DB-Bereinigung: alte Eintraege loeschen + VACUUM."""
|
||||
import sqlite3, os
|
||||
db_path = os.environ.get('DB_PATH', '/data/fuenfvoracht.db')
|
||||
db_path = os.environ.get('DB_PATH', '/data/fuenfvoacht.db')
|
||||
con = sqlite3.connect(db_path)
|
||||
# post_history älter als 90 Tage
|
||||
r1 = con.execute("DELETE FROM post_history WHERE posted_at < datetime('now', '-90 days')").rowcount
|
||||
# article_versions älter als 90 Tage
|
||||
r2 = con.execute("DELETE FROM article_versions WHERE created_at < datetime('now', '-90 days')").rowcount
|
||||
# Artikel die bereits gepostet sind und älter als 180 Tage
|
||||
r3 = con.execute("DELETE FROM articles WHERE status='posted' AND date < date('now', '-180 days')").rowcount
|
||||
con.execute("VACUUM")
|
||||
con.commit()
|
||||
con.close()
|
||||
flog.info('db_cleanup', post_history_deleted=r1, versions_deleted=r2, articles_deleted=r3)
|
||||
vac = sqlite3.connect(db_path, isolation_level=None)
|
||||
vac.execute("VACUUM")
|
||||
vac.close()
|
||||
flog.info('db_cleanup', ph_del=r1, av_del=r2, art_del=r3)
|
||||
|
||||
|
||||
async def job_reminder_afternoon(bot: Bot):
|
||||
"""Nachmittags-Reminder wenn Hauptslot noch nicht freigegeben."""
|
||||
"""Reminder: Entwurf zur Prüfung, oder Hinweis wenn gar nichts da ist."""
|
||||
d = today_str()
|
||||
channel = db.get_channel()
|
||||
post_t = channel.get('post_time', POST_TIME)
|
||||
art = db.get_article_by_date(d, post_t)
|
||||
if art and art['status'] in ('sent_to_bot', 'pending_review', 'draft', 'scheduled'):
|
||||
await notify_reviewers(
|
||||
bot,
|
||||
f"⚠️ <b>Noch nicht freigegeben!</b>\n\n"
|
||||
f"Posting um {post_t} Uhr — bitte jetzt freigeben.",
|
||||
reply_markup=review_keyboard(d, post_t) if art['status'] == 'sent_to_bot' else None
|
||||
articles = db.get_articles_by_date(d)
|
||||
active_statuses = {'approved', 'scheduled', 'sent_to_bot', 'posted'}
|
||||
active_articles = [a for a in articles if a.get('status') in active_statuses]
|
||||
draft_articles = [a for a in articles if a.get('status') in ('draft', 'pending_review')]
|
||||
|
||||
if active_articles:
|
||||
flog.info(
|
||||
'afternoon_reminder_skipped_article_exists',
|
||||
date=d,
|
||||
slots=[a.get('post_time') for a in active_articles],
|
||||
)
|
||||
return
|
||||
|
||||
if draft_articles:
|
||||
lines = [
|
||||
f"📝 <b>Entwurf wartet auf Freigabe</b>\n",
|
||||
f"📅 Heute ({d}) — Auto-5vor8 hat einen Entwurf angelegt, der noch nicht freigegeben ist:\n",
|
||||
]
|
||||
for a in draft_articles:
|
||||
preview = (a.get('content_final') or a.get('content_raw') or '')[:55]
|
||||
lines.append(f" • {a['post_time']} Uhr — {preview}…")
|
||||
lines.append(
|
||||
"\n⚠️ Der Faktencheck war nicht zufriedenstellend — bitte im Dashboard prüfen, "
|
||||
"anpassen und freigeben, sonst geht heute nichts raus."
|
||||
)
|
||||
lines.append("\n👉 https://fuenfvoracht.orbitalo.net")
|
||||
await notify_reviewers(bot, '\n'.join(lines))
|
||||
flog.info('afternoon_reminder_draft_pending', date=d, count=len(draft_articles))
|
||||
return
|
||||
|
||||
channel = db.get_channel()
|
||||
post_t = channel.get('post_time', POST_TIME) if channel else POST_TIME
|
||||
await notify_reviewers(
|
||||
bot,
|
||||
f"⚠️ <b>Noch kein Artikel für heute!</b>\n\n"
|
||||
f"Weder freigegeben noch als Entwurf vorhanden.\n"
|
||||
f"Geplanter Slot: {post_t} Uhr.\n"
|
||||
f"👉 https://fuenfvoracht.orbitalo.net"
|
||||
)
|
||||
|
||||
|
||||
# ── Main ──────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -383,7 +508,7 @@ def main():
|
|||
morning=f"{rem_m_h:02d}:{rem_m_m:02d}",
|
||||
afternoon=f"{rem_a_h:02d}:{rem_a_m:02d}")
|
||||
else:
|
||||
rem_m_h, rem_m_m = 10, 0
|
||||
rem_m_h, rem_m_m = 15, 0
|
||||
rem_a_h, rem_a_m = 17, 45
|
||||
|
||||
scheduler = AsyncIOScheduler(timezone=TZ)
|
||||
|
|
@ -393,7 +518,7 @@ def main():
|
|||
# Alle 5 Min auf fällige Benachrichtigungen prüfen
|
||||
scheduler.add_job(job_check_notify, 'cron', minute='*/5',
|
||||
kwargs={'bot': bot})
|
||||
# Morgen-Briefing 10:00 MEZ
|
||||
# Tages-Briefing 15:00 — nach Dr.-Bine-RSS-Lauf (14:00) + Auto-5vor8
|
||||
scheduler.add_job(job_morning_briefing, 'cron',
|
||||
hour=rem_m_h, minute=rem_m_m,
|
||||
kwargs={'bot': bot})
|
||||
|
|
@ -401,7 +526,6 @@ def main():
|
|||
scheduler.add_job(job_reminder_afternoon, 'cron',
|
||||
hour=rem_a_h, minute=rem_a_m,
|
||||
kwargs={'bot': bot})
|
||||
# Wöchentliche DB-Bereinigung (Sonntags 03:00 Uhr)
|
||||
scheduler.add_job(job_cleanup_db, 'cron', day_of_week='sun', hour=3, minute=0)
|
||||
scheduler.start()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue