import logging import re from aiogram import F, Router from aiogram.filters import Command from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message from telethon import TelegramClient from telethon.errors import ( ChannelInvalidError, ChannelPrivateError, FloodWaitError, UsernameInvalidError, UsernameNotOccupiedError, ) from bot.models import ReportType from bot.services.analyzer import analyze_channel from bot.services.chunker import chunk_messages from bot.services.fetcher import fetch_channel_messages from bot.services.formatter import split_report log = logging.getLogger(__name__) router = Router() # channel_username -> store temporarily per user for callback _pending: dict[int, str] = {} def _extract_channel(text: str) -> str | None: text = text.strip() # @username m = re.match(r"@(\w+)", text) if m: return m.group(1) # https://t.me/username m = re.match(r"https?://t\.me/(\w+)", text) if m: return m.group(1) # bare username if re.match(r"^\w+$", text): return text return None def _report_keyboard() -> InlineKeyboardMarkup: return InlineKeyboardMarkup( inline_keyboard=[ [InlineKeyboardButton(text=rt.label, callback_data=f"report:{rt.value}")] for rt in ReportType ] ) @router.message(Command("analyze")) async def cmd_analyze(message: Message) -> None: args = (message.text or "").split(maxsplit=1) if len(args) < 2: await message.answer( "Please provide a channel: /analyze @channel", parse_mode="HTML", ) return channel = _extract_channel(args[1]) if not channel: await message.answer("Could not parse channel name. Use @username or t.me/username.") return _pending[message.from_user.id] = channel await message.answer( f"Channel: @{channel}\n\nChoose report type:", parse_mode="HTML", reply_markup=_report_keyboard(), ) @router.callback_query(F.data.startswith("report:")) async def on_report_type(callback: CallbackQuery) -> None: await callback.answer() user_id = callback.from_user.id channel = _pending.pop(user_id, None) if not channel: await callback.message.answer("Session expired. Please run /analyze again.") return report_value = callback.data.split(":", 1)[1] report_type = ReportType(report_value) telethon_client: TelegramClient = callback.message.bot.__dict__.get("_telethon_client") if not telethon_client: # Fallback: try dispatcher data from aiogram import Dispatcher # Access via bot's dispatcher isn't directly available in callback, # so we store it on the bot instance in __main__.py await callback.message.answer("Internal error: Telethon client not configured.") return status_msg = await callback.message.answer( f"Starting {report_type.label} for @{channel}...\n\n" "Fetching messages...", parse_mode="HTML", ) async def update_status(text: str) -> None: try: await status_msg.edit_text( f"{report_type.label} for @{channel}\n\n{text}", parse_mode="HTML", ) except Exception: pass try: messages, stats = await fetch_channel_messages(telethon_client, channel) except (ChannelPrivateError, ChannelInvalidError): await status_msg.edit_text("Channel is private or does not exist.") return except (UsernameInvalidError, UsernameNotOccupiedError): await status_msg.edit_text("Channel username not found.") return except FloodWaitError as e: await status_msg.edit_text(f"Rate limited by Telegram. Retry in {e.seconds}s.") return except Exception as e: log.exception("Failed to fetch channel %s", channel) await status_msg.edit_text(f"Failed to fetch channel: {e}") return if not messages: await status_msg.edit_text("No text messages found in this channel.") return await update_status(f"Fetched {len(messages)} messages. Chunking...") chunks = chunk_messages(messages) await update_status(f"{len(messages)} messages in {len(chunks)} chunks. Analyzing...") try: report = await analyze_channel( report_type=report_type, chunks=chunks, channel_title=stats["title"], channel_username=stats.get("username"), subscribers=stats.get("subscribers"), msg_count=len(messages), on_progress=update_status, ) except Exception as e: log.exception("Analysis failed for %s", channel) await status_msg.edit_text(f"Analysis failed: {e}") return await update_status("Sending report...") parts = split_report(report) for part in parts: try: await callback.message.answer(part, parse_mode="HTML") except Exception: # Fallback: send without formatting await callback.message.answer(part) try: await status_msg.delete() except Exception: pass