""" Telegram card moderation bot. Listens to the RabbitMQ "moderation" queue and sends cards to the admin chat with inline buttons "Accept ✅" / "Reject ❌". When a button is pressed, the bot calls protected endpoints /card_accept or /card_reject with a secret header. """ import os import json import asyncio from datetime import datetime import aiohttp from dotenv import load_dotenv from aiogram import Bot, Dispatcher, F from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message from schemas.base_schemas import Card from rabbit_worker import RabbitWorker from logger import logger, setup_logging load_dotenv() setup_logging() # ── Configuration ────────────────────────────────────────────── BOT_TOKEN = os.getenv("TG_BOT_TOKEN") or "" ADMIN_CHAT_ID = int(os.getenv("TG_ADMIN_CHAT_ID", "0")) API_BASE_URL = os.getenv("API_BASE_URL", "http://localhost:5000") MODERATION_SECRET = os.getenv("MODERATION_SECRET", "change-me-in-production") bot = Bot(token=BOT_TOKEN) dp = Dispatcher() rabbit = RabbitWorker() # ── Sending card to admin ────────────────────────── async def _get_author_username(author_id: int) -> str: """Fetches the author's username via API.""" try: async with aiohttp.ClientSession() as session: async with session.get( f"{API_BASE_URL}/get_user", params={"user_id": author_id} ) as resp: if resp.status == 200: data = await resp.json() username = data.get("result", {}).get("username", "") if username: return f"@{username}" except Exception as exc: logger.warning("Failed to fetch username for {}: {}", author_id, exc) return str(author_id) def _format_date(iso_date: str) -> str: """Converts ISO date to DD.MM.YYYY HH:MM:SS format.""" try: dt = datetime.fromisoformat(iso_date) return dt.strftime("%d.%m.%Y %H:%M:%S") except (ValueError, TypeError): return iso_date async def send_card_to_admin(card: Card) -> None: """Formats message and inline keyboard for a card.""" author_display = await _get_author_username(card.author_id) date_display = _format_date(card.creation_date) text = ( f"🆕 Новая карточка #{card.card_id}\n\n" f"🅰️ {card.choice_A}\n" f"🅱️ {card.choice_B}\n\n" f"👤 Автор: {author_display}\n" f"📅 Создана: {date_display}" ) keyboard = InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton( text="Принять ✅", callback_data=f"accept:{card.card_id}", ), InlineKeyboardButton( text="Отклонить ❌", callback_data=f"reject:{card.card_id}", ), ] ] ) await bot.send_message( chat_id=ADMIN_CHAT_ID, text=text, reply_markup=keyboard, parse_mode="HTML", ) logger.info("Sent card {} to admin chat", card.card_id) # ── Calling protected API endpoints ────────────────────────── async def call_moderation_api(action: str, card_id: int) -> dict: """ Calls /card_accept or /card_reject with secret header. action: 'accept' | 'reject' """ endpoint = f"{API_BASE_URL}/card_{action}" headers = {"X-Moderation-Secret": MODERATION_SECRET} params = {"card_id": card_id} async with aiohttp.ClientSession() as session: async with session.patch(endpoint, headers=headers, params=params) as resp: data = await resp.json() return data # ── Callback button handlers ─────────────────────────────── @dp.callback_query(F.data.startswith("accept:")) async def on_accept(callback: CallbackQuery) -> None: if not callback.data or not isinstance(callback.message, Message): return card_id = int(callback.data.split(":")[1]) result = await call_moderation_api("accept", card_id) if result.get("error"): await callback.answer(f"Ошибка: {result['result']}", show_alert=True) return orig_text = callback.message.text or "" await callback.message.edit_text( orig_text + "\n\n✅ ПРИНЯТА", parse_mode="HTML", ) await callback.answer("Карточка принята!") logger.info("Card {} accepted by admin", card_id) @dp.callback_query(F.data.startswith("reject:")) async def on_reject(callback: CallbackQuery) -> None: if not callback.data or not isinstance(callback.message, Message): return card_id = int(callback.data.split(":")[1]) result = await call_moderation_api("reject", card_id) if result.get("error"): await callback.answer(f"Ошибка: {result['result']}", show_alert=True) return orig_text = callback.message.text or "" await callback.message.edit_text( orig_text + "\n\n❌ ОТКЛОНЕНА", parse_mode="HTML", ) await callback.answer("Карточка отклонена!") logger.info("Card {} rejected by admin", card_id) # ── aiogram Lifecycle hooks ──────────────────────────────────── _rabbit_task: asyncio.Task | None = None @dp.startup() async def on_startup() -> None: global _rabbit_task _rabbit_task = asyncio.create_task( rabbit.consume_moderation(send_card_to_admin) ) logger.info("Moderation bot started, RabbitMQ consumer running") @dp.shutdown() async def on_shutdown() -> None: if _rabbit_task: _rabbit_task.cancel() try: await _rabbit_task except asyncio.CancelledError: pass logger.info("Moderation bot stopped") if __name__ == "__main__": dp.run_polling(bot)