From 63e28c279c552350bdbd5ec6680d125424cbbd4f Mon Sep 17 00:00:00 2001 From: vrubelroman Date: Sat, 21 Mar 2026 22:58:47 +0300 Subject: [PATCH] fix by codex --- LichessClientTG_bot/bot.py | 45 +++++++++++++++++-------------- LichessClientTG_bot/database.py | 41 ++++++++++++++++++++++++++++ LichessClientTG_bot/formatters.py | 2 +- 3 files changed, 67 insertions(+), 21 deletions(-) diff --git a/LichessClientTG_bot/bot.py b/LichessClientTG_bot/bot.py index 64fd07a..deff442 100644 --- a/LichessClientTG_bot/bot.py +++ b/LichessClientTG_bot/bot.py @@ -47,6 +47,15 @@ class LichessBot: self.application = None # Will be set when application is created self.counters = MessageCounters() # Message counters self.request_queue = get_request_queue() # Request queue for rate limiting + + async def stop_periodic_task(self, gamer_id: int, user_id: int): + """Stop periodic task for a user-gamer pair.""" + task_key = f"{gamer_id}_{user_id}" + task = self.periodic_tasks.pop(task_key, None) + if task: + task.cancel() + logger.info(f"Cancelled periodic task for gamer_id={gamer_id}, user_id={user_id}") + self.period_start_times.pop(task_key, None) async def _notify_admin_new_player(self, player_username: str, added_by_user_id: int, added_by_username: Optional[str], is_new_gamer: bool = False): """Notify admin about newly linked player (always try to send).""" @@ -1275,8 +1284,10 @@ class LichessBot: # Set period for this user-gamer pair self.db.set_user_gamer_period(user_id, gamer_id, period) + self.db.clear_period_checkpoint(user_id, gamer_id) if period == 0: + await self.stop_periodic_task(gamer_id, user_id) await query.edit_message_text( t('notifications_disabled', lang, username=selected_gamer['username']) ) @@ -1440,8 +1451,12 @@ class LichessBot: task_key = f"{gamer['id']}_{user_id}" username = gamer['username'] - # НЕ устанавливаем period_start_times при инициализации - # Это позволит использовать логику первой проверки (else блок) + checkpoint_ts = self.db.get_period_checkpoint(user_id, gamer['id']) + if checkpoint_ts is not None: + restored_time = datetime.fromtimestamp(checkpoint_ts) + self.period_start_times[task_key] = restored_time + logger.info(f"♻️ Restored periodic checkpoint for {username} (user {user_id}) at {restored_time}") + logger.info(f"🔄 Started periodic monitoring for {username} (user {user_id}) with {period_minutes} minute intervals") consecutive_errors = 0 @@ -1477,9 +1492,9 @@ class LichessBot: # Получаем сохраненное время последней проверки для расчета следующего периода # Используем флаг is_first_check для первой проверки вместо проверки наличия ключа last_check_time = self.period_start_times.get(task_key) - if is_first_check: + if is_first_check and checkpoint_ts is None: last_check_time = None # Принудительно делаем первую проверку - is_first_check = False + is_first_check = False if last_check_time: # Уже была хотя бы одна проверка @@ -1528,6 +1543,8 @@ class LichessBot: self.lichess_api.get_games_period, gamer['username'], since_timestamp, until_timestamp_approx ) + if games_data is None: + raise RuntimeError("Games period API returned no data") # Фиксируем фактическое время получения ответа request_end_time = datetime.now() logger.info(f"✅ Games API response received for {gamer['username']} at {request_end_time}") @@ -1537,10 +1554,8 @@ class LichessBot: if consecutive_errors >= max_consecutive_errors: logger.error(f"Too many consecutive errors for {gamer['username']}, stopping periodic check") break - # Продолжаем с обновлением времени начала на планируемое время окончания периода - # чтобы не создавать пропусков в следующих проверках - self.period_start_times[task_key] = period_end_approx - logger.warning(f"⚠️ Error occurred, updated period_start_time to {period_end_approx} (planned period end)") + logger.warning(f"⚠️ Games data unavailable for {gamer['username']}; retrying the same period in 60 seconds") + await asyncio.sleep(60) continue if gamer.get('token'): @@ -1656,6 +1671,7 @@ class LichessBot: # # period_end_approx уже установлено в начале итерации self.period_start_times[task_key] = period_end_approx + self.db.set_period_checkpoint(user_id, gamer['id'], int(period_end_approx.timestamp())) logger.info(f"📌 Updated period_start_time for {username} to {period_end_approx} (planned period end, next period will start from here)") if 'request_end_time' in locals(): delay = (request_end_time - period_end_approx).total_seconds() @@ -1678,18 +1694,7 @@ class LichessBot: if task_key in self.period_start_times: del self.period_start_times[task_key] break - - # Важно: обновляем period_start_times даже при ошибке, чтобы не зациклиться на одном периоде - # Используем period_end_approx, если он был установлен, иначе используем текущее время - if 'period_end_approx' in locals(): - self.period_start_times[task_key] = period_end_approx - logger.warning(f"⚠️ Error occurred, updated period_start_time to {period_end_approx} to prevent loop") - else: - # Если period_end_approx не был установлен (например, ошибка в начале), используем текущее время - now = datetime.now() - self.period_start_times[task_key] = now - logger.warning(f"⚠️ Error occurred early, updated period_start_time to {now} to prevent loop") - + # Ждем перед повторной попыткой при ошибке await asyncio.sleep(60) # 1 minute delay before retry diff --git a/LichessClientTG_bot/database.py b/LichessClientTG_bot/database.py index 34f2ef5..b60db2a 100644 --- a/LichessClientTG_bot/database.py +++ b/LichessClientTG_bot/database.py @@ -64,6 +64,7 @@ class Database: token TEXT, is_active BOOLEAN DEFAULT FALSE, period_minutes INTEGER DEFAULT 0, + last_period_end_ts INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES telegram_users(user_id), FOREIGN KEY (gamer_id) REFERENCES gamers(id), @@ -77,6 +78,13 @@ class Database: except sqlite3.OperationalError: # Column already exists pass + + # Add last_period_end_ts column to persist periodic checkpoint across restarts + try: + cursor.execute("ALTER TABLE user_gamers ADD COLUMN last_period_end_ts INTEGER") + except sqlite3.OperationalError: + # Column already exists + pass # Create admin_settings table for admin bot configuration cursor.execute(''' @@ -326,6 +334,39 @@ class Database: (period_minutes, user_id, gamer_id) ) conn.commit() + + def get_period_checkpoint(self, user_id: int, gamer_id: int) -> Optional[int]: + """Get persisted period checkpoint timestamp (seconds since epoch).""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + "SELECT last_period_end_ts FROM user_gamers WHERE user_id = ? AND gamer_id = ?", + (user_id, gamer_id) + ) + row = cursor.fetchone() + if not row or row[0] is None: + return None + return int(row[0]) + + def set_period_checkpoint(self, user_id: int, gamer_id: int, checkpoint_ts: int): + """Persist period checkpoint timestamp (seconds since epoch).""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + "UPDATE user_gamers SET last_period_end_ts = ? WHERE user_id = ? AND gamer_id = ?", + (checkpoint_ts, user_id, gamer_id) + ) + conn.commit() + + def clear_period_checkpoint(self, user_id: int, gamer_id: int): + """Clear persisted period checkpoint.""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + "UPDATE user_gamers SET last_period_end_ts = NULL WHERE user_id = ? AND gamer_id = ?", + (user_id, gamer_id) + ) + conn.commit() def remove_user_gamer(self, user_id: int, gamer_id: int) -> bool: """Remove gamer from user's tracked list""" diff --git a/LichessClientTG_bot/formatters.py b/LichessClientTG_bot/formatters.py index e0bf77d..2244740 100644 --- a/LichessClientTG_bot/formatters.py +++ b/LichessClientTG_bot/formatters.py @@ -164,7 +164,7 @@ class StatsFormatter: emoji = StatsFormatter._get_game_type_emoji(game_type) games_count = game_data.get('games_played', 0) rating_change = game_data.get('rating_change', 0) - rating = game_data.get('rating', 0) + rating = game_data.get('final_rating', game_data.get('rating', 0)) wins = game_data.get('wins', 0) losses = game_data.get('losses', 0) draws = game_data.get('draws', 0)