diff --git a/bot/db/models.py b/bot/db/models.py
index b992f48..e4a6095 100644
--- a/bot/db/models.py
+++ b/bot/db/models.py
@@ -19,7 +19,10 @@ 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)
+ # Имя столбца сохраняется для совместимости с существующей базой данных.
+ balance_rubles: Mapped[int] = mapped_column(
+ "balance_credits", 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),
diff --git a/bot/handlers/bots.py b/bot/handlers/bots.py
index c007bb8..f3916ea 100644
--- a/bot/handlers/bots.py
+++ b/bot/handlers/bots.py
@@ -1,5 +1,5 @@
from aiogram import Router
-from aiogram.types import CallbackQuery
+from aiogram.types import CallbackQuery, Message
from bot.api.asf import get_all_bots
from bot.logging_utils import get_logger
@@ -27,6 +27,28 @@ async def owned_callback_bot(callback: CallbackQuery, prefix: str):
return await get_owned_bot(callback.from_user.id, account_id)
+async def send_bots_menu(message: Message, user_id: int) -> None:
+ """Отправляет отдельное сообщение со списком ботов пользователя."""
+ response = get_all_bots()
+ if response.status_code != 200 or not response.json().get("Success"):
+ await message.answer("Не удалось загрузить список ботов")
+ return
+
+ asf_bots = response.json().get("Result", {})
+ if user_id == ADMIN_ID:
+ await ensure_admin_compatibility(list(asf_bots))
+
+ accounts = await list_user_bot_accounts(user_id)
+ rows = [
+ (account.id, account.bot_name, asf_bots.get(account.bot_name, {}))
+ for account in accounts
+ ]
+ await message.answer(
+ 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 == "bot_loading")
async def bot_loading_handler(callback: CallbackQuery) -> None:
await callback.answer(
@@ -37,10 +59,8 @@ async def bot_loading_handler(callback: CallbackQuery) -> None:
@router.callback_query(lambda c: c.data == "bots")
async def bots_handler(callback: CallbackQuery) -> None:
await stop_twofa_task(callback.message.message_id)
-
- response = get_all_bots()
await callback.answer()
-
+ response = get_all_bots()
if response.status_code != 200 or not response.json().get("Success"):
await callback.message.edit_text("Не удалось загрузить список ботов")
return
@@ -54,7 +74,6 @@ async def bots_handler(callback: CallbackQuery) -> None:
(account.id, account.bot_name, asf_bots.get(account.bot_name, {}))
for account in accounts
]
-
await callback.message.edit_text(
f"🤖 Ваших ботов: {len(rows)}\n\n{get_farm_summary({name: bot for _, name, bot in rows})}",
reply_markup=bots_keyboard(rows),
diff --git a/bot/handlers/messages.py b/bot/handlers/messages.py
index 9f15ea8..a5a9e8a 100644
--- a/bot/handlers/messages.py
+++ b/bot/handlers/messages.py
@@ -11,7 +11,10 @@ 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.services.bot_accounts import (
+ get_owned_bot,
+ list_user_activation_bot_accounts,
+)
from bot.states import ConsoleFlow, RedeemFlow
@@ -103,12 +106,19 @@ async def handle_single_bot_redeem(message: Message, state: FSMContext):
bot_name = state_data.get("bot_name")
account_id = state_data.get("account_id")
- if not isinstance(bot_name, str) or not bot_name:
+ if not isinstance(account_id, int):
await message.answer("Контекст потерян, попробуйте еще раз.")
await state.clear()
return
+ account = await get_owned_bot(message.from_user.id, account_id)
+ if account is None:
+ await message.answer("Бот больше недоступен для активации ключей.")
+ await state.clear()
+ return
+ bot_name = account.bot_name
+
chat_id = message.chat.id
menu_message_id = state_data.get("menu_message_id")
@@ -257,7 +267,7 @@ async def handle_all_bots_redeem(message: Message, state: FSMContext):
)
try:
- accounts = await list_user_bot_accounts(message.from_user.id)
+ accounts = await list_user_activation_bot_accounts(message.from_user.id)
await checking.edit_text(
await redeem_all_bots_keys(keys, [account.bot_name for account in accounts])
)
diff --git a/bot/handlers/navigation.py b/bot/handlers/navigation.py
index f89813a..d6badaa 100644
--- a/bot/handlers/navigation.py
+++ b/bot/handlers/navigation.py
@@ -105,9 +105,7 @@ async def restart_asf(callback: CallbackQuery) -> None:
)
return
- # ! Меняется подход к перезапуску ASF: вместо того чтобы ждать
- # ! 6 секунд, лучше проверять статус ASF до тех пор он не запустится
- # await asyncio.sleep(6)
+ # Проверяем статус ASF до его запуска, не ожидая фиксированные 6 секунд.
status = get_asf_status()
await asyncio.sleep(1)
diff --git a/bot/handlers/uploads.py b/bot/handlers/uploads.py
index 0cd7cec..ab64ee9 100644
--- a/bot/handlers/uploads.py
+++ b/bot/handlers/uploads.py
@@ -2,9 +2,8 @@ import asyncio
import hashlib
import io
import json
-import posixpath
-import re
import zipfile
+from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import PurePosixPath
@@ -13,21 +12,26 @@ from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
from bot.api.asf import get_bot, save_bot_config
+from bot.config import ADMIN_ID
+from bot.handlers.bots import send_bots_menu
+from bot.logging_utils import get_logger
+from bot.services.balance import (
+ InsufficientBalance,
+ increase_balance,
+ debit_user,
+ get_bot_slot_price,
+ get_user_balance,
+)
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()
+logger = get_logger(__name__)
_groups: dict[str, list[Message]] = {}
_group_tasks: dict[str, asyncio.Task] = {}
@@ -36,8 +40,14 @@ _group_tasks: dict[str, asyncio.Task] = {}
async def upload_menu(callback: CallbackQuery, state: FSMContext) -> None:
await callback.answer()
await state.set_state(UploadFlow.waiting_upload)
+
+ balance = await get_user_balance(callback.from_user.id)
+ slot_price = await get_bot_slot_price()
+
await callback.message.edit_text(
- "Отправьте JSON-конфиг, несколько JSON одним альбомом или ZIP-архив с JSON-файлами.",
+ "Отправьте документы .json или .zip с JSON-конфигурациями.\n\n"
+ f"💰 Ваш баланс: {balance} ₽\n"
+ f"💳 Цена одного слота: {slot_price} ₽",
reply_markup=back_keyboard(callback_data="bots"),
)
@@ -46,7 +56,9 @@ def _decode_config(raw: bytes) -> dict:
value = json.loads(raw.decode("utf-8-sig"))
if not isinstance(value, dict):
raise ValueError("JSON должен содержать объект конфигурации")
-
+ name = value.get("SteamLogin")
+ if not isinstance(name, str) or not name.strip():
+ raise ValueError("отсутствует непустой SteamLogin")
return value
@@ -58,8 +70,10 @@ def _zip_entries(raw: bytes) -> list[tuple[str, bytes]]:
path = PurePosixPath(name)
if path.is_absolute() or ".." in path.parts:
raise ValueError("ZIP содержит небезопасный путь")
- if item.is_dir() or not name.lower().endswith(".json"):
+ if item.is_dir():
continue
+ if not name.lower().endswith(".json"):
+ raise ValueError(f"ZIP содержит неподдерживаемый файл: {name}")
entries.append((name, archive.read(item)))
return entries
@@ -70,91 +84,235 @@ async def _download(message: Message) -> bytes:
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()
+@dataclass
+class _Candidate:
+ filename: str
+ config: dict
+ digest: str
+
+
+def _digest(config: dict) -> str:
+ canonical = json.dumps(
+ config, ensure_ascii=False, sort_keys=True, separators=(",", ":")
+ )
+ return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
+
+
+async def _preflight(
+ user_id: int, files: list[tuple[str, bytes]]
+) -> tuple[list[_Candidate], list[str]]:
+ candidates: list[_Candidate] = []
+ errors: list[str] = []
+ parsed: list[tuple[str, dict, str]] = []
for filename, raw in files:
try:
config = _decode_config(raw)
- digest = hashlib.sha256(raw).hexdigest()
- name = config.get("SteamLogin")
+ name = config["SteamLogin"].strip()
+ digest = _digest(config)
+ parsed.append((filename, config, digest))
- if name in seen_names or await bot_name_exists(name):
- raise ValueError("бот с таким именем уже существует")
+ except (ValueError, json.JSONDecodeError, UnicodeDecodeError) as error:
+ logger.warning(
+ "Предварительная проверка отклонена: пользователь=%s файл=%s ошибка=%s",
+ user_id,
+ filename,
+ error,
+ )
+ errors.append(f"❌ {filename}: {error}")
- if digest in seen_hashes or await user_hash_exists(user_id, digest):
- raise ValueError("такой конфиг уже загружался")
+ except Exception as error:
+ logger.exception(
+ "Ошибка предварительной проверки: пользователь=%s файл=%s",
+ user_id,
+ filename,
+ )
+ errors.append(f"❌ {filename}: ошибка проверки ({error})")
- existing = get_bot(name)
- if existing.status_code == 200 and existing.json().get("Result"):
- raise ValueError("бот с таким именем уже существует в ASF")
+ name_counts: dict[str, int] = {}
+ hash_counts: dict[str, int] = {}
+ for _, config, digest in parsed:
+ name_counts[config["SteamLogin"].strip()] = (
+ name_counts.get(config["SteamLogin"].strip(), 0) + 1
+ )
+ hash_counts[digest] = hash_counts.get(digest, 0) + 1
- 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})"
- )
+ for filename, config, digest in parsed:
+ name = config["SteamLogin"].strip()
+ try:
+ if name_counts[name] > 1:
+ raise ValueError("дублируется имя бота в этой загрузке")
- 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),
+ if hash_counts[digest] > 1 or await user_hash_exists(user_id, digest):
+ raise ValueError(
+ "такая конфигурация уже загружалась или дублируется в загрузке"
)
- except Exception:
- await credit_user(user_id, slot_price)
- raise
+ if await bot_name_exists(name):
+ raise ValueError("бот с таким именем уже есть в базе данных")
+ existing = get_bot(name)
- seen_names.add(name)
- seen_hashes.add(digest)
- results.append(f"✅ {filename}")
+ if existing.status_code == 200 and existing.json().get("Result"):
+ raise ValueError("бот с таким именем уже есть в ASF")
- except (
- InsufficientBalance,
- ValueError,
- json.JSONDecodeError,
- UnicodeDecodeError,
- zipfile.BadZipFile,
- ) as error:
- results.append(f"❌ {filename}: {error}")
+ candidates.append(_Candidate(filename, config, digest))
+ logger.info(
+ "Предварительная проверка пройдена: пользователь=%s файл=%s бот=%s",
+ user_id,
+ filename,
+ name,
+ )
- except Exception:
- results.append(f"❌ {filename}: внутренняя ошибка")
+ except (ValueError, json.JSONDecodeError, UnicodeDecodeError) as error:
+ logger.warning(
+ "Предварительная проверка отклонена: пользователь=%s файл=%s ошибка=%s",
+ user_id,
+ filename,
+ error,
+ )
+ errors.append(f"❌ {filename}: {error}")
- return f"Обработано файлов: {len(files)}"
+ except Exception as error:
+ logger.exception(
+ "Ошибка предварительной проверки: пользователь=%s файл=%s",
+ user_id,
+ filename,
+ )
+ errors.append(f"❌ {filename}: ошибка проверки ({error})")
+
+ return candidates, errors
+
+
+async def _process(user_id: int, files: list[tuple[str, bytes]]) -> str:
+ candidates, results = await _preflight(user_id, files)
+ if not candidates:
+ return "\n".join(results) or "❌ Нет конфигураций для загрузки"
+
+ is_admin = user_id == ADMIN_ID
+ slot_price = await get_bot_slot_price()
+ total = slot_price * len(candidates)
+ reserved = False
+ if not is_admin:
+ try:
+ await debit_user(user_id, total)
+ reserved = True
+ logger.info(
+ "Средства зарезервированы: пользователь=%s сумма=%s ₽ количество=%s",
+ user_id,
+ total,
+ len(candidates),
+ )
+
+ except InsufficientBalance:
+ balance = await get_user_balance(user_id)
+ logger.warning(
+ "Недостаточно средств для загрузки: пользователь=%s баланс=%s ₽ требуется=%s ₽",
+ user_id,
+ balance,
+ total,
+ )
+
+ results.append(
+ f"❌ Недостаточно средств: нужно {total} ₽ для {len(candidates)} конф. "
+ f"(доступно {balance} ₽)\n"
+ )
+
+ results.extend(
+ f"❌ {candidate.filename}: загрузка не начата"
+ for candidate in candidates
+ )
+ return "\n".join(results)
+
+ for candidate in candidates:
+ name = candidate.config["SteamLogin"].strip()
+ try:
+ response = save_bot_config(name, candidate.config)
+ logger.info(
+ "Ответ ASF на загрузку: пользователь=%s бот=%s статус=%s",
+ user_id,
+ name,
+ response.status_code,
+ )
+
+ payload = response.json()
+ if response.status_code != 200 or not payload.get("Success"):
+ detail = (
+ payload.get("Message")
+ or payload.get("Result")
+ or f"HTTP {response.status_code}"
+ )
+ raise ValueError(f"ASF отклонил конфигурацию: {detail}")
+
+ await create_bot_account(
+ user_id,
+ bot_name=name,
+ steam_id=(
+ str(candidate.config.get("SteamID"))
+ if candidate.config.get("SteamID")
+ else None
+ ),
+ source_filename=candidate.filename[:255],
+ config_sha256=candidate.digest,
+ upload_state="attached",
+ is_attached=True,
+ attached_at=datetime.now(timezone.utc),
+ )
+ logger.info(
+ "Регистрация в базе завершена: пользователь=%s бот=%s", user_id, name
+ )
+ results.append(
+ f"✅ {candidate.filename}: ASF принял, аккаунт зарегистрирован ({name})"
+ )
+
+ except Exception as error:
+ logger.exception(
+ "Ошибка загрузки: пользователь=%s файл=%s бот=%s",
+ user_id,
+ candidate.filename,
+ name,
+ )
+ if reserved:
+ await increase_balance(user_id, slot_price)
+ logger.info(
+ "Средства возвращены: пользователь=%s сумма=%s ₽ файл=%s",
+ user_id,
+ slot_price,
+ candidate.filename,
+ )
+ results.append(f"❌ {candidate.filename}: {error}")
+
+ return "\n".join(results)
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:
+ filename = message.document.file_name or "config.json"
try:
- files.append(
- (message.document.file_name or "config.json", await _download(message))
- )
+ files.append((filename, await _download(message)))
except Exception:
- files.append((message.document.file_name or "config.json", b""))
+ files.append((filename, b""))
- await messages[0].answer(await _process(messages[0].from_user.id, files))
+ await _send_result_and_menu(messages[0], messages[0].from_user.id, files)
await state.clear()
+async def _send_result_and_menu(
+ message: Message, user_id: int, files: list[tuple[str, bytes]]
+) -> None:
+ result = await _process(user_id, files)
+ await message.answer(result)
+ if "✅" in result:
+ await send_bots_menu(message, user_id)
+
+
@router.message(UploadFlow.waiting_upload, lambda message: bool(message.document))
async def upload_document(message: Message, state: FSMContext) -> None:
document = message.document
@@ -167,24 +325,28 @@ async def upload_document(message: Message, state: FSMContext) -> None:
_group_tasks[key] = asyncio.create_task(_finish_group(key, state))
return
- raw = await _download(message)
- if filename.lower().endswith(".zip"):
- try:
+ try:
+ raw = await _download(message)
+ if filename.lower().endswith(".zip"):
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-файлов")
+ if not files:
+ await message.answer("❌ В архиве нет JSON-файлов")
+ else:
+ await _send_result_and_menu(message, message.from_user.id, files)
+ elif filename.lower().endswith(".json"):
+ await _send_result_and_menu(
+ message, message.from_user.id, [(filename, raw)]
+ )
else:
- await message.answer(await _process(message.from_user.id, files))
+ await message.answer("❌ Поддерживаются только документы .json и .zip")
+ except (zipfile.BadZipFile, ValueError) as error:
+ await message.answer(f"❌ {filename}: {error}")
+ finally:
+ await state.clear()
- 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()
+@router.message(UploadFlow.waiting_upload)
+async def upload_non_document(message: Message, state: FSMContext) -> None:
+ await message.answer(
+ "❌ Отправьте конфигурацию именно документом .json или архивом .zip"
+ )
diff --git a/bot/services/balance.py b/bot/services/balance.py
index 2a770e3..9dbf91f 100644
--- a/bot/services/balance.py
+++ b/bot/services/balance.py
@@ -18,14 +18,14 @@ class InsufficientBalance(BalanceError):
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")
+ raise BalanceError("Сумма должна быть положительным целым числом")
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)
+ user = User(telegram_id=telegram_id, balance_rubles=0)
session.add(user)
await session.flush()
return user
@@ -34,7 +34,7 @@ async def _get_or_create_user(session, telegram_id: int) -> 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
+ return int(user.balance_rubles) if user else 0
async def change_user_balance(
@@ -46,26 +46,26 @@ async def change_user_balance(
and telegram_id != actor_telegram_id
and actor_telegram_id != ADMIN_ID
):
- raise PermissionError("Only an administrator can adjust another user")
+ raise PermissionError("Только администратор может изменять баланс другого пользователя")
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)
+ update(User).where(User.id == user.id).values(balance_rubles=amount)
)
await session.commit()
return int(
(
await session.scalar(
- select(User.balance_credits).where(User.id == user.id)
+ select(User.balance_rubles).where(User.id == user.id)
)
)
or 0
)
-async def credit_user(
+async def increase_balance(
telegram_id: int, amount: int, *, actor_telegram_id: int | None = None
) -> int:
amount = _amount(amount)
@@ -74,20 +74,20 @@ async def credit_user(
and telegram_id != actor_telegram_id
and actor_telegram_id != ADMIN_ID
):
- raise PermissionError("Only an administrator can adjust another user")
+ raise PermissionError("Только администратор может изменять баланс другого пользователя")
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)
+ .values(balance_rubles=User.balance_rubles + amount)
)
await session.commit()
return int(
(
await session.scalar(
- select(User.balance_credits).where(User.id == user.id)
+ select(User.balance_rubles).where(User.id == user.id)
)
)
or 0
@@ -103,23 +103,23 @@ async def debit_user(
and telegram_id != actor_telegram_id
and actor_telegram_id != ADMIN_ID
):
- raise PermissionError("Only an administrator can adjust another user")
+ raise PermissionError("Только администратор может изменять баланс другого пользователя")
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)
+ .where(User.id == user.id, User.balance_rubles >= amount)
+ .values(balance_rubles=User.balance_rubles - amount)
)
if result.rowcount != 1:
await session.rollback()
- raise InsufficientBalance("Insufficient balance")
+ raise InsufficientBalance("Недостаточно рублей на балансе")
await session.commit()
return int(
(
await session.scalar(
- select(User.balance_credits).where(User.id == user.id)
+ select(User.balance_rubles).where(User.id == user.id)
)
)
or 0
@@ -166,4 +166,4 @@ async def get_user_profile(telegram_id: int) -> tuple[int, int, int]:
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)
+ return telegram_id, int(user.balance_rubles), int(count or 0)
diff --git a/bot/services/bot_accounts.py b/bot/services/bot_accounts.py
index d620a84..4cb6900 100644
--- a/bot/services/bot_accounts.py
+++ b/bot/services/bot_accounts.py
@@ -17,6 +17,25 @@ async def list_user_bot_accounts(telegram_id: int) -> list[BotAccount]:
return list(result.scalars())
+async def list_user_activation_bot_accounts(telegram_id: int) -> list[BotAccount]:
+ """Возвращает личных ботов, доступных для активации ключей пользователем.
+
+ Боты, импортированные из ASF для совместимости с панелью администратора,
+ являются системными, а не загруженными администратором лично.
+ """
+ async with async_session() as session:
+ result = await session.execute(
+ select(BotAccount)
+ .join(User)
+ .where(
+ User.telegram_id == telegram_id,
+ BotAccount.source_filename != "existing-asf",
+ )
+ .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(
@@ -67,7 +86,7 @@ async def user_hash_exists(telegram_id: int, config_sha256: str) -> bool:
async def ensure_admin_compatibility(bot_names: list[str]) -> None:
- """Keep ASF bots created before ownership was introduced visible to the admin."""
+ """Показывает администратору ботов ASF, созданных до добавления владельцев."""
for bot_name in bot_names:
if await bot_name_exists(bot_name):
continue
diff --git a/bot/services/redeem.py b/bot/services/redeem.py
index 6c32350..ca262bd 100644
--- a/bot/services/redeem.py
+++ b/bot/services/redeem.py
@@ -111,12 +111,26 @@ 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], allowed_bots: list[str] | None = None) -> str:
- logger.info("Начата активация ключей по всем ботам: count=%s", len(keys))
+async def redeem_all_bots_keys(keys: list[str], allowed_bots: list[str]) -> str:
+ """Активирует ключи только на явно указанных личных ботах."""
+ if not allowed_bots:
+ logger.info("Нет личных ботов для массовой активации ключей")
+ return (
+ "Результат активации\n\n"
+ "⚠️ У вас нет ботов, доступных для массовой активации.\n"
+ "Ключи не отправлялись в ASF."
+ )
+
+ logger.info(
+ "Начата активация ключей на разрешенных ботах: keys=%s bots=%s",
+ len(keys),
+ len(allowed_bots),
+ )
response = get_all_bots()
data = response.json()
available = list(data.get("Result", {}).keys())
- bots = [name for name in available if allowed_bots is None or name in allowed_bots]
+ allowed = set(allowed_bots)
+ bots = [name for name in available if name in allowed]
logger.info("Для массовой активации найдено ботов: count=%s", len(bots))
success_count = 0
failed_count = 0
diff --git a/bot/ui/keyboards.py b/bot/ui/keyboards.py
index c971726..30a931b 100644
--- a/bot/ui/keyboards.py
+++ b/bot/ui/keyboards.py
@@ -6,12 +6,11 @@ from bot.config import ADMIN_ID
def main_keyboard(user_id: int | None = None) -> InlineKeyboardMarkup:
keyboard = [
+ [InlineKeyboardButton(text="👤 Профиль", callback_data="profile")],
[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,
@@ -28,11 +27,6 @@ def main_keyboard(user_id: int | None = None) -> InlineKeyboardMarkup:
],
)
- keyboard.insert(
- profile_index,
- [InlineKeyboardButton(text="👤 Профиль", callback_data="profile")],
- )
-
return InlineKeyboardMarkup(inline_keyboard=keyboard)
@@ -102,7 +96,7 @@ def bots_keyboard(bots: list[tuple[int, str, dict]]) -> InlineKeyboardMarkup:
keyboard.extend(
[
[InlineKeyboardButton(text="⬆️ Загрузить конфиг", callback_data="upload")],
- [InlineKeyboardButton(text="🔙 Назад", callback_data="admin_panel")],
+ [InlineKeyboardButton(text="🔙 Назад", callback_data="back")],
]
)
return InlineKeyboardMarkup(inline_keyboard=keyboard)