Files
loggingbot/bot.py
T
admin 4819bdb70e 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
2026-06-09 19:32:07 +02:00

294 lines
14 KiB
Python

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()