""" Datenbankmanager für den Social Media Account Generator. """ import os import json import sqlite3 import logging from datetime import datetime from typing import Dict, List, Any, Optional, Tuple, Union from config.paths import PathConfig logger = logging.getLogger("db_manager") class DatabaseManager: """Klasse zur Verwaltung der Datenbank für Account-Informationen.""" def __init__(self, db_path: str = None): """ Initialisiert den DatabaseManager. Args: db_path: Pfad zur Datenbank-Datei (falls None, wird PathConfig.MAIN_DB verwendet) """ self.db_path = db_path if db_path is not None else PathConfig.MAIN_DB # Stelle sicher, dass das Datenbankverzeichnis existiert os.makedirs(os.path.dirname(self.db_path), exist_ok=True) # Datenbank initialisieren self.init_db() def init_db(self) -> None: """Initialisiert die Datenbank und erstellt die benötigten Tabellen, wenn sie nicht existieren.""" try: conn = sqlite3.connect(self.db_path) cursor = conn.cursor() # Schema v2 laden und ausführen try: self._init_schema_v2(cursor) conn.commit() # Commit nach Schema v2 Initialisierung except Exception as e: logger.warning(f"Konnte Schema v2 nicht initialisieren: {e}") # Accounts-Tabelle erstellen cursor.execute(''' CREATE TABLE IF NOT EXISTS accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, platform TEXT NOT NULL, username TEXT NOT NULL, password TEXT NOT NULL, email TEXT, phone TEXT, full_name TEXT, created_at TEXT NOT NULL, last_login TEXT, notes TEXT, cookies TEXT, status TEXT, fingerprint_id TEXT, session_id TEXT, last_session_update TEXT ) ''') # Migration für bestehende Datenbanken try: cursor.execute("PRAGMA table_info(accounts)") columns = [column[1] for column in cursor.fetchall()] if "fingerprint_id" not in columns: cursor.execute("ALTER TABLE accounts ADD COLUMN fingerprint_id TEXT") logger.info("Added fingerprint_id column to accounts table") if "session_id" not in columns: cursor.execute("ALTER TABLE accounts ADD COLUMN session_id TEXT") logger.info("Added session_id column to accounts table") if "last_session_update" not in columns: cursor.execute("ALTER TABLE accounts ADD COLUMN last_session_update TEXT") logger.info("Added last_session_update column to accounts table") except Exception as e: logger.warning(f"Migration warning: {e}") # Settings-Tabelle erstellen cursor.execute(''' CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL ) ''') conn.commit() conn.close() logger.info("Datenbank initialisiert") except sqlite3.Error as e: logger.error(f"Fehler bei der Datenbankinitialisierung: {e}") def add_account(self, account_data: Dict[str, Any]) -> int: """ Fügt einen Account zur Datenbank hinzu. Args: account_data: Dictionary mit Account-Daten Returns: ID des hinzugefügten Accounts oder -1 im Fehlerfall """ try: # Prüfe, ob erforderliche Felder vorhanden sind required_fields = ["platform", "username", "password"] for field in required_fields: if field not in account_data: logger.error(f"Fehlendes Pflichtfeld: {field}") return -1 # Sicherstellen, dass created_at vorhanden ist if "created_at" not in account_data: account_data["created_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") conn = sqlite3.connect(self.db_path) cursor = conn.cursor() # SQL-Anweisung vorbereiten fields = ", ".join(account_data.keys()) placeholders = ", ".join(["?" for _ in account_data]) query = f"INSERT INTO accounts ({fields}) VALUES ({placeholders})" # Anweisung ausführen cursor.execute(query, list(account_data.values())) # ID des hinzugefügten Datensatzes abrufen account_id = cursor.lastrowid conn.commit() conn.close() logger.info(f"Account hinzugefügt: {account_data['username']} (ID: {account_id})") return account_id except sqlite3.Error as e: logger.error(f"Fehler beim Hinzufügen des Accounts: {e}") return -1 def get_account(self, account_id: int) -> Optional[Dict[str, Any]]: """ Gibt einen Account anhand seiner ID zurück. Args: account_id: ID des Accounts Returns: Dictionary mit Account-Daten oder None, wenn der Account nicht gefunden wurde """ try: conn = sqlite3.connect(self.db_path) conn.row_factory = sqlite3.Row # Für dict-like Zugriff auf Zeilen cursor = conn.cursor() cursor.execute("SELECT * FROM accounts WHERE id = ?", (account_id,)) row = cursor.fetchone() conn.close() if row: # Konvertiere Row in Dictionary account = dict(row) logger.debug(f"Account gefunden: {account['username']} (ID: {account_id})") return account else: logger.warning(f"Account nicht gefunden: ID {account_id}") return None except sqlite3.Error as e: logger.error(f"Fehler beim Abrufen des Accounts: {e}") return None def get_all_accounts(self) -> List[Dict[str, Any]]: """ Gibt alle Accounts zurück. Returns: Liste von Dictionaries mit Account-Daten """ try: conn = sqlite3.connect(self.db_path) conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute("SELECT * FROM accounts ORDER BY id DESC") rows = cursor.fetchall() conn.close() # Konvertiere Rows in Dictionaries accounts = [dict(row) for row in rows] logger.info(f"{len(accounts)} Accounts abgerufen") return accounts except sqlite3.Error as e: logger.error(f"Fehler beim Abrufen aller Accounts: {e}") return [] def get_accounts_by_platform(self, platform: str) -> List[Dict[str, Any]]: """ Gibt alle Accounts einer bestimmten Plattform zurück. Args: platform: Plattformname (z.B. "instagram") Returns: Liste von Dictionaries mit Account-Daten """ try: conn = sqlite3.connect(self.db_path) conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute("SELECT * FROM accounts WHERE platform = ? ORDER BY id DESC", (platform.lower(),)) rows = cursor.fetchall() conn.close() # Konvertiere Rows in Dictionaries accounts = [dict(row) for row in rows] logger.info(f"{len(accounts)} Accounts für Plattform '{platform}' abgerufen") return accounts except sqlite3.Error as e: logger.error(f"Fehler beim Abrufen der Accounts für Plattform '{platform}': {e}") return [] def update_account(self, account_id: int, update_data: Dict[str, Any]) -> bool: """ Aktualisiert einen Account in der Datenbank. Args: account_id: ID des zu aktualisierenden Accounts update_data: Dictionary mit zu aktualisierenden Feldern Returns: True bei Erfolg, False im Fehlerfall """ if not update_data: logger.warning("Keine Aktualisierungsdaten bereitgestellt") return False try: conn = sqlite3.connect(self.db_path) cursor = conn.cursor() # SQL-Anweisung vorbereiten set_clause = ", ".join([f"{field} = ?" for field in update_data.keys()]) values = list(update_data.values()) values.append(account_id) query = f"UPDATE accounts SET {set_clause} WHERE id = ?" # Anweisung ausführen cursor.execute(query, values) conn.commit() conn.close() logger.info(f"Account aktualisiert: ID {account_id}") return True except sqlite3.Error as e: logger.error(f"Fehler beim Aktualisieren des Accounts: {e}") return False def delete_account(self, account_id: int) -> bool: """ Löscht einen Account aus der Datenbank. Args: account_id: ID des zu löschenden Accounts Returns: True bei Erfolg, False im Fehlerfall """ try: conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute("DELETE FROM accounts WHERE id = ?", (account_id,)) conn.commit() conn.close() logger.info(f"Account gelöscht: ID {account_id}") return True except sqlite3.Error as e: logger.error(f"Fehler beim Löschen des Accounts: {e}") return False def search_accounts(self, query: str, platform: Optional[str] = None) -> List[Dict[str, Any]]: """ Sucht nach Accounts in der Datenbank. Args: query: Suchbegriff platform: Optional, Plattform für die Einschränkung der Suche Returns: Liste von Dictionaries mit gefundenen Account-Daten """ try: conn = sqlite3.connect(self.db_path) conn.row_factory = sqlite3.Row cursor = conn.cursor() # Suchbegriff für LIKE-Operator vorbereiten search_term = f"%{query}%" if platform: query_sql = """ SELECT * FROM accounts WHERE (username LIKE ? OR email LIKE ? OR phone LIKE ? OR full_name LIKE ?) AND platform = ? ORDER BY id DESC """ cursor.execute(query_sql, (search_term, search_term, search_term, search_term, platform.lower())) else: query_sql = """ SELECT * FROM accounts WHERE username LIKE ? OR email LIKE ? OR phone LIKE ? OR full_name LIKE ? ORDER BY id DESC """ cursor.execute(query_sql, (search_term, search_term, search_term, search_term)) rows = cursor.fetchall() conn.close() # Konvertiere Rows in Dictionaries accounts = [dict(row) for row in rows] logger.info(f"{len(accounts)} Accounts gefunden für Suchbegriff '{query}'") return accounts except sqlite3.Error as e: logger.error(f"Fehler bei der Suche nach Accounts: {e}") return [] def get_connection(self) -> sqlite3.Connection: """ Gibt eine neue Datenbankverbindung zurück. Returns: SQLite Connection Objekt """ conn = sqlite3.connect(self.db_path) conn.row_factory = sqlite3.Row return conn def get_account_count(self, platform: Optional[str] = None) -> int: """ Gibt die Anzahl der Accounts zurück. Args: platform: Optional, Plattform für die Einschränkung der Zählung Returns: Anzahl der Accounts """ try: conn = sqlite3.connect(self.db_path) cursor = conn.cursor() if platform: cursor.execute("SELECT COUNT(*) FROM accounts WHERE platform = ?", (platform.lower(),)) else: cursor.execute("SELECT COUNT(*) FROM accounts") count = cursor.fetchone()[0] conn.close() return count except sqlite3.Error as e: logger.error(f"Fehler beim Zählen der Accounts: {e}") return 0 def get_setting(self, key: str, default: Any = None) -> Any: """ Gibt einen Einstellungswert zurück. Args: key: Schlüssel der Einstellung default: Standardwert, falls die Einstellung nicht gefunden wurde Returns: Wert der Einstellung oder der Standardwert """ try: conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute("SELECT value FROM settings WHERE key = ?", (key,)) row = cursor.fetchone() conn.close() if row: # Versuche, den Wert als JSON zu parsen try: return json.loads(row[0]) except json.JSONDecodeError: # Wenn kein gültiges JSON, gib den Rohwert zurück return row[0] else: return default except sqlite3.Error as e: logger.error(f"Fehler beim Abrufen der Einstellung '{key}': {e}") return default def set_setting(self, key: str, value: Any) -> bool: """ Setzt einen Einstellungswert. Args: key: Schlüssel der Einstellung value: Wert der Einstellung (wird als JSON gespeichert, wenn es kein String ist) Returns: True bei Erfolg, False im Fehlerfall """ try: conn = sqlite3.connect(self.db_path) cursor = conn.cursor() # Wert als JSON speichern, wenn es kein String ist if not isinstance(value, str): value = json.dumps(value) # Prüfen, ob die Einstellung bereits existiert cursor.execute("SELECT COUNT(*) FROM settings WHERE key = ?", (key,)) exists = cursor.fetchone()[0] > 0 if exists: cursor.execute("UPDATE settings SET value = ? WHERE key = ?", (value, key)) else: cursor.execute("INSERT INTO settings (key, value) VALUES (?, ?)", (key, value)) conn.commit() conn.close() logger.info(f"Einstellung gespeichert: {key}") return True except sqlite3.Error as e: logger.error(f"Fehler beim Speichern der Einstellung '{key}': {e}") return False def delete_setting(self, key: str) -> bool: """ Löscht eine Einstellung. Args: key: Schlüssel der zu löschenden Einstellung Returns: True bei Erfolg, False im Fehlerfall """ try: conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute("DELETE FROM settings WHERE key = ?", (key,)) conn.commit() conn.close() logger.info(f"Einstellung gelöscht: {key}") return True except sqlite3.Error as e: logger.error(f"Fehler beim Löschen der Einstellung '{key}': {e}") return False def backup_database(self, backup_path: Optional[str] = None) -> bool: """ Erstellt ein Backup der Datenbank. Args: backup_path: Optional, Pfad für das Backup Returns: True bei Erfolg, False im Fehlerfall """ if not backup_path: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") backup_path = f"database/backup/accounts_{timestamp}.db" # Stelle sicher, dass das Backup-Verzeichnis existiert os.makedirs(os.path.dirname(backup_path), exist_ok=True) try: # SQLite-Backup-API verwenden conn = sqlite3.connect(self.db_path) backup_conn = sqlite3.connect(backup_path) conn.backup(backup_conn) conn.close() backup_conn.close() logger.info(f"Datenbank-Backup erstellt: {backup_path}") return True except sqlite3.Error as e: logger.error(f"Fehler beim Erstellen des Datenbank-Backups: {e}") return False def _init_schema_v2(self, cursor) -> None: """Initialisiert das Schema v2 mit Session-Tabellen.""" schema_path = PathConfig.SCHEMA_V2 try: # Versuche schema_v2.sql zu laden if PathConfig.file_exists(schema_path): logger.info(f"Lade Schema v2 aus {schema_path}") with open(schema_path, 'r', encoding='utf-8') as f: schema_sql = f.read() # Führe alle SQL-Statements aus # SQLite unterstützt nur ein Statement pro execute(), # daher müssen wir die Statements aufteilen statements = [s.strip() for s in schema_sql.split(';') if s.strip()] for statement in statements: if statement: # Ignoriere leere Statements cursor.execute(statement) logger.info("Schema v2 erfolgreich aus SQL-Datei geladen") else: logger.warning(f"schema_v2.sql nicht gefunden unter {schema_path}") # Fallback: Erstelle minimal notwendige Tabellen self._create_minimal_v2_tables(cursor) except Exception as e: logger.error(f"Fehler beim Laden von Schema v2: {e}") # Fallback: Erstelle minimal notwendige Tabellen self._create_minimal_v2_tables(cursor) def _create_minimal_v2_tables(self, cursor) -> None: """Erstellt minimal notwendige v2 Tabellen als Fallback.""" try: # Nur die wichtigsten Tabellen für One-Click-Login cursor.execute(''' CREATE TABLE IF NOT EXISTS browser_sessions ( id TEXT PRIMARY KEY, fingerprint_id TEXT NOT NULL, cookies TEXT NOT NULL, local_storage TEXT, session_storage TEXT, account_id TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_used TIMESTAMP DEFAULT CURRENT_TIMESTAMP, health_score REAL DEFAULT 1.0 ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS browser_fingerprints ( id TEXT PRIMARY KEY, canvas_noise_config TEXT NOT NULL, webrtc_config TEXT NOT NULL, fonts TEXT NOT NULL, hardware_config TEXT NOT NULL, navigator_props TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') logger.info("Minimale v2 Tabellen erstellt") except sqlite3.Error as e: logger.error(f"Fehler beim Erstellen der minimalen v2 Tabellen: {e}")