From ef9aa3d3df9117788cbf4a63fcf4f4cf9affe980 Mon Sep 17 00:00:00 2001 From: vrubelroman Date: Thu, 20 Nov 2025 01:22:52 +0300 Subject: [PATCH] fix bug push from timer --- LichessClientTG_bot/bot.py | 45 +++++--- analyze_notifications.py | 145 +++++++++++++++++++++++ check_today_activity.py | 230 +++++++++++++++++++++++++++++++++++++ 3 files changed, 402 insertions(+), 18 deletions(-) create mode 100644 analyze_notifications.py create mode 100644 check_today_activity.py diff --git a/LichessClientTG_bot/bot.py b/LichessClientTG_bot/bot.py index 27f3d46..62ea00c 100644 --- a/LichessClientTG_bot/bot.py +++ b/LichessClientTG_bot/bot.py @@ -1243,16 +1243,31 @@ class LichessBot: # Проверяем наличие реальной активности has_games = False total_games = 0 - if games_data and games_data.get('data'): - total_games = games_data.get('data', {}).get('total', {}).get('games_played', 0) - has_games = total_games > 0 + if games_data: + # Логируем структуру ответа для отладки + logger.debug(f"Games data structure for {gamer['username']}: {games_data}") + if games_data.get('data'): + total_games = games_data.get('data', {}).get('total', {}).get('games_played', 0) + has_games = total_games > 0 + # Также проверяем games_count на верхнем уровне + elif games_data.get('games_count', 0) > 0: + total_games = games_data.get('games_count', 0) + has_games = True + else: + logger.warning(f"No games_data returned for {gamer['username']}") has_puzzles = False - if puzzles_data and puzzles_data.get('data'): - total_puzzles = puzzles_data.get('data', {}).get('total_attempts', 0) - has_puzzles = total_puzzles > 0 + total_puzzles = 0 + if puzzles_data: + if puzzles_data.get('data'): + total_puzzles = puzzles_data.get('data', {}).get('total_attempts', 0) + has_puzzles = total_puzzles > 0 + # Также проверяем puzzles_in_period на верхнем уровне + elif puzzles_data.get('puzzles_in_period', 0) > 0: + total_puzzles = puzzles_data.get('puzzles_in_period', 0) + has_puzzles = True - logger.info(f"Activity check for {gamer['username']}: has_games={has_games}, has_puzzles={has_puzzles}") + logger.info(f"Activity check for {gamer['username']}: has_games={has_games} (total={total_games}), has_puzzles={has_puzzles} (total={total_puzzles})") # Отправляем уведомление только если есть реальная активность if has_games or has_puzzles: @@ -1271,29 +1286,23 @@ class LichessBot: text=notification ) logger.info(f"✅ Sent periodic notification for {gamer['username']} to user {user_id}") - # Обновляем время начала на текущее время после успешной отправки уведомления - self.period_start_times[task_key] = now # Increment periodic notification counter self.counters.increment('periodic_notification') except Exception as e: logger.error(f"❌ Failed to send notification to user {user_id}: {e}") import traceback logger.error(f"Traceback: {traceback.format_exc()}") - # Обновляем время начала даже при ошибке отправки, чтобы не зацикливаться - self.period_start_times[task_key] = now else: logger.error(f"❌ Application not initialized, cannot send notification for {gamer['username']} to user {user_id}") - self.period_start_times[task_key] = now except Exception as e: logger.error(f"Error formatting notification for {gamer['username']}: {e}") import traceback logger.error(f"Traceback: {traceback.format_exc()}") - # Обновляем время начала даже при ошибке форматирования - self.period_start_times[task_key] = now - else: - logger.debug(f"⏭️ No activity found for {gamer['username']} in the last {period_minutes} minutes") - # Обновляем время начала на текущее время даже если нет активности - self.period_start_times[task_key] = now + else: + logger.debug(f"⏭️ No activity found for {gamer['username']} in the last {period_minutes} minutes") + + # Всегда обновляем время начала на текущее время после проверки (независимо от наличия активности) + self.period_start_times[task_key] = now except asyncio.CancelledError: logger.info(f"Periodic check cancelled for {gamer['username']}") diff --git a/analyze_notifications.py b/analyze_notifications.py new file mode 100644 index 0000000..c143af3 --- /dev/null +++ b/analyze_notifications.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +""" +Анализ уведомлений: проверяем сколько пользователей отслеживают каждого игрока +и сколько уведомлений должно быть отправлено +""" +import sqlite3 +import os + +# Определяем путь к базе данных +db_path = os.getenv("DATABASE_PATH") +if not db_path: + possible_paths = [ + "LichessClientTG_bot/data/lichess_bot.db", + "data/lichess_bot.db", + "/app/data/lichess_bot.db" + ] + for path in possible_paths: + if os.path.exists(path): + db_path = path + break + +if not db_path: + print("База данных не найдена!") + exit(1) + +print(f"Используется база данных: {db_path}\n") + +# Список игроков, которые играли сегодня (из вашего вывода) +active_players = [ + 'AARON805', 'Aangelin', 'Agorchakov', 'Almansura', 'Anastaysha_champion', + 'Artemij1', 'Benhayatinmagllbuyum', 'Berserkgirl', 'Danila_07', 'DianaVlad', + 'ESP2209', 'Epineyf', 'Ferstep', 'Freakwithprime', 'Kerop', + 'Kmknnaqw', 'Kraev_Arseniy', 'MX2017', 'MaEstRo_234', 'Maestro_234', + 'Matovalov', 'Ovcharovnik', 'PAA17', 'PaholkovMaks', 'PerepechkoD', + 'Petyabro', 'Recep-1903', 'Rudy23042014', 'Sayan_13', 'Sergey_Kleiner', + 'Soup_Maktavish_t', 'Stolypin_2-0', 'Tahamorphyy', 'Taih', 'TasiaArgylova', + 'Timofei700', 'TomasI_25', 'UrielSpb', 'VadimVits', 'Vadimvits', + 'Valeria1309', 'Veselin_27', 'Vinni_Jr', 'Vyachesslav_Rusakov', 'Whateverw', + 'Yaroslavsm', 'Yuliyana2013', 'besfighter', 'braveyounes', 'gobelen', + 'kolobyxov_daniil52', 'kostik2811', 'li_alex2017', 'lmmaster', 'metiforce', + 'motheroflearning', 'polypoker', 'qwertycher', 'speedrunchessgames', + 'warriorchess70', 'wwwspl', 'yasumichi', 'yeti_tais', 'youngking0007' +] + +conn = sqlite3.connect(db_path) +cursor = conn.cursor() + +print("="*80) +print("АНАЛИЗ УВЕДОМЛЕНИЙ") +print("="*80) + +# 1. Сколько всего пар user-gamer с period_minutes > 0 +cursor.execute("SELECT COUNT(*) FROM user_gamers WHERE period_minutes > 0") +total_pairs = cursor.fetchone()[0] +print(f"\n1. Всего пар user-gamer с периодическими уведомлениями: {total_pairs}") + +# 2. Сколько уникальных игроков отслеживается с периодами +cursor.execute("SELECT COUNT(DISTINCT gamer_id) FROM user_gamers WHERE period_minutes > 0") +unique_gamers = cursor.fetchone()[0] +print(f"2. Уникальных игроков с периодами: {unique_gamers}") + +# 3. Для каждого из 64 активных игроков - сколько пользователей их отслеживают +print(f"\n3. Анализ активных игроков (играли сегодня):") +print("-"*80) + +total_expected_notifications = 0 +players_with_trackers = [] + +for player_username in active_players: + # Находим gamer_id по username + cursor.execute("SELECT id FROM gamers WHERE username = ?", (player_username,)) + result = cursor.fetchone() + + if not result: + continue + + gamer_id = result[0] + + # Сколько пользователей отслеживают этого игрока с периодом > 0 + cursor.execute(""" + SELECT COUNT(*) + FROM user_gamers + WHERE gamer_id = ? AND period_minutes > 0 + """, (gamer_id,)) + + tracker_count = cursor.fetchone()[0] + + if tracker_count > 0: + total_expected_notifications += tracker_count + players_with_trackers.append((player_username, tracker_count)) + + # Показываем детали для первых 10 + if len(players_with_trackers) <= 10: + print(f" • {player_username}: {tracker_count} пользователь(ей) отслеживает") + +if len(players_with_trackers) > 10: + print(f" ... и еще {len(players_with_trackers) - 10} игроков с отслеживающими пользователями") + +print(f"\n4. ИТОГО:") +print(f" - Активных игроков (играли сегодня): {len(active_players)}") +print(f" - Из них отслеживаются пользователями: {len(players_with_trackers)}") +print(f" - Ожидаемое количество уведомлений: {total_expected_notifications}") +print(f" - Фактически отправлено уведомлений: 27-28") +print(f" - ПРОПУЩЕНО уведомлений: {total_expected_notifications - 27}") + +# 5. Проверяем распределение периодов +print(f"\n5. Распределение периодов уведомлений:") +cursor.execute(""" + SELECT period_minutes, COUNT(*) + FROM user_gamers + WHERE period_minutes > 0 + GROUP BY period_minutes + ORDER BY period_minutes +""") +for period, count in cursor.fetchall(): + period_hours = period / 60 + print(f" - {period} минут ({period_hours:.1f} ч): {count} пар user-gamer") + +# 6. Проверяем, есть ли игроки с несколькими отслеживающими +print(f"\n6. Игроки, отслеживаемые несколькими пользователями:") +cursor.execute(""" + SELECT g.username, COUNT(*) as tracker_count + FROM user_gamers ug + JOIN gamers g ON ug.gamer_id = g.id + WHERE ug.period_minutes > 0 + GROUP BY g.id, g.username + HAVING tracker_count > 1 + ORDER BY tracker_count DESC + LIMIT 10 +""") +for username, count in cursor.fetchall(): + print(f" - {username}: {count} пользователей") + +conn.close() + +print("\n" + "="*80) +print("ВЫВОД:") +print("="*80) +print(f"Если ожидалось {total_expected_notifications} уведомлений, а отправлено только 27-28,") +print(f"то пропущено примерно {total_expected_notifications - 27} уведомлений.") +print("Возможные причины:") +print("1. Периодические задачи не запускаются для всех пар user-gamer") +print("2. Логика проверки активности работает неправильно") +print("3. Уведомления не отправляются всем пользователям, отслеживающим одного игрока") + diff --git a/check_today_activity.py b/check_today_activity.py new file mode 100644 index 0000000..a5f3c49 --- /dev/null +++ b/check_today_activity.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 +""" +Скрипт для проверки активности всех отслеживаемых игроков за сегодня. +Делает запросы к Lichess API с задержкой 200 мс между запросами. +""" +import sqlite3 +import logging +import requests +import json +import time +from datetime import datetime, timezone +from typing import List, Dict, Any, Optional +import os + +# Настройка логирования +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +def get_all_tracked_players(db_path: str) -> List[str]: + """Получить список всех уникальных отслеживаемых игроков из базы данных""" + try: + with sqlite3.connect(db_path) as conn: + cursor = conn.cursor() + # Получаем все уникальные username из таблицы gamers + cursor.execute("SELECT DISTINCT username FROM gamers ORDER BY username") + players = [row[0] for row in cursor.fetchall()] + return players + except Exception as e: + logger.error(f"Ошибка при получении списка игроков из базы: {e}") + return [] + + +def get_today_timestamps() -> tuple[int, int]: + """Получить начало и конец сегодняшнего дня в миллисекундах""" + now = datetime.now(timezone.utc) + + # Начало сегодняшнего дня (00:00:00 UTC) + today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) + + # Конец сегодняшнего дня (23:59:59.999 UTC) + today_end = now.replace(hour=23, minute=59, second=59, microsecond=999000) + + # Конвертируем в миллисекунды (Unix timestamp * 1000) + since_ms = int(today_start.timestamp() * 1000) + until_ms = int(today_end.timestamp() * 1000) + + return since_ms, until_ms + + +class RateLimiter: + """Простой rate limiter для синхронных запросов""" + def __init__(self, min_delay: float = 1.0): + self.min_delay = min_delay + self.last_request_time = None + + def wait_before_request(self): + """Ждать перед запросом если нужно соблюсти минимальную задержку""" + if self.last_request_time is not None: + elapsed = time.time() - self.last_request_time + if elapsed < self.min_delay: + wait_time = self.min_delay - elapsed + logger.debug(f"Ждем {wait_time:.2f} сек перед следующим запросом") + time.sleep(wait_time) + + def wait_after_response(self): + """Ждать после получения ответа чтобы соблюсти минимальную задержку между запросами""" + self.last_request_time = time.time() + # Всегда ждем 1 секунду после получения ответа перед следующим запросом + time.sleep(self.min_delay) + + +def get_games_of_period(rate_limiter: RateLimiter, username: str, since_ms: int, until_ms: int, + rated_only: bool = False) -> Optional[List[Dict[str, Any]]]: + """Получить игры пользователя за период напрямую из Lichess API""" + # Ждем перед запросом если нужно (для первого запроса не ждем) + rate_limiter.wait_before_request() + + url = f"https://lichess.org/api/games/user/{username}" + params = { + 'since': since_ms, + 'until': until_ms, + 'max': 1000 + } + if rated_only: + params['rated'] = 'true' + + headers = { + 'Accept': 'application/x-ndjson' + } + + try: + # Делаем запрос + response = requests.get(url, params=params, headers=headers, timeout=30) + + # Получаем ответ и парсим + if response.status_code == 404: + logger.warning(f"Пользователь {username} не найден (404)") + rate_limiter.wait_after_response() # Ждем 1 сек после ответа + return None + elif response.status_code != 200: + logger.error(f"Ошибка API для {username}: {response.status_code} - {response.text[:200]}") + rate_limiter.wait_after_response() # Ждем 1 сек после ответа + return None + + # Парсим NDJSON + games = [] + content = response.text.strip() + if content: + for line in content.split('\n'): + if line.strip(): + try: + game = json.loads(line) + games.append(game) + except json.JSONDecodeError as e: + logger.warning(f"Ошибка парсинга JSON для {username}: {e}") + continue + + # Ждем 1 секунду после получения ответа перед следующим запросом + rate_limiter.wait_after_response() + return games + except Exception as e: + logger.error(f"Ошибка при запросе игр для {username}: {e}") + rate_limiter.wait_after_response() # Ждем 1 сек даже при ошибке + return None + + +def check_player_activity(rate_limiter: RateLimiter, username: str, since_ms: int, until_ms: int) -> bool: + """Проверить, играл ли игрок сегодня""" + try: + games = get_games_of_period(rate_limiter, username, since_ms, until_ms, rated_only=False) + if games is None: + # Пользователь не найден или ошибка API + return False + return len(games) > 0 + except Exception as e: + logger.error(f"Ошибка при проверке игрока {username}: {e}") + return False + + +def main(): + """Основная функция скрипта""" + # Определяем путь к базе данных + db_path = os.getenv("DATABASE_PATH") + if not db_path: + # Пробуем найти базу в стандартных местах + possible_paths = [ + "LichessClientTG_bot/data/lichess_bot.db", + "data/lichess_bot.db", + "/app/data/lichess_bot.db" + ] + for path in possible_paths: + if os.path.exists(path): + db_path = path + break + + if not db_path: + logger.error("Не удалось найти базу данных. Укажите DATABASE_PATH в переменных окружения.") + return + + logger.info(f"Используется база данных: {db_path}") + + # Получаем список всех игроков + players = get_all_tracked_players(db_path) + total_players = len(players) + + if total_players == 0: + logger.warning("В базе данных не найдено отслеживаемых игроков") + return + + logger.info(f"Найдено отслеживаемых игроков: {total_players}") + + # Получаем временные метки для сегодняшнего дня + since_ms, until_ms = get_today_timestamps() + today_str = datetime.now(timezone.utc).strftime("%Y-%m-%d") + logger.info(f"Проверяем активность за {today_str} (с {since_ms} по {until_ms})") + + # Создаем rate limiter (10 секунд) + rate_limiter = RateLimiter(min_delay=10.0) + + # Проверяем каждого игрока + active_players = [] + inactive_players = [] + error_players = [] + + logger.info("Начинаем проверку игроков...") + + for i, username in enumerate(players, 1): + logger.info(f"[{i}/{total_players}] Проверяем {username}...") + + try: + is_active = check_player_activity(rate_limiter, username, since_ms, until_ms) + if is_active: + active_players.append(username) + logger.info(f" ✓ {username} играл сегодня") + else: + inactive_players.append(username) + logger.info(f" ✗ {username} не играл сегодня") + except Exception as e: + error_players.append((username, str(e))) + logger.error(f" ✗ Ошибка при проверке {username}: {e}") + + # Выводим итоговую статистику + print("\n" + "="*60) + print("ИТОГОВАЯ СТАТИСТИКА") + print("="*60) + print(f"Всего отслеживаемых игроков: {total_players}") + print(f"Играли сегодня: {len(active_players)}") + print(f"Не играли сегодня: {len(inactive_players)}") + if error_players: + print(f"Ошибки при проверке: {len(error_players)}") + print("="*60) + + if active_players: + print(f"\nИгроки, которые играли сегодня ({len(active_players)}):") + for username in active_players: + print(f" • {username}") + + if error_players: + print(f"\nИгроки с ошибками ({len(error_players)}):") + for username, error in error_players: + print(f" • {username}: {error}") + + +if __name__ == "__main__": + main() +