This commit is contained in:
Sergei Poljanski 2026-02-22 21:33:23 +02:00
commit ec1112e162
Signed by: asxpi
GPG key ID: 4F8851660FA4121B
22 changed files with 837 additions and 0 deletions

0
bot/handlers/__init__.py Normal file
View file

167
bot/handlers/analyze.py Normal file
View file

@ -0,0 +1,167 @@
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

22
bot/handlers/start.py Normal file
View file

@ -0,0 +1,22 @@
from aiogram import Router
from aiogram.filters import Command
from aiogram.types import Message
router = Router()
HELP_TEXT = (
"<b>Telegram Channel Analyzer</b>\n\n"
"Analyze any public Telegram channel using AI.\n\n"
"<b>Usage:</b>\n"
"<code>/analyze @channel</code> — Start analysis\n"
"<code>/analyze https://t.me/channel</code> — Also works\n\n"
"You'll choose a report type:\n"
"• <b>Content Analysis</b> — topics, tone, themes\n"
"• <b>Content + Stats</b> — above + engagement data\n"
"• <b>Full Audit</b> — comprehensive review with recommendations"
)
@router.message(Command("start", "help"))
async def cmd_start(message: Message) -> None:
await message.answer(HELP_TEXT, parse_mode="HTML")