diff --git a/bot/api/asf.py b/bot/api/asf.py
index cc42472..76cb983 100644
--- a/bot/api/asf.py
+++ b/bot/api/asf.py
@@ -14,11 +14,11 @@ def _headers() -> dict[str, str | None]:
def asf_get(path: str) -> requests.Response:
logger.info("Отправка GET-запроса в ASF: path=%s", path)
response = requests.get(f"{ASF_URL}{path}", headers=_headers())
+
logger.info(
- "Получен ответ ASF на GET-запрос: path=%s status=%s",
- path,
- response.status_code,
+ "Получен ответ ASF на GET-запрос: path=%s status=%s", path, response.status_code
)
+
return response
@@ -28,12 +28,14 @@ def asf_post(path: str, payload: dict | None = None) -> requests.Response:
path,
sorted((payload or {}).keys()),
)
+
response = requests.post(f"{ASF_URL}{path}", headers=_headers(), json=payload)
logger.info(
"Получен ответ ASF на POST-запрос: path=%s status=%s",
path,
response.status_code,
)
+
return response
@@ -41,6 +43,7 @@ def get_asf_status() -> requests.Response:
logger.info("Запрос статуса ASF")
response = requests.get(f"{ASF_URL}/Api/ASF", headers=_headers()) # type: ignore[arg-type]
logger.info("Получен статус ASF: status=%s", response.status_code)
+
return response
@@ -71,6 +74,7 @@ async def get_bot_inventory(bot_name: str, appid: int, contextid: int) -> dict |
appid,
contextid,
)
+
async with aiohttp.ClientSession() as session:
async with session.get(
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,
)
return None
+
data = await response.json()
if not data.get("Success"):
logger.warning(
@@ -94,17 +99,20 @@ async def get_bot_inventory(bot_name: str, appid: int, contextid: int) -> dict |
contextid,
)
return None
+
logger.info(
"Инвентарь бота получен: bot=%s appid=%s contextid=%s",
bot_name,
appid,
contextid,
)
+
return data.get("Result")
async def get_2fa_token(bot_name: str) -> str:
logger.info("Запрос 2FA-кода: bot=%s", bot_name)
+
async with aiohttp.ClientSession() as session:
async with session.get(
f"{ASF_URL}/Api/Bot/{bot_name}/TwoFactorAuthentication/Token",
@@ -117,24 +125,29 @@ async def get_2fa_token(bot_name: str) -> str:
response.status,
)
return f"Ошибка HTTP {response.status}"
+
data = await response.json()
if not data.get("Success"):
logger.warning(
"ASF вернул ошибку при запросе 2FA-кода: bot=%s", bot_name
)
return "Ошибка ASF"
+
try:
logger.info("2FA-код успешно получен: bot=%s", bot_name)
return data["Result"][bot_name]["Result"]
+
except Exception:
logger.exception(
"Не удалось извлечь 2FA-код из ответа ASF: bot=%s", bot_name
)
+
return "Не удалось получить код"
async def get_confirmations(bot_name: str) -> list:
logger.info("Запрос подтверждений 2FA: bot=%s", bot_name)
+
async with aiohttp.ClientSession() as session:
async with session.get(
f"{ASF_URL}/Api/Bot/{bot_name}/TwoFactorAuthentication/Confirmations",
@@ -147,6 +160,7 @@ async def get_confirmations(bot_name: str) -> list:
response.status,
)
return []
+
data = await response.json()
if not data.get("Success"):
logger.warning(
@@ -154,6 +168,7 @@ async def get_confirmations(bot_name: str) -> list:
bot_name,
)
return []
+
try:
result = data["Result"][bot_name]["Result"]
logger.info(
@@ -162,6 +177,7 @@ async def get_confirmations(bot_name: str) -> list:
len(result),
)
return result
+
except Exception:
logger.exception(
"Не удалось извлечь подтверждения 2FA из ответа ASF: bot=%s",
@@ -172,6 +188,7 @@ async def get_confirmations(bot_name: str) -> list:
async def accept_confirmations(bot_name: str) -> int:
logger.info("Отправка запроса на подтверждение всех 2FA-операций: bot=%s", bot_name)
+
async with aiohttp.ClientSession() as session:
async with session.post(
f"{ASF_URL}/Api/Bot/{bot_name}/TwoFactorAuthentication/Confirmations/Accept",
@@ -182,6 +199,7 @@ async def accept_confirmations(bot_name: str) -> int:
bot_name,
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]:
logger.info("Запрос активации ключа: bot=%s", bot_name)
+
async with aiohttp.ClientSession() as session:
async with session.post(
f"{ASF_URL}/Api/Command",
@@ -205,11 +224,13 @@ async def redeem_key(bot_name: str, key: str) -> tuple[bool, str]:
response.status,
)
return False, f"HTTP_ERROR_{response.status}"
+
data = await response.json()
if not data.get("Success"):
logger.warning(
"ASF вернул ошибку при активации ключа: bot=%s", bot_name
)
return False, "ASF_ERROR"
+
logger.info("ASF вернул результат активации ключа: bot=%s", bot_name)
return True, str(data.get("Result", ""))
diff --git a/bot/config.py b/bot/config.py
index eabab43..330c78c 100644
--- a/bot/config.py
+++ b/bot/config.py
@@ -9,3 +9,5 @@ ADMIN_ID = int(os.getenv("ADMIN_ID")) # type: ignore[arg-type]
ASF_URL = os.getenv("ASF_URL")
ASF_PASSWORD = os.getenv("ASF_PASSWORD")
+
+DATABASE_URL = os.getenv("DATABASE_URL", "sqlite+aiosqlite:///bot.db")
diff --git a/bot/db/__init__.py b/bot/db/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/bot/db/__init__.py
@@ -0,0 +1 @@
+
diff --git a/bot/db/base.py b/bot/db/base.py
new file mode 100644
index 0000000..fa2b68a
--- /dev/null
+++ b/bot/db/base.py
@@ -0,0 +1,5 @@
+from sqlalchemy.orm import DeclarativeBase
+
+
+class Base(DeclarativeBase):
+ pass
diff --git a/bot/db/init.py b/bot/db/init.py
new file mode 100644
index 0000000..e7a18a3
--- /dev/null
+++ b/bot/db/init.py
@@ -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)
diff --git a/bot/db/models.py b/bot/db/models.py
new file mode 100644
index 0000000..a39af60
--- /dev/null
+++ b/bot/db/models.py
@@ -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,
+ )
diff --git a/bot/db/session.py b/bot/db/session.py
new file mode 100644
index 0000000..31f440b
--- /dev/null
+++ b/bot/db/session.py
@@ -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)
diff --git a/bot/filters.py b/bot/filters.py
index 15bf796..fd26f8f 100644
--- a/bot/filters.py
+++ b/bot/filters.py
@@ -9,22 +9,26 @@ logger = get_logger(__name__)
class AdminFilter(BaseFilter):
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:
logger.warning(
"Отклонено сообщение от неавторизованного пользователя: user_id=%s",
message.from_user.id if message.from_user else None,
)
+
return allowed
class AdminCallbackFilter(BaseFilter):
async def __call__(self, callback: CallbackQuery) -> bool:
allowed = callback.from_user.id == ADMIN_ID
+
if not allowed:
logger.warning(
"Отклонен callback от неавторизованного пользователя: user_id=%s data=%s",
callback.from_user.id,
callback.data,
)
+
return allowed
diff --git a/bot/handlers/bots.py b/bot/handlers/bots.py
index 63b0a55..6411fa1 100644
--- a/bot/handlers/bots.py
+++ b/bot/handlers/bots.py
@@ -1,10 +1,13 @@
+import random
+import asyncio
+
from aiogram import Router
from aiogram.types import CallbackQuery
from bot.api.asf import get_all_bots
from bot.filters import AdminCallbackFilter
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.ui.formatters import format_bot_ui, get_farm_summary
from bot.ui.keyboards import bot_details_keyboard, bots_keyboard, games_keyboard
@@ -31,7 +34,8 @@ async def bots_handler(callback: CallbackQuery) -> None:
"Пользователь %s открыл список ботов",
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()
try:
response = get_all_bots()
@@ -41,16 +45,18 @@ async def bots_handler(callback: CallbackQuery) -> None:
describe_user(callback.from_user),
response.status_code,
)
- await callback.message.edit_text("Ошибка API") # type: ignore[union-attr]
+ await callback.message.edit_text("Ошибка API")
return
+
data = response.json()
if not data.get("Success"):
logger.warning(
"ASF вернул неуспешный ответ при загрузке списка ботов для %s",
describe_user(callback.from_user),
)
- await callback.message.edit_text("ASF вернул ошибку") # type: ignore[union-attr]
+ await callback.message.edit_text("ASF вернул ошибку")
return
+
bots = data.get("Result", {})
logger.info(
"Список ботов для %s загружен: %s бот(ов)",
@@ -58,33 +64,50 @@ async def bots_handler(callback: CallbackQuery) -> None:
len(bots),
)
text = f"🤖 Ботов всего: {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:
logger.exception(
"Ошибка при открытии списка ботов для %s",
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(
lambda c: c.data and c.data.startswith("bot_"), AdminCallbackFilter()
)
async def bot_selected(callback: CallbackQuery) -> None:
- await stop_twofa_task(callback.message.message_id) # type: ignore[union-attr]
- await callback.answer()
+ await stop_twofa_task(callback.message.message_id)
+
if not callback.data:
logger.debug(
"Колбэк выбора бота пришел без data от %s",
describe_user(callback.from_user),
)
+ await callback.answer()
return
+
bot_name = callback.data.replace("bot_", "")
logger.info(
"Пользователь %s выбрал бота %s",
describe_user(callback.from_user),
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:
data = get_all_bots().json()
bots = data.get("Result", {})
@@ -105,20 +128,22 @@ async def bot_selected(callback: CallbackQuery) -> None:
bot_name,
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),
disable_web_page_preview=True,
reply_markup=bot_details_keyboard(
bot_name, bool(bot.get("HasMobileAuthenticator"))
),
)
+
except Exception as error:
logger.exception(
"Ошибка при открытии карточки бота %s для %s",
bot_name,
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(
@@ -132,6 +157,7 @@ async def games(callback: CallbackQuery) -> None:
describe_user(callback.from_user),
)
return
+
bot_name = callback.data.replace("games_", "")
idle_enabled = is_idle_enabled(bot_name, 730)
logger.info(
@@ -140,7 +166,8 @@ async def games(callback: CallbackQuery) -> None:
bot_name,
idle_enabled,
)
- await callback.message.edit_text( # type: ignore[union-attr]
+
+ await callback.message.edit_text(
"⌚ Выберите игру для накрутки часов",
reply_markup=games_keyboard(bot_name, idle_enabled),
)
@@ -157,6 +184,7 @@ async def farm_game(callback: CallbackQuery) -> None:
describe_user(callback.from_user),
)
return
+
_, bot_name, app_id_value = callback.data.split("|")
logger.info(
"Пользователь %s меняет игру для накрутки на боте %s: app_id=%s",
@@ -164,6 +192,7 @@ async def farm_game(callback: CallbackQuery) -> None:
bot_name,
app_id_value,
)
+
success, text, enabled = await update_idle_game(bot_name, int(app_id_value))
if not success:
logger.warning(
@@ -174,20 +203,22 @@ async def farm_game(callback: CallbackQuery) -> None:
)
await callback.answer(text, show_alert=True)
return
+
logger.info(
"Игра для накрутки на боте %s обновлена для %s. CS2=%s",
bot_name,
describe_user(callback.from_user),
enabled,
)
+
await callback.answer(text, show_alert=True)
try:
- await callback.message.edit_reply_markup( # type: ignore[union-attr]
- reply_markup=games_keyboard(bot_name, enabled)
+ await callback.message.edit_text(
+ text=f"⌚ Выберите игру для накрутки часов",
+ reply_markup=games_keyboard(bot_name, enabled),
)
+
except Exception:
logger.debug(
- "Не удалось обновить клавиатуру игр для бота %s",
- bot_name,
- exc_info=True,
+ "Не удалось обновить клавиатуру игр для бота %s", bot_name, exc_info=True
)
diff --git a/bot/handlers/console.py b/bot/handlers/console.py
index b897ebf..fbeb465 100644
--- a/bot/handlers/console.py
+++ b/bot/handlers/console.py
@@ -1,10 +1,11 @@
from aiogram import Router
from aiogram.types import CallbackQuery
+from aiogram.fsm.context import FSMContext
from bot.filters import AdminCallbackFilter
-from bot.logging_utils import describe_user, get_logger
-from bot.state import console_users
+from bot.states import ConsoleFlow
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
router = Router()
@@ -12,21 +13,26 @@ logger = get_logger(__name__)
@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)
logger.info("Пользователь вошел в режим консоли ASF: %s", user)
+
+ await state.set_state(ConsoleFlow.waiting_command)
+
await callback.answer()
- console_users.add(callback.from_user.id)
- await callback.message.edit_text( # type: ignore[union-attr]
- "💻 Консоль ASF\n\nОтправь команду (без !)",
- reply_markup=console_keyboard(),
+ await callback.message.edit_text(
+ "💻 Консоль ASF\n\nОтправь команду (без !)", reply_markup=console_keyboard()
)
@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)
logger.info("Пользователь вышел из режима консоли ASF: %s", user)
+
+ await state.clear()
+
await callback.answer()
- console_users.discard(callback.from_user.id)
- 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()
+ )
diff --git a/bot/handlers/inventory.py b/bot/handlers/inventory.py
index 09ab9a4..2cf495e 100644
--- a/bot/handlers/inventory.py
+++ b/bot/handlers/inventory.py
@@ -15,15 +15,18 @@ logger = get_logger(__name__)
)
async def inventory_menu(callback: CallbackQuery) -> None:
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)
+
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)
if "пуст" in text.lower():
logger.info("Показано пустое меню инвентаря: %s bot=%s", user, 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(
@@ -31,16 +34,19 @@ async def inventory_menu(callback: CallbackQuery) -> None:
)
async def cs2_inventory(callback: CallbackQuery) -> None:
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)
+
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)
if text is None:
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
- 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(
@@ -48,16 +54,21 @@ async def cs2_inventory(callback: CallbackQuery) -> None:
)
async def steam_inventory(callback: CallbackQuery) -> None:
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)
+
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)
if text is None:
- logger.warning("Не удалось загрузить Steam-инвентарь: %s bot=%s", user, bot_name)
- await callback.message.edit_text("Не удалось загрузить inventory") # type: ignore[union-attr]
+ logger.warning(
+ "Не удалось загрузить Steam-инвентарь: %s bot=%s", user, bot_name
+ )
+ await callback.message.edit_text("Не удалось загрузить инвентарь")
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(
@@ -65,7 +76,7 @@ async def steam_inventory(callback: CallbackQuery) -> None:
)
async def inventory_page(callback: CallbackQuery) -> None:
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(
"Открыта страница инвентаря: %s bot=%s type=%s page=%s",
user,
@@ -86,6 +97,6 @@ async def inventory_page(callback: CallbackQuery) -> None:
inv_type,
page_value,
)
- await callback.message.edit_text("Не удалось загрузить inventory") # type: ignore[union-attr]
+ await callback.message.edit_text("Не удалось загрузить инвентарь")
return
- await callback.message.edit_text(text, reply_markup=keyboard) # type: ignore[union-attr]
+ await callback.message.edit_text(text, reply_markup=keyboard)
diff --git a/bot/handlers/messages.py b/bot/handlers/messages.py
index e0b7b5a..0013480 100644
--- a/bot/handlers/messages.py
+++ b/bot/handlers/messages.py
@@ -2,50 +2,57 @@ import html
from aiogram import Router
from aiogram.types import Message
+from aiogram.fsm.context import FSMContext
-from bot.api.asf import get_all_bots, send_command
from bot.filters import AdminFilter
+from bot.api.asf import get_all_bots, send_command
from bot.logging_utils import describe_user, get_logger
from bot.services.redeem import (
extract_keys,
redeem_all_bots_keys,
redeem_single_bot_keys,
)
-from bot.state import (
- bot_redeem_messages,
- bot_redeem_users,
- console_users,
- redeem_messages,
- redeem_users,
-)
+
+from bot.states import ConsoleFlow, RedeemFlow
+
from bot.ui.formatters import format_bot_ui, get_asf_status_text
from bot.ui.keyboards import (
+ main_keyboard,
bot_details_keyboard,
delete_message_keyboard,
- main_keyboard,
)
router = Router()
logger = get_logger(__name__)
-async def handle_console_message(message: Message) -> bool:
- if message.from_user.id not in console_users: # type: ignore[union-attr]
- return False
- command = message.text.strip() # type: ignore[union-attr]
+@router.message(ConsoleFlow.waiting_command, AdminFilter())
+async def handle_console_message(message: Message, state: FSMContext):
+ if not message.text:
+ await message.answer("Отправьте текстовую команду.")
+ return
+
+ command = message.text.strip()
+ if not command:
+ await message.answer("Команда не может быть пустой.")
+ return
+
logger.info(
"Пользователь %s отправил консольную команду: %s",
describe_user(message.from_user),
command,
)
+
try:
await message.delete()
+
except Exception:
logger.debug(
"Не удалось удалить сообщение с консольной командой от %s",
describe_user(message.from_user),
exc_info=True,
)
+
try:
response = send_command(f"!{command}")
if response.status_code != 200:
@@ -55,9 +62,12 @@ async def handle_console_message(message: Message) -> bool:
command,
describe_user(message.from_user),
)
+
text = f"Ошибка HTTP {response.status_code}"
+
else:
data = response.json()
+
if not data.get("Success"):
logger.warning(
"ASF вернул неуспешный ответ на консольную команду '%s' от %s",
@@ -65,6 +75,7 @@ async def handle_console_message(message: Message) -> bool:
describe_user(message.from_user),
)
text = "Ошибка ASF"
+
else:
logger.info(
"Консольная команда '%s' от %s выполнена успешно",
@@ -73,39 +84,55 @@ async def handle_console_message(message: Message) -> bool:
)
result = html.escape(str(data.get("Result", "Нет ответа")))
text = f"Команда: {command}\n\nОтвет:\n{result}"
+
await message.answer(text, reply_markup=delete_message_keyboard())
+
except Exception as error:
logger.exception(
"Ошибка при выполнении консольной команды '%s' от %s",
command,
describe_user(message.from_user),
)
+
await message.answer(f"Ошибка: {error}")
- return True
-async def handle_single_bot_redeem(message: Message) -> bool:
- if message.from_user.id not in bot_redeem_users: # type: ignore[union-attr]
- return False
- bot_name = bot_redeem_users.pop(message.from_user.id) # type: ignore[union-attr]
- menu_message = bot_redeem_messages.pop(message.from_user.id, None) # type: ignore[union-attr]
+@router.message(RedeemFlow.waiting_bot_keys, AdminFilter())
+async def handle_single_bot_redeem(message: Message, state: FSMContext):
+ state_data = await state.get_data()
+
+ bot_name = state_data.get("bot_name")
+ if not isinstance(bot_name, str) or not bot_name:
+ await message.answer("Контекст потерян, попробуйте еще раз.")
+ await state.clear()
+
+ return
+
+ chat_id = message.chat.id
+ menu_message_id = state_data.get("menu_message_id")
+
logger.info(
"Пользователь %s отправил ключи для бота %s",
describe_user(message.from_user),
bot_name,
)
- if menu_message:
+
+ if menu_message_id:
try:
response = get_all_bots()
bot_data = response.json().get("Result", {}).get(bot_name)
+
if bot_data:
- await menu_message.edit_text(
- format_bot_ui(bot_data),
- disable_web_page_preview=True,
+ await message.bot.edit_message_text(
+ text=format_bot_ui(bot_data),
reply_markup=bot_details_keyboard(
bot_name, bool(bot_data.get("HasMobileAuthenticator"))
),
+ disable_web_page_preview=True,
+ chat_id=chat_id,
+ message_id=menu_message_id,
)
+
except Exception:
logger.debug(
"Не удалось восстановить меню бота %s после ввода ключей от %s",
@@ -113,10 +140,12 @@ async def handle_single_bot_redeem(message: Message) -> bool:
describe_user(message.from_user),
exc_info=True,
)
+
try:
await message.bot.delete_message(
chat_id=message.chat.id, message_id=message.message_id
)
+
except Exception:
logger.debug(
"Не удалось удалить сообщение с ключами для бота %s от %s",
@@ -124,6 +153,7 @@ async def handle_single_bot_redeem(message: Message) -> bool:
describe_user(message.from_user),
exc_info=True,
)
+
keys = extract_keys(message.text or "")
if not keys:
logger.info(
@@ -132,7 +162,10 @@ async def handle_single_bot_redeem(message: Message) -> bool:
bot_name,
)
await message.answer("Ключи не найдены")
- return True
+ await state.clear()
+
+ return
+
logger.info(
"Для бота %s получено %s ключ(ей) от %s",
bot_name,
@@ -142,40 +175,66 @@ async def handle_single_bot_redeem(message: Message) -> bool:
checking = await message.answer(
f"Аккаунт: {bot_name}\nНайдено ключей: {len(keys)}\nНачинаю активацию..."
)
- await checking.edit_text(await redeem_single_bot_keys(bot_name, keys))
- return True
+
+ try:
+ await checking.edit_text(await redeem_single_bot_keys(bot_name, keys))
+
+ except Exception as error:
+ logger.exception(
+ "Не удалось активировать ключи для бота %s от %s",
+ bot_name,
+ describe_user(message.from_user),
+ )
+ try:
+ await checking.edit_text(f"Ошибка: {error}")
+
+ except Exception:
+ await message.answer(f"Ошибка: {error}")
+
+ finally:
+ await state.clear()
-async def handle_all_bots_redeem(message: Message) -> bool:
- if message.from_user.id not in redeem_users: # type: ignore[union-attr]
- return False
- redeem_users.discard(message.from_user.id) # type: ignore[union-attr]
- menu_message = redeem_messages.pop(message.from_user.id, None) # type: ignore[union-attr]
+@router.message(RedeemFlow.waiting_all_keys, AdminFilter())
+async def handle_all_bots_redeem(message: Message, state: FSMContext):
+ state_data = await state.get_data()
+
+ chat_id = message.chat.id
+ menu_message_id = state_data.get("menu_message_id")
+
logger.info(
"Пользователь %s отправил ключи для активации на всех ботах",
describe_user(message.from_user),
)
- if menu_message:
+
+ if menu_message_id:
try:
- await menu_message.edit_text(
- get_asf_status_text(), reply_markup=main_keyboard()
+ await message.bot.edit_message_text(
+ text=get_asf_status_text(),
+ reply_markup=main_keyboard(),
+ chat_id=chat_id,
+ message_id=menu_message_id,
)
+
except Exception:
logger.debug(
"Не удалось восстановить главное меню после массовой активации для %s",
describe_user(message.from_user),
exc_info=True,
)
+
try:
await message.bot.delete_message(
chat_id=message.chat.id, message_id=message.message_id
)
+
except Exception:
logger.debug(
"Не удалось удалить сообщение с массовой активацией от %s",
describe_user(message.from_user),
exc_info=True,
)
+
keys = extract_keys(message.text or "")
if not keys:
logger.info(
@@ -183,7 +242,10 @@ async def handle_all_bots_redeem(message: Message) -> bool:
describe_user(message.from_user),
)
await message.answer("Ключи не найдены")
- return True
+ await state.clear()
+
+ return
+
logger.info(
"Для массовой активации получено %s ключ(ей) от %s",
len(keys),
@@ -192,25 +254,15 @@ async def handle_all_bots_redeem(message: Message) -> bool:
checking = await message.answer(
f"Найдено ключей: {len(keys)}\nНачинаю активацию..."
)
+
try:
await checking.edit_text(await redeem_all_bots_keys(keys))
+
except Exception as error:
logger.exception(
"Ошибка при массовой активации ключей от %s",
describe_user(message.from_user),
)
await checking.edit_text(f"Ошибка: {error}")
- return True
-
-@router.message(AdminFilter())
-async def text_router(message: Message) -> None:
- logger.debug(
- "Получено текстовое сообщение от %s для маршрутизации",
- describe_user(message.from_user),
- )
- if await handle_console_message(message):
- return
- if await handle_single_bot_redeem(message):
- return
- await handle_all_bots_redeem(message)
+ await state.clear()
diff --git a/bot/handlers/navigation.py b/bot/handlers/navigation.py
index bc5fb63..d060be5 100644
--- a/bot/handlers/navigation.py
+++ b/bot/handlers/navigation.py
@@ -2,18 +2,15 @@ import asyncio
from aiogram import Router
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.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.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.keyboards import back_keyboard, confirm_action_keyboard, main_keyboard
@@ -21,28 +18,24 @@ router = Router()
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())
-async def back_button(callback: CallbackQuery) -> None:
+async def back_button(callback: CallbackQuery, state: FSMContext) -> None:
user = describe_user(callback.from_user.id)
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()
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)
+
except Exception as error:
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())
@@ -50,22 +43,28 @@ async def refresh_handler(callback: CallbackQuery) -> None:
user = describe_user(callback.from_user.id)
logger.info("Запрошено обновление главного экрана: %s", user)
await callback.answer()
+
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)
+
except Exception as error:
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())
async def delete_message(callback: CallbackQuery) -> None:
user = describe_user(callback.from_user.id)
logger.info("Запрошено удаление сообщения: %s", user)
+
await callback.answer()
try:
- await callback.message.delete() # type: ignore[union-attr]
+ await callback.message.delete()
logger.info("Сообщение удалено: %s", user)
+
except Exception:
logger.exception("Не удалось удалить сообщение: %s", user)
@@ -74,8 +73,9 @@ async def delete_message(callback: CallbackQuery) -> None:
async def restart_asf_confirm(callback: CallbackQuery) -> None:
user = describe_user(callback.from_user.id)
logger.info("Открыто подтверждение перезапуска ASF: %s", user)
+
await callback.answer()
- await callback.message.edit_text( # type: ignore[union-attr]
+ await callback.message.edit_text(
"♻ Точно перезапустить ASF?\n\nВо время перезапуска IPC будет недоступен несколько секунд.",
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:
user = describe_user(callback.from_user.id)
logger.info("Подтвержден перезапуск ASF: %s", user)
+
await callback.answer()
try:
- await callback.message.edit_text("♻ ASF перезапускается...") # type: ignore[union-attr]
+ await callback.message.edit_text("♻ ASF перезапускается...")
response = restart_asf_request()
if response.status_code != 200:
@@ -96,7 +97,7 @@ async def restart_asf(callback: CallbackQuery) -> None:
user,
response.status_code,
)
- await callback.message.edit_text( # type: ignore[union-attr]
+ await callback.message.edit_text(
f"Ошибка HTTP {response.status_code}",
reply_markup=main_keyboard(),
)
@@ -113,7 +114,7 @@ async def restart_asf(callback: CallbackQuery) -> None:
status = get_asf_status()
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()}",
reply_markup=main_keyboard(),
)
@@ -123,7 +124,7 @@ async def restart_asf(callback: CallbackQuery) -> None:
except Exception as error:
logger.exception("Не удалось перезапустить ASF: %s", user)
- await callback.message.edit_text( # type: ignore[union-attr]
+ await callback.message.edit_text(
f"Ошибка: {error}",
reply_markup=main_keyboard(),
)
@@ -133,7 +134,8 @@ async def restart_asf(callback: CallbackQuery) -> None:
async def plugins_handler(callback: CallbackQuery) -> None:
user = describe_user(callback.from_user.id)
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()
try:
response = get_plugins()
@@ -143,13 +145,15 @@ async def plugins_handler(callback: CallbackQuery) -> None:
user,
response.status_code,
)
- await callback.message.edit_text("Ошибка API") # type: ignore[union-attr]
+ await callback.message.edit_text("Ошибка API")
return
+
data = response.json()
if not data.get("Success"):
logger.warning("ASF вернул ошибку при получении плагинов: %s", user)
- await callback.message.edit_text("ASF вернул ошибку") # type: ignore[union-attr]
+ await callback.message.edit_text("ASF вернул ошибку")
return
+
plugins = data.get("Result", [])
if not plugins:
logger.info("Плагины ASF не установлены: %s", user)
@@ -161,7 +165,9 @@ async def plugins_handler(callback: CallbackQuery) -> None:
text += (
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:
logger.exception("Не удалось отобразить список плагинов: %s", user)
- await callback.message.edit_text(f"Ошибка: {error}") # type: ignore[union-attr]
+ await callback.message.edit_text(f"Ошибка: {error}")
diff --git a/bot/handlers/redeem.py b/bot/handlers/redeem.py
index 4592c81..0e0b3ba 100644
--- a/bot/handlers/redeem.py
+++ b/bot/handlers/redeem.py
@@ -1,14 +1,12 @@
from aiogram import Router
from aiogram.types import CallbackQuery
+from aiogram.fsm.context import FSMContext
from bot.filters import AdminCallbackFilter
from bot.logging_utils import describe_user, get_logger
-from bot.state import (
- bot_redeem_messages,
- bot_redeem_users,
- redeem_messages,
- redeem_users,
-)
+
+
+from bot.states import RedeemFlow
from bot.ui.keyboards import back_keyboard
router = Router()
@@ -16,33 +14,40 @@ logger = get_logger(__name__)
@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)
logger.info("Открыт режим активации ключей по всем ботам: %s", user)
+
await callback.answer()
- redeem_users.add(callback.from_user.id)
- redeem_messages[callback.from_user.id] = callback.message
- await callback.message.edit_text( # type: ignore[union-attr]
+ await callback.message.edit_text(
"Отправьте ключ Steam для активации\n\n"
"Можно отправить сразу несколько ключей.\n\n"
"Пример:\n"
"AAAAA-BBBBB-CCCCC\nDDDDD-EEEEE-FFFFF",
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(
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)
- 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()
- bot_redeem_users[callback.from_user.id] = bot_name
- bot_redeem_messages[callback.from_user.id] = callback.message
- await callback.message.edit_text( # type: ignore[union-attr]
+ await callback.message.edit_text(
f"Отправьте ключ Steam для активации на аккаунте:\n{bot_name}\n\n"
"Можно отправить сразу несколько ключей.",
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
+ )
diff --git a/bot/handlers/start.py b/bot/handlers/start.py
index e3bb509..f366af2 100644
--- a/bot/handlers/start.py
+++ b/bot/handlers/start.py
@@ -1,11 +1,14 @@
from aiogram import Router
-from aiogram.filters import Command
from aiogram.types import Message
+from aiogram.filters import Command
from bot.filters import AdminFilter
-from bot.logging_utils import describe_user, get_logger
-from bot.ui.formatters import get_asf_status_text
+from bot.services.users import upsert_user_from_telegram
+
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()
logger = get_logger(__name__)
@@ -14,15 +17,20 @@ logger = get_logger(__name__)
@router.message(Command("start"), AdminFilter())
async def start_handler(message: Message) -> 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)
try:
await message.delete()
logger.info("Сообщение /start удалено: %s", user)
+
except Exception:
logger.exception("Не удалось удалить сообщение /start: %s", user)
+
try:
await message.answer(get_asf_status_text(), reply_markup=main_keyboard())
logger.info("Главное меню отправлено: %s", user)
+
except Exception as error:
logger.exception("Не удалось отправить главное меню: %s", user)
await message.answer(f"Ошибка: {error}")
diff --git a/bot/handlers/twofa.py b/bot/handlers/twofa.py
index ce2f1b5..483d40d 100644
--- a/bot/handlers/twofa.py
+++ b/bot/handlers/twofa.py
@@ -3,11 +3,11 @@ import asyncio
from aiogram import Router
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.logging_utils import describe_user, get_logger
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
router = Router()
@@ -29,14 +29,14 @@ async def twofa_handler(callback: CallbackQuery) -> None:
)
return
bot_name = callback.data.replace("2fa_", "")
- message_id = callback.message.message_id # type: ignore[union-attr]
+ message_id = callback.message.message_id
logger.info(
"Пользователь %s открыл 2FA для бота %s",
describe_user(callback.from_user),
bot_name,
)
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...",
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:
await callback.answer()
- bot_name = callback.data.replace("confirm_", "") # type: ignore[union-attr]
+ bot_name = callback.data.replace("confirm_", "")
logger.info(
"Пользователь %s подтвердил сделки для бота %s",
describe_user(callback.from_user),
@@ -82,14 +82,14 @@ async def confirm_trades(callback: CallbackQuery) -> None:
"Ошибка при подтверждении сделок для бота %s",
bot_name,
)
- 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 and c.data.startswith("confirm_list_"), AdminCallbackFilter()
)
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()
if not callback.data:
logger.debug(
@@ -117,20 +117,23 @@ async def confirm_list(callback: CallbackQuery) -> None:
text = f"🔐 {bot_name}\n\nПодтверждения:\n\n"
for conf in confirmations:
text += f"- {conf['type_name']}\nID: {conf['id']}\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:
logger.exception(
"Ошибка при загрузке списка подтверждений для бота %s",
bot_name,
)
- 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 and c.data.startswith("confirm_all_"), AdminCallbackFilter()
)
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()
if not callback.data:
logger.debug(
@@ -138,12 +141,14 @@ async def confirm_all(callback: CallbackQuery) -> None:
describe_user(callback.from_user),
)
return
+
bot_name = callback.data.replace("confirm_all_", "")
logger.info(
"Пользователь %s запустил подтверждение всех сделок для бота %s",
describe_user(callback.from_user),
bot_name,
)
+
try:
status = await accept_confirmations(bot_name)
if status != 200:
@@ -154,6 +159,7 @@ async def confirm_all(callback: CallbackQuery) -> None:
)
await callback.answer("Ошибка HTTP", show_alert=True)
return
+
confirmations = await get_confirmations(bot_name)
if not confirmations:
logger.info("После подтверждения у бота %s не осталось сделок", bot_name)
@@ -167,11 +173,12 @@ async def confirm_all(callback: CallbackQuery) -> None:
text = f"🔐 {bot_name}\n\nПодтверждения:\n\n"
for conf in confirmations:
text += f"- {conf['type_name']}\nID: {conf['id']}\n\n"
- await callback.message.edit_text(text, reply_markup=confirmations_keyboard(bot_name)) # type: ignore[union-attr]
- await callback.answer("Все подтверждено", show_alert=True)
- except Exception as error:
- logger.exception(
- "Ошибка при подтверждении всех сделок для бота %s",
- bot_name,
+
+ await callback.message.edit_text(
+ text, reply_markup=confirmations_keyboard(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}")
diff --git a/bot/logging_utils.py b/bot/logging_utils.py
index 0edc394..dd0b272 100644
--- a/bot/logging_utils.py
+++ b/bot/logging_utils.py
@@ -12,9 +12,3 @@ def describe_user(user: Any) -> str:
user_id = getattr(user, "id", user)
return f"пользователь={user_id}"
-
-
-def mask_key(key: str) -> str:
- if len(key) < 9:
- return "***"
- return f"{key[:5]}-*****-*****"
diff --git a/bot/services/bots.py b/bot/services/bots.py
index a174522..38b18ce 100644
--- a/bot/services/bots.py
+++ b/bot/services/bots.py
@@ -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,
diff --git a/bot/services/inventory.py b/bot/services/inventory.py
index bb81057..363af46 100644
--- a/bot/services/inventory.py
+++ b/bot/services/inventory.py
@@ -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
)
diff --git a/bot/services/redeem.py b/bot/services/redeem.py
index 08cac8b..d703f5b 100644
--- a/bot/services/redeem.py
+++ b/bot/services/redeem.py
@@ -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))
diff --git a/bot/services/twofa.py b/bot/services/twofa.py
index f35efe2..c7d08cb 100644
--- a/bot/services/twofa.py
+++ b/bot/services/twofa.py
@@ -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(
diff --git a/bot/services/users.py b/bot/services/users.py
new file mode 100644
index 0000000..65db96f
--- /dev/null
+++ b/bot/services/users.py
@@ -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
diff --git a/bot/state.py b/bot/state.py
deleted file mode 100644
index 1560a08..0000000
--- a/bot/state.py
+++ /dev/null
@@ -1,7 +0,0 @@
-console_users = set()
-redeem_users = set()
-twofa_tasks = {}
-inventory_cache = {}
-redeem_messages = {}
-bot_redeem_users = {}
-bot_redeem_messages = {}
diff --git a/bot/states.py b/bot/states.py
new file mode 100644
index 0000000..9790914
--- /dev/null
+++ b/bot/states.py
@@ -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 = {}
diff --git a/bot/ui/formatters.py b/bot/ui/formatters.py
index 65705e0..a8c4f37 100644
--- a/bot/ui/formatters.py
+++ b/bot/ui/formatters.py
@@ -24,46 +24,58 @@ def get_currency_name(currency_id: int) -> str:
def get_inventory_icon(desc: dict) -> str:
tags = desc.get("tags", [])
+
for tag in tags:
category = tag.get("category", "")
name = tag.get("localized_tag_name", "")
+
if category == "item_class":
if "Trading Card" in name:
return "🎏"
- if "Emoticon" in name:
+ elif "Emoticon" in name:
return "😀"
- if "Profile Background" in name:
+ elif "Profile Background" in name:
return "🖼"
- if "Booster Pack" in name:
+ elif "Booster Pack" in name:
return "📦"
- if "Steam Gems" in name:
+ elif "Steam Gems" in name:
return "💎"
+
if "Gift" in name:
return "🎁"
item_type = desc.get("type", "").lower()
if "knife" in item_type:
return "🔪"
- if "pistol" in item_type:
+ elif "pistol" in item_type:
return "🔫"
- if "rifle" in item_type:
+ elif "rifle" in item_type:
return "🎯"
- if "graffiti" in item_type:
+ elif "graffiti" in item_type:
return "🎨"
- if "music kit" in item_type:
+ elif "music kit" in item_type:
return "🎵"
+
return "📦"
def get_uptime(start_time_str: str) -> str:
start_time_str = start_time_str[:26] + "Z"
start_time = datetime.fromisoformat(start_time_str.replace("Z", "+00:00"))
+
now = datetime.now(timezone.utc)
delta = now - start_time
days = delta.days
+
hours, remainder = divmod(delta.seconds, 3600)
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:
@@ -72,9 +84,9 @@ def format_asf(data: dict) -> str:
current_version = result.get("Version")
latest_version = result.get("LatestVersion", current_version)
- lines = [f"Версия ASF: `{current_version}`"]
+ lines = [f"Версия ASF: {current_version}"]
if current_version != latest_version:
- lines.append(f"Доступно обновление: `{latest_version}`")
+ lines.append(f"Доступно обновление: {latest_version}")
memory_kb = result.get("MemoryUsage", 0)
memory_mb = memory_kb / 1024
@@ -94,13 +106,10 @@ def get_bot_status_icon(bot: dict) -> str:
if not loaded:
return "🔄"
-
if farming:
return "🎴"
-
if idle_games and online:
return "⌚"
-
if online:
return "🟢"
@@ -179,11 +188,14 @@ def get_asf_status_text() -> str:
def get_farm_summary(bots: dict) -> str:
total_games = 0
total_time_seconds = 0
+
for bot in bots.values():
farmer = bot.get("CardsFarmer", {})
+
if farmer.get("NowFarming"):
games = farmer.get("CurrentGamesFarming", [])
total_games += len(games)
+
time_str = farmer.get("TimeRemaining", "00:00:00")
hours, minutes, seconds = map(int, time_str.split(":"))
total_time_seconds += hours * 3600 + minutes * 60 + seconds
@@ -193,6 +205,7 @@ def get_farm_summary(bots: dict) -> str:
hours = total_time_seconds // 3600
minutes = (total_time_seconds % 3600) // 60
+
return (
f"Игр фармится: {total_games}\n"
f"Осталось времени: {hours}ч {minutes}м\n"
diff --git a/bot/ui/keyboards.py b/bot/ui/keyboards.py
index c4f60c7..ded018b 100644
--- a/bot/ui/keyboards.py
+++ b/bot/ui/keyboards.py
@@ -36,6 +36,7 @@ def back_keyboard(callback_data: str = "back") -> InlineKeyboardMarkup:
def games_keyboard(bot_name: str, is_enabled: bool) -> InlineKeyboardMarkup:
text = "Остановить накрутку CS2" if is_enabled else "⌚ Накрутить часы CS2"
+
return InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text=text, callback_data=f"farm|{bot_name}|730")],
diff --git a/main.py b/main.py
index 3334329..308979e 100644
--- a/main.py
+++ b/main.py
@@ -2,9 +2,11 @@ import asyncio
import logging
from aiogram import Bot, Dispatcher
+from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.client.default import DefaultBotProperties
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.console import router as console_router
@@ -18,7 +20,7 @@ from bot.handlers.twofa import router as twofa_router
logger = logging.getLogger(__name__)
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(navigation_router)
@@ -31,6 +33,7 @@ dp.include_router(messages_router)
async def main() -> None:
+ await init_db()
await dp.start_polling(bot)
@@ -43,10 +46,13 @@ if __name__ == "__main__":
logging.StreamHandler(),
],
)
+
try:
asyncio.run(main())
+
except KeyboardInterrupt:
logger.info("Получен сигнал остановки, бот завершает работу")
+
except Exception:
logger.exception("Бот завершился из-за необработанной ошибки")
raise
diff --git a/requirements.txt b/requirements.txt
index 9c46aca..1de5602 100644
Binary files a/requirements.txt and b/requirements.txt differ