feat(bot): add async DB and FSM state handling

This commit is contained in:
2026-07-23 22:58:52 +05:00
parent b1c8f6d9f8
commit 87385228ea
28 changed files with 514 additions and 213 deletions
+100 -48
View File
@@ -2,50 +2,57 @@ import html
from aiogram import Router
from aiogram.types import Message
from aiogram.fsm.context import FSMContext
from bot.api.asf import get_all_bots, send_command
from bot.filters import AdminFilter
from bot.api.asf import get_all_bots, send_command
from bot.logging_utils import describe_user, get_logger
from bot.services.redeem import (
extract_keys,
redeem_all_bots_keys,
redeem_single_bot_keys,
)
from bot.state import (
bot_redeem_messages,
bot_redeem_users,
console_users,
redeem_messages,
redeem_users,
)
from bot.states import ConsoleFlow, RedeemFlow
from bot.ui.formatters import format_bot_ui, get_asf_status_text
from bot.ui.keyboards import (
main_keyboard,
bot_details_keyboard,
delete_message_keyboard,
main_keyboard,
)
router = Router()
logger = get_logger(__name__)
async def handle_console_message(message: Message) -> bool:
if message.from_user.id not in console_users: # type: ignore[union-attr]
return False
command = message.text.strip() # type: ignore[union-attr]
@router.message(ConsoleFlow.waiting_command, AdminFilter())
async def handle_console_message(message: Message, state: FSMContext):
if not message.text:
await message.answer("Отправьте текстовую команду.")
return
command = message.text.strip()
if not command:
await message.answer("Команда не может быть пустой.")
return
logger.info(
"Пользователь %s отправил консольную команду: %s",
describe_user(message.from_user),
command,
)
try:
await message.delete()
except Exception:
logger.debug(
"Не удалось удалить сообщение с консольной командой от %s",
describe_user(message.from_user),
exc_info=True,
)
try:
response = send_command(f"!{command}")
if response.status_code != 200:
@@ -55,9 +62,12 @@ async def handle_console_message(message: Message) -> bool:
command,
describe_user(message.from_user),
)
text = f"Ошибка HTTP {response.status_code}"
else:
data = response.json()
if not data.get("Success"):
logger.warning(
"ASF вернул неуспешный ответ на консольную команду '%s' от %s",
@@ -65,6 +75,7 @@ async def handle_console_message(message: Message) -> bool:
describe_user(message.from_user),
)
text = "Ошибка ASF"
else:
logger.info(
"Консольная команда '%s' от %s выполнена успешно",
@@ -73,39 +84,55 @@ async def handle_console_message(message: Message) -> bool:
)
result = html.escape(str(data.get("Result", "Нет ответа")))
text = f"<b>Команда:</b> <code>{command}</code>\n\n<b>Ответ:</b>\n<code>{result}</code>"
await message.answer(text, reply_markup=delete_message_keyboard())
except Exception as error:
logger.exception(
"Ошибка при выполнении консольной команды '%s' от %s",
command,
describe_user(message.from_user),
)
await message.answer(f"Ошибка: {error}")
return True
async def handle_single_bot_redeem(message: Message) -> bool:
if message.from_user.id not in bot_redeem_users: # type: ignore[union-attr]
return False
bot_name = bot_redeem_users.pop(message.from_user.id) # type: ignore[union-attr]
menu_message = bot_redeem_messages.pop(message.from_user.id, None) # type: ignore[union-attr]
@router.message(RedeemFlow.waiting_bot_keys, AdminFilter())
async def handle_single_bot_redeem(message: Message, state: FSMContext):
state_data = await state.get_data()
bot_name = state_data.get("bot_name")
if not isinstance(bot_name, str) or not bot_name:
await message.answer("Контекст потерян, попробуйте еще раз.")
await state.clear()
return
chat_id = message.chat.id
menu_message_id = state_data.get("menu_message_id")
logger.info(
"Пользователь %s отправил ключи для бота %s",
describe_user(message.from_user),
bot_name,
)
if menu_message:
if menu_message_id:
try:
response = get_all_bots()
bot_data = response.json().get("Result", {}).get(bot_name)
if bot_data:
await menu_message.edit_text(
format_bot_ui(bot_data),
disable_web_page_preview=True,
await message.bot.edit_message_text(
text=format_bot_ui(bot_data),
reply_markup=bot_details_keyboard(
bot_name, bool(bot_data.get("HasMobileAuthenticator"))
),
disable_web_page_preview=True,
chat_id=chat_id,
message_id=menu_message_id,
)
except Exception:
logger.debug(
"Не удалось восстановить меню бота %s после ввода ключей от %s",
@@ -113,10 +140,12 @@ async def handle_single_bot_redeem(message: Message) -> bool:
describe_user(message.from_user),
exc_info=True,
)
try:
await message.bot.delete_message(
chat_id=message.chat.id, message_id=message.message_id
)
except Exception:
logger.debug(
"Не удалось удалить сообщение с ключами для бота %s от %s",
@@ -124,6 +153,7 @@ async def handle_single_bot_redeem(message: Message) -> bool:
describe_user(message.from_user),
exc_info=True,
)
keys = extract_keys(message.text or "")
if not keys:
logger.info(
@@ -132,7 +162,10 @@ async def handle_single_bot_redeem(message: Message) -> bool:
bot_name,
)
await message.answer("Ключи не найдены")
return True
await state.clear()
return
logger.info(
"Для бота %s получено %s ключ(ей) от %s",
bot_name,
@@ -142,40 +175,66 @@ async def handle_single_bot_redeem(message: Message) -> bool:
checking = await message.answer(
f"Аккаунт: <code>{bot_name}</code>\nНайдено ключей: {len(keys)}\nНачинаю активацию..."
)
await checking.edit_text(await redeem_single_bot_keys(bot_name, keys))
return True
try:
await checking.edit_text(await redeem_single_bot_keys(bot_name, keys))
except Exception as error:
logger.exception(
"Не удалось активировать ключи для бота %s от %s",
bot_name,
describe_user(message.from_user),
)
try:
await checking.edit_text(f"Ошибка: {error}")
except Exception:
await message.answer(f"Ошибка: {error}")
finally:
await state.clear()
async def handle_all_bots_redeem(message: Message) -> bool:
if message.from_user.id not in redeem_users: # type: ignore[union-attr]
return False
redeem_users.discard(message.from_user.id) # type: ignore[union-attr]
menu_message = redeem_messages.pop(message.from_user.id, None) # type: ignore[union-attr]
@router.message(RedeemFlow.waiting_all_keys, AdminFilter())
async def handle_all_bots_redeem(message: Message, state: FSMContext):
state_data = await state.get_data()
chat_id = message.chat.id
menu_message_id = state_data.get("menu_message_id")
logger.info(
"Пользователь %s отправил ключи для активации на всех ботах",
describe_user(message.from_user),
)
if menu_message:
if menu_message_id:
try:
await menu_message.edit_text(
get_asf_status_text(), reply_markup=main_keyboard()
await message.bot.edit_message_text(
text=get_asf_status_text(),
reply_markup=main_keyboard(),
chat_id=chat_id,
message_id=menu_message_id,
)
except Exception:
logger.debug(
"Не удалось восстановить главное меню после массовой активации для %s",
describe_user(message.from_user),
exc_info=True,
)
try:
await message.bot.delete_message(
chat_id=message.chat.id, message_id=message.message_id
)
except Exception:
logger.debug(
"Не удалось удалить сообщение с массовой активацией от %s",
describe_user(message.from_user),
exc_info=True,
)
keys = extract_keys(message.text or "")
if not keys:
logger.info(
@@ -183,7 +242,10 @@ async def handle_all_bots_redeem(message: Message) -> bool:
describe_user(message.from_user),
)
await message.answer("Ключи не найдены")
return True
await state.clear()
return
logger.info(
"Для массовой активации получено %s ключ(ей) от %s",
len(keys),
@@ -192,25 +254,15 @@ async def handle_all_bots_redeem(message: Message) -> bool:
checking = await message.answer(
f"Найдено ключей: {len(keys)}\nНачинаю активацию..."
)
try:
await checking.edit_text(await redeem_all_bots_keys(keys))
except Exception as error:
logger.exception(
"Ошибка при массовой активации ключей от %s",
describe_user(message.from_user),
)
await checking.edit_text(f"Ошибка: {error}")
return True
@router.message(AdminFilter())
async def text_router(message: Message) -> None:
logger.debug(
"Получено текстовое сообщение от %s для маршрутизации",
describe_user(message.from_user),
)
if await handle_console_message(message):
return
if await handle_single_bot_redeem(message):
return
await handle_all_bots_redeem(message)
await state.clear()