167 lines
5.2 KiB
Python
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
|