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
+24 -3
View File
@@ -14,11 +14,11 @@ def _headers() -> dict[str, str | None]:
def asf_get(path: str) -> requests.Response: def asf_get(path: str) -> requests.Response:
logger.info("Отправка GET-запроса в ASF: path=%s", path) logger.info("Отправка GET-запроса в ASF: path=%s", path)
response = requests.get(f"{ASF_URL}{path}", headers=_headers()) response = requests.get(f"{ASF_URL}{path}", headers=_headers())
logger.info( logger.info(
"Получен ответ ASF на GET-запрос: path=%s status=%s", "Получен ответ ASF на GET-запрос: path=%s status=%s", path, response.status_code
path,
response.status_code,
) )
return response return response
@@ -28,12 +28,14 @@ def asf_post(path: str, payload: dict | None = None) -> requests.Response:
path, path,
sorted((payload or {}).keys()), sorted((payload or {}).keys()),
) )
response = requests.post(f"{ASF_URL}{path}", headers=_headers(), json=payload) response = requests.post(f"{ASF_URL}{path}", headers=_headers(), json=payload)
logger.info( logger.info(
"Получен ответ ASF на POST-запрос: path=%s status=%s", "Получен ответ ASF на POST-запрос: path=%s status=%s",
path, path,
response.status_code, response.status_code,
) )
return response return response
@@ -41,6 +43,7 @@ def get_asf_status() -> requests.Response:
logger.info("Запрос статуса ASF") logger.info("Запрос статуса ASF")
response = requests.get(f"{ASF_URL}/Api/ASF", headers=_headers()) # type: ignore[arg-type] response = requests.get(f"{ASF_URL}/Api/ASF", headers=_headers()) # type: ignore[arg-type]
logger.info("Получен статус ASF: status=%s", response.status_code) logger.info("Получен статус ASF: status=%s", response.status_code)
return response return response
@@ -71,6 +74,7 @@ async def get_bot_inventory(bot_name: str, appid: int, contextid: int) -> dict |
appid, appid,
contextid, contextid,
) )
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get( async with session.get(
f"{ASF_URL}/Api/Bot/{bot_name}/Inventory/{appid}/{contextid}", f"{ASF_URL}/Api/Bot/{bot_name}/Inventory/{appid}/{contextid}",
@@ -85,6 +89,7 @@ async def get_bot_inventory(bot_name: str, appid: int, contextid: int) -> dict |
response.status, response.status,
) )
return None return None
data = await response.json() data = await response.json()
if not data.get("Success"): if not data.get("Success"):
logger.warning( logger.warning(
@@ -94,17 +99,20 @@ async def get_bot_inventory(bot_name: str, appid: int, contextid: int) -> dict |
contextid, contextid,
) )
return None return None
logger.info( logger.info(
"Инвентарь бота получен: bot=%s appid=%s contextid=%s", "Инвентарь бота получен: bot=%s appid=%s contextid=%s",
bot_name, bot_name,
appid, appid,
contextid, contextid,
) )
return data.get("Result") return data.get("Result")
async def get_2fa_token(bot_name: str) -> str: async def get_2fa_token(bot_name: str) -> str:
logger.info("Запрос 2FA-кода: bot=%s", bot_name) logger.info("Запрос 2FA-кода: bot=%s", bot_name)
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get( async with session.get(
f"{ASF_URL}/Api/Bot/{bot_name}/TwoFactorAuthentication/Token", f"{ASF_URL}/Api/Bot/{bot_name}/TwoFactorAuthentication/Token",
@@ -117,24 +125,29 @@ async def get_2fa_token(bot_name: str) -> str:
response.status, response.status,
) )
return f"Ошибка HTTP {response.status}" return f"Ошибка HTTP {response.status}"
data = await response.json() data = await response.json()
if not data.get("Success"): if not data.get("Success"):
logger.warning( logger.warning(
"ASF вернул ошибку при запросе 2FA-кода: bot=%s", bot_name "ASF вернул ошибку при запросе 2FA-кода: bot=%s", bot_name
) )
return "Ошибка ASF" return "Ошибка ASF"
try: try:
logger.info("2FA-код успешно получен: bot=%s", bot_name) logger.info("2FA-код успешно получен: bot=%s", bot_name)
return data["Result"][bot_name]["Result"] return data["Result"][bot_name]["Result"]
except Exception: except Exception:
logger.exception( logger.exception(
"Не удалось извлечь 2FA-код из ответа ASF: bot=%s", bot_name "Не удалось извлечь 2FA-код из ответа ASF: bot=%s", bot_name
) )
return "Не удалось получить код" return "Не удалось получить код"
async def get_confirmations(bot_name: str) -> list: async def get_confirmations(bot_name: str) -> list:
logger.info("Запрос подтверждений 2FA: bot=%s", bot_name) logger.info("Запрос подтверждений 2FA: bot=%s", bot_name)
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get( async with session.get(
f"{ASF_URL}/Api/Bot/{bot_name}/TwoFactorAuthentication/Confirmations", f"{ASF_URL}/Api/Bot/{bot_name}/TwoFactorAuthentication/Confirmations",
@@ -147,6 +160,7 @@ async def get_confirmations(bot_name: str) -> list:
response.status, response.status,
) )
return [] return []
data = await response.json() data = await response.json()
if not data.get("Success"): if not data.get("Success"):
logger.warning( logger.warning(
@@ -154,6 +168,7 @@ async def get_confirmations(bot_name: str) -> list:
bot_name, bot_name,
) )
return [] return []
try: try:
result = data["Result"][bot_name]["Result"] result = data["Result"][bot_name]["Result"]
logger.info( logger.info(
@@ -162,6 +177,7 @@ async def get_confirmations(bot_name: str) -> list:
len(result), len(result),
) )
return result return result
except Exception: except Exception:
logger.exception( logger.exception(
"Не удалось извлечь подтверждения 2FA из ответа ASF: bot=%s", "Не удалось извлечь подтверждения 2FA из ответа ASF: bot=%s",
@@ -172,6 +188,7 @@ async def get_confirmations(bot_name: str) -> list:
async def accept_confirmations(bot_name: str) -> int: async def accept_confirmations(bot_name: str) -> int:
logger.info("Отправка запроса на подтверждение всех 2FA-операций: bot=%s", bot_name) logger.info("Отправка запроса на подтверждение всех 2FA-операций: bot=%s", bot_name)
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.post( async with session.post(
f"{ASF_URL}/Api/Bot/{bot_name}/TwoFactorAuthentication/Confirmations/Accept", f"{ASF_URL}/Api/Bot/{bot_name}/TwoFactorAuthentication/Confirmations/Accept",
@@ -182,6 +199,7 @@ async def accept_confirmations(bot_name: str) -> int:
bot_name, bot_name,
response.status, response.status,
) )
return response.status return response.status
@@ -192,6 +210,7 @@ def save_bot_config(bot_name: str, config: dict) -> requests.Response:
async def redeem_key(bot_name: str, key: str) -> tuple[bool, str]: async def redeem_key(bot_name: str, key: str) -> tuple[bool, str]:
logger.info("Запрос активации ключа: bot=%s", bot_name) logger.info("Запрос активации ключа: bot=%s", bot_name)
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.post( async with session.post(
f"{ASF_URL}/Api/Command", f"{ASF_URL}/Api/Command",
@@ -205,11 +224,13 @@ async def redeem_key(bot_name: str, key: str) -> tuple[bool, str]:
response.status, response.status,
) )
return False, f"HTTP_ERROR_{response.status}" return False, f"HTTP_ERROR_{response.status}"
data = await response.json() data = await response.json()
if not data.get("Success"): if not data.get("Success"):
logger.warning( logger.warning(
"ASF вернул ошибку при активации ключа: bot=%s", bot_name "ASF вернул ошибку при активации ключа: bot=%s", bot_name
) )
return False, "ASF_ERROR" return False, "ASF_ERROR"
logger.info("ASF вернул результат активации ключа: bot=%s", bot_name) logger.info("ASF вернул результат активации ключа: bot=%s", bot_name)
return True, str(data.get("Result", "")) return True, str(data.get("Result", ""))
+2
View File
@@ -9,3 +9,5 @@ ADMIN_ID = int(os.getenv("ADMIN_ID")) # type: ignore[arg-type]
ASF_URL = os.getenv("ASF_URL") ASF_URL = os.getenv("ASF_URL")
ASF_PASSWORD = os.getenv("ASF_PASSWORD") ASF_PASSWORD = os.getenv("ASF_PASSWORD")
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite+aiosqlite:///bot.db")
+1
View File
@@ -0,0 +1 @@
+5
View File
@@ -0,0 +1,5 @@
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
+8
View File
@@ -0,0 +1,8 @@
from bot.db import models # noqa: F401
from bot.db.base import Base
from bot.db.session import engine
async def init_db() -> None:
async with engine.begin() as connection:
await connection.run_sync(Base.metadata.create_all)
+27
View File
@@ -0,0 +1,27 @@
from datetime import datetime, timezone
from sqlalchemy import BigInteger, Boolean, DateTime, String
from sqlalchemy.orm import Mapped, mapped_column
from bot.db.base import Base
def utc_now() -> datetime:
return datetime.now(timezone.utc)
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
telegram_id: Mapped[int] = mapped_column(BigInteger, unique=True, index=True)
username: Mapped[str | None] = mapped_column(String(255), nullable=True)
first_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
last_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
is_admin: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
default=utc_now,
onupdate=utc_now,
)
+6
View File
@@ -0,0 +1,6 @@
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from bot.config import DATABASE_URL
engine = create_async_engine(DATABASE_URL)
async_session = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
+5 -1
View File
@@ -9,22 +9,26 @@ logger = get_logger(__name__)
class AdminFilter(BaseFilter): class AdminFilter(BaseFilter):
async def __call__(self, message: Message) -> bool: async def __call__(self, message: Message) -> bool:
allowed = message.from_user.id == ADMIN_ID # type: ignore[union-attr] allowed = message.from_user.id == ADMIN_ID
if not allowed: if not allowed:
logger.warning( logger.warning(
"Отклонено сообщение от неавторизованного пользователя: user_id=%s", "Отклонено сообщение от неавторизованного пользователя: user_id=%s",
message.from_user.id if message.from_user else None, message.from_user.id if message.from_user else None,
) )
return allowed return allowed
class AdminCallbackFilter(BaseFilter): class AdminCallbackFilter(BaseFilter):
async def __call__(self, callback: CallbackQuery) -> bool: async def __call__(self, callback: CallbackQuery) -> bool:
allowed = callback.from_user.id == ADMIN_ID allowed = callback.from_user.id == ADMIN_ID
if not allowed: if not allowed:
logger.warning( logger.warning(
"Отклонен callback от неавторизованного пользователя: user_id=%s data=%s", "Отклонен callback от неавторизованного пользователя: user_id=%s data=%s",
callback.from_user.id, callback.from_user.id,
callback.data, callback.data,
) )
return allowed return allowed
+47 -16
View File
@@ -1,10 +1,13 @@
import random
import asyncio
from aiogram import Router from aiogram import Router
from aiogram.types import CallbackQuery from aiogram.types import CallbackQuery
from bot.api.asf import get_all_bots from bot.api.asf import get_all_bots
from bot.filters import AdminCallbackFilter from bot.filters import AdminCallbackFilter
from bot.logging_utils import describe_user, get_logger from bot.logging_utils import describe_user, get_logger
from bot.services.bots import is_idle_enabled, update_idle_game from bot.services.bots import is_bot_reloading, is_idle_enabled, update_idle_game
from bot.services.twofa import stop_twofa_task from bot.services.twofa import stop_twofa_task
from bot.ui.formatters import format_bot_ui, get_farm_summary from bot.ui.formatters import format_bot_ui, get_farm_summary
from bot.ui.keyboards import bot_details_keyboard, bots_keyboard, games_keyboard from bot.ui.keyboards import bot_details_keyboard, bots_keyboard, games_keyboard
@@ -31,7 +34,8 @@ async def bots_handler(callback: CallbackQuery) -> None:
"Пользователь %s открыл список ботов", "Пользователь %s открыл список ботов",
describe_user(callback.from_user), describe_user(callback.from_user),
) )
await stop_twofa_task(callback.message.message_id) # type: ignore[union-attr] await stop_twofa_task(callback.message.message_id)
await callback.answer() await callback.answer()
try: try:
response = get_all_bots() response = get_all_bots()
@@ -41,16 +45,18 @@ async def bots_handler(callback: CallbackQuery) -> None:
describe_user(callback.from_user), describe_user(callback.from_user),
response.status_code, response.status_code,
) )
await callback.message.edit_text("Ошибка API") # type: ignore[union-attr] await callback.message.edit_text("Ошибка API")
return return
data = response.json() data = response.json()
if not data.get("Success"): if not data.get("Success"):
logger.warning( logger.warning(
"ASF вернул неуспешный ответ при загрузке списка ботов для %s", "ASF вернул неуспешный ответ при загрузке списка ботов для %s",
describe_user(callback.from_user), describe_user(callback.from_user),
) )
await callback.message.edit_text("ASF вернул ошибку") # type: ignore[union-attr] await callback.message.edit_text("ASF вернул ошибку")
return return
bots = data.get("Result", {}) bots = data.get("Result", {})
logger.info( logger.info(
"Список ботов для %s загружен: %s бот(ов)", "Список ботов для %s загружен: %s бот(ов)",
@@ -58,33 +64,50 @@ async def bots_handler(callback: CallbackQuery) -> None:
len(bots), len(bots),
) )
text = f"<b>🤖 Ботов всего:</b> {len(bots)}\n\n{get_farm_summary(bots)}" text = f"<b>🤖 Ботов всего:</b> {len(bots)}\n\n{get_farm_summary(bots)}"
await callback.message.edit_text(text, reply_markup=bots_keyboard(bots)) # type: ignore[union-attr]
await callback.message.edit_text(text, reply_markup=bots_keyboard(bots))
except Exception as error: except Exception as error:
logger.exception( logger.exception(
"Ошибка при открытии списка ботов для %s", "Ошибка при открытии списка ботов для %s",
describe_user(callback.from_user), describe_user(callback.from_user),
) )
await callback.message.edit_text(f"Ошибка: {error}") # type: ignore[union-attr]
await callback.message.edit_text(f"Ошибка: {error}")
@router.callback_query( @router.callback_query(
lambda c: c.data and c.data.startswith("bot_"), AdminCallbackFilter() lambda c: c.data and c.data.startswith("bot_"), AdminCallbackFilter()
) )
async def bot_selected(callback: CallbackQuery) -> None: async def bot_selected(callback: CallbackQuery) -> None:
await stop_twofa_task(callback.message.message_id) # type: ignore[union-attr] await stop_twofa_task(callback.message.message_id)
await callback.answer()
if not callback.data: if not callback.data:
logger.debug( logger.debug(
"Колбэк выбора бота пришел без data от %s", "Колбэк выбора бота пришел без data от %s",
describe_user(callback.from_user), describe_user(callback.from_user),
) )
await callback.answer()
return return
bot_name = callback.data.replace("bot_", "") bot_name = callback.data.replace("bot_", "")
logger.info( logger.info(
"Пользователь %s выбрал бота %s", "Пользователь %s выбрал бота %s",
describe_user(callback.from_user), describe_user(callback.from_user),
bot_name, bot_name,
) )
if is_bot_reloading(bot_name):
logger.info(
"Бот %s перезагружается, карточка временно не отображается для %s",
bot_name,
describe_user(callback.from_user),
)
await callback.answer(
"Аккаунт перезагружается. Попробуйте через несколько секунд.",
show_alert=True,
)
return
try: try:
data = get_all_bots().json() data = get_all_bots().json()
bots = data.get("Result", {}) bots = data.get("Result", {})
@@ -105,20 +128,22 @@ async def bot_selected(callback: CallbackQuery) -> None:
bot_name, bot_name,
describe_user(callback.from_user), describe_user(callback.from_user),
) )
await callback.message.edit_text( # type: ignore[union-attr] await callback.answer()
await callback.message.edit_text(
format_bot_ui(bot), format_bot_ui(bot),
disable_web_page_preview=True, disable_web_page_preview=True,
reply_markup=bot_details_keyboard( reply_markup=bot_details_keyboard(
bot_name, bool(bot.get("HasMobileAuthenticator")) bot_name, bool(bot.get("HasMobileAuthenticator"))
), ),
) )
except Exception as error: except Exception as error:
logger.exception( logger.exception(
"Ошибка при открытии карточки бота %s для %s", "Ошибка при открытии карточки бота %s для %s",
bot_name, bot_name,
describe_user(callback.from_user), describe_user(callback.from_user),
) )
await callback.message.edit_text(f"Ошибка: {error}") # type: ignore[union-attr] await callback.message.edit_text(f"Ошибка: {error}")
@router.callback_query( @router.callback_query(
@@ -132,6 +157,7 @@ async def games(callback: CallbackQuery) -> None:
describe_user(callback.from_user), describe_user(callback.from_user),
) )
return return
bot_name = callback.data.replace("games_", "") bot_name = callback.data.replace("games_", "")
idle_enabled = is_idle_enabled(bot_name, 730) idle_enabled = is_idle_enabled(bot_name, 730)
logger.info( logger.info(
@@ -140,7 +166,8 @@ async def games(callback: CallbackQuery) -> None:
bot_name, bot_name,
idle_enabled, idle_enabled,
) )
await callback.message.edit_text( # type: ignore[union-attr]
await callback.message.edit_text(
"⌚ Выберите игру для накрутки часов", "⌚ Выберите игру для накрутки часов",
reply_markup=games_keyboard(bot_name, idle_enabled), reply_markup=games_keyboard(bot_name, idle_enabled),
) )
@@ -157,6 +184,7 @@ async def farm_game(callback: CallbackQuery) -> None:
describe_user(callback.from_user), describe_user(callback.from_user),
) )
return return
_, bot_name, app_id_value = callback.data.split("|") _, bot_name, app_id_value = callback.data.split("|")
logger.info( logger.info(
"Пользователь %s меняет игру для накрутки на боте %s: app_id=%s", "Пользователь %s меняет игру для накрутки на боте %s: app_id=%s",
@@ -164,6 +192,7 @@ async def farm_game(callback: CallbackQuery) -> None:
bot_name, bot_name,
app_id_value, app_id_value,
) )
success, text, enabled = await update_idle_game(bot_name, int(app_id_value)) success, text, enabled = await update_idle_game(bot_name, int(app_id_value))
if not success: if not success:
logger.warning( logger.warning(
@@ -174,20 +203,22 @@ async def farm_game(callback: CallbackQuery) -> None:
) )
await callback.answer(text, show_alert=True) await callback.answer(text, show_alert=True)
return return
logger.info( logger.info(
"Игра для накрутки на боте %s обновлена для %s. CS2=%s", "Игра для накрутки на боте %s обновлена для %s. CS2=%s",
bot_name, bot_name,
describe_user(callback.from_user), describe_user(callback.from_user),
enabled, enabled,
) )
await callback.answer(text, show_alert=True) await callback.answer(text, show_alert=True)
try: try:
await callback.message.edit_reply_markup( # type: ignore[union-attr] await callback.message.edit_text(
reply_markup=games_keyboard(bot_name, enabled) text=f"⌚ Выберите игру для накрутки часов",
reply_markup=games_keyboard(bot_name, enabled),
) )
except Exception: except Exception:
logger.debug( logger.debug(
"Не удалось обновить клавиатуру игр для бота %s", "Не удалось обновить клавиатуру игр для бота %s", bot_name, exc_info=True
bot_name,
exc_info=True,
) )
+16 -10
View File
@@ -1,10 +1,11 @@
from aiogram import Router from aiogram import Router
from aiogram.types import CallbackQuery from aiogram.types import CallbackQuery
from aiogram.fsm.context import FSMContext
from bot.filters import AdminCallbackFilter from bot.filters import AdminCallbackFilter
from bot.logging_utils import describe_user, get_logger from bot.states import ConsoleFlow
from bot.state import console_users
from bot.ui.formatters import get_asf_status_text from bot.ui.formatters import get_asf_status_text
from bot.logging_utils import describe_user, get_logger
from bot.ui.keyboards import console_keyboard, main_keyboard from bot.ui.keyboards import console_keyboard, main_keyboard
router = Router() router = Router()
@@ -12,21 +13,26 @@ logger = get_logger(__name__)
@router.callback_query(lambda c: c.data == "console", AdminCallbackFilter()) @router.callback_query(lambda c: c.data == "console", AdminCallbackFilter())
async def console_enter(callback: CallbackQuery) -> None: async def console_enter(callback: CallbackQuery, state: FSMContext) -> None:
user = describe_user(callback.from_user.id) user = describe_user(callback.from_user.id)
logger.info("Пользователь вошел в режим консоли ASF: %s", user) logger.info("Пользователь вошел в режим консоли ASF: %s", user)
await state.set_state(ConsoleFlow.waiting_command)
await callback.answer() await callback.answer()
console_users.add(callback.from_user.id) await callback.message.edit_text(
await callback.message.edit_text( # type: ignore[union-attr] "💻 Консоль ASF\n\nОтправь команду (без !)", reply_markup=console_keyboard()
"💻 Консоль ASF\n\nОтправь команду (без !)",
reply_markup=console_keyboard(),
) )
@router.callback_query(lambda c: c.data == "console_exit", AdminCallbackFilter()) @router.callback_query(lambda c: c.data == "console_exit", AdminCallbackFilter())
async def console_exit(callback: CallbackQuery) -> None: async def console_exit(callback: CallbackQuery, state: FSMContext) -> None:
user = describe_user(callback.from_user.id) user = describe_user(callback.from_user.id)
logger.info("Пользователь вышел из режима консоли ASF: %s", user) logger.info("Пользователь вышел из режима консоли ASF: %s", user)
await state.clear()
await callback.answer() await callback.answer()
console_users.discard(callback.from_user.id) await callback.message.edit_text(
await callback.message.edit_text(get_asf_status_text(), reply_markup=main_keyboard()) # type: ignore[union-attr] get_asf_status_text(), reply_markup=main_keyboard()
)
+26 -15
View File
@@ -15,15 +15,18 @@ logger = get_logger(__name__)
) )
async def inventory_menu(callback: CallbackQuery) -> None: async def inventory_menu(callback: CallbackQuery) -> None:
user = describe_user(callback.from_user.id) user = describe_user(callback.from_user.id)
bot_name = callback.data.replace("inventory_", "") # type: ignore[union-attr] bot_name = callback.data.replace("inventory_", "")
logger.info("Открыто меню инвентаря: %s bot=%s", user, bot_name) logger.info("Открыто меню инвентаря: %s bot=%s", user, bot_name)
await callback.answer() await callback.answer()
await callback.message.edit_text("Проверка inventory...") # type: ignore[union-attr] await callback.message.edit_text("Проверка инвентаря...")
text, keyboard = await build_inventory_menu(bot_name) text, keyboard = await build_inventory_menu(bot_name)
if "пуст" in text.lower(): if "пуст" in text.lower():
logger.info("Показано пустое меню инвентаря: %s bot=%s", user, bot_name) logger.info("Показано пустое меню инвентаря: %s bot=%s", user, bot_name)
keyboard = back_keyboard(f"bot_{bot_name}") keyboard = back_keyboard(f"bot_{bot_name}")
await callback.message.edit_text(text, reply_markup=keyboard) # type: ignore[union-attr]
await callback.message.edit_text(text, reply_markup=keyboard)
@router.callback_query( @router.callback_query(
@@ -31,16 +34,19 @@ async def inventory_menu(callback: CallbackQuery) -> None:
) )
async def cs2_inventory(callback: CallbackQuery) -> None: async def cs2_inventory(callback: CallbackQuery) -> None:
user = describe_user(callback.from_user.id) user = describe_user(callback.from_user.id)
bot_name = callback.data.replace("inv_cs2_", "") # type: ignore[union-attr] bot_name = callback.data.replace("inv_cs2_", "")
logger.info("Открыт CS2-инвентарь: %s bot=%s", user, bot_name) logger.info("Открыт CS2-инвентарь: %s bot=%s", user, bot_name)
await callback.answer() await callback.answer()
await callback.message.edit_text("Загрузка inventory...") # type: ignore[union-attr] await callback.message.edit_text("Загрузка инвентаря...")
text, keyboard = await render_inventory_page(bot_name, "cs2", 730, 2, 0) text, keyboard = await render_inventory_page(bot_name, "cs2", 730, 2, 0)
if text is None: if text is None:
logger.warning("Не удалось загрузить CS2-инвентарь: %s bot=%s", user, bot_name) logger.warning("Не удалось загрузить CS2-инвентарь: %s bot=%s", user, bot_name)
await callback.message.edit_text("Не удалось загрузить inventory") # type: ignore[union-attr] await callback.message.edit_text("Не удалось загрузить инвентарь")
return return
await callback.message.edit_text(text, reply_markup=keyboard) # type: ignore[union-attr]
await callback.message.edit_text(text, reply_markup=keyboard)
@router.callback_query( @router.callback_query(
@@ -48,16 +54,21 @@ async def cs2_inventory(callback: CallbackQuery) -> None:
) )
async def steam_inventory(callback: CallbackQuery) -> None: async def steam_inventory(callback: CallbackQuery) -> None:
user = describe_user(callback.from_user.id) user = describe_user(callback.from_user.id)
bot_name = callback.data.replace("inv_steam_", "") # type: ignore[union-attr] bot_name = callback.data.replace("inv_steam_", "")
logger.info("Открыт Steam-инвентарь: %s bot=%s", user, bot_name) logger.info("Открыт Steam-инвентарь: %s bot=%s", user, bot_name)
await callback.answer() await callback.answer()
await callback.message.edit_text("Загрузка inventory...") # type: ignore[union-attr] await callback.message.edit_text("Загрузка инвентаря...")
text, keyboard = await render_inventory_page(bot_name, "steam", 753, 6, 0) text, keyboard = await render_inventory_page(bot_name, "steam", 753, 6, 0)
if text is None: if text is None:
logger.warning("Не удалось загрузить Steam-инвентарь: %s bot=%s", user, bot_name) logger.warning(
await callback.message.edit_text("Не удалось загрузить inventory") # type: ignore[union-attr] "Не удалось загрузить Steam-инвентарь: %s bot=%s", user, bot_name
)
await callback.message.edit_text("Не удалось загрузить инвентарь")
return return
await callback.message.edit_text(text, reply_markup=keyboard) # type: ignore[union-attr]
await callback.message.edit_text(text, reply_markup=keyboard)
@router.callback_query( @router.callback_query(
@@ -65,7 +76,7 @@ async def steam_inventory(callback: CallbackQuery) -> None:
) )
async def inventory_page(callback: CallbackQuery) -> None: async def inventory_page(callback: CallbackQuery) -> None:
user = describe_user(callback.from_user.id) user = describe_user(callback.from_user.id)
_, inv_type, bot_name, page_value = callback.data.split("|") # type: ignore[union-attr] _, inv_type, bot_name, page_value = callback.data.split("|")
logger.info( logger.info(
"Открыта страница инвентаря: %s bot=%s type=%s page=%s", "Открыта страница инвентаря: %s bot=%s type=%s page=%s",
user, user,
@@ -86,6 +97,6 @@ async def inventory_page(callback: CallbackQuery) -> None:
inv_type, inv_type,
page_value, page_value,
) )
await callback.message.edit_text("Не удалось загрузить inventory") # type: ignore[union-attr] await callback.message.edit_text("Не удалось загрузить инвентарь")
return return
await callback.message.edit_text(text, reply_markup=keyboard) # type: ignore[union-attr] await callback.message.edit_text(text, reply_markup=keyboard)
+100 -48
View File
@@ -2,50 +2,57 @@ import html
from aiogram import Router from aiogram import Router
from aiogram.types import Message 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.filters import AdminFilter
from bot.api.asf import get_all_bots, send_command
from bot.logging_utils import describe_user, get_logger from bot.logging_utils import describe_user, get_logger
from bot.services.redeem import ( from bot.services.redeem import (
extract_keys, extract_keys,
redeem_all_bots_keys, redeem_all_bots_keys,
redeem_single_bot_keys, redeem_single_bot_keys,
) )
from bot.state import (
bot_redeem_messages, from bot.states import ConsoleFlow, RedeemFlow
bot_redeem_users,
console_users,
redeem_messages,
redeem_users,
)
from bot.ui.formatters import format_bot_ui, get_asf_status_text from bot.ui.formatters import format_bot_ui, get_asf_status_text
from bot.ui.keyboards import ( from bot.ui.keyboards import (
main_keyboard,
bot_details_keyboard, bot_details_keyboard,
delete_message_keyboard, delete_message_keyboard,
main_keyboard,
) )
router = Router() router = Router()
logger = get_logger(__name__) logger = get_logger(__name__)
async def handle_console_message(message: Message) -> bool: @router.message(ConsoleFlow.waiting_command, AdminFilter())
if message.from_user.id not in console_users: # type: ignore[union-attr] async def handle_console_message(message: Message, state: FSMContext):
return False if not message.text:
command = message.text.strip() # type: ignore[union-attr] await message.answer("Отправьте текстовую команду.")
return
command = message.text.strip()
if not command:
await message.answer("Команда не может быть пустой.")
return
logger.info( logger.info(
"Пользователь %s отправил консольную команду: %s", "Пользователь %s отправил консольную команду: %s",
describe_user(message.from_user), describe_user(message.from_user),
command, command,
) )
try: try:
await message.delete() await message.delete()
except Exception: except Exception:
logger.debug( logger.debug(
"Не удалось удалить сообщение с консольной командой от %s", "Не удалось удалить сообщение с консольной командой от %s",
describe_user(message.from_user), describe_user(message.from_user),
exc_info=True, exc_info=True,
) )
try: try:
response = send_command(f"!{command}") response = send_command(f"!{command}")
if response.status_code != 200: if response.status_code != 200:
@@ -55,9 +62,12 @@ async def handle_console_message(message: Message) -> bool:
command, command,
describe_user(message.from_user), describe_user(message.from_user),
) )
text = f"Ошибка HTTP {response.status_code}" text = f"Ошибка HTTP {response.status_code}"
else: else:
data = response.json() data = response.json()
if not data.get("Success"): if not data.get("Success"):
logger.warning( logger.warning(
"ASF вернул неуспешный ответ на консольную команду '%s' от %s", "ASF вернул неуспешный ответ на консольную команду '%s' от %s",
@@ -65,6 +75,7 @@ async def handle_console_message(message: Message) -> bool:
describe_user(message.from_user), describe_user(message.from_user),
) )
text = "Ошибка ASF" text = "Ошибка ASF"
else: else:
logger.info( logger.info(
"Консольная команда '%s' от %s выполнена успешно", "Консольная команда '%s' от %s выполнена успешно",
@@ -73,39 +84,55 @@ async def handle_console_message(message: Message) -> bool:
) )
result = html.escape(str(data.get("Result", "Нет ответа"))) result = html.escape(str(data.get("Result", "Нет ответа")))
text = f"<b>Команда:</b> <code>{command}</code>\n\n<b>Ответ:</b>\n<code>{result}</code>" text = f"<b>Команда:</b> <code>{command}</code>\n\n<b>Ответ:</b>\n<code>{result}</code>"
await message.answer(text, reply_markup=delete_message_keyboard()) await message.answer(text, reply_markup=delete_message_keyboard())
except Exception as error: except Exception as error:
logger.exception( logger.exception(
"Ошибка при выполнении консольной команды '%s' от %s", "Ошибка при выполнении консольной команды '%s' от %s",
command, command,
describe_user(message.from_user), describe_user(message.from_user),
) )
await message.answer(f"Ошибка: {error}") await message.answer(f"Ошибка: {error}")
return True
async def handle_single_bot_redeem(message: Message) -> bool: @router.message(RedeemFlow.waiting_bot_keys, AdminFilter())
if message.from_user.id not in bot_redeem_users: # type: ignore[union-attr] async def handle_single_bot_redeem(message: Message, state: FSMContext):
return False state_data = await state.get_data()
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] 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( logger.info(
"Пользователь %s отправил ключи для бота %s", "Пользователь %s отправил ключи для бота %s",
describe_user(message.from_user), describe_user(message.from_user),
bot_name, bot_name,
) )
if menu_message:
if menu_message_id:
try: try:
response = get_all_bots() response = get_all_bots()
bot_data = response.json().get("Result", {}).get(bot_name) bot_data = response.json().get("Result", {}).get(bot_name)
if bot_data: if bot_data:
await menu_message.edit_text( await message.bot.edit_message_text(
format_bot_ui(bot_data), text=format_bot_ui(bot_data),
disable_web_page_preview=True,
reply_markup=bot_details_keyboard( reply_markup=bot_details_keyboard(
bot_name, bool(bot_data.get("HasMobileAuthenticator")) bot_name, bool(bot_data.get("HasMobileAuthenticator"))
), ),
disable_web_page_preview=True,
chat_id=chat_id,
message_id=menu_message_id,
) )
except Exception: except Exception:
logger.debug( logger.debug(
"Не удалось восстановить меню бота %s после ввода ключей от %s", "Не удалось восстановить меню бота %s после ввода ключей от %s",
@@ -113,10 +140,12 @@ async def handle_single_bot_redeem(message: Message) -> bool:
describe_user(message.from_user), describe_user(message.from_user),
exc_info=True, exc_info=True,
) )
try: try:
await message.bot.delete_message( await message.bot.delete_message(
chat_id=message.chat.id, message_id=message.message_id chat_id=message.chat.id, message_id=message.message_id
) )
except Exception: except Exception:
logger.debug( logger.debug(
"Не удалось удалить сообщение с ключами для бота %s от %s", "Не удалось удалить сообщение с ключами для бота %s от %s",
@@ -124,6 +153,7 @@ async def handle_single_bot_redeem(message: Message) -> bool:
describe_user(message.from_user), describe_user(message.from_user),
exc_info=True, exc_info=True,
) )
keys = extract_keys(message.text or "") keys = extract_keys(message.text or "")
if not keys: if not keys:
logger.info( logger.info(
@@ -132,7 +162,10 @@ async def handle_single_bot_redeem(message: Message) -> bool:
bot_name, bot_name,
) )
await message.answer("Ключи не найдены") await message.answer("Ключи не найдены")
return True await state.clear()
return
logger.info( logger.info(
"Для бота %s получено %s ключ(ей) от %s", "Для бота %s получено %s ключ(ей) от %s",
bot_name, bot_name,
@@ -142,40 +175,66 @@ async def handle_single_bot_redeem(message: Message) -> bool:
checking = await message.answer( checking = await message.answer(
f"Аккаунт: <code>{bot_name}</code>\nНайдено ключей: {len(keys)}\nНачинаю активацию..." 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: @router.message(RedeemFlow.waiting_all_keys, AdminFilter())
if message.from_user.id not in redeem_users: # type: ignore[union-attr] async def handle_all_bots_redeem(message: Message, state: FSMContext):
return False state_data = await state.get_data()
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] chat_id = message.chat.id
menu_message_id = state_data.get("menu_message_id")
logger.info( logger.info(
"Пользователь %s отправил ключи для активации на всех ботах", "Пользователь %s отправил ключи для активации на всех ботах",
describe_user(message.from_user), describe_user(message.from_user),
) )
if menu_message:
if menu_message_id:
try: try:
await menu_message.edit_text( await message.bot.edit_message_text(
get_asf_status_text(), reply_markup=main_keyboard() text=get_asf_status_text(),
reply_markup=main_keyboard(),
chat_id=chat_id,
message_id=menu_message_id,
) )
except Exception: except Exception:
logger.debug( logger.debug(
"Не удалось восстановить главное меню после массовой активации для %s", "Не удалось восстановить главное меню после массовой активации для %s",
describe_user(message.from_user), describe_user(message.from_user),
exc_info=True, exc_info=True,
) )
try: try:
await message.bot.delete_message( await message.bot.delete_message(
chat_id=message.chat.id, message_id=message.message_id chat_id=message.chat.id, message_id=message.message_id
) )
except Exception: except Exception:
logger.debug( logger.debug(
"Не удалось удалить сообщение с массовой активацией от %s", "Не удалось удалить сообщение с массовой активацией от %s",
describe_user(message.from_user), describe_user(message.from_user),
exc_info=True, exc_info=True,
) )
keys = extract_keys(message.text or "") keys = extract_keys(message.text or "")
if not keys: if not keys:
logger.info( logger.info(
@@ -183,7 +242,10 @@ async def handle_all_bots_redeem(message: Message) -> bool:
describe_user(message.from_user), describe_user(message.from_user),
) )
await message.answer("Ключи не найдены") await message.answer("Ключи не найдены")
return True await state.clear()
return
logger.info( logger.info(
"Для массовой активации получено %s ключ(ей) от %s", "Для массовой активации получено %s ключ(ей) от %s",
len(keys), len(keys),
@@ -192,25 +254,15 @@ async def handle_all_bots_redeem(message: Message) -> bool:
checking = await message.answer( checking = await message.answer(
f"Найдено ключей: {len(keys)}\nНачинаю активацию..." f"Найдено ключей: {len(keys)}\nНачинаю активацию..."
) )
try: try:
await checking.edit_text(await redeem_all_bots_keys(keys)) await checking.edit_text(await redeem_all_bots_keys(keys))
except Exception as error: except Exception as error:
logger.exception( logger.exception(
"Ошибка при массовой активации ключей от %s", "Ошибка при массовой активации ключей от %s",
describe_user(message.from_user), describe_user(message.from_user),
) )
await checking.edit_text(f"Ошибка: {error}") await checking.edit_text(f"Ошибка: {error}")
return True
await state.clear()
@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)
+41 -35
View File
@@ -2,18 +2,15 @@ import asyncio
from aiogram import Router from aiogram import Router
from aiogram.types import CallbackQuery from aiogram.types import CallbackQuery
from aiogram.fsm.context import FSMContext
from bot.api.asf import get_plugins, restart_asf as restart_asf_request
from bot.filters import AdminCallbackFilter from bot.filters import AdminCallbackFilter
from bot.logging_utils import describe_user, get_logger from bot.logging_utils import describe_user, get_logger
from bot.api.asf import get_plugins, restart_asf as restart_asf_request
from bot.services.twofa import stop_twofa_task from bot.services.twofa import stop_twofa_task
from bot.state import (
bot_redeem_messages,
bot_redeem_users,
console_users,
redeem_messages,
redeem_users,
)
from bot.ui.formatters import get_asf_status_text, get_asf_status from bot.ui.formatters import get_asf_status_text, get_asf_status
from bot.ui.keyboards import back_keyboard, confirm_action_keyboard, main_keyboard from bot.ui.keyboards import back_keyboard, confirm_action_keyboard, main_keyboard
@@ -21,28 +18,24 @@ router = Router()
logger = get_logger(__name__) logger = get_logger(__name__)
def clear_user_state(user_id: int) -> None:
redeem_users.discard(user_id)
redeem_messages.pop(user_id, None)
console_users.discard(user_id)
bot_redeem_users.pop(user_id, None)
bot_redeem_messages.pop(user_id, None)
logger.info("Состояние пользователя очищено: user_id=%s", user_id)
@router.callback_query(lambda c: c.data == "back", AdminCallbackFilter()) @router.callback_query(lambda c: c.data == "back", AdminCallbackFilter())
async def back_button(callback: CallbackQuery) -> None: async def back_button(callback: CallbackQuery, state: FSMContext) -> None:
user = describe_user(callback.from_user.id) user = describe_user(callback.from_user.id)
logger.info("Нажата кнопка возврата: %s", user) logger.info("Нажата кнопка возврата: %s", user)
clear_user_state(callback.from_user.id)
await stop_twofa_task(callback.message.message_id) # type: ignore[union-attr] await state.clear()
await stop_twofa_task(callback.message.message_id)
await callback.answer() await callback.answer()
try: try:
await callback.message.edit_text(get_asf_status_text(), reply_markup=main_keyboard()) # type: ignore[union-attr] await callback.message.edit_text(
get_asf_status_text(), reply_markup=main_keyboard()
)
logger.info("Пользователь возвращен в главное меню: %s", user) logger.info("Пользователь возвращен в главное меню: %s", user)
except Exception as error: except Exception as error:
logger.exception("Не удалось вернуть пользователя в главное меню: %s", user) logger.exception("Не удалось вернуть пользователя в главное меню: %s", user)
await callback.message.edit_text(f"Ошибка: {error}") # type: ignore[union-attr] await callback.message.edit_text(f"Ошибка: {error}")
@router.callback_query(lambda c: c.data == "refresh", AdminCallbackFilter()) @router.callback_query(lambda c: c.data == "refresh", AdminCallbackFilter())
@@ -50,22 +43,28 @@ async def refresh_handler(callback: CallbackQuery) -> None:
user = describe_user(callback.from_user.id) user = describe_user(callback.from_user.id)
logger.info("Запрошено обновление главного экрана: %s", user) logger.info("Запрошено обновление главного экрана: %s", user)
await callback.answer() await callback.answer()
try: try:
await callback.message.edit_text(get_asf_status_text(), reply_markup=main_keyboard()) # type: ignore[union-attr] await callback.message.edit_text(
get_asf_status_text(), reply_markup=main_keyboard()
)
logger.info("Главный экран обновлен: %s", user) logger.info("Главный экран обновлен: %s", user)
except Exception as error: except Exception as error:
logger.exception("Не удалось обновить главный экран: %s", user) logger.exception("Не удалось обновить главный экран: %s", user)
await callback.message.edit_text(f"Ошибка: {error}") # type: ignore[union-attr] await callback.message.edit_text(f"Ошибка: {error}")
@router.callback_query(lambda c: c.data == "delete_msg", AdminCallbackFilter()) @router.callback_query(lambda c: c.data == "delete_msg", AdminCallbackFilter())
async def delete_message(callback: CallbackQuery) -> None: async def delete_message(callback: CallbackQuery) -> None:
user = describe_user(callback.from_user.id) user = describe_user(callback.from_user.id)
logger.info("Запрошено удаление сообщения: %s", user) logger.info("Запрошено удаление сообщения: %s", user)
await callback.answer() await callback.answer()
try: try:
await callback.message.delete() # type: ignore[union-attr] await callback.message.delete()
logger.info("Сообщение удалено: %s", user) logger.info("Сообщение удалено: %s", user)
except Exception: except Exception:
logger.exception("Не удалось удалить сообщение: %s", user) logger.exception("Не удалось удалить сообщение: %s", user)
@@ -74,8 +73,9 @@ async def delete_message(callback: CallbackQuery) -> None:
async def restart_asf_confirm(callback: CallbackQuery) -> None: async def restart_asf_confirm(callback: CallbackQuery) -> None:
user = describe_user(callback.from_user.id) user = describe_user(callback.from_user.id)
logger.info("Открыто подтверждение перезапуска ASF: %s", user) logger.info("Открыто подтверждение перезапуска ASF: %s", user)
await callback.answer() await callback.answer()
await callback.message.edit_text( # type: ignore[union-attr] await callback.message.edit_text(
"♻ Точно перезапустить ASF?\n\nВо время перезапуска IPC будет недоступен несколько секунд.", "♻ Точно перезапустить ASF?\n\nВо время перезапуска IPC будет недоступен несколько секунд.",
reply_markup=confirm_action_keyboard("restart_asf", "back"), reply_markup=confirm_action_keyboard("restart_asf", "back"),
) )
@@ -85,9 +85,10 @@ async def restart_asf_confirm(callback: CallbackQuery) -> None:
async def restart_asf(callback: CallbackQuery) -> None: async def restart_asf(callback: CallbackQuery) -> None:
user = describe_user(callback.from_user.id) user = describe_user(callback.from_user.id)
logger.info("Подтвержден перезапуск ASF: %s", user) logger.info("Подтвержден перезапуск ASF: %s", user)
await callback.answer() await callback.answer()
try: try:
await callback.message.edit_text("♻ ASF перезапускается...") # type: ignore[union-attr] await callback.message.edit_text("♻ ASF перезапускается...")
response = restart_asf_request() response = restart_asf_request()
if response.status_code != 200: if response.status_code != 200:
@@ -96,7 +97,7 @@ async def restart_asf(callback: CallbackQuery) -> None:
user, user,
response.status_code, response.status_code,
) )
await callback.message.edit_text( # type: ignore[union-attr] await callback.message.edit_text(
f"Ошибка HTTP {response.status_code}", f"Ошибка HTTP {response.status_code}",
reply_markup=main_keyboard(), reply_markup=main_keyboard(),
) )
@@ -113,7 +114,7 @@ async def restart_asf(callback: CallbackQuery) -> None:
status = get_asf_status() status = get_asf_status()
await asyncio.sleep(1) await asyncio.sleep(1)
await callback.message.edit_text( # type: ignore[union-attr] await callback.message.edit_text(
f"ASF успешно перезапущен\n\n{get_asf_status_text()}", f"ASF успешно перезапущен\n\n{get_asf_status_text()}",
reply_markup=main_keyboard(), reply_markup=main_keyboard(),
) )
@@ -123,7 +124,7 @@ async def restart_asf(callback: CallbackQuery) -> None:
except Exception as error: except Exception as error:
logger.exception("Не удалось перезапустить ASF: %s", user) logger.exception("Не удалось перезапустить ASF: %s", user)
await callback.message.edit_text( # type: ignore[union-attr] await callback.message.edit_text(
f"Ошибка: {error}", f"Ошибка: {error}",
reply_markup=main_keyboard(), reply_markup=main_keyboard(),
) )
@@ -133,7 +134,8 @@ async def restart_asf(callback: CallbackQuery) -> None:
async def plugins_handler(callback: CallbackQuery) -> None: async def plugins_handler(callback: CallbackQuery) -> None:
user = describe_user(callback.from_user.id) user = describe_user(callback.from_user.id)
logger.info("Открыт список плагинов ASF: %s", user) logger.info("Открыт список плагинов ASF: %s", user)
await stop_twofa_task(callback.message.message_id) # type: ignore[union-attr] await stop_twofa_task(callback.message.message_id)
await callback.answer() await callback.answer()
try: try:
response = get_plugins() response = get_plugins()
@@ -143,13 +145,15 @@ async def plugins_handler(callback: CallbackQuery) -> None:
user, user,
response.status_code, response.status_code,
) )
await callback.message.edit_text("Ошибка API") # type: ignore[union-attr] await callback.message.edit_text("Ошибка API")
return return
data = response.json() data = response.json()
if not data.get("Success"): if not data.get("Success"):
logger.warning("ASF вернул ошибку при получении плагинов: %s", user) logger.warning("ASF вернул ошибку при получении плагинов: %s", user)
await callback.message.edit_text("ASF вернул ошибку") # type: ignore[union-attr] await callback.message.edit_text("ASF вернул ошибку")
return return
plugins = data.get("Result", []) plugins = data.get("Result", [])
if not plugins: if not plugins:
logger.info("Плагины ASF не установлены: %s", user) logger.info("Плагины ASF не установлены: %s", user)
@@ -161,7 +165,9 @@ async def plugins_handler(callback: CallbackQuery) -> None:
text += ( text += (
f"{plugin.get('Name', '???')} ({plugin.get('Version', '???')})\n" f"{plugin.get('Name', '???')} ({plugin.get('Version', '???')})\n"
) )
await callback.message.edit_text(text.strip(), reply_markup=back_keyboard()) # type: ignore[union-attr]
await callback.message.edit_text(text.strip(), reply_markup=back_keyboard())
except Exception as error: except Exception as error:
logger.exception("Не удалось отобразить список плагинов: %s", user) logger.exception("Не удалось отобразить список плагинов: %s", user)
await callback.message.edit_text(f"Ошибка: {error}") # type: ignore[union-attr] await callback.message.edit_text(f"Ошибка: {error}")
+21 -16
View File
@@ -1,14 +1,12 @@
from aiogram import Router from aiogram import Router
from aiogram.types import CallbackQuery from aiogram.types import CallbackQuery
from aiogram.fsm.context import FSMContext
from bot.filters import AdminCallbackFilter from bot.filters import AdminCallbackFilter
from bot.logging_utils import describe_user, get_logger from bot.logging_utils import describe_user, get_logger
from bot.state import (
bot_redeem_messages,
bot_redeem_users, from bot.states import RedeemFlow
redeem_messages,
redeem_users,
)
from bot.ui.keyboards import back_keyboard from bot.ui.keyboards import back_keyboard
router = Router() router = Router()
@@ -16,33 +14,40 @@ logger = get_logger(__name__)
@router.callback_query(lambda c: c.data == "redeem_keys", AdminCallbackFilter()) @router.callback_query(lambda c: c.data == "redeem_keys", AdminCallbackFilter())
async def redeem_keys_menu(callback: CallbackQuery) -> None: async def redeem_keys_menu(callback: CallbackQuery, state: FSMContext) -> None:
user = describe_user(callback.from_user.id) user = describe_user(callback.from_user.id)
logger.info("Открыт режим активации ключей по всем ботам: %s", user) logger.info("Открыт режим активации ключей по всем ботам: %s", user)
await callback.answer() await callback.answer()
redeem_users.add(callback.from_user.id) await callback.message.edit_text(
redeem_messages[callback.from_user.id] = callback.message
await callback.message.edit_text( # type: ignore[union-attr]
"Отправьте ключ Steam для активации\n\n" "Отправьте ключ Steam для активации\n\n"
"Можно отправить сразу несколько ключей.\n\n" "Можно отправить сразу несколько ключей.\n\n"
"Пример:\n" "Пример:\n"
"<code>AAAAA-BBBBB-CCCCC\nDDDDD-EEEEE-FFFFF</code>", "<code>AAAAA-BBBBB-CCCCC\nDDDDD-EEEEE-FFFFF</code>",
reply_markup=back_keyboard(), reply_markup=back_keyboard(),
) )
await state.set_state(RedeemFlow.waiting_all_keys)
await state.update_data(menu_message_id=callback.message.message_id)
@router.callback_query( @router.callback_query(
lambda c: c.data and c.data.startswith("redeem_bot_"), AdminCallbackFilter() lambda c: c.data and c.data.startswith("redeem_bot_"), AdminCallbackFilter()
) )
async def redeem_bot_menu(callback: CallbackQuery) -> None: async def redeem_bot_menu(callback: CallbackQuery, state: FSMContext) -> None:
user = describe_user(callback.from_user.id) user = describe_user(callback.from_user.id)
bot_name = callback.data.replace("redeem_bot_", "") # type: ignore[union-attr]
logger.info("Открыт режим активации ключей для одного бота: %s bot=%s", user, bot_name) bot_name = callback.data.replace("redeem_bot_", "")
logger.info(
"Открыт режим активации ключей для одного бота: %s bot=%s", user, bot_name
)
await callback.answer() await callback.answer()
bot_redeem_users[callback.from_user.id] = bot_name await callback.message.edit_text(
bot_redeem_messages[callback.from_user.id] = callback.message
await callback.message.edit_text( # type: ignore[union-attr]
f"Отправьте ключ Steam для активации на аккаунте:\n<code>{bot_name}</code>\n\n" f"Отправьте ключ Steam для активации на аккаунте:\n<code>{bot_name}</code>\n\n"
"Можно отправить сразу несколько ключей.", "Можно отправить сразу несколько ключей.",
reply_markup=back_keyboard(f"bot_{bot_name}"), reply_markup=back_keyboard(f"bot_{bot_name}"),
) )
await state.set_state(RedeemFlow.waiting_bot_keys)
await state.update_data(
bot_name=bot_name, menu_message_id=callback.message.message_id
)
+11 -3
View File
@@ -1,11 +1,14 @@
from aiogram import Router from aiogram import Router
from aiogram.filters import Command
from aiogram.types import Message from aiogram.types import Message
from aiogram.filters import Command
from bot.filters import AdminFilter from bot.filters import AdminFilter
from bot.logging_utils import describe_user, get_logger from bot.services.users import upsert_user_from_telegram
from bot.ui.formatters import get_asf_status_text
from bot.ui.keyboards import main_keyboard from bot.ui.keyboards import main_keyboard
from bot.ui.formatters import get_asf_status_text
from bot.logging_utils import describe_user, get_logger
router = Router() router = Router()
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -14,15 +17,20 @@ logger = get_logger(__name__)
@router.message(Command("start"), AdminFilter()) @router.message(Command("start"), AdminFilter())
async def start_handler(message: Message) -> None: async def start_handler(message: Message) -> None:
user = describe_user(message.from_user.id if message.from_user else None) user = describe_user(message.from_user.id if message.from_user else None)
await upsert_user_from_telegram(message.from_user)
logger.info("Получена команда /start: %s", user) logger.info("Получена команда /start: %s", user)
try: try:
await message.delete() await message.delete()
logger.info("Сообщение /start удалено: %s", user) logger.info("Сообщение /start удалено: %s", user)
except Exception: except Exception:
logger.exception("Не удалось удалить сообщение /start: %s", user) logger.exception("Не удалось удалить сообщение /start: %s", user)
try: try:
await message.answer(get_asf_status_text(), reply_markup=main_keyboard()) await message.answer(get_asf_status_text(), reply_markup=main_keyboard())
logger.info("Главное меню отправлено: %s", user) logger.info("Главное меню отправлено: %s", user)
except Exception as error: except Exception as error:
logger.exception("Не удалось отправить главное меню: %s", user) logger.exception("Не удалось отправить главное меню: %s", user)
await message.answer(f"Ошибка: {error}") await message.answer(f"Ошибка: {error}")
+24 -17
View File
@@ -3,11 +3,11 @@ import asyncio
from aiogram import Router from aiogram import Router
from aiogram.types import CallbackQuery from aiogram.types import CallbackQuery
from bot.api.asf import accept_confirmations, get_confirmations from bot.states import twofa_tasks
from bot.filters import AdminCallbackFilter from bot.filters import AdminCallbackFilter
from bot.logging_utils import describe_user, get_logger from bot.logging_utils import describe_user, get_logger
from bot.services.twofa import auto_update_2fa, stop_twofa_task from bot.services.twofa import auto_update_2fa, stop_twofa_task
from bot.state import twofa_tasks from bot.api.asf import accept_confirmations, get_confirmations
from bot.ui.keyboards import back_keyboard, confirmations_keyboard from bot.ui.keyboards import back_keyboard, confirmations_keyboard
router = Router() router = Router()
@@ -29,14 +29,14 @@ async def twofa_handler(callback: CallbackQuery) -> None:
) )
return return
bot_name = callback.data.replace("2fa_", "") bot_name = callback.data.replace("2fa_", "")
message_id = callback.message.message_id # type: ignore[union-attr] message_id = callback.message.message_id
logger.info( logger.info(
"Пользователь %s открыл 2FA для бота %s", "Пользователь %s открыл 2FA для бота %s",
describe_user(callback.from_user), describe_user(callback.from_user),
bot_name, bot_name,
) )
await stop_twofa_task(message_id) await stop_twofa_task(message_id)
await callback.message.edit_text( # type: ignore[union-attr] await callback.message.edit_text(
f"🔐 {bot_name}\n\nЗагрузка 2FA...", f"🔐 {bot_name}\n\nЗагрузка 2FA...",
reply_markup=back_keyboard(f"bot_{bot_name}"), reply_markup=back_keyboard(f"bot_{bot_name}"),
) )
@@ -58,7 +58,7 @@ async def twofa_handler(callback: CallbackQuery) -> None:
) )
async def confirm_trades(callback: CallbackQuery) -> None: async def confirm_trades(callback: CallbackQuery) -> None:
await callback.answer() await callback.answer()
bot_name = callback.data.replace("confirm_", "") # type: ignore[union-attr] bot_name = callback.data.replace("confirm_", "")
logger.info( logger.info(
"Пользователь %s подтвердил сделки для бота %s", "Пользователь %s подтвердил сделки для бота %s",
describe_user(callback.from_user), describe_user(callback.from_user),
@@ -82,14 +82,14 @@ async def confirm_trades(callback: CallbackQuery) -> None:
"Ошибка при подтверждении сделок для бота %s", "Ошибка при подтверждении сделок для бота %s",
bot_name, bot_name,
) )
await callback.message.edit_text(f"Ошибка: {error}") # type: ignore[union-attr] await callback.message.edit_text(f"Ошибка: {error}")
@router.callback_query( @router.callback_query(
lambda c: c.data and c.data.startswith("confirm_list_"), AdminCallbackFilter() lambda c: c.data and c.data.startswith("confirm_list_"), AdminCallbackFilter()
) )
async def confirm_list(callback: CallbackQuery) -> None: async def confirm_list(callback: CallbackQuery) -> None:
await stop_twofa_task(callback.message.message_id) # type: ignore[union-attr] await stop_twofa_task(callback.message.message_id)
await callback.answer() await callback.answer()
if not callback.data: if not callback.data:
logger.debug( logger.debug(
@@ -117,20 +117,23 @@ async def confirm_list(callback: CallbackQuery) -> None:
text = f"🔐 {bot_name}\n\nПодтверждения:\n\n" text = f"🔐 {bot_name}\n\nПодтверждения:\n\n"
for conf in confirmations: for conf in confirmations:
text += f"- {conf['type_name']}\nID: <code>{conf['id']}</code>\n\n" text += f"- {conf['type_name']}\nID: <code>{conf['id']}</code>\n\n"
await callback.message.edit_text(text, reply_markup=confirmations_keyboard(bot_name)) # type: ignore[union-attr] await callback.message.edit_text(
text, reply_markup=confirmations_keyboard(bot_name)
)
except Exception as error: except Exception as error:
logger.exception( logger.exception(
"Ошибка при загрузке списка подтверждений для бота %s", "Ошибка при загрузке списка подтверждений для бота %s",
bot_name, bot_name,
) )
await callback.message.edit_text(f"Ошибка: {error}") # type: ignore[union-attr] await callback.message.edit_text(f"Ошибка: {error}")
@router.callback_query( @router.callback_query(
lambda c: c.data and c.data.startswith("confirm_all_"), AdminCallbackFilter() lambda c: c.data and c.data.startswith("confirm_all_"), AdminCallbackFilter()
) )
async def confirm_all(callback: CallbackQuery) -> None: async def confirm_all(callback: CallbackQuery) -> None:
await stop_twofa_task(callback.message.message_id) # type: ignore[union-attr] await stop_twofa_task(callback.message.message_id)
await callback.answer() await callback.answer()
if not callback.data: if not callback.data:
logger.debug( logger.debug(
@@ -138,12 +141,14 @@ async def confirm_all(callback: CallbackQuery) -> None:
describe_user(callback.from_user), describe_user(callback.from_user),
) )
return return
bot_name = callback.data.replace("confirm_all_", "") bot_name = callback.data.replace("confirm_all_", "")
logger.info( logger.info(
"Пользователь %s запустил подтверждение всех сделок для бота %s", "Пользователь %s запустил подтверждение всех сделок для бота %s",
describe_user(callback.from_user), describe_user(callback.from_user),
bot_name, bot_name,
) )
try: try:
status = await accept_confirmations(bot_name) status = await accept_confirmations(bot_name)
if status != 200: if status != 200:
@@ -154,6 +159,7 @@ async def confirm_all(callback: CallbackQuery) -> None:
) )
await callback.answer("Ошибка HTTP", show_alert=True) await callback.answer("Ошибка HTTP", show_alert=True)
return return
confirmations = await get_confirmations(bot_name) confirmations = await get_confirmations(bot_name)
if not confirmations: if not confirmations:
logger.info("После подтверждения у бота %s не осталось сделок", bot_name) logger.info("После подтверждения у бота %s не осталось сделок", bot_name)
@@ -167,11 +173,12 @@ async def confirm_all(callback: CallbackQuery) -> None:
text = f"🔐 {bot_name}\n\nПодтверждения:\n\n" text = f"🔐 {bot_name}\n\nПодтверждения:\n\n"
for conf in confirmations: for conf in confirmations:
text += f"- {conf['type_name']}\nID: <code>{conf['id']}</code>\n\n" text += f"- {conf['type_name']}\nID: <code>{conf['id']}</code>\n\n"
await callback.message.edit_text(text, reply_markup=confirmations_keyboard(bot_name)) # type: ignore[union-attr]
await callback.answer("Все подтверждено", show_alert=True) await callback.message.edit_text(
except Exception as error: text, reply_markup=confirmations_keyboard(bot_name)
logger.exception(
"Ошибка при подтверждении всех сделок для бота %s",
bot_name,
) )
await callback.message.edit_text(f"Ошибка: {error}") # type: ignore[union-attr] await callback.answer("Все подтверждено", show_alert=True)
except Exception as error:
logger.exception("Ошибка при подтверждении всех сделок для бота %s", bot_name)
await callback.message.edit_text(f"Ошибка: {error}")
-6
View File
@@ -12,9 +12,3 @@ def describe_user(user: Any) -> str:
user_id = getattr(user, "id", user) user_id = getattr(user, "id", user)
return f"пользователь={user_id}" return f"пользователь={user_id}"
def mask_key(key: str) -> str:
if len(key) < 9:
return "***"
return f"{key[:5]}-*****-*****"
+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 from bot.logging_utils import get_logger
logger = get_logger(__name__) 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: 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: def is_idle_enabled(bot_name: str, app_id: int) -> bool:
logger.info("Проверка статуса idle-игры: bot=%s app_id=%s", bot_name, app_id) logger.info("Проверка статуса idle-игры: bot=%s app_id=%s", bot_name, app_id)
response = get_bot(bot_name) response = get_bot(bot_name)
if response.status_code != 200: if response.status_code != 200:
logger.warning( logger.warning(
"Не удалось получить конфиг бота для проверки idle: bot=%s status=%s", "Не удалось получить конфиг бота для проверки 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: async def reload_bot(bot_name: str) -> None:
_reloading_bots.add(bot_name)
try: try:
logger.info("Отправка команды перезагрузки бота: bot=%s", bot_name) logger.info("Отправка команды перезагрузки бота: bot=%s", bot_name)
send_command(f"!reload {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: except Exception:
logger.exception( logger.exception(
"Не удалось отправить команду перезагрузки бота: bot=%s", bot_name "Не удалось отправить команду перезагрузки бота: 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]: 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) logger.info("Изменение настройки idle-игры: bot=%s app_id=%s", bot_name, app_id)
response = get_bot(bot_name) response = get_bot(bot_name)
if response.status_code != 200: if response.status_code != 200:
logger.warning( logger.warning(
@@ -81,6 +113,7 @@ async def update_idle_game(bot_name: str, app_id: int) -> tuple[bool, str, bool]
bot_name, bot_name,
response.status_code, response.status_code,
) )
return False, "Ошибка API", False return False, "Ошибка API", False
data = response.json() 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 return False, "Ошибка сохранения", enabled
_reloading_bots.add(bot_name)
asyncio.create_task(reload_bot(bot_name)) asyncio.create_task(reload_bot(bot_name))
action = "Idle включен" if enabled else "Idle выключен" action = "Накрутка часов включена" if enabled else "Накрутка часов выключена"
logger.info( logger.info(
"Настройка idle-игры изменена: bot=%s app_id=%s enabled=%s", "Настройка idle-игры изменена: bot=%s app_id=%s enabled=%s",
bot_name, bot_name,
+12 -5
View File
@@ -14,11 +14,11 @@ async def build_inventory_menu(bot_name: str) -> tuple[str, object]:
cs2_assets = [] cs2_assets = []
steam_assets = [] steam_assets = []
try: 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: except Exception:
logger.exception("Ошибка чтения CS2-инвентаря: bot=%s", bot_name) logger.exception("Ошибка чтения CS2-инвентаря: bot=%s", bot_name)
try: 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: except Exception:
logger.exception("Ошибка чтения Steam-инвентаря: bot=%s", bot_name) logger.exception("Ошибка чтения Steam-инвентаря: bot=%s", bot_name)
if not cs2_assets and not steam_assets: if not cs2_assets and not steam_assets:
@@ -67,7 +67,9 @@ async def render_inventory_page(
assets = bot_inventory.get("Assets", []) assets = bot_inventory.get("Assets", [])
descriptions = bot_inventory.get("Descriptions", []) descriptions = bot_inventory.get("Descriptions", [])
if not assets: 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) return f"Инвентарь {bot_name} пуст", inventory_menu_keyboard(bot_name, 0, 0)
desc_map = {} desc_map = {}
for desc in descriptions: for desc in descriptions:
@@ -87,7 +89,7 @@ async def render_inventory_page(
for asset in page_assets: for asset in page_assets:
key = (str(asset.get("classid")), str(asset.get("instanceid"))) key = (str(asset.get("classid")), str(asset.get("instanceid")))
desc = desc_map.get(key, {}) desc = desc_map.get(key, {})
name = desc.get("market_name", "Unknown") name = desc.get("market_name", "Неизвестно")
amount = asset.get("amount", 1) amount = asset.get("amount", 1)
tradable = "🔄" if desc.get("tradable") else "" tradable = "🔄" if desc.get("tradable") else ""
marketable = "💰" if desc.get("marketable") else "🔒" marketable = "💰" if desc.get("marketable") else "🔒"
@@ -105,12 +107,14 @@ async def render_inventory_page(
elif icon == "💎": elif icon == "💎":
gems_count += amount gems_count += amount
lines.append(f"{icon} {tradable}{marketable} {name} x{amount}") lines.append(f"{icon} {tradable}{marketable} {name} x{amount}")
inv_name = "CS2" if appid == 730 else "Steam" inv_name = "CS2" if appid == 730 else "Steam"
stats = ( stats = (
f"📦 Предметов: {len(assets)}\n" f"📦 Предметов: {len(assets)}\n"
f"💰 Можно продать: {marketable_count}\n" f"💰 Можно продать: {marketable_count}\n"
f"🔄 Можно трейдить: {tradable_count}" f"🔄 Можно трейдить: {tradable_count}"
) )
if appid == 753: if appid == 753:
stats += ( stats += (
f"\n🎏 Карточек: {cards_count}" f"\n🎏 Карточек: {cards_count}"
@@ -118,11 +122,13 @@ async def render_inventory_page(
f"\n🖼 Фонов: {backgrounds_count}" f"\n🖼 Фонов: {backgrounds_count}"
f"\n💎 Самоцветов: {gems_count}" f"\n💎 Самоцветов: {gems_count}"
) )
text = ( text = (
f"📦 {inv_name} Inventory {bot_name}\n" f"📦 Инвентарь {inv_name} {bot_name}\n"
f"📄 Страница {page + 1}/{total_pages}\n\n" f"📄 Страница {page + 1}/{total_pages}\n\n"
f"{stats}\n\n" + "\n".join(lines) f"{stats}\n\n" + "\n".join(lines)
) )
logger.info( logger.info(
"Страница инвентаря сформирована: bot=%s type=%s page=%s total_pages=%s items_on_page=%s", "Страница инвентаря сформирована: bot=%s type=%s page=%s total_pages=%s items_on_page=%s",
bot_name, bot_name,
@@ -131,6 +137,7 @@ async def render_inventory_page(
total_pages, total_pages,
len(page_assets), len(page_assets),
) )
return text[:4000], inventory_page_keyboard( return text[:4000], inventory_page_keyboard(
inventory_type, bot_name, page, total_pages inventory_type, bot_name, page, total_pages
) )
+13 -13
View File
@@ -1,8 +1,8 @@
import asyncio import asyncio
import re import re
from bot.logging_utils import get_logger
from bot.api.asf import get_all_bots, redeem_key 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}" KEY_PATTERN = r"[A-Z0-9]{5}(?:-[A-Z0-9]{5}){2}"
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -52,7 +52,7 @@ async def redeem_single_bot_keys(bot_name: str, keys: list[str]) -> str:
logger.info( logger.info(
"Попытка активации ключа для одного бота: bot=%s key=%s", "Попытка активации ключа для одного бота: bot=%s key=%s",
bot_name, bot_name,
mask_key(key), key,
) )
success, result = await redeem_key(bot_name, key) success, result = await redeem_key(bot_name, key)
await asyncio.sleep(2) await asyncio.sleep(2)
@@ -62,16 +62,16 @@ async def redeem_single_bot_keys(bot_name: str, keys: list[str]) -> str:
logger.warning( logger.warning(
"Активация ключа завершилась ошибкой ASF/HTTP: bot=%s key=%s", "Активация ключа завершилась ошибкой ASF/HTTP: bot=%s key=%s",
bot_name, bot_name,
mask_key(key), key,
) )
results.append(f"{key}: ASF ERROR") results.append(f"{key}: ошибка ASF")
continue continue
status = parse_redeem_result(result) status = parse_redeem_result(result)
logger.info( logger.info(
"Получен результат активации ключа: bot=%s key=%s status=%s", "Получен результат активации ключа: bot=%s key=%s status=%s",
bot_name, bot_name,
mask_key(key), key,
status, status,
) )
@@ -80,7 +80,7 @@ async def redeem_single_bot_keys(bot_name: str, keys: list[str]) -> str:
results.append(f"{key}: активировано") results.append(f"{key}: активировано")
elif status == "rate_limited": elif status == "rate_limited":
failed_count += 1 failed_count += 1
results.append(f"{key}: rate limit") results.append(f"{key}: лимит запросов")
elif status == "already_owned": elif status == "already_owned":
failed_count += 1 failed_count += 1
results.append(f"⚠️ {key}: игра уже есть") results.append(f"⚠️ {key}: игра уже есть")
@@ -124,13 +124,13 @@ async def redeem_all_bots_keys(keys: list[str]) -> str:
for key in keys: for key in keys:
activated = False activated = False
key_report = [] key_report = []
logger.info("Начата обработка ключа по всем ботам: key=%s", mask_key(key)) logger.info("Начата обработка ключа по всем ботам: key=%s", key)
for bot_name in bots: for bot_name in bots:
logger.info( logger.info(
"Попытка активации ключа на боте: bot=%s key=%s", "Попытка активации ключа на боте: bot=%s key=%s",
bot_name, bot_name,
mask_key(key), key,
) )
success, result = await redeem_key(bot_name, key) success, result = await redeem_key(bot_name, key)
await asyncio.sleep(2) await asyncio.sleep(2)
@@ -139,16 +139,16 @@ async def redeem_all_bots_keys(keys: list[str]) -> str:
logger.warning( logger.warning(
"Активация ключа завершилась ошибкой ASF/HTTP: bot=%s key=%s", "Активация ключа завершилась ошибкой ASF/HTTP: bot=%s key=%s",
bot_name, bot_name,
mask_key(key), key,
) )
key_report.append(f"{bot_name}: ASF ERROR") key_report.append(f"{bot_name}: ошибка ASF")
continue continue
status = parse_redeem_result(result, bot_name) status = parse_redeem_result(result, bot_name)
logger.info( logger.info(
"Получен результат активации ключа: bot=%s key=%s status=%s", "Получен результат активации ключа: bot=%s key=%s status=%s",
bot_name, bot_name,
mask_key(key), key,
status, status,
) )
@@ -158,7 +158,7 @@ async def redeem_all_bots_keys(keys: list[str]) -> str:
activated = True activated = True
break break
if status == "rate_limited": if status == "rate_limited":
key_report.append(f"{bot_name}: rate limit") key_report.append(f"{bot_name}: лимит запросов")
continue continue
if status == "already_owned": if status == "already_owned":
key_report.append(f"⚠️ {bot_name}: игра уже есть") key_report.append(f"⚠️ {bot_name}: игра уже есть")
@@ -178,7 +178,7 @@ async def redeem_all_bots_keys(keys: list[str]) -> str:
failed_count += 1 failed_count += 1
logger.warning( logger.warning(
"Ключ не удалось активировать ни на одном боте: key=%s", "Ключ не удалось активировать ни на одном боте: key=%s",
mask_key(key), key,
) )
results.append(f"\n🔑 {key}\n" + "\n".join(key_report)) results.append(f"\n🔑 {key}\n" + "\n".join(key_report))
+12 -2
View File
@@ -1,22 +1,25 @@
import asyncio 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.logging_utils import get_logger
from bot.state import twofa_tasks
from bot.ui.keyboards import twofa_keyboard from bot.ui.keyboards import twofa_keyboard
from bot.api.asf import get_2fa_token, get_confirmations
logger = get_logger(__name__) logger = get_logger(__name__)
async def stop_twofa_task(message_id: int) -> None: async def stop_twofa_task(message_id: int) -> None:
task = twofa_tasks.pop(message_id, None) task = twofa_tasks.pop(message_id, None)
if task: if task:
logger.info( logger.info(
"Остановка фоновой задачи обновления 2FA: message_id=%s", message_id "Остановка фоновой задачи обновления 2FA: message_id=%s", message_id
) )
task.cancel() task.cancel()
try: try:
await task await task
except Exception: except Exception:
logger.exception( logger.exception(
"Ошибка при завершении фоновой задачи обновления 2FA: message_id=%s", "Ошибка при завершении фоновой задачи обновления 2FA: message_id=%s",
@@ -32,6 +35,7 @@ async def auto_update_2fa(message, bot_name: str) -> None:
bot_name, bot_name,
message_id, message_id,
) )
try: try:
while True: while True:
code = await get_2fa_token(bot_name) code = await get_2fa_token(bot_name)
@@ -43,6 +47,7 @@ async def auto_update_2fa(message, bot_name: str) -> None:
text, text,
reply_markup=twofa_keyboard(bot_name, bool(confirmations)), reply_markup=twofa_keyboard(bot_name, bool(confirmations)),
) )
last_code = code last_code = code
logger.info( logger.info(
"Сообщение 2FA обновлено: bot=%s message_id=%s confirmations=%s", "Сообщение 2FA обновлено: bot=%s message_id=%s confirmations=%s",
@@ -50,6 +55,7 @@ async def auto_update_2fa(message, bot_name: str) -> None:
message_id, message_id,
len(confirmations), len(confirmations),
) )
except Exception: except Exception:
logger.exception( logger.exception(
"Не удалось обновить сообщение 2FA: bot=%s message_id=%s", "Не удалось обновить сообщение 2FA: bot=%s message_id=%s",
@@ -57,13 +63,16 @@ async def auto_update_2fa(message, bot_name: str) -> None:
message_id, message_id,
) )
break break
await asyncio.sleep(15) await asyncio.sleep(15)
except asyncio.CancelledError: except asyncio.CancelledError:
logger.info( logger.info(
"Фоновое обновление 2FA отменено: bot=%s message_id=%s", "Фоновое обновление 2FA отменено: bot=%s message_id=%s",
bot_name, bot_name,
message_id, message_id,
) )
except Exception as error: except Exception as error:
logger.exception( logger.exception(
"Фоновое обновление 2FA завершилось ошибкой: bot=%s message_id=%s error=%s", "Фоновое обновление 2FA завершилось ошибкой: bot=%s message_id=%s error=%s",
@@ -71,6 +80,7 @@ async def auto_update_2fa(message, bot_name: str) -> None:
message_id, message_id,
error, error,
) )
finally: finally:
twofa_tasks.pop(message_id, None) twofa_tasks.pop(message_id, None)
logger.info( 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
-7
View File
@@ -1,7 +0,0 @@
console_users = set()
redeem_users = set()
twofa_tasks = {}
inventory_cache = {}
redeem_messages = {}
bot_redeem_users = {}
bot_redeem_messages = {}
+13
View File
@@ -0,0 +1,13 @@
from aiogram.fsm.state import State, StatesGroup
class ConsoleFlow(StatesGroup):
waiting_command = State()
class RedeemFlow(StatesGroup):
waiting_all_keys = State()
waiting_bot_keys = State()
twofa_tasks = {}
+27 -14
View File
@@ -24,46 +24,58 @@ def get_currency_name(currency_id: int) -> str:
def get_inventory_icon(desc: dict) -> str: def get_inventory_icon(desc: dict) -> str:
tags = desc.get("tags", []) tags = desc.get("tags", [])
for tag in tags: for tag in tags:
category = tag.get("category", "") category = tag.get("category", "")
name = tag.get("localized_tag_name", "") name = tag.get("localized_tag_name", "")
if category == "item_class": if category == "item_class":
if "Trading Card" in name: if "Trading Card" in name:
return "🎏" return "🎏"
if "Emoticon" in name: elif "Emoticon" in name:
return "😀" return "😀"
if "Profile Background" in name: elif "Profile Background" in name:
return "🖼" return "🖼"
if "Booster Pack" in name: elif "Booster Pack" in name:
return "📦" return "📦"
if "Steam Gems" in name: elif "Steam Gems" in name:
return "💎" return "💎"
if "Gift" in name: if "Gift" in name:
return "🎁" return "🎁"
item_type = desc.get("type", "").lower() item_type = desc.get("type", "").lower()
if "knife" in item_type: if "knife" in item_type:
return "🔪" return "🔪"
if "pistol" in item_type: elif "pistol" in item_type:
return "🔫" return "🔫"
if "rifle" in item_type: elif "rifle" in item_type:
return "🎯" return "🎯"
if "graffiti" in item_type: elif "graffiti" in item_type:
return "🎨" return "🎨"
if "music kit" in item_type: elif "music kit" in item_type:
return "🎵" return "🎵"
return "📦" return "📦"
def get_uptime(start_time_str: str) -> str: def get_uptime(start_time_str: str) -> str:
start_time_str = start_time_str[:26] + "Z" start_time_str = start_time_str[:26] + "Z"
start_time = datetime.fromisoformat(start_time_str.replace("Z", "+00:00")) start_time = datetime.fromisoformat(start_time_str.replace("Z", "+00:00"))
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
delta = now - start_time delta = now - start_time
days = delta.days days = delta.days
hours, remainder = divmod(delta.seconds, 3600) hours, remainder = divmod(delta.seconds, 3600)
minutes, seconds = divmod(remainder, 60) minutes, seconds = divmod(remainder, 60)
return f"{days}д {hours}ч {minutes}м {seconds}с"
days_measure = f"{days}д" if days > 0 else ""
hours_measure = f" {hours}ч" if hours > 0 else ""
minutes_measure = f" {minutes}м" if minutes > 0 else ""
seconds_measure = f" {seconds}с" if seconds > 0 else ""
return f"{days_measure}{hours_measure}{minutes_measure}{seconds_measure}"
def format_asf(data: dict) -> str: def format_asf(data: dict) -> str:
@@ -72,9 +84,9 @@ def format_asf(data: dict) -> str:
current_version = result.get("Version") current_version = result.get("Version")
latest_version = result.get("LatestVersion", current_version) latest_version = result.get("LatestVersion", current_version)
lines = [f"Версия ASF: `{current_version}`"] lines = [f"Версия ASF: {current_version}"]
if current_version != latest_version: if current_version != latest_version:
lines.append(f"Доступно обновление: `{latest_version}`") lines.append(f"Доступно обновление: {latest_version}")
memory_kb = result.get("MemoryUsage", 0) memory_kb = result.get("MemoryUsage", 0)
memory_mb = memory_kb / 1024 memory_mb = memory_kb / 1024
@@ -94,13 +106,10 @@ def get_bot_status_icon(bot: dict) -> str:
if not loaded: if not loaded:
return "🔄" return "🔄"
if farming: if farming:
return "🎴" return "🎴"
if idle_games and online: if idle_games and online:
return "" return ""
if online: if online:
return "🟢" return "🟢"
@@ -179,11 +188,14 @@ def get_asf_status_text() -> str:
def get_farm_summary(bots: dict) -> str: def get_farm_summary(bots: dict) -> str:
total_games = 0 total_games = 0
total_time_seconds = 0 total_time_seconds = 0
for bot in bots.values(): for bot in bots.values():
farmer = bot.get("CardsFarmer", {}) farmer = bot.get("CardsFarmer", {})
if farmer.get("NowFarming"): if farmer.get("NowFarming"):
games = farmer.get("CurrentGamesFarming", []) games = farmer.get("CurrentGamesFarming", [])
total_games += len(games) total_games += len(games)
time_str = farmer.get("TimeRemaining", "00:00:00") time_str = farmer.get("TimeRemaining", "00:00:00")
hours, minutes, seconds = map(int, time_str.split(":")) hours, minutes, seconds = map(int, time_str.split(":"))
total_time_seconds += hours * 3600 + minutes * 60 + seconds total_time_seconds += hours * 3600 + minutes * 60 + seconds
@@ -193,6 +205,7 @@ def get_farm_summary(bots: dict) -> str:
hours = total_time_seconds // 3600 hours = total_time_seconds // 3600
minutes = (total_time_seconds % 3600) // 60 minutes = (total_time_seconds % 3600) // 60
return ( return (
f"Игр фармится: {total_games}\n" f"Игр фармится: {total_games}\n"
f"Осталось времени: {hours}ч {minutes}м\n" f"Осталось времени: {hours}ч {minutes}м\n"
+1
View File
@@ -36,6 +36,7 @@ def back_keyboard(callback_data: str = "back") -> InlineKeyboardMarkup:
def games_keyboard(bot_name: str, is_enabled: bool) -> InlineKeyboardMarkup: def games_keyboard(bot_name: str, is_enabled: bool) -> InlineKeyboardMarkup:
text = "Остановить накрутку CS2" if is_enabled else "⌚ Накрутить часы CS2" text = "Остановить накрутку CS2" if is_enabled else "⌚ Накрутить часы CS2"
return InlineKeyboardMarkup( return InlineKeyboardMarkup(
inline_keyboard=[ inline_keyboard=[
[InlineKeyboardButton(text=text, callback_data=f"farm|{bot_name}|730")], [InlineKeyboardButton(text=text, callback_data=f"farm|{bot_name}|730")],
+7 -1
View File
@@ -2,9 +2,11 @@ import asyncio
import logging import logging
from aiogram import Bot, Dispatcher from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.client.default import DefaultBotProperties from aiogram.client.default import DefaultBotProperties
from bot.config import BOT_TOKEN from bot.config import BOT_TOKEN
from bot.db.init import init_db
from bot.handlers.bots import router as bots_router from bot.handlers.bots import router as bots_router
from bot.handlers.console import router as console_router from bot.handlers.console import router as console_router
@@ -18,7 +20,7 @@ from bot.handlers.twofa import router as twofa_router
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
bot = Bot(token=BOT_TOKEN, default=DefaultBotProperties(parse_mode="HTML")) # type: ignore[arg-type] bot = Bot(token=BOT_TOKEN, default=DefaultBotProperties(parse_mode="HTML")) # type: ignore[arg-type]
dp = Dispatcher() dp = Dispatcher(storage=MemoryStorage())
dp.include_router(start_router) dp.include_router(start_router)
dp.include_router(navigation_router) dp.include_router(navigation_router)
@@ -31,6 +33,7 @@ dp.include_router(messages_router)
async def main() -> None: async def main() -> None:
await init_db()
await dp.start_polling(bot) await dp.start_polling(bot)
@@ -43,10 +46,13 @@ if __name__ == "__main__":
logging.StreamHandler(), logging.StreamHandler(),
], ],
) )
try: try:
asyncio.run(main()) asyncio.run(main())
except KeyboardInterrupt: except KeyboardInterrupt:
logger.info("Получен сигнал остановки, бот завершает работу") logger.info("Получен сигнал остановки, бот завершает работу")
except Exception: except Exception:
logger.exception("Бот завершился из-за необработанной ошибки") logger.exception("Бот завершился из-за необработанной ошибки")
raise raise
BIN
View File
Binary file not shown.