import uuid from datetime import datetime import html from typing import Optional, Callable, Dict, Any, Awaitable from aiogram import Router, BaseMiddleware, F from aiogram.filters import CommandStart, Command, CommandObject from aiogram.types import Message, TelegramObject, InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery from aiogram.utils.keyboard import InlineKeyboardBuilder from aiogram.utils.formatting import as_list, as_marked_section, Bold, TextLink from src.processor.dto import EnrichedNewsItemDTO from src.processor.base import ILLMProvider from src.storage.base import IVectorStore class AccessMiddleware(BaseMiddleware): def __init__(self, allowed_chat_id: str): self.allowed_chat_id = allowed_chat_id async def __call__( self, handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], event: TelegramObject, data: Dict[str, Any] ) -> Any: if isinstance(event, Message): if str(event.chat.id) != self.allowed_chat_id: await event.answer("Access Denied") return return await handler(event, data) def get_router(storage: IVectorStore, processor: ILLMProvider, allowed_chat_id: str) -> Router: router = Router(name="main_router") router.message.middleware(AccessMiddleware(allowed_chat_id)) @router.message(CommandStart()) async def command_start_handler(message: Message) -> None: """ This handler receives messages with `/start` command """ user_name = html.escape(message.from_user.full_name) if message.from_user else 'user' await message.answer(f"Welcome to Trend-Scout AI, {user_name}!") @router.message(Command("help")) async def command_help_handler(message: Message) -> None: """ This handler receives messages with `/help` command """ help_text = ( "Available commands:\n" "/start - Start the bot\n" "/help - Show this help message\n" "/latest [category] - Show the latest enriched news trends\n" "/search query - Search for news\n" "/stats - Show database statistics\n" "/params - Show LLM processor parameters\n" ) await message.answer(help_text) @router.message(Command("params")) async def command_params_handler(message: Message) -> None: """ This handler receives messages with `/params` command """ info = processor.get_info() response = "🤖 LLM Processor Parameters\n\n" response += f"Model: {html.escape(info.get('model', 'Unknown'))}\n" response += f"Base URL: {html.escape(info.get('base_url', 'Unknown'))}\n" response += f"Prompt Summary: {html.escape(info.get('prompt_summary', 'Unknown'))}" await message.answer(response, parse_mode="HTML") @router.message(Command("latest")) async def command_latest_handler(message: Message, command: CommandObject) -> None: """ This handler receives messages with `/latest` command """ category = command.args if command.args else "" items = await storage.search(query=category, limit=10) if not items: await message.answer("No results found.") return builder = InlineKeyboardBuilder() for item in items: item_id = str(uuid.uuid5(uuid.NAMESPACE_URL, item.url)) builder.row(InlineKeyboardButton( text=f"[{item.relevance_score}/10] {item.title}", callback_data=f"detail:{item_id}" )) await message.answer("Latest news:", reply_markup=builder.as_markup()) @router.message(Command("search")) async def command_search_handler(message: Message, command: CommandObject) -> None: """ This handler receives messages with `/search` command """ query = command.args if not query: await message.answer("Please provide a search query. Usage: /search query") return items = await storage.search(query=query, limit=10) if not items: await message.answer("No results found.") return builder = InlineKeyboardBuilder() for item in items: item_id = str(uuid.uuid5(uuid.NAMESPACE_URL, item.url)) builder.row(InlineKeyboardButton( text=f"[{item.relevance_score}/10] {item.title}", callback_data=f"detail:{item_id}" )) await message.answer("Search results:", reply_markup=builder.as_markup()) @router.callback_query(F.data.startswith("detail:")) async def detail_callback_handler(callback: CallbackQuery) -> None: """ This handler receives callback queries for news details """ item_id = callback.data.split(":")[1] item = await storage.get_by_id(item_id) if not item: await callback.answer("Item not found.", show_alert=True) return title = html.escape(item.title) source = html.escape(item.source) summary = html.escape(item.summary_ru) category = html.escape(item.category) anomalies = [html.escape(a) for a in item.anomalies_detected] if item.anomalies_detected else [] anomalies_text = ", ".join(anomalies) url = html.escape(item.url) response_text = ( f"🌟 {title}\n\n" f"Source: {source}\n" f"Category: {category}\n" f"Relevance Score: {item.relevance_score}/10\n" f"Summary: {summary}\n" ) if anomalies_text: response_text += f"Anomalies Detected: {anomalies_text}\n\n" else: response_text += "\n" response_text += f"Read more" await callback.message.answer(response_text, parse_mode="HTML", disable_web_page_preview=False) await callback.answer() @router.message(Command("stats")) async def command_stats_handler(message: Message) -> None: """ This handler receives messages with `/stats` command """ stats = await storage.get_stats() total = stats.get("total_count", 0) breakdown = [] for key, count in stats.items(): if key.startswith("category_"): cat_name = key.replace("category_", "") breakdown.append(f"- {cat_name}: {count}") response = f"📊 Database Statistics\n\nTotal items: {total}\n" if breakdown: response += "\nBreakdown by category:\n" + "\n".join(breakdown) await message.answer(response, parse_mode="HTML") return router