- CT 112 auf pve-hetzner: Bot + Dashboard produktiv - Strukturiertes JSON-Logging (logger.py, /logs/fuenfvoracht.log) - 15-Min-Zeitslots: UNIQUE(date, post_time), DB-Migration, Konflikterkennung - Einplan-Flow: scheduled-Status, notify_at, automatische Bot-Benachrichtigung - Board-API: Umplanen (reschedule) + Löschen per Article-ID - Morgen-Briefing täglich 10:00 MEZ: Tagesplan + Ausblick 3 Tage - Fehler-Alarm: detaillierte Meldung an alle Redakteure bei Posting-Fehler - Reviewer-Verwaltung: DB-Tabelle, API add/remove, Willkommensnachricht - Zweiter Redakteur (1329146910) parallel eingebunden - Markenzeichen automatisch unter jeden Beitrag (Duplikat-Schutz) - Tailwind CSS self-hosted im Docker-Image (kein CDN, schnelle Ladezeiten) - TELEGRAM_CHANNEL_ID gesetzt (-1001285446620) - Hilfe-Seite /hilfe: 6 Aufgabenbereiche mit Klickpfaden - STATE.md aktualisiert und vollständig dokumentiert Made-with: Cursor
694 lines
23 KiB
Python
694 lines
23 KiB
Python
import sqlite3
|
|
import os
|
|
import logging
|
|
from datetime import datetime
|
|
|
|
import logger as flog
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
DB_PATH = os.environ.get('DB_PATH', '/data/fuenfvoracht.db')
|
|
|
|
DEFAULT_PROMPT = '''Du erstellst einen strukturierten Beitrag für den Telegram-Kanal "Fünf vor Acht".
|
|
Der Beitrag präsentiert einen Inhalt (Video, Artikel, Vortrag) neutral und informativ.
|
|
Leser sollen sich selbst ein Bild machen können.
|
|
|
|
EINGABE: {source}
|
|
DATUM: {date}
|
|
THEMA: {tag}
|
|
|
|
AUFGABE:
|
|
Analysiere die Quelle und erstelle einen Telegram-Beitrag nach exakt diesem FORMAT.
|
|
Wähle passende Emojis für die Sektions-Überschriften je nach Thema.
|
|
Schreibe sachlich und ohne eigene Wertung.
|
|
|
|
FORMAT — exakt so ausgeben (Telegram-kompatibel, kein HTML):
|
|
|
|
[Kategorie-Emoji] [Typ]: [Vollständiger Titel]
|
|
|
|
🔗 [Quelle ansehen / Artikel lesen / Video ansehen]:
|
|
[URL aus der Eingabe]
|
|
|
|
[Themen-Emoji] Inhaltlicher Schwerpunkt
|
|
|
|
[2-3 Sätze: Wer spricht/schreibt worüber und in welchem Kontext]
|
|
|
|
Themen im Überblick:
|
|
• [Kernthema 1]
|
|
• [Kernthema 2]
|
|
• [Kernthema 3]
|
|
• [Kernthema 4]
|
|
• [Kernthema 5]
|
|
|
|
[2. Themen-Emoji] [Zweiter Schwerpunkt falls vorhanden]
|
|
|
|
[2-3 Sätze zum zweiten Teil]
|
|
|
|
• [Unterpunkt 1]
|
|
• [Unterpunkt 2]
|
|
• [Unterpunkt 3]
|
|
|
|
📌 Einordnung
|
|
|
|
[2-3 Sätze: Neutrale Beschreibung des Formats/der Methode. Kein Urteil. Leser können Quellen selbst prüfen.]'''
|
|
|
|
|
|
def get_conn():
|
|
conn = sqlite3.connect(DB_PATH, check_same_thread=False)
|
|
conn.row_factory = sqlite3.Row
|
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
conn.execute("PRAGMA foreign_keys=ON")
|
|
return conn
|
|
|
|
|
|
def init_db():
|
|
conn = get_conn()
|
|
c = conn.cursor()
|
|
|
|
# Basis-Tabellen
|
|
c.executescript('''
|
|
CREATE TABLE IF NOT EXISTS articles (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
date TEXT NOT NULL,
|
|
post_time TEXT NOT NULL DEFAULT '19:55',
|
|
source_input TEXT,
|
|
content_raw TEXT,
|
|
content_final TEXT,
|
|
status TEXT DEFAULT 'draft',
|
|
version INTEGER DEFAULT 1,
|
|
review_message_id INTEGER,
|
|
review_chat_id INTEGER,
|
|
prompt_id INTEGER,
|
|
tag TEXT,
|
|
notify_at TEXT,
|
|
scheduled_at TEXT,
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
sent_to_bot_at TEXT,
|
|
approved_at TEXT,
|
|
posted_at TEXT,
|
|
UNIQUE(date, post_time)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS article_versions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
article_id INTEGER NOT NULL,
|
|
version_nr INTEGER NOT NULL,
|
|
content TEXT,
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
FOREIGN KEY (article_id) REFERENCES articles(id)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS post_history (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
article_id INTEGER NOT NULL,
|
|
channel_message_id INTEGER,
|
|
posted_at TEXT DEFAULT (datetime('now')),
|
|
FOREIGN KEY (article_id) REFERENCES articles(id)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS prompts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
system_prompt TEXT NOT NULL,
|
|
is_default INTEGER DEFAULT 0,
|
|
last_tested_at TEXT,
|
|
test_result TEXT,
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
CREATE TABLE IF NOT EXISTS sources_favorites (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
label TEXT NOT NULL,
|
|
url TEXT,
|
|
used_count INTEGER DEFAULT 0,
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
CREATE TABLE IF NOT EXISTS tags (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE
|
|
);
|
|
CREATE TABLE IF NOT EXISTS article_tags (
|
|
article_id INTEGER NOT NULL,
|
|
tag_id INTEGER NOT NULL,
|
|
PRIMARY KEY (article_id, tag_id),
|
|
FOREIGN KEY (article_id) REFERENCES articles(id),
|
|
FOREIGN KEY (tag_id) REFERENCES tags(id)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS channels (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
telegram_id TEXT,
|
|
post_time TEXT DEFAULT '19:55',
|
|
timezone TEXT DEFAULT 'Europe/Berlin',
|
|
active INTEGER DEFAULT 1
|
|
);
|
|
CREATE TABLE IF NOT EXISTS locations (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
flag TEXT NOT NULL,
|
|
timezone TEXT NOT NULL,
|
|
reminder_morning TEXT DEFAULT '10:00',
|
|
reminder_afternoon TEXT DEFAULT '18:00'
|
|
);
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT
|
|
);
|
|
CREATE TABLE IF NOT EXISTS reviewers (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
chat_id INTEGER NOT NULL UNIQUE,
|
|
name TEXT NOT NULL,
|
|
active INTEGER DEFAULT 1,
|
|
added_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
''')
|
|
|
|
# Migration: post_time Spalte zu bestehender articles-Tabelle hinzufügen falls fehlt
|
|
cols = [r[1] for r in c.execute("PRAGMA table_info(articles)").fetchall()]
|
|
if 'post_time' not in cols:
|
|
c.execute("ALTER TABLE articles ADD COLUMN post_time TEXT NOT NULL DEFAULT '19:55'")
|
|
_logger.info("Migration: post_time Spalte hinzugefügt")
|
|
if 'notify_at' not in cols:
|
|
c.execute("ALTER TABLE articles ADD COLUMN notify_at TEXT")
|
|
if 'scheduled_at' not in cols:
|
|
c.execute("ALTER TABLE articles ADD COLUMN scheduled_at TEXT")
|
|
|
|
# reviewers-Tabelle Migration
|
|
reviewer_cols = [r[1] for r in c.execute("PRAGMA table_info(reviewers)").fetchall()]
|
|
if not reviewer_cols:
|
|
_logger.info("Migration: reviewers Tabelle erstellt")
|
|
|
|
# Standard-Daten
|
|
c.execute("SELECT COUNT(*) FROM prompts")
|
|
if c.fetchone()[0] == 0:
|
|
c.execute(
|
|
"INSERT INTO prompts (name, system_prompt, is_default) VALUES (?, ?, 1)",
|
|
("Standard", DEFAULT_PROMPT)
|
|
)
|
|
c.execute("SELECT COUNT(*) FROM channels")
|
|
if c.fetchone()[0] == 0:
|
|
c.execute(
|
|
"INSERT INTO channels (name, telegram_id, post_time, timezone) VALUES (?, ?, ?, ?)",
|
|
("Fünf vor Acht", "", "19:55", "Europe/Berlin")
|
|
)
|
|
for tag in ["Politik", "Wirtschaft", "Tech", "Gesellschaft", "Umwelt", "Kultur", "Sport"]:
|
|
c.execute("INSERT OR IGNORE INTO tags (name) VALUES (?)", (tag,))
|
|
|
|
locations = [
|
|
("Deutschland", "🇩🇪", "Europe/Berlin", "10:00", "18:00"),
|
|
("Kambodscha", "🇰🇭", "Asia/Phnom_Penh", "10:00", "18:00"),
|
|
("Thailand", "🇹🇭", "Asia/Bangkok", "10:00", "18:00"),
|
|
("USA Ostküste", "🇺🇸", "America/New_York", "10:00", "18:00"),
|
|
("USA Westküste","🇺🇸", "America/Los_Angeles", "10:00", "18:00"),
|
|
("Spanien", "🇪🇸", "Europe/Madrid", "10:00", "18:00"),
|
|
]
|
|
c.execute("SELECT COUNT(*) FROM locations")
|
|
if c.fetchone()[0] == 0:
|
|
for loc in locations:
|
|
c.execute(
|
|
"INSERT INTO locations (name, flag, timezone, reminder_morning, reminder_afternoon) VALUES (?,?,?,?,?)",
|
|
loc
|
|
)
|
|
|
|
c.execute("INSERT OR IGNORE INTO settings (key, value) VALUES ('user_location_id', '1')")
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
_logger.info("DB initialisiert: %s", DB_PATH)
|
|
|
|
|
|
# ── Article CRUD ──────────────────────────────────────────────────────────────
|
|
|
|
def get_article_by_date(date_str, post_time=None):
|
|
"""Gibt ersten Artikel des Tages zurück, oder den mit spezifischer post_time."""
|
|
conn = get_conn()
|
|
if post_time:
|
|
row = conn.execute(
|
|
"SELECT * FROM articles WHERE date=? AND post_time=?", (date_str, post_time)
|
|
).fetchone()
|
|
else:
|
|
row = conn.execute(
|
|
"SELECT * FROM articles WHERE date=? ORDER BY post_time ASC LIMIT 1", (date_str,)
|
|
).fetchone()
|
|
conn.close()
|
|
return dict(row) if row else None
|
|
|
|
|
|
def get_articles_by_date(date_str):
|
|
"""Gibt alle Artikel eines Tages zurück (mehrere Slots)."""
|
|
conn = get_conn()
|
|
rows = conn.execute(
|
|
"SELECT * FROM articles WHERE date=? ORDER BY post_time ASC", (date_str,)
|
|
).fetchall()
|
|
conn.close()
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
def get_article_by_id(article_id):
|
|
conn = get_conn()
|
|
row = conn.execute("SELECT * FROM articles WHERE id=?", (article_id,)).fetchone()
|
|
conn.close()
|
|
return dict(row) if row else None
|
|
|
|
|
|
def slot_is_taken(date_str, post_time, exclude_id=None):
|
|
"""Prüft ob ein Zeitslot bereits belegt ist."""
|
|
conn = get_conn()
|
|
if exclude_id:
|
|
row = conn.execute(
|
|
"SELECT id FROM articles WHERE date=? AND post_time=? AND id!=? AND status NOT IN ('skipped','rejected')",
|
|
(date_str, post_time, exclude_id)
|
|
).fetchone()
|
|
else:
|
|
row = conn.execute(
|
|
"SELECT id FROM articles WHERE date=? AND post_time=? AND status NOT IN ('skipped','rejected')",
|
|
(date_str, post_time)
|
|
).fetchone()
|
|
conn.close()
|
|
return row is not None
|
|
|
|
|
|
def get_taken_slots(date_str):
|
|
"""Gibt alle belegten Zeitslots eines Tages zurück."""
|
|
conn = get_conn()
|
|
rows = conn.execute(
|
|
"SELECT post_time FROM articles WHERE date=? AND status NOT IN ('skipped','rejected')",
|
|
(date_str,)
|
|
).fetchall()
|
|
conn.close()
|
|
return [r[0] for r in rows]
|
|
|
|
|
|
def create_article(date_str, source_input, content, prompt_id, tag="allgemein", post_time="19:55"):
|
|
conn = get_conn()
|
|
try:
|
|
conn.execute(
|
|
"INSERT INTO articles (date, post_time, source_input, content_raw, content_final, prompt_id, tag) VALUES (?,?,?,?,?,?,?)",
|
|
(date_str, post_time, source_input, content, content, prompt_id, tag)
|
|
)
|
|
conn.execute(
|
|
"INSERT INTO article_versions (article_id, version_nr, content) "
|
|
"VALUES ((SELECT id FROM articles WHERE date=? AND post_time=?), 1, ?)",
|
|
(date_str, post_time, content)
|
|
)
|
|
conn.commit()
|
|
flog.article_generated(date_str, source_input or '', 1, tag)
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def reschedule_article(article_id, new_date, new_post_time):
|
|
"""Verschiebt einen Artikel auf einen neuen Datum/Zeit-Slot."""
|
|
conn = get_conn()
|
|
try:
|
|
conn.execute(
|
|
"UPDATE articles SET date=?, post_time=? WHERE id=?",
|
|
(new_date, new_post_time, article_id)
|
|
)
|
|
conn.commit()
|
|
flog.info('article_rescheduled', article_id=article_id,
|
|
new_date=new_date, new_post_time=new_post_time)
|
|
return True
|
|
except sqlite3.IntegrityError:
|
|
flog.slot_conflict(new_date, new_post_time)
|
|
return False
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def delete_article(article_id):
|
|
conn = get_conn()
|
|
conn.execute("DELETE FROM article_versions WHERE article_id=?", (article_id,))
|
|
conn.execute("DELETE FROM articles WHERE id=?", (article_id,))
|
|
conn.commit()
|
|
conn.close()
|
|
flog.info('article_deleted', article_id=article_id)
|
|
|
|
|
|
def update_article_status(date_str, status, message_id=None, chat_id=None, post_time=None):
|
|
conn = get_conn()
|
|
ts = datetime.utcnow().isoformat()
|
|
where = "date=? AND post_time=?" if post_time else "date=?"
|
|
params_base = (date_str, post_time) if post_time else (date_str,)
|
|
|
|
if status == 'approved':
|
|
conn.execute(
|
|
f"UPDATE articles SET status=?, approved_at=?, review_message_id=?, review_chat_id=? WHERE {where}",
|
|
(status, ts, message_id, chat_id) + params_base
|
|
)
|
|
elif status == 'posted':
|
|
conn.execute(
|
|
f"UPDATE articles SET status=?, posted_at=? WHERE {where}",
|
|
(status, ts) + params_base
|
|
)
|
|
elif status == 'sent_to_bot':
|
|
conn.execute(
|
|
f"UPDATE articles SET status=?, sent_to_bot_at=?, review_message_id=?, review_chat_id=? WHERE {where}",
|
|
(status, ts, message_id, chat_id) + params_base
|
|
)
|
|
elif status == 'scheduled':
|
|
conn.execute(
|
|
f"UPDATE articles SET status=?, scheduled_at=? WHERE {where}",
|
|
(status, ts) + params_base
|
|
)
|
|
else:
|
|
conn.execute(f"UPDATE articles SET status=? WHERE {where}", (status,) + params_base)
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
def schedule_article(date_str, post_time, notify_at):
|
|
"""Artikel einplanen mit Bot-Benachrichtigungs-Zeitpunkt."""
|
|
conn = get_conn()
|
|
ts = datetime.utcnow().isoformat()
|
|
conn.execute(
|
|
"UPDATE articles SET status='scheduled', scheduled_at=?, post_time=?, notify_at=? WHERE date=? AND post_time=?",
|
|
(ts, post_time, notify_at, date_str, post_time)
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
flog.article_scheduled(date_str, post_time, notify_at)
|
|
|
|
|
|
def get_due_notifications():
|
|
"""Gibt alle scheduled-Artikel zurück deren notify_at <= jetzt."""
|
|
conn = get_conn()
|
|
now = datetime.utcnow().isoformat()
|
|
rows = conn.execute(
|
|
"SELECT * FROM articles WHERE status='scheduled' AND notify_at IS NOT NULL AND notify_at <= ?",
|
|
(now,)
|
|
).fetchall()
|
|
conn.close()
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
def update_article_content(date_str, content, new_version=False, post_time=None):
|
|
conn = get_conn()
|
|
where = "date=? AND post_time=?" if post_time else "date=?"
|
|
params_base = (date_str, post_time) if post_time else (date_str,)
|
|
|
|
if new_version:
|
|
version = conn.execute(
|
|
f"SELECT version FROM articles WHERE {where}", params_base
|
|
).fetchone()
|
|
new_v = (version[0] or 1) + 1
|
|
conn.execute(
|
|
f"UPDATE articles SET content_raw=?, content_final=?, version=?, status='pending_review' WHERE {where}",
|
|
(content, content, new_v) + params_base
|
|
)
|
|
article_id = conn.execute(
|
|
f"SELECT id FROM articles WHERE {where}", params_base
|
|
).fetchone()[0]
|
|
conn.execute(
|
|
"INSERT INTO article_versions (article_id, version_nr, content) VALUES (?,?,?)",
|
|
(article_id, new_v, content)
|
|
)
|
|
else:
|
|
conn.execute(
|
|
f"UPDATE articles SET content_final=? WHERE {where}", (content,) + params_base
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
def save_post_history(date_str, channel_message_id, post_time=None):
|
|
conn = get_conn()
|
|
where = "date=? AND post_time=?" if post_time else "date=?"
|
|
params = (date_str, post_time) if post_time else (date_str,)
|
|
article_id = conn.execute(
|
|
f"SELECT id FROM articles WHERE {where}", params
|
|
).fetchone()
|
|
if article_id:
|
|
conn.execute(
|
|
"INSERT INTO post_history (article_id, channel_message_id) VALUES (?,?)",
|
|
(article_id[0], channel_message_id)
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
def get_recent_articles(limit=30):
|
|
conn = get_conn()
|
|
rows = conn.execute(
|
|
"SELECT * FROM articles ORDER BY date DESC, post_time ASC LIMIT ?", (limit,)
|
|
).fetchall()
|
|
conn.close()
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
def get_week_articles(from_date, to_date):
|
|
conn = get_conn()
|
|
rows = conn.execute(
|
|
"SELECT * FROM articles WHERE date BETWEEN ? AND ? ORDER BY date ASC, post_time ASC",
|
|
(from_date, to_date)
|
|
).fetchall()
|
|
conn.close()
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
def get_last_posted():
|
|
conn = get_conn()
|
|
row = conn.execute(
|
|
"SELECT date, post_time, posted_at FROM articles WHERE status='posted' ORDER BY posted_at DESC LIMIT 1"
|
|
).fetchone()
|
|
conn.close()
|
|
return dict(row) if row else None
|
|
|
|
|
|
# ── Prompts ───────────────────────────────────────────────────────────────────
|
|
|
|
def get_prompts():
|
|
conn = get_conn()
|
|
rows = conn.execute("SELECT * FROM prompts ORDER BY is_default DESC, id ASC").fetchall()
|
|
conn.close()
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
def get_default_prompt():
|
|
conn = get_conn()
|
|
row = conn.execute("SELECT * FROM prompts WHERE is_default=1 LIMIT 1").fetchone()
|
|
if not row:
|
|
row = conn.execute("SELECT * FROM prompts LIMIT 1").fetchone()
|
|
conn.close()
|
|
return dict(row) if row else None
|
|
|
|
|
|
def save_prompt(prompt_id, name, system_prompt):
|
|
conn = get_conn()
|
|
conn.execute("UPDATE prompts SET name=?, system_prompt=? WHERE id=?", (name, system_prompt, prompt_id))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
def create_prompt(name, system_prompt):
|
|
conn = get_conn()
|
|
conn.execute("INSERT INTO prompts (name, system_prompt) VALUES (?,?)", (name, system_prompt))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
def set_default_prompt(prompt_id):
|
|
conn = get_conn()
|
|
conn.execute("UPDATE prompts SET is_default=0")
|
|
conn.execute("UPDATE prompts SET is_default=1 WHERE id=?", (prompt_id,))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
def delete_prompt(prompt_id):
|
|
conn = get_conn()
|
|
conn.execute("DELETE FROM prompts WHERE id=? AND is_default=0", (prompt_id,))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
def save_prompt_test_result(prompt_id, result):
|
|
conn = get_conn()
|
|
conn.execute(
|
|
"UPDATE prompts SET test_result=?, last_tested_at=? WHERE id=?",
|
|
(result, datetime.utcnow().isoformat(), prompt_id)
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
# ── Channel / Settings ────────────────────────────────────────────────────────
|
|
|
|
def get_channel():
|
|
conn = get_conn()
|
|
row = conn.execute("SELECT * FROM channels WHERE active=1 LIMIT 1").fetchone()
|
|
conn.close()
|
|
return dict(row) if row else {}
|
|
|
|
|
|
def update_channel(telegram_id, post_time="19:55"):
|
|
conn = get_conn()
|
|
conn.execute(
|
|
"UPDATE channels SET telegram_id=?, post_time=? WHERE active=1",
|
|
(telegram_id, post_time)
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
# ── Reviewers ─────────────────────────────────────────────────────────────────
|
|
|
|
def get_reviewers(active_only=True):
|
|
conn = get_conn()
|
|
if active_only:
|
|
rows = conn.execute(
|
|
"SELECT * FROM reviewers WHERE active=1 ORDER BY added_at ASC"
|
|
).fetchall()
|
|
else:
|
|
rows = conn.execute("SELECT * FROM reviewers ORDER BY added_at ASC").fetchall()
|
|
conn.close()
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
def add_reviewer(chat_id: int, name: str):
|
|
conn = get_conn()
|
|
try:
|
|
conn.execute(
|
|
"INSERT INTO reviewers (chat_id, name, active) VALUES (?,?,1)",
|
|
(chat_id, name)
|
|
)
|
|
conn.commit()
|
|
flog.reviewer_added(chat_id, name)
|
|
return True
|
|
except sqlite3.IntegrityError:
|
|
return False
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def remove_reviewer(chat_id: int):
|
|
conn = get_conn()
|
|
conn.execute("UPDATE reviewers SET active=0 WHERE chat_id=?", (chat_id,))
|
|
conn.commit()
|
|
conn.close()
|
|
flog.reviewer_removed(chat_id)
|
|
|
|
|
|
def get_reviewer_chat_ids():
|
|
"""Gibt aktive Reviewer-Chat-IDs aus DB zurück, Fallback auf ENV."""
|
|
reviewers = get_reviewers(active_only=True)
|
|
if reviewers:
|
|
return [r['chat_id'] for r in reviewers]
|
|
# Fallback: ENV
|
|
import os
|
|
ids = []
|
|
raw = os.environ.get('REVIEW_CHAT_IDS', '')
|
|
if raw.strip():
|
|
for part in raw.split(','):
|
|
try:
|
|
ids.append(int(part.strip()))
|
|
except ValueError:
|
|
pass
|
|
admin = os.environ.get('ADMIN_CHAT_ID', '')
|
|
if admin:
|
|
try:
|
|
ids.append(int(admin))
|
|
except ValueError:
|
|
pass
|
|
unique = []
|
|
for cid in ids:
|
|
if cid not in unique:
|
|
unique.append(cid)
|
|
return unique
|
|
|
|
|
|
# ── Favorites ─────────────────────────────────────────────────────────────────
|
|
|
|
def get_favorites():
|
|
conn = get_conn()
|
|
rows = conn.execute("SELECT * FROM sources_favorites ORDER BY used_count DESC").fetchall()
|
|
conn.close()
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
def add_favorite(label, url):
|
|
conn = get_conn()
|
|
conn.execute("INSERT INTO sources_favorites (label, url) VALUES (?,?)", (label, url))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
def increment_favorite(fav_id):
|
|
conn = get_conn()
|
|
conn.execute("UPDATE sources_favorites SET used_count=used_count+1 WHERE id=?", (fav_id,))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
# ── Tags ──────────────────────────────────────────────────────────────────────
|
|
|
|
def get_tags():
|
|
conn = get_conn()
|
|
rows = conn.execute("SELECT * FROM tags ORDER BY name").fetchall()
|
|
conn.close()
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
# ── Locations ─────────────────────────────────────────────────────────────────
|
|
|
|
def get_locations():
|
|
conn = get_conn()
|
|
rows = conn.execute("SELECT * FROM locations ORDER BY id").fetchall()
|
|
conn.close()
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
def get_current_location():
|
|
conn = get_conn()
|
|
loc_id = conn.execute(
|
|
"SELECT value FROM settings WHERE key='user_location_id'"
|
|
).fetchone()
|
|
if not loc_id:
|
|
conn.close()
|
|
return None
|
|
row = conn.execute("SELECT * FROM locations WHERE id=?", (loc_id[0],)).fetchone()
|
|
conn.close()
|
|
return dict(row) if row else None
|
|
|
|
|
|
def set_location(location_id):
|
|
conn = get_conn()
|
|
conn.execute(
|
|
"INSERT OR REPLACE INTO settings (key, value) VALUES ('user_location_id', ?)",
|
|
(str(location_id),)
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
def get_reminder_times_in_berlin(location: dict) -> tuple:
|
|
import pytz
|
|
from datetime import datetime, date
|
|
user_tz = pytz.timezone(location['timezone'])
|
|
berlin_tz = pytz.timezone('Europe/Berlin')
|
|
today = date.today()
|
|
|
|
def convert(local_time_str):
|
|
h, m = map(int, local_time_str.split(':'))
|
|
local_dt = user_tz.localize(datetime(today.year, today.month, today.day, h, m))
|
|
berlin_dt = local_dt.astimezone(berlin_tz)
|
|
return berlin_dt.hour, berlin_dt.minute
|
|
|
|
return convert(location['reminder_morning']), convert(location['reminder_afternoon'])
|
|
|
|
|
|
# ── Stats ─────────────────────────────────────────────────────────────────────
|
|
|
|
def get_monthly_stats():
|
|
conn = get_conn()
|
|
from datetime import date
|
|
month = date.today().strftime('%Y-%m')
|
|
total = conn.execute(
|
|
"SELECT COUNT(*) FROM articles WHERE date LIKE ?", (f"{month}%",)
|
|
).fetchone()[0]
|
|
posted = conn.execute(
|
|
"SELECT COUNT(*) FROM articles WHERE date LIKE ? AND status='posted'", (f"{month}%",)
|
|
).fetchone()[0]
|
|
skipped = conn.execute(
|
|
"SELECT COUNT(*) FROM articles WHERE date LIKE ? AND status='skipped'", (f"{month}%",)
|
|
).fetchone()[0]
|
|
avg_version = conn.execute(
|
|
"SELECT AVG(version) FROM articles WHERE date LIKE ?", (f"{month}%",)
|
|
).fetchone()[0] or 0
|
|
conn.close()
|
|
return {"total": total, "posted": posted, "skipped": skipped, "avg_version": round(avg_version, 1)}
|