428th-exchange-bot/bot/handlers/client.py
Artemii Peretiachenko 7588d74ceb Show the wait-ack only on a client's first relayed message.
Stops repeating the ack on every client message so the private chat is not cluttered after the first contact.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 19:00:21 +02:00

202 lines
6.2 KiB
Python

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)