From f23944d4c5141cdc1bded9aa926555da15d61ff0 Mon Sep 17 00:00:00 2001 From: imletbruh <60918217+qiyanaitsme@users.noreply.github.com> Date: Tue, 8 Sep 2026 18:46:51 +0500 Subject: [PATCH] Add files via upload --- api_client.py | 2 ++ app.py | 31 +++++++++++--------- config_manager.py | 2 +- database.py | 72 ++++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 86 insertions(+), 21 deletions(-) diff --git a/api_client.py b/api_client.py index 893bfca..00fb0f8 100644 --- a/api_client.py +++ b/api_client.py @@ -306,6 +306,7 @@ class APIClient: wait = self._rate_limit_wait_seconds(resp.headers, body) wait = wait if wait is not None else DEFAULT_429_WAIT_SECONDS wait = min(wait, MAX_RATE_LIMIT_WAIT_SECONDS) + last_error = f"rate limited (429), waited {wait:.0f}s" logger.warning(f"Batch rate-limited (429), waiting {wait:.0f}s (attempt {attempt + 1})") await asyncio.sleep(wait) continue @@ -357,6 +358,7 @@ class APIClient: wait = self._rate_limit_wait_seconds(resp.headers, body) wait = wait if wait is not None else DEFAULT_429_WAIT_SECONDS wait = min(wait, MAX_RATE_LIMIT_WAIT_SECONDS) + last_error = f"rate limited (429), waited {wait:.0f}s" logger.warning(f"GET {path} rate-limited (429), waiting {wait:.0f}s") await asyncio.sleep(wait) continue diff --git a/app.py b/app.py index dac2764..e5da0b4 100644 --- a/app.py +++ b/app.py @@ -68,18 +68,22 @@ class AuthMiddleware(BaseMiddleware): event: TelegramObject, data: dict[str, Any], ) -> Any: - from_user = getattr(event, "from_user", None) - if from_user is None: - cq = getattr(event, "callback_query", None) - if cq is not None: - from_user = cq.from_user - else: - msg = getattr(event, "message", None) - if msg is not None: - from_user = msg.from_user + # `event` is the raw Update; `.event` resolves it to whichever concrete + # sub-object is actually set (message, callback_query, chat_member, ...). + try: + actual_event = event.event + except Exception: + actual_event = event + from_user = getattr(actual_event, "from_user", None) if from_user is None: - return await handler(event, data) + # Unknown identity (e.g. channel_post, poll, message_reaction) — fail + # closed instead of letting it through unauthenticated. + logger.warning( + f"AuthMiddleware: update without from_user " + f"(type={getattr(event, 'event_type', '?')}), denying" + ) + return None if from_user.id != self._admin_user_id: chat_id = None @@ -267,8 +271,8 @@ class AutoBumpBot: f"⏰ Автоподнятие каждые {_format_interval(interval)}" ), ) - except TelegramBadRequest as e: - logger.error(f"Failed to send start message: {e}") + except Exception as e: + logger.error(f"Failed to send start message: {e}", exc_info=True) await message.answer("❌ Ошибка отправки сообщения. Попробуйте /start снова.") # ─── Add Thread ───────────────────────────────────────────── @@ -783,12 +787,13 @@ class AutoBumpBot: f"✅ BUMP SUCCESS | Thread: {result.thread_id} | " f"Status: {result.status.value} | Message: {result.message}" ) - await self._db.update_last_bumped(result.thread_id) + await self._db.record_bump_success(result.thread_id) else: logger.error( f"❌ BUMP FAILED | Thread: {result.thread_id} | " f"Status: {result.status.value} | Message: {result.message}" ) + await self._db.record_bump_failure(result.thread_id) total = len(results) await self._db.increment_bump_stats(success_count, total) diff --git a/config_manager.py b/config_manager.py index 727c376..aa1d2d3 100644 --- a/config_manager.py +++ b/config_manager.py @@ -94,7 +94,7 @@ class Config: scheduling = SchedulingConfig( bump_interval_minutes=interval_minutes, - bump_delay_seconds=float(os.getenv("BUMP_DELAY_SECONDS", "2")), + bump_delay_seconds=float(os.getenv("BUMP_DELAY_SECONDS", "1")), enable_auto_bump=os.getenv("ENABLE_AUTO_BUMP", "true").lower() == "true", scheduler_tick_seconds=tick, ) diff --git a/database.py b/database.py index b1084fc..aad2b0b 100644 --- a/database.py +++ b/database.py @@ -73,15 +73,32 @@ class Database: id TEXT PRIMARY KEY, title TEXT NOT NULL, last_bumped TEXT, + last_attempt TEXT, + fail_streak INTEGER NOT NULL DEFAULT 0, created_at TEXT DEFAULT CURRENT_TIMESTAMP ) """) + # Migration for DBs created before last_attempt/fail_streak existed. + async with self._connection.execute("PRAGMA table_info(threads)") as cursor: + existing_columns = {row[1] for row in await cursor.fetchall()} + if "last_attempt" not in existing_columns: + await self._connection.execute("ALTER TABLE threads ADD COLUMN last_attempt TEXT") + if "fail_streak" not in existing_columns: + await self._connection.execute( + "ALTER TABLE threads ADD COLUMN fail_streak INTEGER NOT NULL DEFAULT 0" + ) + await self._connection.execute(""" CREATE INDEX IF NOT EXISTS idx_last_bumped ON threads(last_bumped) """) + await self._connection.execute(""" + CREATE INDEX IF NOT EXISTS idx_last_attempt + ON threads(last_attempt) + """) + await self._connection.execute(""" CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, @@ -228,7 +245,12 @@ class Database: raise async def get_threads_to_bump(self, interval_minutes: float) -> list[Thread]: - """Threads due for a bump: the interval has passed since the last one.""" + """Threads due for a bump: the interval has passed since the last attempt. + + Threads that keep failing back off exponentially (interval * 2**fail_streak, + capped at 60x) so a permanently broken thread doesn't get retried on every + single tick and burn through the batch rate limit. + """ self._ensure_connected() if interval_minutes < 0: raise ValueError("interval_minutes must be non-negative") @@ -236,9 +258,20 @@ class Database: async with self._connection.execute( """ SELECT id, title, last_bumped FROM threads - WHERE last_bumped IS NULL - OR datetime(last_bumped, '+' || ? || ' minutes') <= datetime('now') - ORDER BY last_bumped ASC NULLS FIRST + WHERE last_attempt IS NULL + OR datetime( + last_attempt, + '+' || (? * CASE + WHEN fail_streak <= 0 THEN 1 + WHEN fail_streak = 1 THEN 2 + WHEN fail_streak = 2 THEN 4 + WHEN fail_streak = 3 THEN 8 + WHEN fail_streak = 4 THEN 16 + WHEN fail_streak = 5 THEN 32 + ELSE 60 + END) || ' minutes' + ) <= datetime('now') + ORDER BY last_attempt ASC NULLS FIRST """, (interval_minutes,), ) as cursor: @@ -248,16 +281,41 @@ class Database: logger.error(f"Error fetching threads to bump: {e}") raise - async def update_last_bumped(self, thread_id: str) -> None: + async def record_bump_success(self, thread_id: str) -> None: + """Mark a thread as successfully bumped and reset its failure backoff.""" self._ensure_connected() try: await self._connection.execute( - "UPDATE threads SET last_bumped = datetime('now') WHERE id = ?", + """ + UPDATE threads SET + last_bumped = datetime('now'), + last_attempt = datetime('now'), + fail_streak = 0 + WHERE id = ? + """, (thread_id,), ) await self._connection.commit() except Exception as e: - logger.error(f"Error updating last_bumped for thread {thread_id}: {e}") + logger.error(f"Error recording bump success for thread {thread_id}: {e}") + raise + + async def record_bump_failure(self, thread_id: str) -> None: + """Record a failed bump attempt and increase its retry backoff.""" + self._ensure_connected() + try: + await self._connection.execute( + """ + UPDATE threads SET + last_attempt = datetime('now'), + fail_streak = fail_streak + 1 + WHERE id = ? + """, + (thread_id,), + ) + await self._connection.commit() + except Exception as e: + logger.error(f"Error recording bump failure for thread {thread_id}: {e}") raise async def delete_thread(self, thread_id: str) -> bool: