# stats_database.py - ПОЛНАЯ ВЕРСИЯ (как database.py) import sqlite3 import os import requests from datetime import datetime, timedelta from config import MSK_TZ # ============================================================ # НАСТРОЙКА БАКЕТА (как в database.py) # ============================================================ HF_TOKEN = os.environ.get("HF_TOKEN") if not HF_TOKEN: print("[STATS DB] ❌ НЕТ HF_TOKEN В ПЕРЕМЕННЫХ ОКРУЖЕНИЯ!") print("[STATS DB] Установи: export HF_TOKEN='твой_токен'") REPO_ID = "RomanJordansky/Messages-Jordan-manager" DB_PATH_IN_REPO = "bot_messages.db" HF_URL = f"https://huggingface.co/{REPO_ID}/resolve/main/{DB_PATH_IN_REPO}" DATA_DIR = "bot_data" os.makedirs(DATA_DIR, exist_ok=True) STATS_DB_PATH = os.path.join(DATA_DIR, "bot_messages.db") print(f"[STATS DB] База данных: {STATS_DB_PATH}") # ============================================================ # СОЗДАНИЕ РЕПОЗИТОРИЯ (ЕСЛИ ЕГО НЕТ) # ============================================================ def create_repo_if_not_exists(): """Создаёт репозиторий на Hugging Face, если его нет""" if not HF_TOKEN: print("[STATS DB] ❌ НЕТ HF_TOKEN!") return False try: from huggingface_hub import HfApi, create_repo, repo_exists if repo_exists(repo_id=REPO_ID, token=HF_TOKEN): print(f"[STATS DB] ✅ Репозиторий {REPO_ID} уже существует") return True create_repo( repo_id=REPO_ID, token=HF_TOKEN, repo_type="model", private=False, exist_ok=True ) print(f"[STATS DB] ✅ Репозиторий {REPO_ID} создан!") return True except Exception as e: print(f"[STATS DB] ❌ Ошибка создания репозитория: {e}") return False # ============================================================ # ЗАГРУЗКА/СОХРАНЕНИЕ БД (как в database.py) # ============================================================ def download_stats_db(): """Скачивает БД с Hugging Face""" print("[STATS DB] Попытка загрузить базу данных...") try: headers = {"User-Agent": "Mozilla/5.0", "Authorization": f"Bearer {HF_TOKEN}"} response = requests.get(HF_URL, headers=headers, timeout=30) if response.status_code == 200: with open(STATS_DB_PATH, 'wb') as f: f.write(response.content) print(f"[STATS DB] ✅ База данных загружена! Размер: {os.path.getsize(STATS_DB_PATH)} байт") return True else: print(f"[STATS DB] БД не найдена (код {response.status_code})") return False except Exception as e: print(f"[STATS DB] Ошибка загрузки: {e}") return False def upload_stats_db(): """Загружает БД на Hugging Face""" if not HF_TOKEN: print("[STATS DB] ❌ НЕТ HF_TOKEN!") return False try: from huggingface_hub import HfApi api = HfApi(token=HF_TOKEN) api.upload_file( path_or_fileobj=STATS_DB_PATH, path_in_repo=DB_PATH_IN_REPO, repo_id=REPO_ID, repo_type="model", ) print(f"[STATS DB] ✅ БД сохранена на HF! Размер: {os.path.getsize(STATS_DB_PATH)} байт") return True except Exception as e: print(f"[STATS DB] ❌ Ошибка сохранения: {e}") return False # ============================================================ # СОЗДАНИЕ РЕПОЗИТОРИЯ И ЗАГРУЗКА БД # ============================================================ # Создаём репозиторий, если его нет create_repo_if_not_exists() # Пробуем загрузить БД db_loaded = download_stats_db() if not db_loaded: print("[STATS DB] БД НЕ НАЙДЕНА! СОЗДАЮ НОВУЮ...") # ============================================================ # ПОДКЛЮЧЕНИЕ К БД СТАТИСТИКИ # ============================================================ stats_conn = sqlite3.connect(STATS_DB_PATH, check_same_thread=False) stats_cursor = stats_conn.cursor() def init_stats_database(): """Инициализирует таблицы в базе данных""" stats_cursor.execute(""" CREATE TABLE IF NOT EXISTS message_stats ( chat_id INTEGER, user_id INTEGER, date TEXT, messages_count INTEGER DEFAULT 0, last_activity TEXT, PRIMARY KEY (chat_id, user_id, date) ) """) stats_cursor.execute("CREATE INDEX IF NOT EXISTS idx_chat_date ON message_stats (chat_id, date)") stats_cursor.execute("CREATE INDEX IF NOT EXISTS idx_user ON message_stats (user_id)") stats_cursor.execute("CREATE INDEX IF NOT EXISTS idx_chat_user ON message_stats (chat_id, user_id)") stats_cursor.execute(""" CREATE TABLE IF NOT EXISTS total_stats ( chat_id INTEGER, user_id INTEGER, total_messages INTEGER DEFAULT 0, last_updated TEXT, PRIMARY KEY (chat_id, user_id) ) """) try: stats_cursor.execute("ALTER TABLE message_stats ADD COLUMN last_activity TEXT") except: pass stats_conn.commit() print("[STATS DB] Таблицы созданы/проверены") # После инициализации загружаем БД upload_stats_db() init_stats_database() # ============================================================ # ФУНКЦИИ ДЛЯ РАБОТЫ СО СТАТИСТИКОЙ # ============================================================ async def update_message_stats(chat_id: int, user_id: int): """Обновляет статистику сообщений для пользователя в беседе""" today = datetime.now(MSK_TZ).strftime("%Y-%m-%d") now = datetime.now(MSK_TZ).strftime("%Y-%m-%d %H:%M:%S") try: # 1. Обновляем дневную статистику stats_cursor.execute(""" INSERT INTO message_stats (chat_id, user_id, date, messages_count, last_activity) VALUES (?, ?, ?, 1, ?) ON CONFLICT(chat_id, user_id, date) DO UPDATE SET messages_count = messages_count + 1, last_activity = ? """, (chat_id, user_id, today, now, now)) # 2. Обновляем общую статистику stats_cursor.execute(""" INSERT INTO total_stats (chat_id, user_id, total_messages, last_updated) VALUES (?, ?, 1, ?) ON CONFLICT(chat_id, user_id) DO UPDATE SET total_messages = total_messages + 1, last_updated = ? """, (chat_id, user_id, now, now)) stats_conn.commit() upload_stats_db() except Exception as e: print(f"[STATS DB] Ошибка обновления статистики: {e}") async def get_total_messages(chat_id: int, user_id: int) -> int: """Получает общее количество сообщений пользователя в беседе""" try: stats_cursor.execute(""" SELECT total_messages FROM total_stats WHERE chat_id = ? AND user_id = ? """, (chat_id, user_id)) result = stats_cursor.fetchone() if result: return result[0] # Если нет в total_stats, считаем из message_stats stats_cursor.execute(""" SELECT SUM(messages_count) FROM message_stats WHERE chat_id = ? AND user_id = ? """, (chat_id, user_id)) result = stats_cursor.fetchone() total = result[0] if result and result[0] else 0 # Сохраняем в total_stats if total > 0: now = datetime.now(MSK_TZ).strftime("%Y-%m-%d %H:%M:%S") stats_cursor.execute(""" INSERT OR REPLACE INTO total_stats (chat_id, user_id, total_messages, last_updated) VALUES (?, ?, ?, ?) """, (chat_id, user_id, total, now)) stats_conn.commit() upload_stats_db() return total except Exception as e: print(f"[STATS DB] Ошибка get_total_messages: {e}") return 0 async def get_today_messages(chat_id: int, user_id: int) -> int: """Получает количество сообщений пользователя за сегодня""" try: today = datetime.now(MSK_TZ).strftime("%Y-%m-%d") stats_cursor.execute(""" SELECT messages_count FROM message_stats WHERE chat_id = ? AND user_id = ? AND date = ? """, (chat_id, user_id, today)) result = stats_cursor.fetchone() return result[0] if result else 0 except Exception as e: print(f"[STATS DB] Ошибка get_today_messages: {e}") return 0 async def get_last_activity(chat_id: int, user_id: int): """Получает время последней активности пользователя""" try: stats_cursor.execute(""" SELECT last_activity FROM total_stats WHERE chat_id = ? AND user_id = ? """, (chat_id, user_id)) result = stats_cursor.fetchone() if result and result[0]: return result[0] stats_cursor.execute(""" SELECT last_activity FROM message_stats WHERE chat_id = ? AND user_id = ? ORDER BY last_activity DESC LIMIT 1 """, (chat_id, user_id)) result = stats_cursor.fetchone() return result[0] if result and result[0] else None except Exception as e: print(f"[STATS DB] Ошибка get_last_activity: {e}") return None async def get_chat_top(chat_id: int, limit: int = 10) -> list: """Получает топ пользователей по общему количеству сообщений в беседе""" try: stats_cursor.execute(""" SELECT user_id, total_messages FROM total_stats WHERE chat_id = ? ORDER BY total_messages DESC LIMIT ? """, (chat_id, limit)) return stats_cursor.fetchall() except Exception as e: print(f"[STATS DB] Ошибка get_chat_top: {e}") return [] async def get_chat_top_today(chat_id: int, limit: int = 10) -> list: """Получает топ пользователей по сообщениям за сегодня""" try: today = datetime.now(MSK_TZ).strftime("%Y-%m-%d") stats_cursor.execute(""" SELECT user_id, messages_count FROM message_stats WHERE chat_id = ? AND date = ? ORDER BY messages_count DESC LIMIT ? """, (chat_id, today, limit)) return stats_cursor.fetchall() except Exception as e: print(f"[STATS DB] Ошибка get_chat_top_today: {e}") return [] async def clean_old_stats(days: int = 90) -> int: """Удаляет старые записи из message_stats (старше days дней)""" try: cutoff_date = (datetime.now(MSK_TZ) - timedelta(days=days)).strftime("%Y-%m-%d") stats_cursor.execute("DELETE FROM message_stats WHERE date < ?", (cutoff_date,)) deleted = stats_cursor.rowcount stats_conn.commit() upload_stats_db() return deleted except Exception as e: print(f"[STATS DB] Ошибка clean_old_stats: {e}") return 0 async def recalc_total_stats(chat_id: int = None): """Пересчитывает total_stats из message_stats""" try: if chat_id: stats_cursor.execute(""" SELECT user_id, SUM(messages_count) as total FROM message_stats WHERE chat_id = ? GROUP BY user_id """, (chat_id,)) else: stats_cursor.execute(""" SELECT user_id, chat_id, SUM(messages_count) as total FROM message_stats GROUP BY user_id, chat_id """) rows = stats_cursor.fetchall() now = datetime.now(MSK_TZ).strftime("%Y-%m-%d %H:%M:%S") for row in rows: if chat_id: user_id, total = row stats_cursor.execute(""" INSERT OR REPLACE INTO total_stats (chat_id, user_id, total_messages, last_updated) VALUES (?, ?, ?, ?) """, (chat_id, user_id, total, now)) else: user_id, chat_id, total = row stats_cursor.execute(""" INSERT OR REPLACE INTO total_stats (chat_id, user_id, total_messages, last_updated) VALUES (?, ?, ?, ?) """, (chat_id, user_id, total, now)) stats_conn.commit() upload_stats_db() return len(rows) except Exception as e: print(f"[STATS DB] Ошибка recalc_total_stats: {e}") return 0 def close_stats_connection(): """Закрывает соединение с БД статистики""" try: upload_stats_db() stats_conn.close() print("[STATS DB] Соединение закрыто, БД сохранена") except Exception as e: print(f"[STATS DB] Ошибка при закрытии: {e}") # ============================================================ # АВТОМАТИЧЕСКОЕ СОХРАНЕНИЕ ПРИ ЗАВЕРШЕНИИ # ============================================================ import atexit atexit.register(close_stats_connection) print("[STATS DB] База данных статистики инициализирована")