diff --git a/.gitignore b/.gitignore
index 7c7d8de..d76ee96 100644
--- a/.gitignore
+++ b/.gitignore
@@ -189,3 +189,5 @@ pyvenv.cfg
.venv
pip-selfcheck.json
+bot.db
+v1.json
diff --git a/bot/db/models.py b/bot/db/models.py
index a39af60..b992f48 100644
--- a/bot/db/models.py
+++ b/bot/db/models.py
@@ -1,7 +1,7 @@
from datetime import datetime, timezone
-from sqlalchemy import BigInteger, Boolean, DateTime, String
-from sqlalchemy.orm import Mapped, mapped_column
+from sqlalchemy import BigInteger, Boolean, DateTime, ForeignKey, String, Text
+from sqlalchemy.orm import Mapped, mapped_column, relationship
from bot.db.base import Base
@@ -19,9 +19,38 @@ class User(Base):
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)
+ balance_credits: Mapped[int] = mapped_column(BigInteger, default=0, nullable=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,
)
+ bots: Mapped[list["BotAccount"]] = relationship(back_populates="owner", cascade="all, delete-orphan")
+
+
+class AppSetting(Base):
+ __tablename__ = "app_settings"
+
+ key: Mapped[str] = mapped_column(String(128), primary_key=True)
+ integer_value: Mapped[int] = mapped_column(BigInteger, nullable=False)
+ updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now, onupdate=utc_now)
+ updated_by: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
+
+
+class BotAccount(Base):
+ __tablename__ = "bot_accounts"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ owner_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
+ bot_name: Mapped[str] = mapped_column(String(255), unique=True, index=True)
+ steam_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
+ source_filename: Mapped[str] = mapped_column(String(255))
+ config_sha256: Mapped[str] = mapped_column(String(64), index=True)
+ upload_state: Mapped[str] = mapped_column(String(32), default="attached")
+ last_error: Mapped[str | None] = mapped_column(Text, nullable=True)
+ is_attached: 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)
+ attached_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
+ owner: Mapped[User] = relationship(back_populates="bots")
diff --git a/bot/handlers/admin.py b/bot/handlers/admin.py
new file mode 100644
index 0000000..b0ac2f8
--- /dev/null
+++ b/bot/handlers/admin.py
@@ -0,0 +1,177 @@
+from aiogram import Router
+from aiogram.filters import Command
+from aiogram.fsm.context import FSMContext
+from aiogram.types import CallbackQuery, Message
+
+from bot.config import ADMIN_ID
+from bot.states import AdminFlow
+from bot.filters import AdminCallbackFilter, AdminFilter
+from bot.ui.keyboards import admin_keyboard, back_keyboard, admin_profile_keyboard
+from bot.services.balance import (
+ BalanceError,
+ change_user_balance,
+ get_bot_slot_price,
+ get_user_profile,
+ set_bot_slot_price,
+)
+
+router = Router()
+
+
+@router.message(Command("admin"), AdminFilter())
+async def admin_menu(message: Message, state: FSMContext) -> None:
+ await state.clear()
+ await message.answer("🤵 Админ-панель", reply_markup=admin_keyboard())
+
+
+@router.callback_query(lambda c: c.data == "admin_panel", AdminCallbackFilter())
+async def admin_panel(callback: CallbackQuery, state: FSMContext) -> None:
+ await state.clear()
+
+ await callback.answer()
+ await callback.message.edit_text(
+ "🤵 Админ-панель", reply_markup=admin_keyboard()
+ )
+
+
+@router.callback_query(
+ AdminCallbackFilter(), lambda c: c.data.startswith("admin_lookup")
+)
+async def admin_lookup_prompt(callback: CallbackQuery, state: FSMContext) -> None:
+ user_id = None
+ try:
+ user_id = int(callback.data.split(":")[1])
+ except:
+ pass
+
+ if user_id:
+ try:
+ if user_id <= 0:
+ raise ValueError
+ _, balance, bot_count = await get_user_profile(user_id)
+
+ except (ValueError, TypeError):
+ await callback.message.edit_text("❌ Некорректный Telegram ID")
+ return
+
+ await callback.message.edit_text(
+ f"👤 Профиль\n\n"
+ f"🆔: {user_id}\n"
+ f"💰 Баланс: {balance} ₽\n"
+ f"🤖 Ботов: {bot_count}",
+ reply_markup=admin_profile_keyboard(user_id=user_id),
+ )
+
+ return
+
+ await callback.answer()
+ await callback.message.edit_text(
+ "🆔 Отправьте ID пользователя",
+ reply_markup=back_keyboard(callback_data="admin_panel"),
+ )
+
+ await state.set_state(AdminFlow.waiting_user_id)
+ await state.update_data(action="lookup")
+
+
+@router.callback_query(
+ AdminCallbackFilter(), lambda c: c.data.startswith("admin_balance:")
+)
+async def admin_balance_prompt(callback: CallbackQuery, state: FSMContext) -> None:
+ user_id = int(callback.data.split(":")[1])
+
+ await callback.answer()
+ await callback.message.edit_text(
+ "💲 Отправьте сумму для изменения баланса пользователя",
+ reply_markup=back_keyboard(callback_data=f"admin_lookup:{user_id}"),
+ )
+
+ await state.set_state(AdminFlow.waiting_amount)
+ await state.update_data(action="balance", user_id=user_id)
+
+
+@router.callback_query(AdminCallbackFilter(), lambda c: c.data == "admin_price")
+async def admin_price_prompt(callback: CallbackQuery, state: FSMContext) -> None:
+ await callback.answer()
+ await callback.message.edit_text(
+ f"💲 Текущая цена слота: {await get_bot_slot_price()} ₽\n"
+ "Отправьте новую положительную цену.",
+ reply_markup=back_keyboard(callback_data="admin_panel"),
+ )
+
+ await state.set_state(AdminFlow.waiting_amount)
+ await state.update_data(action="price")
+
+
+@router.message(AdminFlow.waiting_user_id, AdminFilter())
+async def admin_lookup(message: Message, state: FSMContext) -> None:
+ try:
+ user_id = int((message.text or "").strip())
+ if user_id <= 0:
+ raise ValueError
+ _, balance, bot_count = await get_user_profile(user_id)
+
+ except (ValueError, TypeError):
+ await message.answer("❌ Некорректный Telegram ID")
+ return
+
+ await message.answer(
+ f"👤 Профиль\n\n"
+ f"🆔: {user_id}\n"
+ f"💰 Баланс: {balance} ₽\n"
+ f"🤖 Ботов: {bot_count}",
+ reply_markup=admin_profile_keyboard(user_id=user_id),
+ )
+
+ await state.clear()
+
+
+@router.message(AdminFlow.waiting_amount, AdminFilter())
+async def admin_amount(message: Message, state: FSMContext) -> None:
+ data = await state.get_data()
+ text = (message.text or "").strip()
+
+ try:
+ if data.get("action") == "price":
+ price = int(text)
+ await set_bot_slot_price(price, updated_by=ADMIN_ID)
+ response = f"✅ Цена слота обновлена: {price} ₽"
+
+ else:
+ amount_text = text
+ user_text = data.get("user_id", None)
+
+ print(amount_text, user_text)
+
+ user_id, signed_amount = int(user_text), int(amount_text)
+
+ if user_id <= 0 or signed_amount == 0:
+ raise ValueError
+
+ balance = await change_user_balance(
+ telegram_id=user_id,
+ amount=signed_amount,
+ actor_telegram_id=message.from_user.id,
+ )
+
+ response = f"✅ Баланс пользователя обновлён: {balance} ₽"
+
+ except (ValueError, TypeError, BalanceError):
+ response = "❌ Некорректные данные: нужны положительные целые числа"
+
+ await message.answer(response)
+
+ if data.get("action") == "price":
+ return await admin_menu(message, state)
+
+ _, balance, bot_count = await get_user_profile(user_id)
+
+ await message.answer(
+ f"👤 Профиль\n\n"
+ f"🆔: {user_id}\n"
+ f"💰 Баланс: {balance} ₽\n"
+ f"🤖 Ботов: {bot_count}",
+ reply_markup=admin_profile_keyboard(user_id=user_id),
+ )
+
+ await state.clear()
diff --git a/bot/handlers/bots.py b/bot/handlers/bots.py
index 6411fa1..c007bb8 100644
--- a/bot/handlers/bots.py
+++ b/bot/handlers/bots.py
@@ -1,224 +1,125 @@
-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_bot_reloading, is_idle_enabled, update_idle_game
+from bot.logging_utils import get_logger
+from bot.services.bot_accounts import (
+ ensure_admin_compatibility,
+ get_owned_bot,
+ list_user_bot_accounts,
+)
+from bot.config import ADMIN_ID
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
+from bot.services.bots import is_bot_reloading, is_idle_enabled, update_idle_game
router = Router()
logger = get_logger(__name__)
-@router.callback_query(lambda c: c.data == "bot_loading", AdminCallbackFilter())
+async def owned_callback_bot(callback: CallbackQuery, prefix: str):
+ try:
+ account_id = int((callback.data or "").removeprefix(prefix))
+ except ValueError:
+ return None
+
+ return await get_owned_bot(callback.from_user.id, account_id)
+
+
+@router.callback_query(lambda c: c.data == "bot_loading")
async def bot_loading_handler(callback: CallbackQuery) -> None:
- logger.info(
- "Пользователь %s нажал на еще не загруженного бота",
- describe_user(callback.from_user),
- )
await callback.answer(
- "Аккаунт еще загружается. Попробуйте через несколько секунд.",
- show_alert=True,
+ "Аккаунт ещё загружается. Попробуйте через несколько секунд.", show_alert=True
)
-@router.callback_query(lambda c: c.data == "bots", AdminCallbackFilter())
+@router.callback_query(lambda c: c.data == "bots")
async def bots_handler(callback: CallbackQuery) -> None:
- logger.info(
- "Пользователь %s открыл список ботов",
- describe_user(callback.from_user),
- )
await stop_twofa_task(callback.message.message_id)
+ response = get_all_bots()
await callback.answer()
- try:
- response = get_all_bots()
- if response.status_code != 200:
- logger.warning(
- "Не удалось загрузить список ботов для %s: HTTP %s",
- describe_user(callback.from_user),
- response.status_code,
- )
- 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 вернул ошибку")
- return
-
- bots = data.get("Result", {})
- logger.info(
- "Список ботов для %s загружен: %s бот(ов)",
- describe_user(callback.from_user),
- len(bots),
- )
- text = f"🤖 Ботов всего: {len(bots)}\n\n{get_farm_summary(bots)}"
-
- 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}")
-
-
-@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)
-
- if not callback.data:
- logger.debug(
- "Колбэк выбора бота пришел без data от %s",
- describe_user(callback.from_user),
- )
- await callback.answer()
+ if response.status_code != 200 or not response.json().get("Success"):
+ await callback.message.edit_text("Не удалось загрузить список ботов")
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
+ asf_bots = response.json().get("Result", {})
+ if callback.from_user.id == ADMIN_ID:
+ await ensure_admin_compatibility(list(asf_bots))
- try:
- data = get_all_bots().json()
- bots = data.get("Result", {})
- bot = bots.get(bot_name)
- if not bot or not bot.get("BotName") or not bot.get("SteamID"):
- logger.info(
- "Бот %s еще не готов к отображению для %s",
- bot_name,
- describe_user(callback.from_user),
- )
- await callback.answer(
- "Аккаунт еще загружается. Попробуйте через несколько секунд.",
- show_alert=True,
- )
- return
- logger.info(
- "Открыта карточка бота %s для %s",
- bot_name,
- describe_user(callback.from_user),
- )
- 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}")
-
-
-@router.callback_query(
- lambda c: c.data and c.data.startswith("games_"), AdminCallbackFilter()
-)
-async def games(callback: CallbackQuery) -> None:
- await callback.answer()
- if not callback.data:
- logger.debug(
- "Колбэк выбора игры пришел без data от %s",
- describe_user(callback.from_user),
- )
- return
-
- bot_name = callback.data.replace("games_", "")
- idle_enabled = is_idle_enabled(bot_name, 730)
- logger.info(
- "Пользователь %s открыл меню накрутки часов для бота %s. CS2=%s",
- describe_user(callback.from_user),
- bot_name,
- idle_enabled,
- )
+ accounts = await list_user_bot_accounts(callback.from_user.id)
+ rows = [
+ (account.id, account.bot_name, asf_bots.get(account.bot_name, {}))
+ for account in accounts
+ ]
await callback.message.edit_text(
- "⌚ Выберите игру для накрутки часов",
- reply_markup=games_keyboard(bot_name, idle_enabled),
+ f"🤖 Ваших ботов: {len(rows)}\n\n{get_farm_summary({name: bot for _, name, bot in rows})}",
+ reply_markup=bots_keyboard(rows),
)
@router.callback_query(
- lambda c: c.data and c.data.startswith("farm|"), AdminCallbackFilter()
+ lambda c: c.data and c.data.startswith("bot_") and c.data != "bot_loading"
)
-async def farm_game(callback: CallbackQuery) -> None:
+async def bot_selected(callback: CallbackQuery) -> None:
+ account = await owned_callback_bot(callback, "bot_")
+ if account is None:
+ await callback.answer("Бот не найден или вам не принадлежит", show_alert=True)
+ return
+ await stop_twofa_task(callback.message.message_id)
+ if is_bot_reloading(account.bot_name):
+ await callback.answer(
+ "Аккаунт перезагружается. Попробуйте позже.", show_alert=True
+ )
+ return
+ bot = get_all_bots().json().get("Result", {}).get(account.bot_name)
+ if not bot or not bot.get("BotName") or not bot.get("SteamID"):
+ await callback.answer(
+ "Аккаунт ещё загружается. Попробуйте позже.", show_alert=True
+ )
+ return
await callback.answer()
- if not callback.data:
- logger.debug(
- "Колбэк изменения фарма пришел без data от %s",
- describe_user(callback.from_user),
- )
- return
-
- _, bot_name, app_id_value = callback.data.split("|")
- logger.info(
- "Пользователь %s меняет игру для накрутки на боте %s: app_id=%s",
- describe_user(callback.from_user),
- bot_name,
- app_id_value,
+ await callback.message.edit_text(
+ format_bot_ui(bot),
+ disable_web_page_preview=True,
+ reply_markup=bot_details_keyboard(
+ account.id, bool(bot.get("HasMobileAuthenticator"))
+ ),
)
- success, text, enabled = await update_idle_game(bot_name, int(app_id_value))
- if not success:
- logger.warning(
- "Не удалось изменить игру для накрутки на боте %s для %s: %s",
- bot_name,
- describe_user(callback.from_user),
- text,
- )
- await callback.answer(text, show_alert=True)
- return
- logger.info(
- "Игра для накрутки на боте %s обновлена для %s. CS2=%s",
- bot_name,
- describe_user(callback.from_user),
- enabled,
+@router.callback_query(lambda c: c.data and c.data.startswith("games_"))
+async def games(callback: CallbackQuery) -> None:
+ account = await owned_callback_bot(callback, "games_")
+ if account is None:
+ await callback.answer("Бот не найден или вам не принадлежит", show_alert=True)
+ return
+ await callback.answer()
+ enabled = is_idle_enabled(account.bot_name, 730)
+ await callback.message.edit_text(
+ "⌚ Выберите игру для накрутки часов",
+ reply_markup=games_keyboard(account.id, enabled),
)
- await callback.answer(text, show_alert=True)
+
+@router.callback_query(lambda c: c.data and c.data.startswith("farm|"))
+async def farm_game(callback: CallbackQuery) -> None:
try:
+ _, account_id, app_id = (callback.data or "").split("|")
+ account = await get_owned_bot(callback.from_user.id, int(account_id))
+ app_id = int(app_id)
+ except (ValueError, TypeError):
+ account = None
+ if account is None:
+ await callback.answer("Бот не найден или вам не принадлежит", show_alert=True)
+ return
+ success, text, enabled = await update_idle_game(account.bot_name, app_id)
+ await callback.answer(text, show_alert=True)
+ if success:
await callback.message.edit_text(
- text=f"⌚ Выберите игру для накрутки часов",
- reply_markup=games_keyboard(bot_name, enabled),
- )
-
- except Exception:
- logger.debug(
- "Не удалось обновить клавиатуру игр для бота %s", bot_name, exc_info=True
+ "⌚ Выберите игру для накрутки часов",
+ reply_markup=games_keyboard(account.id, enabled),
)
diff --git a/bot/handlers/console.py b/bot/handlers/console.py
index fbeb465..0212fb1 100644
--- a/bot/handlers/console.py
+++ b/bot/handlers/console.py
@@ -2,8 +2,8 @@ from aiogram import Router
from aiogram.types import CallbackQuery
from aiogram.fsm.context import FSMContext
-from bot.filters import AdminCallbackFilter
from bot.states import ConsoleFlow
+from bot.filters import AdminFilter
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
@@ -12,7 +12,7 @@ router = Router()
logger = get_logger(__name__)
-@router.callback_query(lambda c: c.data == "console", AdminCallbackFilter())
+@router.callback_query(lambda c: c.data == "console", AdminFilter())
async def console_enter(callback: CallbackQuery, state: FSMContext) -> None:
user = describe_user(callback.from_user.id)
logger.info("Пользователь вошел в режим консоли ASF: %s", user)
@@ -23,16 +23,3 @@ async def console_enter(callback: CallbackQuery, state: FSMContext) -> None:
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, state: FSMContext) -> None:
- user = describe_user(callback.from_user.id)
- logger.info("Пользователь вышел из режима консоли ASF: %s", user)
-
- await state.clear()
-
- await callback.answer()
- 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 2cf495e..6dadad2 100644
--- a/bot/handlers/inventory.py
+++ b/bot/handlers/inventory.py
@@ -1,102 +1,61 @@
from aiogram import Router
from aiogram.types import CallbackQuery
-from bot.filters import AdminCallbackFilter
-from bot.logging_utils import describe_user, get_logger
+from bot.services.bot_accounts import get_owned_bot
from bot.services.inventory import build_inventory_menu, render_inventory_page
from bot.ui.keyboards import back_keyboard
router = Router()
-logger = get_logger(__name__)
-@router.callback_query(
- lambda c: c.data and c.data.startswith("inventory_"), AdminCallbackFilter()
-)
+async def account_from_data(callback: CallbackQuery, prefix: str):
+ try:
+ return await get_owned_bot(callback.from_user.id, int((callback.data or "").removeprefix(prefix)))
+ except ValueError:
+ return None
+
+
+@router.callback_query(lambda c: c.data and c.data.startswith("inventory_"))
async def inventory_menu(callback: CallbackQuery) -> None:
- user = describe_user(callback.from_user.id)
- bot_name = callback.data.replace("inventory_", "")
- logger.info("Открыто меню инвентаря: %s bot=%s", user, bot_name)
-
+ account = await account_from_data(callback, "inventory_")
+ if account is None:
+ await callback.answer("Бот не найден или вам не принадлежит", show_alert=True); return
await callback.answer()
- 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}")
-
+ text, keyboard = await build_inventory_menu(account.bot_name, account.id)
await callback.message.edit_text(text, reply_markup=keyboard)
-@router.callback_query(
- lambda c: c.data and c.data.startswith("inv_cs2_"), AdminCallbackFilter()
-)
-async def cs2_inventory(callback: CallbackQuery) -> None:
- user = describe_user(callback.from_user.id)
- bot_name = callback.data.replace("inv_cs2_", "")
- logger.info("Открыт CS2-инвентарь: %s bot=%s", user, bot_name)
-
+@router.callback_query(lambda c: c.data and c.data.startswith("inv_cs2_"))
+async def inventory_cs2(callback: CallbackQuery) -> None:
+ account = await account_from_data(callback, "inv_cs2_")
+ if account is None:
+ await callback.answer("Бот не найден или вам не принадлежит", show_alert=True); return
await callback.answer()
- 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("Не удалось загрузить инвентарь")
- return
-
+ text, keyboard = await render_inventory_page(account.bot_name, "cs2", 730, 2, 0, account.id)
await callback.message.edit_text(text, reply_markup=keyboard)
-@router.callback_query(
- lambda c: c.data and c.data.startswith("inv_steam_"), AdminCallbackFilter()
-)
-async def steam_inventory(callback: CallbackQuery) -> None:
- user = describe_user(callback.from_user.id)
- bot_name = callback.data.replace("inv_steam_", "")
- logger.info("Открыт Steam-инвентарь: %s bot=%s", user, bot_name)
-
+@router.callback_query(lambda c: c.data and c.data.startswith("inv_steam_"))
+async def inventory_steam(callback: CallbackQuery) -> None:
+ account = await account_from_data(callback, "inv_steam_")
+ if account is None:
+ await callback.answer("Бот не найден или вам не принадлежит", show_alert=True); return
await callback.answer()
- 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("Не удалось загрузить инвентарь")
- return
-
+ text, keyboard = await render_inventory_page(account.bot_name, "steam", 753, 6, 0, account.id)
await callback.message.edit_text(text, reply_markup=keyboard)
-@router.callback_query(
- lambda c: c.data and c.data.startswith("invpage|"), AdminCallbackFilter()
-)
+@router.callback_query(lambda c: c.data and c.data.startswith("invpage|"))
async def inventory_page(callback: CallbackQuery) -> None:
- user = describe_user(callback.from_user.id)
- _, inv_type, bot_name, page_value = callback.data.split("|")
- logger.info(
- "Открыта страница инвентаря: %s bot=%s type=%s page=%s",
- user,
- bot_name,
- inv_type,
- page_value,
- )
+ try:
+ _, inv_type, account_id, page = (callback.data or "").split("|")
+ account = await get_owned_bot(callback.from_user.id, int(account_id))
+ page = int(page)
+ except (ValueError, TypeError):
+ account = None
+ if account is None or inv_type not in {"cs2", "steam"}:
+ await callback.answer("Бот не найден или вам не принадлежит", show_alert=True); return
+ appid, contextid = (730, 2) if inv_type == "cs2" else (753, 6)
await callback.answer()
- app_id, context_id = (730, 2) if inv_type == "cs2" else (753, 6)
- text, keyboard = await render_inventory_page(
- bot_name, inv_type, app_id, context_id, int(page_value)
- )
- if text is None:
- logger.warning(
- "Не удалось загрузить страницу инвентаря: %s bot=%s type=%s page=%s",
- user,
- bot_name,
- inv_type,
- page_value,
- )
- await callback.message.edit_text("Не удалось загрузить инвентарь")
- return
+ text, keyboard = await render_inventory_page(account.bot_name, inv_type, appid, contextid, page, account.id)
await callback.message.edit_text(text, reply_markup=keyboard)
diff --git a/bot/handlers/messages.py b/bot/handlers/messages.py
index 0013480..9f15ea8 100644
--- a/bot/handlers/messages.py
+++ b/bot/handlers/messages.py
@@ -4,7 +4,6 @@ from aiogram import Router
from aiogram.types import Message
from aiogram.fsm.context import FSMContext
-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 (
@@ -12,6 +11,7 @@ from bot.services.redeem import (
redeem_all_bots_keys,
redeem_single_bot_keys,
)
+from bot.services.bot_accounts import list_user_bot_accounts
from bot.states import ConsoleFlow, RedeemFlow
@@ -26,7 +26,7 @@ router = Router()
logger = get_logger(__name__)
-@router.message(ConsoleFlow.waiting_command, AdminFilter())
+@router.message(ConsoleFlow.waiting_command)
async def handle_console_message(message: Message, state: FSMContext):
if not message.text:
await message.answer("Отправьте текстовую команду.")
@@ -97,11 +97,12 @@ async def handle_console_message(message: Message, state: FSMContext):
await message.answer(f"Ошибка: {error}")
-@router.message(RedeemFlow.waiting_bot_keys, AdminFilter())
+@router.message(RedeemFlow.waiting_bot_keys)
async def handle_single_bot_redeem(message: Message, state: FSMContext):
state_data = await state.get_data()
bot_name = state_data.get("bot_name")
+ account_id = state_data.get("account_id")
if not isinstance(bot_name, str) or not bot_name:
await message.answer("Контекст потерян, попробуйте еще раз.")
await state.clear()
@@ -126,7 +127,7 @@ async def handle_single_bot_redeem(message: Message, state: FSMContext):
await message.bot.edit_message_text(
text=format_bot_ui(bot_data),
reply_markup=bot_details_keyboard(
- bot_name, bool(bot_data.get("HasMobileAuthenticator"))
+ account_id, bool(bot_data.get("HasMobileAuthenticator"))
),
disable_web_page_preview=True,
chat_id=chat_id,
@@ -195,7 +196,7 @@ async def handle_single_bot_redeem(message: Message, state: FSMContext):
await state.clear()
-@router.message(RedeemFlow.waiting_all_keys, AdminFilter())
+@router.message(RedeemFlow.waiting_all_keys)
async def handle_all_bots_redeem(message: Message, state: FSMContext):
state_data = await state.get_data()
@@ -210,8 +211,8 @@ async def handle_all_bots_redeem(message: Message, state: FSMContext):
if menu_message_id:
try:
await message.bot.edit_message_text(
- text=get_asf_status_text(),
- reply_markup=main_keyboard(),
+ text=get_asf_status_text(message.from_user.id),
+ reply_markup=main_keyboard(message.from_user.id),
chat_id=chat_id,
message_id=menu_message_id,
)
@@ -256,7 +257,10 @@ async def handle_all_bots_redeem(message: Message, state: FSMContext):
)
try:
- await checking.edit_text(await redeem_all_bots_keys(keys))
+ accounts = await list_user_bot_accounts(message.from_user.id)
+ await checking.edit_text(
+ await redeem_all_bots_keys(keys, [account.bot_name for account in accounts])
+ )
except Exception as error:
logger.exception(
diff --git a/bot/handlers/navigation.py b/bot/handlers/navigation.py
index d060be5..f89813a 100644
--- a/bot/handlers/navigation.py
+++ b/bot/handlers/navigation.py
@@ -4,7 +4,7 @@ from aiogram import Router
from aiogram.types import CallbackQuery
from aiogram.fsm.context import FSMContext
-from bot.filters import AdminCallbackFilter
+from bot.filters import AdminCallbackFilter, AdminFilter
from bot.logging_utils import describe_user, get_logger
from bot.api.asf import get_plugins, restart_asf as restart_asf_request
@@ -18,7 +18,7 @@ router = Router()
logger = get_logger(__name__)
-@router.callback_query(lambda c: c.data == "back", AdminCallbackFilter())
+@router.callback_query(lambda c: c.data == "back")
async def back_button(callback: CallbackQuery, state: FSMContext) -> None:
user = describe_user(callback.from_user.id)
logger.info("Нажата кнопка возврата: %s", user)
@@ -29,7 +29,8 @@ async def back_button(callback: CallbackQuery, state: FSMContext) -> None:
await callback.answer()
try:
await callback.message.edit_text(
- get_asf_status_text(), reply_markup=main_keyboard()
+ get_asf_status_text(callback.from_user.id),
+ reply_markup=main_keyboard(callback.from_user.id),
)
logger.info("Пользователь возвращен в главное меню: %s", user)
@@ -46,7 +47,8 @@ async def refresh_handler(callback: CallbackQuery) -> None:
try:
await callback.message.edit_text(
- get_asf_status_text(), reply_markup=main_keyboard()
+ get_asf_status_text(callback.from_user.id),
+ reply_markup=main_keyboard(callback.from_user.id),
)
logger.info("Главный экран обновлен: %s", user)
@@ -55,7 +57,7 @@ async def refresh_handler(callback: CallbackQuery) -> None:
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")
async def delete_message(callback: CallbackQuery) -> None:
user = describe_user(callback.from_user.id)
logger.info("Запрошено удаление сообщения: %s", user)
@@ -77,7 +79,7 @@ async def restart_asf_confirm(callback: CallbackQuery) -> None:
await callback.answer()
await callback.message.edit_text(
"♻ Точно перезапустить ASF?\n\nВо время перезапуска IPC будет недоступен несколько секунд.",
- reply_markup=confirm_action_keyboard("restart_asf", "back"),
+ reply_markup=confirm_action_keyboard("restart_asf", "admin_panel"),
)
@@ -99,7 +101,7 @@ async def restart_asf(callback: CallbackQuery) -> None:
)
await callback.message.edit_text(
f"Ошибка HTTP {response.status_code}",
- reply_markup=main_keyboard(),
+ reply_markup=main_keyboard(callback.from_user.id),
)
return
@@ -115,8 +117,8 @@ async def restart_asf(callback: CallbackQuery) -> None:
await asyncio.sleep(1)
await callback.message.edit_text(
- f"ASF успешно перезапущен\n\n{get_asf_status_text()}",
- reply_markup=main_keyboard(),
+ f"ASF успешно перезапущен\n\n{get_asf_status_text(callback.from_user.id)}",
+ reply_markup=main_keyboard(callback.from_user.id),
)
logger.info("ASF успешно перезапущен: %s", user)
@@ -126,7 +128,7 @@ async def restart_asf(callback: CallbackQuery) -> None:
await callback.message.edit_text(
f"Ошибка: {error}",
- reply_markup=main_keyboard(),
+ reply_markup=main_keyboard(callback.from_user.id),
)
diff --git a/bot/handlers/profile.py b/bot/handlers/profile.py
new file mode 100644
index 0000000..61e9cf3
--- /dev/null
+++ b/bot/handlers/profile.py
@@ -0,0 +1,21 @@
+from aiogram import Router
+from aiogram.types import CallbackQuery
+
+from bot.services.balance import get_user_profile
+from bot.ui.keyboards import back_keyboard
+
+router = Router()
+
+
+@router.callback_query(lambda c: c.data == "profile")
+async def profile_handler(callback: CallbackQuery) -> None:
+ telegram_id, balance, bot_count = await get_user_profile(callback.from_user.id)
+
+ await callback.answer()
+ await callback.message.edit_text(
+ f"👤 Профиль\n\n"
+ f"🆔: {telegram_id}\n"
+ f"💰 Баланс: {balance} ₽\n"
+ f"🤖 Ботов: {bot_count}",
+ reply_markup=back_keyboard(),
+ )
diff --git a/bot/handlers/redeem.py b/bot/handlers/redeem.py
index 0e0b3ba..11cab29 100644
--- a/bot/handlers/redeem.py
+++ b/bot/handlers/redeem.py
@@ -2,8 +2,8 @@ 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.services.bot_accounts import get_owned_bot
from bot.states import RedeemFlow
@@ -13,7 +13,7 @@ router = Router()
logger = get_logger(__name__)
-@router.callback_query(lambda c: c.data == "redeem_keys", AdminCallbackFilter())
+@router.callback_query(lambda c: c.data == "redeem_keys")
async def redeem_keys_menu(callback: CallbackQuery, state: FSMContext) -> None:
user = describe_user(callback.from_user.id)
logger.info("Открыт режим активации ключей по всем ботам: %s", user)
@@ -30,13 +30,18 @@ async def redeem_keys_menu(callback: CallbackQuery, state: FSMContext) -> None:
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()
-)
+@router.callback_query(lambda c: c.data and c.data.startswith("redeem_bot_"))
async def redeem_bot_menu(callback: CallbackQuery, state: FSMContext) -> None:
user = describe_user(callback.from_user.id)
- bot_name = callback.data.replace("redeem_bot_", "")
+ try:
+ account = await get_owned_bot(callback.from_user.id, int(callback.data.replace("redeem_bot_", "")))
+ except ValueError:
+ account = None
+ if account is None:
+ await callback.answer("Бот не найден или вам не принадлежит", show_alert=True)
+ return
+ bot_name = account.bot_name
logger.info(
"Открыт режим активации ключей для одного бота: %s bot=%s", user, bot_name
)
@@ -45,9 +50,9 @@ async def redeem_bot_menu(callback: CallbackQuery, state: FSMContext) -> None:
await callback.message.edit_text(
f"Отправьте ключ Steam для активации на аккаунте:\n{bot_name}\n\n"
"Можно отправить сразу несколько ключей.",
- reply_markup=back_keyboard(f"bot_{bot_name}"),
+ reply_markup=back_keyboard(f"bot_{account.id}"),
)
await state.set_state(RedeemFlow.waiting_bot_keys)
await state.update_data(
- bot_name=bot_name, menu_message_id=callback.message.message_id
+ bot_name=bot_name, account_id=account.id, menu_message_id=callback.message.message_id
)
diff --git a/bot/handlers/start.py b/bot/handlers/start.py
index f366af2..a3ce6b3 100644
--- a/bot/handlers/start.py
+++ b/bot/handlers/start.py
@@ -2,7 +2,6 @@ from aiogram import Router
from aiogram.types import Message
from aiogram.filters import Command
-from bot.filters import AdminFilter
from bot.services.users import upsert_user_from_telegram
from bot.ui.keyboards import main_keyboard
@@ -14,7 +13,7 @@ router = Router()
logger = get_logger(__name__)
-@router.message(Command("start"), AdminFilter())
+@router.message(Command("start"))
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)
@@ -28,7 +27,10 @@ async def start_handler(message: Message) -> None:
logger.exception("Не удалось удалить сообщение /start: %s", user)
try:
- await message.answer(get_asf_status_text(), reply_markup=main_keyboard())
+ await message.answer(
+ get_asf_status_text(message.from_user.id),
+ reply_markup=main_keyboard(message.from_user.id),
+ )
logger.info("Главное меню отправлено: %s", user)
except Exception as error:
diff --git a/bot/handlers/twofa.py b/bot/handlers/twofa.py
index 483d40d..745c271 100644
--- a/bot/handlers/twofa.py
+++ b/bot/handlers/twofa.py
@@ -4,7 +4,7 @@ from aiogram import Router
from aiogram.types import CallbackQuery
from bot.states import twofa_tasks
-from bot.filters import AdminCallbackFilter
+from bot.services.bot_accounts import get_owned_bot
from bot.logging_utils import describe_user, get_logger
from bot.services.twofa import auto_update_2fa, stop_twofa_task
from bot.api.asf import accept_confirmations, get_confirmations
@@ -18,7 +18,6 @@ logger = get_logger(__name__)
lambda c: c.data
and c.data.startswith("2fa_")
and not c.data.startswith("2fa_refresh_"),
- AdminCallbackFilter(),
)
async def twofa_handler(callback: CallbackQuery) -> None:
await callback.answer()
@@ -28,7 +27,13 @@ async def twofa_handler(callback: CallbackQuery) -> None:
describe_user(callback.from_user),
)
return
- bot_name = callback.data.replace("2fa_", "")
+ try:
+ account = await get_owned_bot(callback.from_user.id, int(callback.data.replace("2fa_", "")))
+ except ValueError:
+ account = None
+ if account is None:
+ await callback.answer("Бот не найден или вам не принадлежит", show_alert=True); return
+ bot_name = account.bot_name
message_id = callback.message.message_id
logger.info(
"Пользователь %s открыл 2FA для бота %s",
@@ -38,9 +43,9 @@ async def twofa_handler(callback: CallbackQuery) -> None:
await stop_twofa_task(message_id)
await callback.message.edit_text(
f"🔐 {bot_name}\n\nЗагрузка 2FA...",
- reply_markup=back_keyboard(f"bot_{bot_name}"),
+ reply_markup=back_keyboard(f"bot_{account.id}"),
)
- task = asyncio.create_task(auto_update_2fa(callback.message, bot_name))
+ task = asyncio.create_task(auto_update_2fa(callback.message, bot_name, account.id))
twofa_tasks[message_id] = task
logger.info(
"Запущено фоновое обновление 2FA для бота %s в сообщении %s",
@@ -52,13 +57,19 @@ async def twofa_handler(callback: CallbackQuery) -> None:
@router.callback_query(
lambda c: c.data
and c.data.startswith("confirm_")
+ and c.data.removeprefix("confirm_").isdigit()
and not c.data.startswith("confirm_list_")
and not c.data.startswith("confirm_all_"),
- AdminCallbackFilter(),
)
async def confirm_trades(callback: CallbackQuery) -> None:
await callback.answer()
- bot_name = callback.data.replace("confirm_", "")
+ try:
+ account = await get_owned_bot(callback.from_user.id, int(callback.data.replace("confirm_", "")))
+ except ValueError:
+ account = None
+ if account is None:
+ await callback.answer("Бот не найден или вам не принадлежит", show_alert=True); return
+ bot_name = account.bot_name
logger.info(
"Пользователь %s подтвердил сделки для бота %s",
describe_user(callback.from_user),
@@ -86,7 +97,7 @@ async def confirm_trades(callback: CallbackQuery) -> None:
@router.callback_query(
- lambda c: c.data and c.data.startswith("confirm_list_"), AdminCallbackFilter()
+ lambda c: c.data and c.data.startswith("confirm_list_")
)
async def confirm_list(callback: CallbackQuery) -> None:
await stop_twofa_task(callback.message.message_id)
@@ -97,7 +108,13 @@ async def confirm_list(callback: CallbackQuery) -> None:
describe_user(callback.from_user),
)
return
- bot_name = callback.data.replace("confirm_list_", "")
+ try:
+ account = await get_owned_bot(callback.from_user.id, int(callback.data.replace("confirm_list_", "")))
+ except ValueError:
+ account = None
+ if account is None:
+ await callback.answer("Бот не найден или вам не принадлежит", show_alert=True); return
+ bot_name = account.bot_name
logger.info(
"Пользователь %s открыл список подтверждений для бота %s",
describe_user(callback.from_user),
@@ -118,7 +135,7 @@ async def confirm_list(callback: CallbackQuery) -> None:
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)
+ text, reply_markup=confirmations_keyboard(account.id)
)
except Exception as error:
logger.exception(
@@ -129,7 +146,7 @@ async def confirm_list(callback: CallbackQuery) -> None:
@router.callback_query(
- lambda c: c.data and c.data.startswith("confirm_all_"), AdminCallbackFilter()
+ lambda c: c.data and c.data.startswith("confirm_all_")
)
async def confirm_all(callback: CallbackQuery) -> None:
await stop_twofa_task(callback.message.message_id)
@@ -142,7 +159,13 @@ async def confirm_all(callback: CallbackQuery) -> None:
)
return
- bot_name = callback.data.replace("confirm_all_", "")
+ try:
+ account = await get_owned_bot(callback.from_user.id, int(callback.data.replace("confirm_all_", "")))
+ except ValueError:
+ account = None
+ if account is None:
+ await callback.answer("Бот не найден или вам не принадлежит", show_alert=True); return
+ bot_name = account.bot_name
logger.info(
"Пользователь %s запустил подтверждение всех сделок для бота %s",
describe_user(callback.from_user),
@@ -175,7 +198,7 @@ async def confirm_all(callback: CallbackQuery) -> None:
text += f"- {conf['type_name']}\nID: {conf['id']}\n\n"
await callback.message.edit_text(
- text, reply_markup=confirmations_keyboard(bot_name)
+ text, reply_markup=confirmations_keyboard(account.id)
)
await callback.answer("Все подтверждено", show_alert=True)
diff --git a/bot/handlers/uploads.py b/bot/handlers/uploads.py
new file mode 100644
index 0000000..0cd7cec
--- /dev/null
+++ b/bot/handlers/uploads.py
@@ -0,0 +1,190 @@
+import asyncio
+import hashlib
+import io
+import json
+import posixpath
+import re
+import zipfile
+from datetime import datetime, timezone
+from pathlib import PurePosixPath
+
+from aiogram import Router
+from aiogram.fsm.context import FSMContext
+from aiogram.types import CallbackQuery, Message
+
+from bot.api.asf import get_bot, save_bot_config
+from bot.services.bot_accounts import (
+ bot_name_exists,
+ create_bot_account,
+ user_hash_exists,
+)
+from bot.services.balance import (
+ InsufficientBalance,
+ credit_user,
+ debit_user,
+ get_bot_slot_price,
+)
+from bot.states import UploadFlow
+from bot.ui.keyboards import back_keyboard
+
+router = Router()
+_groups: dict[str, list[Message]] = {}
+_group_tasks: dict[str, asyncio.Task] = {}
+
+
+@router.callback_query(lambda c: c.data == "upload")
+async def upload_menu(callback: CallbackQuery, state: FSMContext) -> None:
+ await callback.answer()
+ await state.set_state(UploadFlow.waiting_upload)
+ await callback.message.edit_text(
+ "Отправьте JSON-конфиг, несколько JSON одним альбомом или ZIP-архив с JSON-файлами.",
+ reply_markup=back_keyboard(callback_data="bots"),
+ )
+
+
+def _decode_config(raw: bytes) -> dict:
+ value = json.loads(raw.decode("utf-8-sig"))
+ if not isinstance(value, dict):
+ raise ValueError("JSON должен содержать объект конфигурации")
+
+ return value
+
+
+def _zip_entries(raw: bytes) -> list[tuple[str, bytes]]:
+ entries = []
+ with zipfile.ZipFile(io.BytesIO(raw)) as archive:
+ for item in archive.infolist():
+ name = item.filename.replace("\\", "/")
+ path = PurePosixPath(name)
+ if path.is_absolute() or ".." in path.parts:
+ raise ValueError("ZIP содержит небезопасный путь")
+ if item.is_dir() or not name.lower().endswith(".json"):
+ continue
+ entries.append((name, archive.read(item)))
+ return entries
+
+
+async def _download(message: Message) -> bytes:
+ buffer = io.BytesIO()
+ await message.bot.download(message.document.file_id, destination=buffer)
+ return buffer.getvalue()
+
+
+async def _process(user_id: int, files: list[tuple[str, bytes]]) -> str:
+ results = []
+ seen_hashes: set[str] = set()
+ seen_names: set[str] = set()
+
+ for filename, raw in files:
+ try:
+ config = _decode_config(raw)
+ digest = hashlib.sha256(raw).hexdigest()
+ name = config.get("SteamLogin")
+
+ if name in seen_names or await bot_name_exists(name):
+ raise ValueError("бот с таким именем уже существует")
+
+ if digest in seen_hashes or await user_hash_exists(user_id, digest):
+ raise ValueError("такой конфиг уже загружался")
+
+ existing = get_bot(name)
+ if existing.status_code == 200 and existing.json().get("Result"):
+ raise ValueError("бот с таким именем уже существует в ASF")
+
+ slot_price = await get_bot_slot_price()
+ await debit_user(user_id, slot_price)
+ try:
+ response = save_bot_config(name, config)
+ if response.status_code != 200 or not response.json().get("Success"):
+ raise ValueError(
+ f"ASF отклонил конфиг (HTTP {response.status_code})"
+ )
+
+ await create_bot_account(
+ user_id,
+ bot_name=name,
+ steam_id=(
+ str(config.get("SteamID")) if config.get("SteamID") else None
+ ),
+ source_filename=filename[:255],
+ config_sha256=digest,
+ upload_state="attached",
+ is_attached=True,
+ attached_at=datetime.now(timezone.utc),
+ )
+
+ except Exception:
+ await credit_user(user_id, slot_price)
+ raise
+
+ seen_names.add(name)
+ seen_hashes.add(digest)
+ results.append(f"✅ {filename}")
+
+ except (
+ InsufficientBalance,
+ ValueError,
+ json.JSONDecodeError,
+ UnicodeDecodeError,
+ zipfile.BadZipFile,
+ ) as error:
+ results.append(f"❌ {filename}: {error}")
+
+ except Exception:
+ results.append(f"❌ {filename}: внутренняя ошибка")
+
+ return f"Обработано файлов: {len(files)}"
+
+
+async def _finish_group(key: str, state: FSMContext) -> None:
+ await asyncio.sleep(0.8)
+ messages = _groups.pop(key, [])
+ _group_tasks.pop(key, None)
+ if not messages:
+ return
+ files = []
+ for message in messages:
+ try:
+ files.append(
+ (message.document.file_name or "config.json", await _download(message))
+ )
+ except Exception:
+ files.append((message.document.file_name or "config.json", b""))
+
+ await messages[0].answer(await _process(messages[0].from_user.id, files))
+ await state.clear()
+
+
+@router.message(UploadFlow.waiting_upload, lambda message: bool(message.document))
+async def upload_document(message: Message, state: FSMContext) -> None:
+ document = message.document
+ filename = document.file_name or "config.json"
+
+ if message.media_group_id:
+ key = f"{message.chat.id}:{message.media_group_id}"
+ _groups.setdefault(key, []).append(message)
+ if key not in _group_tasks:
+ _group_tasks[key] = asyncio.create_task(_finish_group(key, state))
+ return
+
+ raw = await _download(message)
+ if filename.lower().endswith(".zip"):
+ try:
+ files = _zip_entries(raw)
+
+ except Exception as error:
+ await message.answer(f"❌ {filename}: {error}")
+ await state.clear()
+ return
+
+ if not files:
+ await message.answer("❌ В архиве нет JSON-файлов")
+ else:
+ await message.answer(await _process(message.from_user.id, files))
+
+ elif filename.lower().endswith(".json"):
+ await message.answer(await _process(message.from_user.id, [(filename, raw)]))
+ else:
+ await message.answer("❌ Поддерживаются только .json и .zip")
+
+ await state.clear()
diff --git a/bot/services/balance.py b/bot/services/balance.py
new file mode 100644
index 0000000..2a770e3
--- /dev/null
+++ b/bot/services/balance.py
@@ -0,0 +1,169 @@
+from sqlalchemy import func, select, update
+
+from bot.config import ADMIN_ID
+from bot.db.models import AppSetting, BotAccount, User
+from bot.db.session import async_session
+
+BOT_SLOT_PRICE_KEY = "bot_slot_price"
+DEFAULT_BOT_SLOT_PRICE = 1
+
+
+class BalanceError(ValueError):
+ pass
+
+
+class InsufficientBalance(BalanceError):
+ pass
+
+
+def _amount(value: int) -> int:
+ if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
+ raise BalanceError("Amount must be a positive integer")
+ return value
+
+
+async def _get_or_create_user(session, telegram_id: int) -> User:
+ user = await session.scalar(select(User).where(User.telegram_id == telegram_id))
+ if user is None:
+ user = User(telegram_id=telegram_id, balance_credits=0)
+ session.add(user)
+ await session.flush()
+ return user
+
+
+async def get_user_balance(telegram_id: int) -> int:
+ async with async_session() as session:
+ user = await session.scalar(select(User).where(User.telegram_id == telegram_id))
+ return int(user.balance_credits) if user else 0
+
+
+async def change_user_balance(
+ telegram_id: int, amount: int, *, actor_telegram_id: int | None = None
+) -> int:
+ amount = _amount(amount)
+ if (
+ actor_telegram_id is not None
+ and telegram_id != actor_telegram_id
+ and actor_telegram_id != ADMIN_ID
+ ):
+ raise PermissionError("Only an administrator can adjust another user")
+
+ async with async_session() as session:
+ user = await _get_or_create_user(session, telegram_id)
+ await session.execute(
+ update(User).where(User.id == user.id).values(balance_credits=amount)
+ )
+
+ await session.commit()
+ return int(
+ (
+ await session.scalar(
+ select(User.balance_credits).where(User.id == user.id)
+ )
+ )
+ or 0
+ )
+
+
+async def credit_user(
+ telegram_id: int, amount: int, *, actor_telegram_id: int | None = None
+) -> int:
+ amount = _amount(amount)
+ if (
+ actor_telegram_id is not None
+ and telegram_id != actor_telegram_id
+ and actor_telegram_id != ADMIN_ID
+ ):
+ raise PermissionError("Only an administrator can adjust another user")
+
+ async with async_session() as session:
+ user = await _get_or_create_user(session, telegram_id)
+ await session.execute(
+ update(User)
+ .where(User.id == user.id)
+ .values(balance_credits=User.balance_credits + amount)
+ )
+ await session.commit()
+ return int(
+ (
+ await session.scalar(
+ select(User.balance_credits).where(User.id == user.id)
+ )
+ )
+ or 0
+ )
+
+
+async def debit_user(
+ telegram_id: int, amount: int, *, actor_telegram_id: int | None = None
+) -> int:
+ amount = _amount(amount)
+ if (
+ actor_telegram_id is not None
+ and telegram_id != actor_telegram_id
+ and actor_telegram_id != ADMIN_ID
+ ):
+ raise PermissionError("Only an administrator can adjust another user")
+
+ async with async_session() as session:
+ user = await _get_or_create_user(session, telegram_id)
+ result = await session.execute(
+ update(User)
+ .where(User.id == user.id, User.balance_credits >= amount)
+ .values(balance_credits=User.balance_credits - amount)
+ )
+ if result.rowcount != 1:
+ await session.rollback()
+ raise InsufficientBalance("Insufficient balance")
+ await session.commit()
+ return int(
+ (
+ await session.scalar(
+ select(User.balance_credits).where(User.id == user.id)
+ )
+ )
+ or 0
+ )
+
+
+async def get_bot_slot_price() -> int:
+ async with async_session() as session:
+ setting = await session.get(AppSetting, BOT_SLOT_PRICE_KEY)
+ if setting is None:
+ setting = AppSetting(
+ key=BOT_SLOT_PRICE_KEY, integer_value=DEFAULT_BOT_SLOT_PRICE
+ )
+ session.add(setting)
+ await session.commit()
+ return DEFAULT_BOT_SLOT_PRICE
+ if setting.integer_value <= 0:
+ return DEFAULT_BOT_SLOT_PRICE
+ return int(setting.integer_value)
+
+
+async def set_bot_slot_price(price: int, *, updated_by: int) -> int:
+ price = _amount(price)
+ async with async_session() as session:
+ setting = await session.get(AppSetting, BOT_SLOT_PRICE_KEY)
+ if setting is None:
+ setting = AppSetting(
+ key=BOT_SLOT_PRICE_KEY, integer_value=price, updated_by=updated_by
+ )
+ session.add(setting)
+ else:
+ setting.integer_value = price
+ setting.updated_by = updated_by
+ await session.commit()
+ return price
+
+
+async def get_user_profile(telegram_id: int) -> tuple[int, int, int]:
+ async with async_session() as session:
+ user = await session.scalar(select(User).where(User.telegram_id == telegram_id))
+ if user is None:
+ return telegram_id, 0, 0
+
+ count = await session.scalar(
+ select(func.count(BotAccount.id)).where(BotAccount.owner_id == user.id)
+ )
+ return telegram_id, int(user.balance_credits), int(count or 0)
diff --git a/bot/services/bot_accounts.py b/bot/services/bot_accounts.py
new file mode 100644
index 0000000..d620a84
--- /dev/null
+++ b/bot/services/bot_accounts.py
@@ -0,0 +1,82 @@
+from sqlalchemy import select
+
+from bot.db.models import BotAccount, User
+from bot.db.session import async_session
+from bot.config import ADMIN_ID
+import hashlib
+
+
+async def list_user_bot_accounts(telegram_id: int) -> list[BotAccount]:
+ async with async_session() as session:
+ result = await session.execute(
+ select(BotAccount)
+ .join(User)
+ .where(User.telegram_id == telegram_id)
+ .order_by(BotAccount.bot_name)
+ )
+ return list(result.scalars())
+
+
+async def get_owned_bot(telegram_id: int, account_id: int) -> BotAccount | None:
+ async with async_session() as session:
+ result = await session.execute(
+ select(BotAccount)
+ .join(User)
+ .where(BotAccount.id == account_id, User.telegram_id == telegram_id)
+ )
+ return result.scalar_one_or_none()
+
+
+async def create_bot_account(telegram_id: int, **values) -> BotAccount:
+ async with async_session() as session:
+ user = await session.scalar(select(User).where(User.telegram_id == telegram_id))
+ if user is None:
+ user = User(telegram_id=telegram_id)
+ session.add(user)
+ await session.flush()
+ account = BotAccount(owner_id=user.id, **values)
+ session.add(account)
+ await session.commit()
+ await session.refresh(account)
+ return account
+
+
+async def bot_name_exists(bot_name: str) -> bool:
+ async with async_session() as session:
+ return (
+ await session.scalar(
+ select(BotAccount.id).where(BotAccount.bot_name == bot_name)
+ )
+ is not None
+ )
+
+
+async def user_hash_exists(telegram_id: int, config_sha256: str) -> bool:
+ async with async_session() as session:
+ return (
+ await session.scalar(
+ select(BotAccount.id)
+ .join(User)
+ .where(
+ User.telegram_id == telegram_id,
+ BotAccount.config_sha256 == config_sha256,
+ )
+ )
+ is not None
+ )
+
+
+async def ensure_admin_compatibility(bot_names: list[str]) -> None:
+ """Keep ASF bots created before ownership was introduced visible to the admin."""
+ for bot_name in bot_names:
+ if await bot_name_exists(bot_name):
+ continue
+
+ await create_bot_account(
+ ADMIN_ID,
+ bot_name=bot_name,
+ source_filename="existing-asf",
+ config_sha256=hashlib.sha256(bot_name.encode()).hexdigest(),
+ upload_state="attached",
+ is_attached=True,
+ )
diff --git a/bot/services/inventory.py b/bot/services/inventory.py
index 363af46..3f88d42 100644
--- a/bot/services/inventory.py
+++ b/bot/services/inventory.py
@@ -7,7 +7,7 @@ from bot.ui.keyboards import inventory_menu_keyboard, inventory_page_keyboard
logger = get_logger(__name__)
-async def build_inventory_menu(bot_name: str) -> tuple[str, object]:
+async def build_inventory_menu(bot_name: str, account_id: int) -> tuple[str, object]:
logger.info("Формирование меню инвентаря: bot=%s", bot_name)
cs2_inventory = await get_bot_inventory(bot_name, 730, 2)
steam_inventory = await get_bot_inventory(bot_name, 753, 6)
@@ -25,7 +25,7 @@ async def build_inventory_menu(bot_name: str) -> tuple[str, object]:
logger.info("Инвентарь пуст: bot=%s", bot_name)
return (
f"📦 Инвентарь {bot_name} пуст",
- inventory_menu_keyboard(bot_name, 0, 0),
+ inventory_menu_keyboard(account_id, 0, 0),
)
logger.info(
"Меню инвентаря сформировано: bot=%s cs2_items=%s steam_items=%s",
@@ -40,11 +40,11 @@ async def build_inventory_menu(bot_name: str) -> tuple[str, object]:
"🔒 — нельзя продавать\n"
"❌ — нельзя трейдить"
)
- return text, inventory_menu_keyboard(bot_name, len(cs2_assets), len(steam_assets))
+ return text, inventory_menu_keyboard(account_id, len(cs2_assets), len(steam_assets))
async def render_inventory_page(
- bot_name: str, inventory_type: str, appid: int, contextid: int, page: int
+ bot_name: str, inventory_type: str, appid: int, contextid: int, page: int, account_id: int
) -> tuple[str, object] | tuple[None, None]:
logger.info(
"Формирование страницы инвентаря: bot=%s type=%s appid=%s contextid=%s page=%s",
@@ -70,7 +70,7 @@ async def render_inventory_page(
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(account_id, 0, 0)
desc_map = {}
for desc in descriptions:
key = (str(desc.get("classid")), str(desc.get("instanceid")))
@@ -139,5 +139,5 @@ async def render_inventory_page(
)
return text[:4000], inventory_page_keyboard(
- inventory_type, bot_name, page, total_pages
+ inventory_type, account_id, page, total_pages
)
diff --git a/bot/services/redeem.py b/bot/services/redeem.py
index d703f5b..6c32350 100644
--- a/bot/services/redeem.py
+++ b/bot/services/redeem.py
@@ -111,11 +111,12 @@ async def redeem_single_bot_keys(bot_name: str, keys: list[str]) -> str:
return text[:4000] + ("\n\n...обрезано" if len(text) > 4000 else "")
-async def redeem_all_bots_keys(keys: list[str]) -> str:
+async def redeem_all_bots_keys(keys: list[str], allowed_bots: list[str] | None = None) -> str:
logger.info("Начата активация ключей по всем ботам: count=%s", len(keys))
response = get_all_bots()
data = response.json()
- bots = list(data.get("Result", {}).keys())
+ available = list(data.get("Result", {}).keys())
+ bots = [name for name in available if allowed_bots is None or name in allowed_bots]
logger.info("Для массовой активации найдено ботов: count=%s", len(bots))
success_count = 0
failed_count = 0
diff --git a/bot/services/twofa.py b/bot/services/twofa.py
index c7d08cb..faa4a46 100644
--- a/bot/services/twofa.py
+++ b/bot/services/twofa.py
@@ -27,7 +27,7 @@ async def stop_twofa_task(message_id: int) -> None:
)
-async def auto_update_2fa(message, bot_name: str) -> None:
+async def auto_update_2fa(message, bot_name: str, account_id: int) -> None:
last_code = None
message_id = message.message_id
logger.info(
@@ -45,7 +45,7 @@ async def auto_update_2fa(message, bot_name: str) -> None:
try:
await message.edit_text(
text,
- reply_markup=twofa_keyboard(bot_name, bool(confirmations)),
+ reply_markup=twofa_keyboard(account_id, bool(confirmations)),
)
last_code = code
diff --git a/bot/states.py b/bot/states.py
index 9790914..d38c0f6 100644
--- a/bot/states.py
+++ b/bot/states.py
@@ -10,4 +10,13 @@ class RedeemFlow(StatesGroup):
waiting_bot_keys = State()
+class UploadFlow(StatesGroup):
+ waiting_upload = State()
+
+
+class AdminFlow(StatesGroup):
+ waiting_user_id = State()
+ waiting_amount = State()
+
+
twofa_tasks = {}
diff --git a/bot/ui/formatters.py b/bot/ui/formatters.py
index a8c4f37..c1134f7 100644
--- a/bot/ui/formatters.py
+++ b/bot/ui/formatters.py
@@ -3,6 +3,7 @@ import json
from datetime import datetime, timezone
from bot.api.asf import get_asf_status
+from bot.config import ADMIN_ID
from bot.constants import GAMES
@@ -78,21 +79,23 @@ def get_uptime(start_time_str: str) -> str:
return f"{days_measure}{hours_measure}{minutes_measure}{seconds_measure}"
-def format_asf(data: dict) -> str:
+def format_asf(data: dict, user_id: int) -> str:
result = data.get("Result", {})
current_version = result.get("Version")
latest_version = result.get("LatestVersion", current_version)
lines = [f"Версия ASF: {current_version}"]
- if current_version != latest_version:
- lines.append(f"Доступно обновление: {latest_version}")
- memory_kb = result.get("MemoryUsage", 0)
- memory_mb = memory_kb / 1024
+ if user_id == ADMIN_ID:
+ if current_version != latest_version:
+ lines.append(f"Доступно обновление: {latest_version}")
- lines.append(f"Используется памяти: {memory_mb:.2f} MB")
- lines.append(f"Работает: {get_uptime(result.get('ProcessStartTime'))}")
+ memory_kb = result.get("MemoryUsage", 0)
+ memory_mb = memory_kb / 1024
+
+ lines.append(f"Используется памяти: {memory_mb:.2f} MB")
+ lines.append(f"Работает: {get_uptime(result.get('ProcessStartTime'))}")
return "\n".join(lines)
@@ -172,7 +175,7 @@ def format_bot_ui(bot: dict) -> str:
return text
-def get_asf_status_text() -> str:
+def get_asf_status_text(user_id: int) -> str:
response = get_asf_status()
if response.status_code != 200:
@@ -182,7 +185,7 @@ def get_asf_status_text() -> str:
if not data.get("Success"):
return "ASF вернул ошибку"
- return f"💻 Панель управления ASF\n\n{format_asf(data)}"
+ return f"💻 Панель управления ASF\n\n{format_asf(data, user_id)}"
def get_farm_summary(bots: dict) -> str:
diff --git a/bot/ui/keyboards.py b/bot/ui/keyboards.py
index ded018b..c971726 100644
--- a/bot/ui/keyboards.py
+++ b/bot/ui/keyboards.py
@@ -1,30 +1,40 @@
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from bot.ui.formatters import get_bot_status_icon
+from bot.config import ADMIN_ID
-def main_keyboard() -> InlineKeyboardMarkup:
- return InlineKeyboardMarkup(
- inline_keyboard=[
+def main_keyboard(user_id: int | None = None) -> InlineKeyboardMarkup:
+ keyboard = [
+ [InlineKeyboardButton(text="🤖 Боты", callback_data="bots")],
+ [InlineKeyboardButton(text="🔑 Активация ключей", callback_data="redeem_keys")],
+ ]
+
+ profile_index = -1 if user_id == ADMIN_ID else 0
+
+ if user_id == ADMIN_ID:
+ keyboard.insert(
+ 0,
[InlineKeyboardButton(text="🆙 Обновить", callback_data="refresh")],
- [InlineKeyboardButton(text="🤖 Боты", callback_data="bots")],
+ )
+
+ keyboard.extend(
[
- InlineKeyboardButton(
- text="🔑 Активация ключей",
- callback_data="redeem_keys",
- )
+ [
+ InlineKeyboardButton(
+ text="🤵 Админ-панель", callback_data="admin_panel"
+ )
+ ],
],
- [InlineKeyboardButton(text="📁 Плагины", callback_data="plugins")],
- [InlineKeyboardButton(text="💻 Консоль", callback_data="console")],
- [
- InlineKeyboardButton(
- text="♻ Перезапустить ASF",
- callback_data="restart_asf_confirm",
- )
- ],
- ]
+ )
+
+ keyboard.insert(
+ profile_index,
+ [InlineKeyboardButton(text="👤 Профиль", callback_data="profile")],
)
+ return InlineKeyboardMarkup(inline_keyboard=keyboard)
+
def back_keyboard(callback_data: str = "back") -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
@@ -34,57 +44,96 @@ def back_keyboard(callback_data: str = "back") -> InlineKeyboardMarkup:
)
-def games_keyboard(bot_name: str, is_enabled: bool) -> InlineKeyboardMarkup:
+def admin_keyboard() -> InlineKeyboardMarkup:
+ return InlineKeyboardMarkup(
+ inline_keyboard=[
+ [
+ InlineKeyboardButton(
+ text="🔍 Найти пользователя", callback_data="admin_lookup"
+ )
+ ],
+ [InlineKeyboardButton(text="💲 Цена слота", callback_data="admin_price")],
+ [InlineKeyboardButton(text="📁 Плагины", callback_data="plugins")],
+ [InlineKeyboardButton(text="💻 Консоль", callback_data="console")],
+ [
+ InlineKeyboardButton(
+ text="♻ Перезапустить ASF", callback_data="restart_asf_confirm"
+ )
+ ],
+ [InlineKeyboardButton(text="🔙 Назад", callback_data="back")],
+ ]
+ )
+
+
+def admin_profile_keyboard(user_id: int) -> InlineKeyboardMarkup:
+ return InlineKeyboardMarkup(
+ inline_keyboard=[
+ [
+ InlineKeyboardButton(
+ text="💲 Изменить баланс", callback_data=f"admin_balance:{user_id}"
+ )
+ ],
+ [InlineKeyboardButton(text="🔙 Назад", callback_data="admin_panel")],
+ ]
+ )
+
+
+def games_keyboard(account_id: int, 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")],
- [InlineKeyboardButton(text="🔙 Назад", callback_data=f"bot_{bot_name}")],
+ [InlineKeyboardButton(text=text, callback_data=f"farm|{account_id}|730")],
+ [InlineKeyboardButton(text="🔙 Назад", callback_data=f"bot_{account_id}")],
]
)
-def bots_keyboard(bots: dict) -> InlineKeyboardMarkup:
+def bots_keyboard(bots: list[tuple[int, str, dict]]) -> InlineKeyboardMarkup:
keyboard = []
- for name, bot in bots.items():
+ for account_id, name, bot in bots:
status = get_bot_status_icon(bot)
loaded = bool(bot.get("BotName")) and bool(bot.get("SteamID"))
- callback_data = f"bot_{name}" if loaded else "bot_loading"
+ callback_data = f"bot_{account_id}" if loaded else "bot_loading"
keyboard.append(
[InlineKeyboardButton(text=f"{status} {name}", callback_data=callback_data)]
)
- keyboard.append([InlineKeyboardButton(text="🔙 Назад", callback_data="back")])
+
+ keyboard.extend(
+ [
+ [InlineKeyboardButton(text="⬆️ Загрузить конфиг", callback_data="upload")],
+ [InlineKeyboardButton(text="🔙 Назад", callback_data="admin_panel")],
+ ]
+ )
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def bot_details_keyboard(
- bot_name: str, has_mobile_authenticator: bool
+ account_id: int, has_mobile_authenticator: bool
) -> InlineKeyboardMarkup:
keyboard = []
if has_mobile_authenticator:
keyboard.append(
- [InlineKeyboardButton(text="🔐 2FA", callback_data=f"2fa_{bot_name}")]
+ [InlineKeyboardButton(text="🔐 2FA", callback_data=f"2fa_{account_id}")]
)
keyboard.append(
[
InlineKeyboardButton(
- text="⌚ Накрутка часов", callback_data=f"games_{bot_name}"
+ text="⌚ Накрутка часов", callback_data=f"games_{account_id}"
)
]
)
keyboard.append(
[
InlineKeyboardButton(
- text="🔑 Активировать ключ",
- callback_data=f"redeem_bot_{bot_name}",
+ text="🔑 Активировать ключ", callback_data=f"redeem_bot_{account_id}"
)
]
)
keyboard.append(
[
InlineKeyboardButton(
- text="📦 Инвентарь", callback_data=f"inventory_{bot_name}"
+ text="📦 Инвентарь", callback_data=f"inventory_{account_id}"
)
]
)
@@ -92,33 +141,31 @@ def bot_details_keyboard(
return InlineKeyboardMarkup(inline_keyboard=keyboard)
-def twofa_keyboard(bot_name: str, has_confirmations: bool) -> InlineKeyboardMarkup:
+def twofa_keyboard(account_id: int, has_confirmations: bool) -> InlineKeyboardMarkup:
keyboard = []
if has_confirmations:
keyboard.append(
[
InlineKeyboardButton(
- text="Подтверждения",
- callback_data=f"confirm_list_{bot_name}",
+ text="Подтверждения", callback_data=f"confirm_list_{account_id}"
)
]
)
keyboard.append(
- [InlineKeyboardButton(text="🔙 Назад", callback_data=f"bot_{bot_name}")]
+ [InlineKeyboardButton(text="🔙 Назад", callback_data=f"bot_{account_id}")]
)
return InlineKeyboardMarkup(inline_keyboard=keyboard)
-def confirmations_keyboard(bot_name: str) -> InlineKeyboardMarkup:
+def confirmations_keyboard(account_id: int) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
- text="Подтвердить всё",
- callback_data=f"confirm_all_{bot_name}",
+ text="Подтвердить всё", callback_data=f"confirm_all_{account_id}"
)
],
- [InlineKeyboardButton(text="🔙 Назад", callback_data=f"2fa_{bot_name}")],
+ [InlineKeyboardButton(text="🔙 Назад", callback_data=f"2fa_{account_id}")],
]
)
@@ -135,15 +182,14 @@ def confirm_action_keyboard(
def inventory_menu_keyboard(
- bot_name: str, cs2_count: int, steam_count: int
+ account_id: int, cs2_count: int, steam_count: int
) -> InlineKeyboardMarkup:
keyboard = []
if cs2_count:
keyboard.append(
[
InlineKeyboardButton(
- text=f"CS2 ({cs2_count})",
- callback_data=f"inv_cs2_{bot_name}",
+ text=f"CS2 ({cs2_count})", callback_data=f"inv_cs2_{account_id}"
)
]
)
@@ -152,40 +198,37 @@ def inventory_menu_keyboard(
[
InlineKeyboardButton(
text=f"Steam ({steam_count})",
- callback_data=f"inv_steam_{bot_name}",
+ callback_data=f"inv_steam_{account_id}",
)
]
)
keyboard.append(
- [InlineKeyboardButton(text="🔙 Назад", callback_data=f"bot_{bot_name}")]
+ [InlineKeyboardButton(text="🔙 Назад", callback_data=f"bot_{account_id}")]
)
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def inventory_page_keyboard(
- inventory_type: str, bot_name: str, page: int, total_pages: int
+ inventory_type: str, account_id: int, page: int, total_pages: int
) -> InlineKeyboardMarkup:
- nav_buttons = []
+ nav = []
if page > 0:
- nav_buttons.append(
+ nav.append(
InlineKeyboardButton(
text="◀",
- callback_data=f"invpage|{inventory_type}|{bot_name}|{page - 1}",
+ callback_data=f"invpage|{inventory_type}|{account_id}|{page - 1}",
)
)
if page < total_pages - 1:
- nav_buttons.append(
+ nav.append(
InlineKeyboardButton(
text="▶",
- callback_data=f"invpage|{inventory_type}|{bot_name}|{page + 1}",
+ callback_data=f"invpage|{inventory_type}|{account_id}|{page + 1}",
)
)
-
- keyboard = []
- if nav_buttons:
- keyboard.append(nav_buttons)
+ keyboard = [nav] if nav else []
keyboard.append(
- [InlineKeyboardButton(text="🔙 Назад", callback_data=f"inventory_{bot_name}")]
+ [InlineKeyboardButton(text="🔙 Назад", callback_data=f"inventory_{account_id}")]
)
return InlineKeyboardMarkup(inline_keyboard=keyboard)
@@ -193,7 +236,7 @@ def inventory_page_keyboard(
def console_keyboard() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
- [InlineKeyboardButton(text="🔙 Назад", callback_data="console_exit")]
+ [InlineKeyboardButton(text="🔙 Назад", callback_data="admin_panel")]
]
)
diff --git a/main.py b/main.py
index 308979e..0d0bd37 100644
--- a/main.py
+++ b/main.py
@@ -16,6 +16,9 @@ from bot.handlers.navigation import router as navigation_router
from bot.handlers.redeem import router as redeem_router
from bot.handlers.start import router as start_router
from bot.handlers.twofa import router as twofa_router
+from bot.handlers.uploads import router as uploads_router
+from bot.handlers.profile import router as profile_router
+from bot.handlers.admin import router as admin_router
logger = logging.getLogger(__name__)
@@ -30,6 +33,9 @@ dp.include_router(twofa_router)
dp.include_router(inventory_router)
dp.include_router(redeem_router)
dp.include_router(messages_router)
+dp.include_router(uploads_router)
+dp.include_router(profile_router)
+dp.include_router(admin_router)
async def main() -> None: