Bot fragt CT109 /api/auto5vor8/style-preview ab und zeigt den rotierenden Auto-5vor8-Stil (heute/morgen) im 15-Uhr-Briefing an.
568 lines
23 KiB
Python
568 lines
23 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
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
|
||
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, Bot
|
||
from telegram.ext import (Application, CallbackQueryHandler, CommandHandler,
|
||
MessageHandler, filters, ContextTypes)
|
||
from telegram.constants import ParseMode
|
||
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'))
|
||
POST_TIME = os.environ.get('POST_TIME', '19:55')
|
||
|
||
BRAND_MARKER = "Pax et Lux Terranaut01 https://t.me/DieneDemLeben"
|
||
BRAND_SIGNATURE = (
|
||
"Wir schützen die Zukunft unserer Kinder und das Leben❤️\n\n"
|
||
"Pax et Lux Terranaut01 https://t.me/DieneDemLeben\n\n"
|
||
"Unterstützt die Menschen, die für Uns einstehen❗️"
|
||
)
|
||
|
||
edit_pending = {}
|
||
|
||
|
||
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 = 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
|
||
|
||
|
||
def review_keyboard(date_str: str, post_time: str):
|
||
return InlineKeyboardMarkup([[
|
||
InlineKeyboardButton("✅ Freigeben", callback_data=f"approve:{date_str}:{post_time}"),
|
||
InlineKeyboardButton("✏️ Bearbeiten", callback_data=f"edit:{date_str}:{post_time}"),
|
||
]])
|
||
|
||
|
||
def is_reviewer(user_id: int) -> bool:
|
||
return user_id in db.get_reviewer_chat_ids()
|
||
|
||
|
||
async def notify_reviewers(bot: Bot, text: str, parse_mode=ParseMode.HTML,
|
||
reply_markup=None):
|
||
for chat_id in db.get_reviewer_chat_ids():
|
||
try:
|
||
await bot.send_message(chat_id, text,
|
||
parse_mode=parse_mode,
|
||
reply_markup=reply_markup)
|
||
except Exception as e:
|
||
flog.error('notify_reviewer_failed', chat_id=chat_id, reason=str(e))
|
||
|
||
|
||
# ── Commands ──────────────────────────────────────────────────────────────────
|
||
|
||
async def cmd_start(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||
if not is_reviewer(update.effective_user.id):
|
||
return
|
||
await update.message.reply_text(
|
||
"🕗 <b>FünfVorAcht — Review Bot</b>\n\n"
|
||
"Artikel werden im Dashboard erstellt und eingeplant.\n"
|
||
"Hier kannst du sie freigeben oder letzte Änderungen vornehmen.\n\n"
|
||
"<b>Befehle:</b>\n"
|
||
"/heute — Alle Slots von heute\n"
|
||
"/queue — Nächste 3 Tage\n"
|
||
"/skip — Heutigen Hauptslot überspringen",
|
||
parse_mode=ParseMode.HTML
|
||
)
|
||
|
||
|
||
async def cmd_heute(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||
if not is_reviewer(update.effective_user.id):
|
||
return
|
||
d = today_str()
|
||
articles = db.get_articles_by_date(d)
|
||
if not articles:
|
||
await update.message.reply_text(
|
||
f"📭 Noch keine Artikel für heute ({d}).\n"
|
||
"👉 Dashboard: http://100.73.171.62:8080"
|
||
)
|
||
return
|
||
status_map = {
|
||
'draft': '📝 Entwurf',
|
||
'scheduled': '🗓️ Eingeplant',
|
||
'sent_to_bot': '📱 Zum Review gesendet',
|
||
'approved': '✅ Freigegeben',
|
||
'posted': '📤 Gepostet ✓',
|
||
'skipped': '⏭️ Übersprungen',
|
||
'pending_review': '⏳ Wartet auf Freigabe',
|
||
}
|
||
lines = [f"📅 <b>{d}</b> — {len(articles)} Slot(s)\n"]
|
||
for art in articles:
|
||
lines.append(
|
||
f"<b>{art['post_time']} Uhr</b> · "
|
||
f"{status_map.get(art['status'], art['status'])} · "
|
||
f"v{art['version']}"
|
||
)
|
||
await update.message.reply_text('\n'.join(lines), parse_mode=ParseMode.HTML)
|
||
|
||
|
||
async def cmd_queue(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||
if not is_reviewer(update.effective_user.id):
|
||
return
|
||
lines = ["📆 <b>Nächste 3 Tage:</b>\n"]
|
||
status_icons = {
|
||
'draft': '📝', 'scheduled': '🗓️', 'sent_to_bot': '📱',
|
||
'approved': '✅', 'posted': '📤', 'skipped': '⏭️',
|
||
}
|
||
for i in range(3):
|
||
d = (datetime.now(TZ) + timedelta(days=i)).strftime('%Y-%m-%d')
|
||
arts = db.get_articles_by_date(d)
|
||
if arts:
|
||
slots = ', '.join(
|
||
f"{a['post_time']} {status_icons.get(a['status'], '❓')}"
|
||
for a in arts
|
||
)
|
||
lines.append(f"<b>{d}</b>: {slots}")
|
||
else:
|
||
lines.append(f"❌ <b>{d}</b> — keine Artikel")
|
||
await update.message.reply_text('\n'.join(lines), parse_mode=ParseMode.HTML)
|
||
|
||
|
||
async def cmd_skip(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||
if not is_reviewer(update.effective_user.id):
|
||
return
|
||
d = today_str()
|
||
channel = db.get_channel()
|
||
pt = channel.get('post_time', POST_TIME)
|
||
art = db.get_article_by_date(d, pt)
|
||
if not art:
|
||
db.create_article(d, "SKIP", "", None, "allgemein", pt)
|
||
db.update_article_status(d, 'skipped', post_time=pt)
|
||
flog.article_skipped(d, pt)
|
||
await update.message.reply_text(f"⏭️ {d} {pt} Uhr übersprungen.")
|
||
|
||
|
||
# ── Callbacks ─────────────────────────────────────────────────────────────────
|
||
|
||
async def handle_callback(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||
query = update.callback_query
|
||
await query.answer()
|
||
parts = query.data.split(':', 2)
|
||
action = parts[0]
|
||
date_str = parts[1] if len(parts) > 1 else today_str()
|
||
post_time = parts[2] if len(parts) > 2 else POST_TIME
|
||
|
||
if action == "approve":
|
||
db.update_article_status(date_str, 'approved',
|
||
query.message.message_id,
|
||
query.message.chat_id,
|
||
post_time=post_time)
|
||
article = db.get_article_by_date(date_str, post_time)
|
||
flog.article_approved(date_str, post_time, query.message.chat_id)
|
||
await query.edit_message_text(
|
||
f"✅ <b>Freigegeben!</b>\n\n"
|
||
f"Wird automatisch um <b>{post_time} Uhr</b> gepostet.\n\n"
|
||
f"{article['content_final']}",
|
||
parse_mode=ParseMode.HTML
|
||
)
|
||
|
||
elif action == "edit":
|
||
article = db.get_article_by_date(date_str, post_time)
|
||
edit_pending[f"{date_str}:{post_time}"] = True
|
||
await query.edit_message_text(
|
||
f"✏️ <b>Bearbeiten</b> — {date_str} {post_time} Uhr\n\n"
|
||
f"Schick mir den neuen Text als nächste Nachricht.\n\n"
|
||
f"<i>Aktueller Text:</i>\n{article['content_final']}",
|
||
parse_mode=ParseMode.HTML
|
||
)
|
||
|
||
|
||
# ── Textnachrichten ───────────────────────────────────────────────────────────
|
||
|
||
async def handle_message(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||
if update.effective_user is None:
|
||
return
|
||
if not is_reviewer(update.effective_user.id):
|
||
return
|
||
# Suche ob ein Edit-Pending-Slot aktiv ist
|
||
pending_key = next((k for k in edit_pending), None)
|
||
if not pending_key:
|
||
await update.message.reply_text(
|
||
"ℹ️ Artikel werden im Dashboard erstellt.\n"
|
||
"http://100.73.171.62:8080"
|
||
)
|
||
return
|
||
date_str, post_time = pending_key.split(':', 1)
|
||
new_text = update.message.text.strip()
|
||
db.update_article_content(date_str, new_text, post_time=post_time)
|
||
del edit_pending[pending_key]
|
||
await update.message.reply_text(
|
||
f"✏️ <b>Aktualisiert</b> — {date_str} {post_time} Uhr\n\n{new_text}",
|
||
parse_mode=ParseMode.HTML,
|
||
reply_markup=review_keyboard(date_str, post_time)
|
||
)
|
||
db.update_article_status(date_str, 'sent_to_bot',
|
||
update.message.message_id,
|
||
update.effective_chat.id,
|
||
post_time=post_time)
|
||
|
||
|
||
# ── Scheduler Jobs ────────────────────────────────────────────────────────────
|
||
|
||
async def job_post_articles(bot: Bot):
|
||
"""Postet alle freigegebenen Artikel deren post_time jetzt fällig ist."""
|
||
now_berlin = datetime.now(TZ)
|
||
d = now_berlin.strftime('%Y-%m-%d')
|
||
current_slot = now_berlin.strftime('%H:%M')
|
||
|
||
articles = db.get_articles_by_date(d)
|
||
for article in articles:
|
||
if article['status'] != 'approved':
|
||
continue
|
||
if article['post_time'] != current_slot:
|
||
continue
|
||
if not CHANNEL_ID:
|
||
await notify_reviewers(bot, "⚠️ Kanal-ID nicht konfiguriert!")
|
||
flog.error('no_channel_id', date=d, post_time=current_slot)
|
||
return
|
||
try:
|
||
final_text = with_branding(article['content_final'])
|
||
|
||
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"
|
||
)
|
||
except Exception as e:
|
||
flog.posting_failed(d, current_slot, str(e))
|
||
await notify_reviewers(
|
||
bot,
|
||
f"❌ <b>Posting fehlgeschlagen!</b>\n\n"
|
||
f"📅 {d} · ⏰ {current_slot} Uhr\n"
|
||
f"Kanal: {CHANNEL_ID}\n\n"
|
||
f"<b>Ursache:</b> {str(e)[:250]}\n\n"
|
||
f"👉 Dashboard: http://100.73.171.62:8080"
|
||
)
|
||
|
||
|
||
async def job_check_notify(bot: Bot):
|
||
"""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']
|
||
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."
|
||
)
|
||
flog.info('article_auto_approved', date=d, post_time=pt)
|
||
|
||
|
||
def _fetch_style_preview():
|
||
"""Holt heutigen/morgigen 5vor8-Schreibstil von rss-manager (CT109)."""
|
||
try:
|
||
req = urllib.request.Request(
|
||
"http://10.10.10.109:8080/api/auto5vor8/style-preview",
|
||
headers={"User-Agent": "fuenfvoracht-briefing/1.0"},
|
||
)
|
||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||
import json
|
||
return json.loads(resp.read().decode("utf-8"))
|
||
except Exception:
|
||
return None
|
||
|
||
|
||
async def job_morning_briefing(bot: Bot):
|
||
"""Morgen-Briefing: was ist heute geplant, was fehlt."""
|
||
d = today_str()
|
||
now_berlin = datetime.now(TZ)
|
||
channel = db.get_channel()
|
||
|
||
articles_today = db.get_articles_by_date(d)
|
||
approved = [a for a in articles_today if a['status'] in ('approved', 'scheduled', 'sent_to_bot')]
|
||
missing = [a for a in articles_today if a['status'] in ('draft', 'pending_review')]
|
||
|
||
# Nächste 3 Tage für Ausblick
|
||
plan_lines = []
|
||
for i in range(1, 4):
|
||
next_d = (now_berlin + timedelta(days=i)).strftime('%Y-%m-%d')
|
||
arts = db.get_articles_by_date(next_d)
|
||
if arts:
|
||
slots = ', '.join(f"{a['post_time']} ✅" if a['status'] in ('approved', 'scheduled') else f"{a['post_time']} 📝" for a in arts)
|
||
plan_lines.append(f" {next_d}: {slots}")
|
||
else:
|
||
plan_lines.append(f" {next_d}: ❌ leer")
|
||
|
||
lines = [f"☀️ <b>Guten Morgen — FünfVorAcht Briefing</b>\n",
|
||
f"📅 <b>Heute: {d}</b>"]
|
||
|
||
if approved:
|
||
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(
|
||
"⚠️ <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 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)
|
||
|
||
style_preview = _fetch_style_preview()
|
||
if style_preview:
|
||
lines.append(f"\n🎨 <b>Schreibstil Auto-5vor8:</b>")
|
||
lines.append(f" Heute: {style_preview[today][style]}")
|
||
lines.append(f" Morgen: {style_preview[tomorrow][style]}")
|
||
|
||
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():
|
||
"""Woechentliche DB-Bereinigung: alte Eintraege loeschen + VACUUM."""
|
||
import sqlite3, os
|
||
db_path = os.environ.get('DB_PATH', '/data/fuenfvoacht.db')
|
||
con = sqlite3.connect(db_path)
|
||
r1 = con.execute("DELETE FROM post_history WHERE posted_at < datetime('now', '-90 days')").rowcount
|
||
r2 = con.execute("DELETE FROM article_versions WHERE created_at < datetime('now', '-90 days')").rowcount
|
||
r3 = con.execute("DELETE FROM articles WHERE status='posted' AND date < date('now', '-180 days')").rowcount
|
||
con.commit()
|
||
con.close()
|
||
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):
|
||
"""Reminder: Entwurf zur Prüfung, oder Hinweis wenn gar nichts da ist."""
|
||
d = today_str()
|
||
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 ──────────────────────────────────────────────────────────────────────
|
||
|
||
def main():
|
||
db.init_db()
|
||
|
||
# Standard-Reviewer aus ENV falls DB leer
|
||
reviewers_in_db = db.get_reviewers()
|
||
if not reviewers_in_db:
|
||
import os as _os
|
||
raw = _os.environ.get('REVIEW_CHAT_IDS', '')
|
||
admin = _os.environ.get('ADMIN_CHAT_ID', '')
|
||
ids_raw = [x.strip() for x in raw.split(',') if x.strip()] if raw else []
|
||
if admin and admin not in ids_raw:
|
||
ids_raw.append(admin)
|
||
for cid in ids_raw:
|
||
try:
|
||
db.add_reviewer(int(cid), f"Redakteur {cid}")
|
||
except Exception:
|
||
pass
|
||
|
||
application = Application.builder().token(BOT_TOKEN).build()
|
||
bot = application.bot
|
||
|
||
channel = db.get_channel()
|
||
post_t = channel.get('post_time', POST_TIME) if channel else POST_TIME
|
||
|
||
location = db.get_current_location()
|
||
if location:
|
||
(rem_m_h, rem_m_m), (rem_a_h, rem_a_m) = db.get_reminder_times_in_berlin(location)
|
||
flog.info('scheduler_init',
|
||
location=location['name'],
|
||
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 = 15, 0
|
||
rem_a_h, rem_a_m = 17, 45
|
||
|
||
scheduler = AsyncIOScheduler(timezone=TZ)
|
||
# Jede Minute prüfen ob ein Artikel zu posten ist
|
||
scheduler.add_job(job_post_articles, 'cron', minute='*',
|
||
kwargs={'bot': bot})
|
||
# Alle 5 Min auf fällige Benachrichtigungen prüfen
|
||
scheduler.add_job(job_check_notify, 'cron', minute='*/5',
|
||
kwargs={'bot': bot})
|
||
# 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})
|
||
# Nachmittags-Reminder
|
||
scheduler.add_job(job_reminder_afternoon, 'cron',
|
||
hour=rem_a_h, minute=rem_a_m,
|
||
kwargs={'bot': bot})
|
||
scheduler.add_job(job_cleanup_db, 'cron', day_of_week='sun', hour=3, minute=0)
|
||
scheduler.start()
|
||
|
||
flog.info('bot_started', post_time=post_t,
|
||
briefing=f"{rem_m_h:02d}:{rem_m_m:02d}",
|
||
afternoon=f"{rem_a_h:02d}:{rem_a_m:02d}")
|
||
|
||
application.add_handler(CommandHandler("start", cmd_start))
|
||
application.add_handler(CommandHandler("heute", cmd_heute))
|
||
application.add_handler(CommandHandler("queue", cmd_queue))
|
||
application.add_handler(CommandHandler("skip", cmd_skip))
|
||
application.add_handler(CallbackQueryHandler(handle_callback))
|
||
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
|
||
|
||
application.run_polling(drop_pending_updates=True)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|