diff --git a/fuenfvoracht/src/app.py b/fuenfvoracht/src/app.py index eea568dfa..8de67eeea 100644 --- a/fuenfvoracht/src/app.py +++ b/fuenfvoracht/src/app.py @@ -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"⚠️ Auto-5vor8: Bitte manuell prΓΌfen\n\n" + f"πŸ“… Slot: {date_str} {post_time}\n" + f"πŸ“° {title_line}\n" + ) + if reason_line: + msg += f"\n❗ Faktencheck: {reason_line}\n" + msg += ( + f"\nDer KI-Text hat die automatische QualitΓ€tsprΓΌfung nicht bestanden. " + f"Ein Entwurf liegt im Dashboard β€” bitte lesen, ggf. anpassen und freigeben.\n\n" + f"πŸ‘‰ https://fuenfvoracht.orbitalo.net" + ) + else: + msg = ( + f"🚨 Auto-5vor8: Generierung fehlgeschlagen\n\n" + f"πŸ“° {title_line}\n" + ) + if reason_line: + msg += f"\n❗ {reason_line}\n" + msg += ( + f"\nEs wurde kein 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"βœ… Auto-5vor8: Artikel eingeplant\n\n" + f"πŸ“… {date_str} Β· {post_time} Uhr\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//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"πŸ“… Artikel eingeplant\n\n" + f"πŸ“† {date_str} um {post_time} Uhr\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//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__': diff --git a/fuenfvoracht/src/bot.py b/fuenfvoracht/src/bot.py index 3a3b50aaa..9c2222201 100644 --- a/fuenfvoracht/src/bot.py +++ b/fuenfvoracht/src/bot.py @@ -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"β›” Posting abgebrochen β€” kaputter Link!\n\n" + f"πŸ“… {d} Β· ⏰ {current_slot} Uhr\n\n" + f"Fehler:\n{link_err}\n\n" + f"Artikel bleibt auf approved (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"πŸ“€ FΓΌnf vor Acht gepostet!\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"πŸ“‹ Review: {d} Β· {pt} Uhr\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"πŸ“… Artikel eingeplant\n\n" + f"πŸ“† {d} um {pt} Uhr\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("⚠️ Entwurf wartet auf Freigabe (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( + "⚠️ Heute noch kein Artikel im Plan.\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πŸ“† NΓ€chste 3 Tage:") 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"⚠️ Noch nicht freigegeben!\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"πŸ“ Entwurf wartet auf Freigabe\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"⚠️ Noch kein Artikel fΓΌr heute!\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()