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