feat(bot): proactive cookie health-check + simpler user-facing errors
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 51s

Add /cookies/check to youtube-downloader and instagram-downloader,
polled every 30 min from bot.py; alerts the admin bot on
healthy->broken transitions (with a 24h re-reminder cap while still
broken) and on recovery. Also stop leaking raw exception text to end
users - they now see a plain, friendly English message while the
admin bot still gets full technical detail via notify_admin_error.
This commit is contained in:
vrubelroman 2026-07-25 22:30:49 +00:00
parent be1acaaf2b
commit 5092d882e5
3 changed files with 166 additions and 4 deletions

109
bot.py
View file

@ -1,5 +1,6 @@
import os
import re
import json
import logging
import asyncio
import sqlite3
@ -109,7 +110,7 @@ TEXTS = {
'downloading': "⬇️ Скачиваю видео...",
'sending': "📤 Отправляю видео...",
'caption': "Видео скачано с @{bot_username}",
'error': "Произошла ошибка при обработке видео:\n{error}",
'error': "Something went wrong while processing your video. Please try again in a bit.",
'error_unknown_source': "Пардон, не умеем работать с этим источником",
'error_vk_not_video': "❌ Это ссылка VK, но не на видео. Пришлите ссылку вида vk.com/video... или vk.com/clip...",
'error_file_too_large': "❌ Видео слишком большое ({size_mb:.1f} МБ, max = 50)",
@ -167,7 +168,7 @@ TEXTS = {
'downloading': "⬇️ Downloading video...",
'sending': "📤 Sending video...",
'caption': "Video downloaded via @{bot_username}",
'error': "Error processing video:\n{error}",
'error': "Something went wrong while processing your video. Please try again in a bit.",
'error_unknown_source': "Sorry, this source is not supported",
'error_vk_not_video': "❌ This is a VK link, but not a video. Send a vk.com/video... or vk.com/clip... link.",
'error_file_too_large': "❌ Video is too large ({size_mb:.1f} MB, max = 50)",
@ -597,6 +598,94 @@ async def notify_admin_error(url: str, error_text: str, from_user=None):
logger.error(f"Ошибка при отправке уведомления об ошибке админ боту: {e}")
COOKIE_STATUS_FILE = DATA_DIR / 'cookie_health_state.json'
COOKIE_CHECK_INTERVAL = 30 * 60 # 30 минут — дешёвая read-only проверка
COOKIE_REMINDER_INTERVAL = 24 * 3600 # не чаще раза в сутки повторное напоминание, пока всё ещё сломано
def _load_cookie_state() -> dict:
try:
return json.loads(COOKIE_STATUS_FILE.read_text())
except Exception:
return {"youtube": "healthy", "instagram": "healthy", "last_reminder_ts": {}}
def _save_cookie_state(state: dict):
try:
COOKIE_STATUS_FILE.write_text(json.dumps(state))
except Exception as e:
logger.error(f"Не удалось сохранить состояние cookie-health: {e}")
async def notify_admin_cookie_alert(service: str, healthy: bool, detail: str):
"""Проактивное уведомление о состоянии cookies (health-check), не связано с ошибкой конкретного пользователя"""
if not ADMIN_BOT_TOKEN:
return
admin_chat_id = get_admin_chat_id()
if not admin_chat_id:
return
try:
request = HTTPXRequest(
read_timeout=30,
write_timeout=30,
connect_timeout=15,
pool_timeout=15
)
admin_bot = Bot(token=ADMIN_BOT_TOKEN, request=request)
if healthy:
text = f"✅ Cookies восстановлены\n\n🍪 Сервис: {service}\nCookies снова рабочие."
else:
text = f"🍪⚠️ ПРОАКТИВНАЯ ПРОВЕРКА COOKIES\n\n🍪 Сервис: {service}\n{detail}"
if len(text) > 4000:
text = text[:4000] + ""
await admin_bot.send_message(chat_id=admin_chat_id, text=text)
logger.info(f"Уведомление о состоянии cookies ({service}) отправлено админ боту")
except Exception as e:
logger.error(f"Ошибка при отправке уведомления о cookies админ боту: {e}")
async def check_cookies_health():
"""Периодическая проверка здоровья cookies YouTube и Instagram"""
state = _load_cookie_state()
checks = [
('youtube', f"{YOUTUBE_DOWNLOADER_URL}/cookies/check"),
('instagram', f"{INSTAGRAM_DOWNLOADER_URL}/cookies/check"),
]
for service, endpoint in checks:
try:
async with httpx.AsyncClient(timeout=FORMATS_TIMEOUT) as client:
response = await client.post(endpoint)
data = response.json()
except Exception as e:
logger.warning(f"Проверка cookies {service} не удалась (сервис недоступен?): {e}")
continue # не меняем состояние при сетевой ошибке — только при чистом ответе cookies_valid:false
if data.get('cookies_valid') is None:
continue # куки для этого сервиса не настроены — нечего проверять
currently_healthy = bool(data.get('cookies_valid'))
was_healthy = state.get(service, 'healthy') == 'healthy'
if not currently_healthy and was_healthy:
state[service] = 'broken'
state.setdefault('last_reminder_ts', {})[service] = time.time()
await notify_admin_cookie_alert(service, healthy=False, detail=data.get('detail', ''))
elif not currently_healthy and not was_healthy:
last = state.get('last_reminder_ts', {}).get(service, 0)
if time.time() - last >= COOKIE_REMINDER_INTERVAL:
state.setdefault('last_reminder_ts', {})[service] = time.time()
await notify_admin_cookie_alert(service, healthy=False, detail=data.get('detail', ''))
elif currently_healthy and not was_healthy:
state[service] = 'healthy'
await notify_admin_cookie_alert(service, healthy=True, detail='')
_save_cookie_state(state)
# ============================================================================
# СИСТЕМА ОЧЕРЕДЕЙ
# ============================================================================
@ -731,7 +820,7 @@ async def process_queue_item(item: QueueItem):
await notify_admin_error(item.url, str(e), item.original_message.from_user)
error_msg = get_text(item.locale, 'error', error=str(e))
error_msg = get_text(item.locale, 'error')
try:
await item.status_message.edit_text(error_msg)
except:
@ -1447,7 +1536,19 @@ def main():
asyncio.create_task(periodic_cleanup())
logger.info("Фоновая задача периодической очистки файлов запущена")
# Проактивная проверка здоровья cookies (YouTube, Instagram)
async def periodic_cookie_check():
while True:
await asyncio.sleep(COOKIE_CHECK_INTERVAL)
try:
await check_cookies_health()
except Exception as e:
logger.error(f"Ошибка при проверке здоровья cookies: {e}")
asyncio.create_task(periodic_cookie_check())
logger.info("Фоновая задача проверки здоровья cookies запущена")
application.post_init = post_init
# Запускаем бота