tg-channel-analysis/bot/handlers/analyze.py
2026-02-22 21:33:23 +02:00

167 lines
5.2 KiB
Python

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: <code>/analyze @channel</code>",
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: <b>@{channel}</b>\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 <b>{report_type.label}</b> for @{channel}...\n\n"
"Fetching messages...",
parse_mode="HTML",
)
async def update_status(text: str) -> None:
try:
await status_msg.edit_text(
f"<b>{report_type.label}</b> 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