from __future__ import annotations import logging from aiogram import F, Router from aiogram.enums import ChatType, ParseMode from aiogram.filters import Command, CommandStart from aiogram.types import CallbackQuery, Message, MessageReactionUpdated from bot.db.repository import ClientRepository from bot.i18n import ( format_bot_html, format_bot_message, language_keyboard, normalize_language, services_keyboard, t, ) from bot.services.cooldown import CooldownService from bot.services.relay import RelayService, is_relayable_content logger = logging.getLogger(__name__) router = Router(name="client") async def _answer_bot(message: Message, language: str | None, key: str) -> None: await message.answer( format_bot_message(language, t(language, key)), parse_mode=ParseMode.HTML, ) async def _answer_welcome(message: Message, language: str) -> None: await message.answer( format_bot_message(language, t(language, "welcome")), reply_markup=services_keyboard(language), parse_mode=ParseMode.HTML, ) async def _prompt_language(message: Message) -> None: await message.answer( t(None, "choose_language"), reply_markup=language_keyboard(), parse_mode=None, ) @router.message(CommandStart(), F.chat.type == ChatType.PRIVATE) async def cmd_start(message: Message, relay: RelayService) -> None: if message.from_user is None: return client = await relay.ensure_client(message.from_user) if client.language is None: await _prompt_language(message) return await _answer_welcome(message, client.language) @router.message(Command("lang"), F.chat.type == ChatType.PRIVATE) async def cmd_lang(message: Message, relay: RelayService) -> None: if message.from_user is None: return await relay.ensure_client(message.from_user) await _prompt_language(message) @router.message(Command("del"), F.chat.type == ChatType.PRIVATE) async def cmd_del(message: Message, relay: RelayService) -> None: """Reply to a relayed message with /del to remove it on both sides.""" if message.from_user is None: return client = await relay.ensure_client(message.from_user) if message.reply_to_message is None: return try: deleted = await relay.delete_paired_from_client(message.bot, message) except Exception: logger.exception("Failed to delete paired message for client %s", message.from_user.id) return if deleted: try: await message.delete() except Exception: pass await _answer_bot(message, client.language, "deleted_notice") @router.callback_query(F.data.startswith("lang:")) async def on_language_chosen( callback: CallbackQuery, relay: RelayService, repo: ClientRepository, ) -> None: if callback.from_user is None or callback.data is None: return if callback.message is None or callback.message.chat.type != ChatType.PRIVATE: await callback.answer() return language = normalize_language(callback.data.split(":", 1)[1]) if language is None: await callback.answer() return await relay.ensure_client(callback.from_user) await repo.set_language(callback.from_user.id, language) await callback.answer() body = f"{t(language, 'language_set')}\n\n{t(language, 'welcome')}" await callback.message.edit_text( format_bot_message(language, body), reply_markup=services_keyboard(language), parse_mode=ParseMode.HTML, ) @router.callback_query(F.data == "services") async def on_services( callback: CallbackQuery, relay: RelayService, cooldown: CooldownService, ) -> None: if callback.from_user is None or callback.message is None: return if callback.message.chat.type != ChatType.PRIVATE: await callback.answer() return client = await relay.ensure_client(callback.from_user) if client.language is None: await callback.answer() await _prompt_language(callback.message) return if not cooldown.try_take_services(callback.from_user.id): await callback.answer() return await callback.answer() await callback.message.answer( format_bot_html(client.language, t(client.language, "services")), parse_mode=ParseMode.HTML, ) @router.message_reaction(F.chat.type == ChatType.PRIVATE) async def client_reaction(event: MessageReactionUpdated, relay: RelayService) -> None: try: await relay.relay_reaction(event.bot, event) except Exception: logger.exception( "Failed to relay client reaction on message %s", event.message_id, ) @router.edited_message(F.chat.type == ChatType.PRIVATE) async def client_edited(message: Message, relay: RelayService) -> None: if message.from_user is None or not is_relayable_content(message): return try: await relay.relay_client_edit(message.bot, message) except Exception: logger.exception( "Failed to relay client edit from %s", message.from_user.id, ) @router.message(F.chat.type == ChatType.PRIVATE) async def client_message( message: Message, relay: RelayService, cooldown: CooldownService, repo: ClientRepository, ) -> None: if message.from_user is None or not is_relayable_content(message): return client = await relay.ensure_client(message.from_user) if client.language is None: await _prompt_language(message) return decision = cooldown.check(message.from_user.id, message.media_group_id) if not decision.allowed: await _answer_bot(message, client.language, "cooldown") return try: await relay.relay_client_to_manager(message.bot, message) except Exception: logger.exception("Failed to relay client message from %s", message.from_user.id) await _answer_bot(message, client.language, "send_failed") return cooldown.commit(message.from_user.id, message.media_group_id) if decision.should_ack and not client.ack_sent: await _answer_bot(message, client.language, "ack") await repo.mark_ack_sent(message.from_user.id)