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
+35 -1
View File
@@ -4,6 +4,27 @@ from bot.api.asf import get_all_bots, get_bot, save_bot_config, send_command
from bot.logging_utils import get_logger
logger = get_logger(__name__)
RELOAD_POLL_INTERVAL_SECONDS = 1
RELOAD_TIMEOUT_SECONDS = 20
_reloading_bots: set[str] = set()
def is_bot_reloading(bot_name: str) -> bool:
return bot_name in _reloading_bots
def _is_bot_loaded_and_online(bot_name: str) -> bool:
response = get_all_bots()
if response.status_code != 200:
return False
data = response.json()
bot = data.get("Result", {}).get(bot_name, {})
return bool(
bot.get("BotName")
and bot.get("SteamID")
and bot.get("IsConnectedAndLoggedOn")
)
def is_bot_playing(bot_name: str) -> bool:
@@ -29,6 +50,7 @@ def is_bot_playing(bot_name: str) -> bool:
def is_idle_enabled(bot_name: str, app_id: int) -> bool:
logger.info("Проверка статуса idle-игры: bot=%s app_id=%s", bot_name, app_id)
response = get_bot(bot_name)
if response.status_code != 200:
logger.warning(
"Не удалось получить конфиг бота для проверки idle: bot=%s status=%s",
@@ -63,17 +85,27 @@ def toggle_idle_game(config: dict, app_id: int) -> tuple[dict, bool]:
async def reload_bot(bot_name: str) -> None:
_reloading_bots.add(bot_name)
try:
logger.info("Отправка команды перезагрузки бота: bot=%s", bot_name)
send_command(f"!reload {bot_name}")
for _ in range(RELOAD_TIMEOUT_SECONDS):
await asyncio.sleep(RELOAD_POLL_INTERVAL_SECONDS)
if _is_bot_loaded_and_online(bot_name):
logger.info("Перезагрузка бота завершена: bot=%s", bot_name)
return
logger.warning("Истекло время ожидания перезагрузки бота: bot=%s", bot_name)
except Exception:
logger.exception(
"Не удалось отправить команду перезагрузки бота: bot=%s", bot_name
)
finally:
_reloading_bots.discard(bot_name)
async def update_idle_game(bot_name: str, app_id: int) -> tuple[bool, str, bool]:
logger.info("Изменение настройки idle-игры: bot=%s app_id=%s", bot_name, app_id)
response = get_bot(bot_name)
if response.status_code != 200:
logger.warning(
@@ -81,6 +113,7 @@ async def update_idle_game(bot_name: str, app_id: int) -> tuple[bool, str, bool]
bot_name,
response.status_code,
)
return False, "Ошибка API", False
data = response.json()
@@ -97,8 +130,9 @@ async def update_idle_game(bot_name: str, app_id: int) -> tuple[bool, str, bool]
)
return False, "Ошибка сохранения", enabled
_reloading_bots.add(bot_name)
asyncio.create_task(reload_bot(bot_name))
action = "Idle включен" if enabled else "Idle выключен"
action = "Накрутка часов включена" if enabled else "Накрутка часов выключена"
logger.info(
"Настройка idle-игры изменена: bot=%s app_id=%s enabled=%s",
bot_name,
+12 -5
View File
@@ -14,11 +14,11 @@ async def build_inventory_menu(bot_name: str) -> tuple[str, object]:
cs2_assets = []
steam_assets = []
try:
cs2_assets = cs2_inventory.get(bot_name, {}).get("Assets", []) # type: ignore[union-attr]
cs2_assets = cs2_inventory.get(bot_name, {}).get("Assets", [])
except Exception:
logger.exception("Ошибка чтения CS2-инвентаря: bot=%s", bot_name)
try:
steam_assets = steam_inventory.get(bot_name, {}).get("Assets", []) # type: ignore[union-attr]
steam_assets = steam_inventory.get(bot_name, {}).get("Assets", [])
except Exception:
logger.exception("Ошибка чтения Steam-инвентаря: bot=%s", bot_name)
if not cs2_assets and not steam_assets:
@@ -67,7 +67,9 @@ async def render_inventory_page(
assets = bot_inventory.get("Assets", [])
descriptions = bot_inventory.get("Descriptions", [])
if not assets:
logger.info("Инвентарь выбранного типа пуст: bot=%s type=%s", bot_name, inventory_type)
logger.info(
"Инвентарь выбранного типа пуст: bot=%s type=%s", bot_name, inventory_type
)
return f"Инвентарь {bot_name} пуст", inventory_menu_keyboard(bot_name, 0, 0)
desc_map = {}
for desc in descriptions:
@@ -87,7 +89,7 @@ async def render_inventory_page(
for asset in page_assets:
key = (str(asset.get("classid")), str(asset.get("instanceid")))
desc = desc_map.get(key, {})
name = desc.get("market_name", "Unknown")
name = desc.get("market_name", "Неизвестно")
amount = asset.get("amount", 1)
tradable = "🔄" if desc.get("tradable") else ""
marketable = "💰" if desc.get("marketable") else "🔒"
@@ -105,12 +107,14 @@ async def render_inventory_page(
elif icon == "💎":
gems_count += amount
lines.append(f"{icon} {tradable}{marketable} {name} x{amount}")
inv_name = "CS2" if appid == 730 else "Steam"
stats = (
f"📦 Предметов: {len(assets)}\n"
f"💰 Можно продать: {marketable_count}\n"
f"🔄 Можно трейдить: {tradable_count}"
)
if appid == 753:
stats += (
f"\n🎏 Карточек: {cards_count}"
@@ -118,11 +122,13 @@ async def render_inventory_page(
f"\n🖼 Фонов: {backgrounds_count}"
f"\n💎 Самоцветов: {gems_count}"
)
text = (
f"📦 {inv_name} Inventory {bot_name}\n"
f"📦 Инвентарь {inv_name} {bot_name}\n"
f"📄 Страница {page + 1}/{total_pages}\n\n"
f"{stats}\n\n" + "\n".join(lines)
)
logger.info(
"Страница инвентаря сформирована: bot=%s type=%s page=%s total_pages=%s items_on_page=%s",
bot_name,
@@ -131,6 +137,7 @@ async def render_inventory_page(
total_pages,
len(page_assets),
)
return text[:4000], inventory_page_keyboard(
inventory_type, bot_name, page, total_pages
)
+13 -13
View File
@@ -1,8 +1,8 @@
import asyncio
import re
from bot.logging_utils import get_logger
from bot.api.asf import get_all_bots, redeem_key
from bot.logging_utils import get_logger, mask_key
KEY_PATTERN = r"[A-Z0-9]{5}(?:-[A-Z0-9]{5}){2}"
logger = get_logger(__name__)
@@ -52,7 +52,7 @@ async def redeem_single_bot_keys(bot_name: str, keys: list[str]) -> str:
logger.info(
"Попытка активации ключа для одного бота: bot=%s key=%s",
bot_name,
mask_key(key),
key,
)
success, result = await redeem_key(bot_name, key)
await asyncio.sleep(2)
@@ -62,16 +62,16 @@ async def redeem_single_bot_keys(bot_name: str, keys: list[str]) -> str:
logger.warning(
"Активация ключа завершилась ошибкой ASF/HTTP: bot=%s key=%s",
bot_name,
mask_key(key),
key,
)
results.append(f"{key}: ASF ERROR")
results.append(f"{key}: ошибка ASF")
continue
status = parse_redeem_result(result)
logger.info(
"Получен результат активации ключа: bot=%s key=%s status=%s",
bot_name,
mask_key(key),
key,
status,
)
@@ -80,7 +80,7 @@ async def redeem_single_bot_keys(bot_name: str, keys: list[str]) -> str:
results.append(f"{key}: активировано")
elif status == "rate_limited":
failed_count += 1
results.append(f"{key}: rate limit")
results.append(f"{key}: лимит запросов")
elif status == "already_owned":
failed_count += 1
results.append(f"⚠️ {key}: игра уже есть")
@@ -124,13 +124,13 @@ async def redeem_all_bots_keys(keys: list[str]) -> str:
for key in keys:
activated = False
key_report = []
logger.info("Начата обработка ключа по всем ботам: key=%s", mask_key(key))
logger.info("Начата обработка ключа по всем ботам: key=%s", key)
for bot_name in bots:
logger.info(
"Попытка активации ключа на боте: bot=%s key=%s",
bot_name,
mask_key(key),
key,
)
success, result = await redeem_key(bot_name, key)
await asyncio.sleep(2)
@@ -139,16 +139,16 @@ async def redeem_all_bots_keys(keys: list[str]) -> str:
logger.warning(
"Активация ключа завершилась ошибкой ASF/HTTP: bot=%s key=%s",
bot_name,
mask_key(key),
key,
)
key_report.append(f"{bot_name}: ASF ERROR")
key_report.append(f"{bot_name}: ошибка ASF")
continue
status = parse_redeem_result(result, bot_name)
logger.info(
"Получен результат активации ключа: bot=%s key=%s status=%s",
bot_name,
mask_key(key),
key,
status,
)
@@ -158,7 +158,7 @@ async def redeem_all_bots_keys(keys: list[str]) -> str:
activated = True
break
if status == "rate_limited":
key_report.append(f"{bot_name}: rate limit")
key_report.append(f"{bot_name}: лимит запросов")
continue
if status == "already_owned":
key_report.append(f"⚠️ {bot_name}: игра уже есть")
@@ -178,7 +178,7 @@ async def redeem_all_bots_keys(keys: list[str]) -> str:
failed_count += 1
logger.warning(
"Ключ не удалось активировать ни на одном боте: key=%s",
mask_key(key),
key,
)
results.append(f"\n🔑 {key}\n" + "\n".join(key_report))
+12 -2
View File
@@ -1,22 +1,25 @@
import asyncio
from bot.api.asf import get_2fa_token, get_confirmations
from bot.states import twofa_tasks
from bot.logging_utils import get_logger
from bot.state import twofa_tasks
from bot.ui.keyboards import twofa_keyboard
from bot.api.asf import get_2fa_token, get_confirmations
logger = get_logger(__name__)
async def stop_twofa_task(message_id: int) -> None:
task = twofa_tasks.pop(message_id, None)
if task:
logger.info(
"Остановка фоновой задачи обновления 2FA: message_id=%s", message_id
)
task.cancel()
try:
await task
except Exception:
logger.exception(
"Ошибка при завершении фоновой задачи обновления 2FA: message_id=%s",
@@ -32,6 +35,7 @@ async def auto_update_2fa(message, bot_name: str) -> None:
bot_name,
message_id,
)
try:
while True:
code = await get_2fa_token(bot_name)
@@ -43,6 +47,7 @@ async def auto_update_2fa(message, bot_name: str) -> None:
text,
reply_markup=twofa_keyboard(bot_name, bool(confirmations)),
)
last_code = code
logger.info(
"Сообщение 2FA обновлено: bot=%s message_id=%s confirmations=%s",
@@ -50,6 +55,7 @@ async def auto_update_2fa(message, bot_name: str) -> None:
message_id,
len(confirmations),
)
except Exception:
logger.exception(
"Не удалось обновить сообщение 2FA: bot=%s message_id=%s",
@@ -57,13 +63,16 @@ async def auto_update_2fa(message, bot_name: str) -> None:
message_id,
)
break
await asyncio.sleep(15)
except asyncio.CancelledError:
logger.info(
"Фоновое обновление 2FA отменено: bot=%s message_id=%s",
bot_name,
message_id,
)
except Exception as error:
logger.exception(
"Фоновое обновление 2FA завершилось ошибкой: bot=%s message_id=%s error=%s",
@@ -71,6 +80,7 @@ async def auto_update_2fa(message, bot_name: str) -> None:
message_id,
error,
)
finally:
twofa_tasks.pop(message_id, None)
logger.info(
+30
View File
@@ -0,0 +1,30 @@
from aiogram.types import User as TelegramUser
from sqlalchemy import select
from bot.config import ADMIN_ID
from bot.db.models import User
from bot.db.session import async_session
async def upsert_user_from_telegram(telegram_user: TelegramUser | None) -> User | None:
if telegram_user is None:
return None
async with async_session() as session:
result = await session.execute(
select(User).where(User.telegram_id == telegram_user.id)
)
user = result.scalar_one_or_none()
if user is None:
user = User(telegram_id=telegram_user.id)
session.add(user)
user.username = telegram_user.username
user.first_name = telegram_user.first_name
user.last_name = telegram_user.last_name
user.is_admin = telegram_user.id == ADMIN_ID
await session.commit()
await session.refresh(user)
return user