Files
instaloader_1/bot.py
smolkik-code 330e48beaa refactor: full async, rate limit backoff, graceful shutdown, health endpoint
- Switch from requests to httpx.AsyncClient (full async HTTP)
- Add _request() with exponential backoff on 429/401
- Cache followees with 5-min TTL to reduce API calls
- Remove dead code (_get_all_stories, instagram_password, session_file)
- Graceful shutdown via asyncio.Event + SIGTERM/SIGINT handlers
- Add /health command with connection status
- Stream media downloads with httpx + aiofiles
- Track story expiring_at for smart cleanup
- Sanitize .env.example (remove real tokens)
2026-06-17 16:03:47 +07:00

198 lines
6.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import asyncio
import signal
import sys
from datetime import datetime, timezone
from pathlib import Path
from aiogram import Bot, Dispatcher, F, Router
from aiogram.filters import CommandStart
from aiogram.types import FSInputFile, Message
from loguru import logger
logger.remove()
logger.add(sys.stderr, level="DEBUG")
from config import config
from instagram_monitor import InstagramMonitor
router = Router()
bot = Bot(token=config.telegram_bot_token)
monitor = InstagramMonitor()
_shutdown_event = asyncio.Event()
@router.message(CommandStart())
async def cmd_start(message: Message) -> None:
if message.from_user and message.from_user.id == config.telegram_chat_id:
await message.answer(
"Instagram Monitor Bot запущен.\n"
f"Проверяю подписки каждые {config.check_interval_minutes} минут.\n"
"Команды:\n"
"/check — проверить сейчас\n"
"/status — статус бота\n"
"/health — здоровье бота\n"
"/stop — остановить мониторинг"
)
else:
await message.answer("Этот бот не для тебя.")
@router.message(F.text == "/status")
async def cmd_status(message: Message) -> None:
if message.from_user and message.from_user.id == config.telegram_chat_id:
stats = monitor.db.get_stats()
stats_text = ", ".join(f"{k}: {v}" for k, v in stats.items()) if stats else "пусто"
await message.answer(
f"В базе: {stats_text}\n"
f"Интервал: {config.check_interval_minutes} мин\n"
f"Instagram: {config.instagram_username}"
)
@router.message(F.text == "/health")
async def cmd_health(message: Message) -> None:
if message.from_user and message.from_user.id == config.telegram_chat_id:
now = datetime.now(timezone.utc).isoformat(timespec="seconds")
await message.answer(
f"Бот: жив\n"
f"Время: {now} UTC\n"
f"Instagram: {'авторизован' if monitor._logged_in else 'не авторизован'}\n"
f"Подписки: {len(monitor._followees_cache) if monitor._followees_cache else 'не загружены'}"
)
@router.message(F.text == "/stop")
async def cmd_stop(message: Message) -> None:
if message.from_user and message.from_user.id == config.telegram_chat_id:
await message.answer("Останавливаю мониторинг...")
_shutdown_event.set()
@router.message(F.text == "/check")
async def cmd_check(message: Message) -> None:
if message.from_user and message.from_user.id == config.telegram_chat_id:
await message.answer("Проверяю обновления...")
count = 0
async for update in monitor.check_updates():
await send_media(update)
count += 1
await message.answer(f"Готово. Найдено: {count}")
async def send_media(update: dict) -> None:
chat_id = config.telegram_chat_id
media_path = update["media_path"]
username = update["username"]
media_type = update["type"]
caption = update["caption"]
header = f"{'Сторис' if media_type == 'story' else 'Пост'} от @{username}"
text = f"{header}\n\n{caption}" if caption else header
path = Path(media_path)
if not path.exists():
logger.warning(f"Media file not found: {media_path}")
return
try:
if path.suffix == ".mp4":
video = FSInputFile(str(path))
await bot.send_video(chat_id=chat_id, video=video, caption=text, parse_mode=None)
elif path.suffix in (".jpg", ".jpeg", ".png", ".webp"):
photo = FSInputFile(str(path))
await bot.send_photo(chat_id=chat_id, photo=photo, caption=text, parse_mode=None)
else:
document = FSInputFile(str(path))
await bot.send_document(chat_id=chat_id, document=document, caption=text, parse_mode=None)
except Exception as e:
logger.error(f"Failed to send media: {e}")
await bot.send_message(chat_id=chat_id, text=f"{header}\n\n{caption}\n\n[Файл не удалось отправить]")
@router.message(F.text.startswith("/debug"))
async def cmd_debug(message: Message) -> None:
if message.from_user and message.from_user.id == config.telegram_chat_id:
parts = message.text.split()
if len(parts) < 2:
await message.answer("Использование: /debug @username")
return
username = parts[1].lstrip("@")
await message.answer(f"Ищу @{username}...")
followees = await monitor._get_followees()
user = next((u for u in followees if u["username"] == username), None)
if not user:
await message.answer(f"@{username} не найден в подписках")
return
user_pk = user["pk"]
await message.answer(f"user_pk: {user_pk}\nПроверяю сториз...")
resp = await monitor.client.get(
"https://www.instagram.com/api/v1/feed/user/"
f"{user_pk}/story/",
params={"user_id": user_pk},
headers={
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36",
"X-IG-App-ID": "936619743392459",
"Accept": "*/*",
},
)
await message.answer(f"HTTP {resp.status_code}\nResponse: {resp.text[:800]}")
async def monitor_loop() -> None:
logger.info("Starting monitor loop")
while not _shutdown_event.is_set():
try:
async for update in monitor.check_updates():
await send_media(update)
monitor.cleanup_downloads(max_age_hours=48)
except Exception as e:
logger.error(f"Monitor loop error: {e}")
try:
await asyncio.wait_for(
_shutdown_event.wait(),
timeout=config.check_interval_minutes * 60,
)
except asyncio.TimeoutError:
pass
logger.info("Monitor loop stopped")
async def shutdown() -> None:
logger.info("Shutting down...")
_shutdown_event.set()
await bot.session.close()
await monitor.close()
logger.info("Shutdown complete")
async def main() -> None:
errors = config.validate()
if errors:
for e in errors:
logger.error(e)
return
loop = asyncio.get_running_loop()
for sig in (signal.SIGTERM, signal.SIGINT):
loop.add_signal_handler(sig, lambda: asyncio.create_task(shutdown()))
dp = Dispatcher()
dp.include_router(router)
asyncio.create_task(monitor_loop())
logger.info("Bot started")
await dp.start_polling(bot)
await shutdown()
if __name__ == "__main__":
asyncio.run(main())