commit 4819bdb70e329e6d753fad9762189aa899aa4d98 Author: Thomas Kerpe Date: Tue Jun 9 19:32:07 2026 +0200 Initial commit: Telegram monitoring bot with external configuration - Externalize configuration to YAML (config_example.yaml) - Add config loader with validation in bot.py - Implement DatabaseManager for activity logging and user stats - Add message handlers with topic awareness - Include setup_venv.sh for automated environment setup - Add requirements.txt with dependencies (python-telegram-bot, PyYAML) - Comprehensive README with setup and configuration instructions diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0fc3ffa --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +config.yaml + +venv +*.db +*.pyc diff --git a/README.md b/README.md new file mode 100644 index 0000000..e118061 --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# LoggingBot + +Kurze Anleitung zum Einrichten und Starten des Bots. + +Voraussetzungen: +- Python 3.8+ +- `git` (optional) + +## Schnellstart + +### 1. Virtuelle Umgebung anlegen und aktivieren + +```bash +python3 -m venv .venv +source .venv/bin/activate +``` + +### 2. Abhängigkeiten installieren + +```bash +pip install --upgrade pip +pip install -r requirements.txt +``` + +### 3. Konfiguration erstellen + +Kopiere `config_example.yaml` zu `config.yaml` und passe die Werte an: + +```bash +cp config_example.yaml config.yaml +``` + +Öffne `config.yaml` und ersetze: +- `YOUR_BOT_TOKEN_HERE` mit deinem echten Telegram Bot Token +- Optional: Passe `debug_mode`, `database.name` und andere Einstellungen an + +### 4. Bot starten + +```bash +python bot.py +``` + +## Konfiguration + +Die Konfiguration erfolgt über die Datei `config.yaml`: + +- **bot.token**: Dein Telegram Bot Token (von BotFather) +- **bot.debug_mode**: Debug-Ausgaben aktivieren (true/false) +- **database.name**: SQLite-Datenbankdatei +- **logging.write_to_file**: Aktiviere Datei-Logging (true/false) +- **logging.file_name**: Name der Protokolldatei +- **logging.level**: Python Logging Level (DEBUG, INFO, WARNING, ERROR, CRITICAL) + +Optional: Ein kurzes Setup-Skript ist enthalten: `setup_venv.sh`. diff --git a/activity_log.txt b/activity_log.txt new file mode 100644 index 0000000..0a51a76 --- /dev/null +++ b/activity_log.txt @@ -0,0 +1,14 @@ +[2026-06-09 15:52:24] | CHAT_ID=-1001465084814 | TOPIC=MAIN | TYPE=MESSAGE | USER=151261436 (tokelemma) | DETAILS=Nachricht gesendet: '....' +[2026-06-09 15:52:24] | CHAT_ID=-1001465084814 | TOPIC=MAIN | TYPE=MESSAGE | USER=151261436 (tokelemma) | DETAILS=Nachricht gesendet: '....' +[2026-06-09 16:19:40] | CHAT_ID=-1001465084814 | TOPIC=MAIN | TYPE=MESSAGE | USER=884807537 (None) | DETAILS=Nachricht gesendet: 'Liege zu verschenken, abzuholen in Palmbach....' +[2026-06-09 16:19:40] | CHAT_ID=-1001465084814 | TOPIC=MAIN | TYPE=MESSAGE | USER=884807537 (None) | DETAILS=Nachricht gesendet: 'Liege zu verschenken, abzuholen in Palmbach....' +[2026-06-09 16:25:07] | CHAT_ID=-1001465084814 | TOPIC=MAIN | TYPE=MESSAGE | USER=1392350107 (None) | DETAILS=Nachricht gesendet: 'Hab geschrieben...' +[2026-06-09 16:25:07] | CHAT_ID=-1001465084814 | TOPIC=MAIN | TYPE=MESSAGE | USER=1392350107 (None) | DETAILS=Nachricht gesendet: 'Hab geschrieben...' +[2026-06-09 16:27:17] | CHAT_ID=-1001465084814 | TOPIC=MAIN | TYPE=MESSAGE | USER=884807537 (None) | DETAILS=Nachricht gesendet: 'Reserviert für Mariiam...' +[2026-06-09 16:27:17] | CHAT_ID=-1001465084814 | TOPIC=MAIN | TYPE=MESSAGE | USER=884807537 (None) | DETAILS=Nachricht gesendet: 'Reserviert für Mariiam...' +[2026-06-09 18:12:25] | CHAT_ID=-1001465084814 | TOPIC=MAIN | TYPE=MESSAGE | USER=8062402525 (nwskhhh) | DETAILS=Nachricht gesendet: 'Доброго времени суток дорогие друзья😁 +Хотелось бы ...' +[2026-06-09 18:12:25] | CHAT_ID=-1001465084814 | TOPIC=MAIN | TYPE=MESSAGE | USER=8062402525 (nwskhhh) | DETAILS=Nachricht gesendet: 'Доброго времени суток дорогие друзья😁 +Хотелось бы ...' +[2026-06-09 18:28:43] | CHAT_ID=-1001465084814 | TOPIC=MAIN | TYPE=MESSAGE | USER=93813065 (chrisarnu) | DETAILS=Nachricht gesendet: '@admin...' +[2026-06-09 18:28:43] | CHAT_ID=-1001465084814 | TOPIC=MAIN | TYPE=MESSAGE | USER=93813065 (chrisarnu) | DETAILS=Nachricht gesendet: '@admin...' diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..117fc13 --- /dev/null +++ b/bot.py @@ -0,0 +1,294 @@ +import logging +import sqlite3 +import os +import yaml +from pathlib import Path +from telegram import Update +from telegram.ext import Application, MessageHandler, filters, ContextTypes, CommandHandler +from datetime import datetime + + +# --- KONFIGURATION LADEN --- +def load_config(config_path: str = 'config.yaml') -> dict: + """Lädt die Konfiguration aus einer YAML-Datei.""" + if not os.path.exists(config_path): + raise FileNotFoundError( + f"Konfigurationsdatei '{config_path}' nicht gefunden.\n" + f"Bitte kopiere 'config_example.yaml' zu '{config_path}' und passe die Werte an." + ) + + with open(config_path, 'r', encoding='utf-8') as f: + config = yaml.safe_load(f) + + return config + + +# Config laden +try: + config = load_config('config.yaml') +except FileNotFoundError as e: + print(f"❌ Fehler: {e}") + exit(1) + +# Konfigurationsvariablen extrahieren +BOT_TOKEN = config['bot']['token'] +ENABLE_DEBUG_MODE = config['bot']['debug_mode'] +DATABASE_NAME = config['database']['name'] +WRITE_ACTIVITY_TO_FILE = config['logging']['write_to_file'] +LOGGING_FILE = config['logging']['file_name'] +LOG_LEVEL = getattr(logging, config['logging']['level'], logging.INFO) + +# Logging konfigurieren +logging.basicConfig(level=LOG_LEVEL, format='%(asctime)s - %(levelname)s - %(message)s') + + + +class DatabaseManager: + """Kapselt alle Datenbankinteraktionen und verwaltet Benutzer-/Gruppenzustände.""" + def __init__(self): + # ACHTUNG: Führen Sie vor dem Start einmal manuell die Migration durch, + # oder lassen Sie Python das Schema anpassen (siehe unten). + self.conn = sqlite3.connect(DATABASE_NAME) + self.cursor = self.conn.cursor() + self._create_tables() + + def _create_tables(self): + """Stellt sicher, dass alle notwendigen Tabellen und Spalten existieren.""" + # 1. activity_log (Bleibt gleich) + self.cursor.execute(""" + CREATE TABLE IF NOT EXISTS activity_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + group_chat_id TEXT NOT NULL, + topic_id TEXT DEFAULT 'MAIN', + timestamp TEXT NOT NULL, + activity_type TEXT NOT NULL, + user_id INTEGER NOT NULL, + username TEXT, + details TEXT + ) + """) + + # 2. user_stats (Angepasst mit Name/Username) + self.cursor.execute(""" + CREATE TABLE IF NOT EXISTS user_stats ( + user_id INTEGER PRIMARY KEY, + message_count INTEGER DEFAULT 0, + last_message_timestamp TEXT NULL, + full_name TEXT, -- NEU: Benutzername speichern + username TEXT -- NEU: Benutzernamen speichern + ) + """) + + # 3. NEUE TABELLE: Profile History Log (Protokolliert Änderungen) + self.cursor.execute(""" + CREATE TABLE IF NOT EXISTS profile_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + group_chat_id TEXT NOT NULL, + timestamp TEXT NOT NULL, + activity_type TEXT NOT NULL, + old_full_name TEXT, -- Name vor der Änderung + new_full_name TEXT, -- Neuer Name + old_username TEXT, -- Username vor der Änderung + new_username TEXT, -- Neuer Username + details TEXT + ); + """) + print(f"✅ Datenbank '{DATABASE_NAME}' erfolgreich initialisiert/überprüft.") + + def log_activity(self, group_chat_id: int, topic_id: str, activity_type: str, user_id: int, username: str, details: str = None): + """Speichert eine allgemeine Aktivität im Protokoll.""" + timestamp = datetime.now().isoformat() + try: + self.cursor.execute(""" + INSERT INTO activity_log (group_chat_id, topic_id, timestamp, activity_type, user_id, username, details) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, (str(group_chat_id), topic_id, timestamp, activity_type, user_id, username or None, details)) + self.conn.commit() + except Exception as e: + logging.error(f"Fehler beim Logging der Aktivität: {e}") + + # NEU: Funktion zur Speicherung von Profiländerungen im History Log + def log_profile_change(self, group_chat_id: int, user_id: int, topic_id: str, old_name: str, new_name: str, old_user: str, new_user: str, details: str): + """Speichert die detaillierte Historie einer Profiländerung.""" + timestamp = datetime.now().isoformat() + try: + self.cursor.execute(""" + INSERT INTO profile_history (user_id, group_chat_id, timestamp, activity_type, + old_full_name, new_full_name, old_username, new_username, details) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, (user_id, str(group_chat_id), timestamp, "PROFILE_CHANGE", + old_name, new_name, old_user, new_user, details)) + self.conn.commit() + except Exception as e: + logging.error(f"Fehler beim Protokollieren der Profiländerung: {e}") + + def update_user_stats(self, user_id: int, message_count: int, last_message_timestamp: str, full_name: str = None, username: str = None): + """Aktualisiert die Statistiken und das letzte bekannte Profil des Benutzers.""" + try: + # Die Spaltenliste muss genau den Reihenfolge der Argumente entsprechen. + self.cursor.execute(""" + INSERT INTO user_stats (user_id, message_count, last_message_timestamp, full_name, username) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(user_id) DO UPDATE SET + message_count = excluded.message_count, + last_message_timestamp = excluded.last_message_timestamp, + full_name = (CASE WHEN user_stats.full_name IS NULL THEN excluded.full_name ELSE user_stats.full_name END), + username = (CASE WHEN user_stats.username IS NULL THEN excluded.username ELSE user_stats.username END) + """, (user_id, message_count, last_message_timestamp, full_name, username)) + self.conn.commit() + except Exception as e: + logging.error(f"Fehler beim Aktualisieren der User Stats für {user_id}: {e}") + + def close(self): + """Schließt die Datenbankverbindung.""" + self.conn.close() + + +class MonitoringBot: + """Der Hauptbot-Klasse, erweitert um Topic- und Profilprotokollierung.""" + def __init__(self): + self.db = DatabaseManager() + + # Start-Methode (Muss auf der Hauptebene des Klassenblocks sein) + async def start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Verarbeitet den /start Befehl.""" + await update.message.reply_text("Monitoring-Bot ist online und protokolliert Aktivitäten.") + + # --- HILFSFUNKTIONEN ZUR TOPIC-ERKENNUNG --- + def _get_topic_id(self, update: Update) -> str: + """Extrahiert die Topic ID aus dem Update (Topic Aware).""" + if update.effective_chat and hasattr(update.message, 'reply_to_topic'): + # Dies ist ein sehr spezifischer API-Pfad und muss je nach PTB-Version/Update angepasst werden. + return str(update.message.reply_to_topic) + # Fallback: Hauptthema (MAIN). + return 'MAIN' + + # --- EVENT HANDLER 1: BEITRITT/AUSSTIEG --- + async def handle_member_update(self, client: Update, context: ContextTypes.DEFAULT_TYPE): + """Verarbeitet JOIN- und LEAVE-Ereignisse.""" + if not client or not context.message: return + + new_member = client.effective_user + chat_id = client.effective_chat.id + topic_id = self._get_topic_id(client.update) + + self._log_event(client, context, chat_id, topic_id, "JOIN", new_member.id, new_member.full_name, f"Beitritt von {new_member.full_name}") + + + # --- EVENT HANDLER 2: PROFILDÄNDERUNG (Jetzt History Loggend) --- + async def handle_profile_update(self, client: Update, context: ContextTypes.DEFAULT_TYPE): + """Verarbeitet simulierte Profiländerungen und protokolliert sie.""" + user = client.effective_user + chat_id = client.effective_chat.id + topic_id = self._get_topic_id(client.update) + + # Da PTB keine direkten Profile-Update Events liefert, simulieren wir das Loggen eines + # gewünschten Change-Events basierend auf einem Trigger (z.B. /changename). + if context.args: + new_name = " ".join(context.args) # Annahme: Der User sendet den neuen Namen als Argument + # Hier müsste die Logik für alte vs neue Werte komplexer sein, + # aber wir loggen das gewünschte Ergebnis: + old_details = "N/A" + new_details = new_name + self._log_event(client, context, chat_id, topic_id, "PROFILE_CHANGE", user.id, user.full_name, f"Profiländerung: {old_details} -> {new_details}") + + # Speichern der Änderung im dedizierten History Log + self.db.log_profile_change( + group_chat_id=chat_id, + user_id=user.id, + topic_id=topic_id, + old_name="Simulated Old Name", + new_name=new_details, + old_user="Simulated Old User", + new_user=getattr(user, 'username', 'N/A'), + details="Profil manuell via Bot-Befehl geändert." + ) + + + # --- EVENT HANDLER 3: MESSAGE (Statistik & Topic Aware) --- + async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """Verarbeitet jede Nachricht und aktualisiert die Statistiken des ABSENDERS.""" + if not update or not update.effective_chat or not update.effective_user: + return + + # 1. TOPIC ID EXTRAHIEREN + topic_id = self._get_topic_id(update) + + chat_id = update.effective_chat.id + message_text = update.message.text if update.message else None + user = update.effective_user + timestamp_str = datetime.now().isoformat() + + # 2. Statistik-Update: User ID, Count erhöhen und aktuelle Profildaten speichern + self._update_user_stats( + user_id=user.id, + message_count=+1, + last_message_timestamp=timestamp_str, + full_name=user.full_name, # Aktueller Name vom Telegram Update + username=getattr(user, 'username', None) # Optional: Username + ) + + # General Logging (Protokollierung des Ereignisses) + if message_text and not message_text.startswith('/'): + self._log_event( + client=update, context=context, chat_id=chat_id, topic_id=topic_id, + activity_type="MESSAGE", user_id=user.id, username=user.username, details=f"Nachricht gesendet: '{message_text[:50]}...'" + ) + + # --- INTERNE HILFSFUNKTIONEN (General Logging bleibt stabil) --- + def _log_event(self, client, context, chat_id, topic_id, activity_type, user_id, username, details): + """Interne Funktion zur Durchführung des Datenbank- und File-Logging.""" + self.db.log_activity(group_chat_id=chat_id, topic_id=topic_id, activity_type=activity_type, user_id=user_id, username=username, details=details) + + # Debug Output (Bleibt stabil) + if ENABLE_DEBUG_MODE: + logging.info(f"[LOG] Aktivität erfasst ({activity_type}): User {user_id} | Topic '{topic_id}'") + if WRITE_ACTIVITY_TO_FILE: + print(f"--- DEBUG LOG ENTRY --- Type: {activity_type}, Topic: {topic_id}, User: {username or user_id}") + + # Optional File Logging (Bleibt stabil) + if WRITE_ACTIVITY_TO_FILE: + log_message = f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] | CHAT_ID={chat_id} | TOPIC={topic_id} | TYPE={activity_type} | USER={user_id} ({username}) | DETAILS={details}\n" + with open(LOGGING_FILE, 'a', encoding='utf-8') as f: + f.write(log_message) + + + # Optional File Logging + if WRITE_ACTIVITY_TO_FILE: + log_message = f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] | CHAT_ID={chat_id} | TOPIC={topic_id} | TYPE={activity_type} | USER={user_id} ({username}) | DETAILS={details}\n" + with open(LOGGING_FILE, 'a', encoding='utf-8') as f: + f.write(log_message) + + def _update_user_stats(self, user_id: int, message_count: int, last_message_timestamp: str, full_name: str = None, username: str = None): + """Aktualisiert die Statistiken und das Profil des Benutzers via DatabaseManager.""" + # Ruft die korrigierte Methode aus dem Manager auf. + self.db.update_user_stats(user_id, message_count, last_message_timestamp, full_name=full_name, username=username) + + + # --- HAUPT-RUN FUNKTION (Bleibt gleich) --- + def run(self): + print("\n=============================================") + print(" 🚀 Starting Monitoring Bot...") + print("=============================================\n") + + application = Application.builder().token(BOT_TOKEN).build() + + # Handlers registrieren + application.add_handler(CommandHandler("start", self.start_command)) + application.add_handler(MessageHandler(filters.ALL & ~filters.COMMAND, self.handle_message)) + + print("🤖 Bot wird gestartet. Warte auf Updates...") + try: + application.run_polling() + except KeyboardInterrupt: + pass + finally: + self.db.close() + logging.info("Bot wurde manuell gestoppt und Datenbankverbindung geschlossen.") + + + +if __name__ == '__main__': + monitor = MonitoringBot() + monitor.run() \ No newline at end of file diff --git a/config_example.yaml b/config_example.yaml new file mode 100644 index 0000000..57e02d1 --- /dev/null +++ b/config_example.yaml @@ -0,0 +1,20 @@ +# LoggingBot Konfiguration +# Kopiere diese Datei zu 'config.yaml' und passe die Werte an + +bot: + # Telegram Bot Token (von BotFather erhalten) + token: "YOUR_BOT_TOKEN_HERE" + # Debug-Ausgaben aktivieren/deaktivieren + debug_mode: true + +database: + # Name der SQLite-Datenbank + name: "group_monitor.db" + +logging: + # Aktiviere Datei-Logging (activity_log.txt) + write_to_file: true + # Dateiname für Aktivitätsprotokolle + file_name: "activity_log.txt" + # Python Logging Level (DEBUG, INFO, WARNING, ERROR, CRITICAL) + level: "INFO" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..292f48e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +python-telegram-bot>=20.0 +PyYAML>=6.0 diff --git a/setup_venv.sh b/setup_venv.sh new file mode 100644 index 0000000..dad9a8a --- /dev/null +++ b/setup_venv.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Kurzes Skript zum Erzeugen und Vorbereiten einer virtuellen Umgebung +set -euo pipefail + +python3 -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip +pip install -r requirements.txt + +echo "Virtuelle Umgebung eingerichtet und Abhängigkeiten installiert." +echo "Aktiviere sie mit: source .venv/bin/activate"