fix by codex

This commit is contained in:
vrubelroman 2026-03-21 22:58:47 +03:00
parent 3ffcf97c3f
commit 63e28c279c
3 changed files with 67 additions and 21 deletions

View file

@ -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

View file

@ -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"""

View file

@ -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)