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
This commit is contained in:
2026-06-09 19:32:07 +02:00
commit 4819bdb70e
7 changed files with 400 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
config.yaml
venv
*.db
*.pyc
+54
View File
@@ -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`.
+14
View File
@@ -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...'
+294
View File
@@ -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()
+20
View File
@@ -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"
+2
View File
@@ -0,0 +1,2 @@
python-telegram-bot>=20.0
PyYAML>=6.0
+11
View File
@@ -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"