11 Commits

43 changed files with 3570 additions and 1006 deletions
-4
View File
@@ -1,4 +0,0 @@
API_TOKEN = "" #Токен бота Телеграм
ADMIN_ID = 0 # Ваш Тг Ид
ASF_URL = "http://127.0.0.1:1242/Api/ASF"
ASF_PASSWORD = "0000" # Пароль от IPC ASF
+9
View File
@@ -0,0 +1,9 @@
#Токен бота Телеграм
BOT_TOKEN=
# Ваш Тг Ид
ADMIN_ID=
# конфиг ASF
ASF_URL=http://127.0.0.1:1242
ASF_PASSWORD=0000
+193
View File
@@ -0,0 +1,193 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
bot.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# ---> VirtualEnv
# Virtualenv
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
.Python
[Bb]in
[Ii]nclude
[Ll]ib
[Ll]ib64
[Ll]ocal
[Ss]cripts
pyvenv.cfg
.venv
pip-selfcheck.json
bot.db
v1.json
-14
View File
@@ -1,14 +0,0 @@
ASFTGCONTROLBOT - многофункциональная Telegram-панель управления для ArchiSteamFarm
Зависимости:<code>pip install -r requirements.txt</code>
Настройка <code>.env</code>:
<code>API_TOKEN = "Токен Вашего бота телеграм"
ADMIN_ID = Ваш телеграм ИД
ASF_URL = "http://127.0.0.1:1242/Api/ASF"
ASF_PASSWORD = "Пароль от IPC ASF"</code>
Идея и задумка автора. Проэкт написан с помощью AI(ChatGPT)
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
+236
View File
@@ -0,0 +1,236 @@
import aiohttp
import requests
from bot.config import ASF_PASSWORD, ASF_URL
from bot.logging_utils import get_logger
logger = get_logger(__name__)
def _headers() -> dict[str, str | None]:
return {"Authentication": ASF_PASSWORD}
def asf_get(path: str) -> requests.Response:
logger.info("Отправка GET-запроса в ASF: path=%s", path)
response = requests.get(f"{ASF_URL}{path}", headers=_headers())
logger.info(
"Получен ответ ASF на GET-запрос: path=%s status=%s", path, response.status_code
)
return response
def asf_post(path: str, payload: dict | None = None) -> requests.Response:
logger.info(
"Отправка POST-запроса в ASF: path=%s payload_keys=%s",
path,
sorted((payload or {}).keys()),
)
response = requests.post(f"{ASF_URL}{path}", headers=_headers(), json=payload)
logger.info(
"Получен ответ ASF на POST-запрос: path=%s status=%s",
path,
response.status_code,
)
return response
def get_asf_status() -> requests.Response:
logger.info("Запрос статуса ASF")
response = requests.get(f"{ASF_URL}/Api/ASF", headers=_headers()) # type: ignore[arg-type]
logger.info("Получен статус ASF: status=%s", response.status_code)
return response
def get_all_bots() -> requests.Response:
return asf_get("/Api/Bot/ASF")
def get_bot(bot_name: str) -> requests.Response:
return asf_get(f"/Api/Bot/{bot_name}")
def get_plugins() -> requests.Response:
return asf_get("/Api/Plugins")
def send_command(command: str) -> requests.Response:
return asf_post("/Api/Command", {"Command": command})
def restart_asf() -> requests.Response:
return send_command("!restart")
async def get_bot_inventory(bot_name: str, appid: int, contextid: int) -> dict | None:
logger.info(
"Запрос инвентаря бота: bot=%s appid=%s contextid=%s",
bot_name,
appid,
contextid,
)
async with aiohttp.ClientSession() as session:
async with session.get(
f"{ASF_URL}/Api/Bot/{bot_name}/Inventory/{appid}/{contextid}",
headers=_headers(),
) as response:
if response.status != 200:
logger.warning(
"Не удалось получить инвентарь бота: bot=%s appid=%s contextid=%s status=%s",
bot_name,
appid,
contextid,
response.status,
)
return None
data = await response.json()
if not data.get("Success"):
logger.warning(
"ASF вернул ошибку при получении инвентаря: bot=%s appid=%s contextid=%s",
bot_name,
appid,
contextid,
)
return None
logger.info(
"Инвентарь бота получен: bot=%s appid=%s contextid=%s",
bot_name,
appid,
contextid,
)
return data.get("Result")
async def get_2fa_token(bot_name: str) -> str:
logger.info("Запрос 2FA-кода: bot=%s", bot_name)
async with aiohttp.ClientSession() as session:
async with session.get(
f"{ASF_URL}/Api/Bot/{bot_name}/TwoFactorAuthentication/Token",
headers=_headers(),
) as response:
if response.status != 200:
logger.warning(
"Не удалось получить 2FA-код: bot=%s status=%s",
bot_name,
response.status,
)
return f"Ошибка HTTP {response.status}"
data = await response.json()
if not data.get("Success"):
logger.warning(
"ASF вернул ошибку при запросе 2FA-кода: bot=%s", bot_name
)
return "Ошибка ASF"
try:
logger.info("2FA-код успешно получен: bot=%s", bot_name)
return data["Result"][bot_name]["Result"]
except Exception:
logger.exception(
"Не удалось извлечь 2FA-код из ответа ASF: bot=%s", bot_name
)
return "Не удалось получить код"
async def get_confirmations(bot_name: str) -> list:
logger.info("Запрос подтверждений 2FA: bot=%s", bot_name)
async with aiohttp.ClientSession() as session:
async with session.get(
f"{ASF_URL}/Api/Bot/{bot_name}/TwoFactorAuthentication/Confirmations",
headers=_headers(),
) as response:
if response.status != 200:
logger.warning(
"Не удалось получить подтверждения 2FA: bot=%s status=%s",
bot_name,
response.status,
)
return []
data = await response.json()
if not data.get("Success"):
logger.warning(
"ASF вернул ошибку при запросе подтверждений 2FA: bot=%s",
bot_name,
)
return []
try:
result = data["Result"][bot_name]["Result"]
logger.info(
"Подтверждения 2FA получены: bot=%s count=%s",
bot_name,
len(result),
)
return result
except Exception:
logger.exception(
"Не удалось извлечь подтверждения 2FA из ответа ASF: bot=%s",
bot_name,
)
return []
async def accept_confirmations(bot_name: str) -> int:
logger.info("Отправка запроса на подтверждение всех 2FA-операций: bot=%s", bot_name)
async with aiohttp.ClientSession() as session:
async with session.post(
f"{ASF_URL}/Api/Bot/{bot_name}/TwoFactorAuthentication/Confirmations/Accept",
headers=_headers(),
) as response:
logger.info(
"Получен ответ на подтверждение 2FA-операций: bot=%s status=%s",
bot_name,
response.status,
)
return response.status
def save_bot_config(bot_name: str, config: dict) -> requests.Response:
logger.info("Сохранение конфигурации бота: bot=%s", bot_name)
return asf_post(f"/Api/Bot/{bot_name}", {"BotConfig": config})
async def redeem_key(bot_name: str, key: str) -> tuple[bool, str]:
logger.info("Запрос активации ключа: bot=%s", bot_name)
async with aiohttp.ClientSession() as session:
async with session.post(
f"{ASF_URL}/Api/Command",
headers=_headers(),
json={"Command": f"!redeem {bot_name} {key}"},
) as response:
if response.status != 200:
logger.warning(
"Не удалось активировать ключ: bot=%s status=%s",
bot_name,
response.status,
)
return False, f"HTTP_ERROR_{response.status}"
data = await response.json()
if not data.get("Success"):
logger.warning(
"ASF вернул ошибку при активации ключа: bot=%s", bot_name
)
return False, "ASF_ERROR"
logger.info("ASF вернул результат активации ключа: bot=%s", bot_name)
return True, str(data.get("Result", ""))
+13
View File
@@ -0,0 +1,13 @@
import os
from dotenv import load_dotenv
load_dotenv()
BOT_TOKEN = os.getenv("BOT_TOKEN")
ADMIN_ID = int(os.getenv("ADMIN_ID")) # type: ignore[arg-type]
ASF_URL = os.getenv("ASF_URL")
ASF_PASSWORD = os.getenv("ASF_PASSWORD")
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite+aiosqlite:///bot.db")
+5
View File
@@ -0,0 +1,5 @@
PAGE_SIZE = 40
GAMES = {730: "CS2", 440: "TF2", 570: "Dota 2"}
appid = 753
contextid = 6
+1
View File
@@ -0,0 +1 @@
+5
View File
@@ -0,0 +1,5 @@
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
+8
View File
@@ -0,0 +1,8 @@
from bot.db import models # noqa: F401
from bot.db.base import Base
from bot.db.session import engine
async def init_db() -> None:
async with engine.begin() as connection:
await connection.run_sync(Base.metadata.create_all)
+59
View File
@@ -0,0 +1,59 @@
from datetime import datetime, timezone
from sqlalchemy import BigInteger, Boolean, DateTime, ForeignKey, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from bot.db.base import Base
def utc_now() -> datetime:
return datetime.now(timezone.utc)
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
telegram_id: Mapped[int] = mapped_column(BigInteger, unique=True, index=True)
username: Mapped[str | None] = mapped_column(String(255), nullable=True)
first_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
last_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
is_admin: Mapped[bool] = mapped_column(Boolean, default=False)
# Имя столбца сохраняется для совместимости с существующей базой данных.
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),
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")
+6
View File
@@ -0,0 +1,6 @@
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from bot.config import DATABASE_URL
engine = create_async_engine(DATABASE_URL)
async_session = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
+34
View File
@@ -0,0 +1,34 @@
from aiogram.filters import BaseFilter
from aiogram.types import CallbackQuery, Message
from bot.config import ADMIN_ID
from bot.logging_utils import get_logger
logger = get_logger(__name__)
class AdminFilter(BaseFilter):
async def __call__(self, message: Message) -> bool:
allowed = message.from_user.id == ADMIN_ID
if not allowed:
logger.warning(
"Отклонено сообщение от неавторизованного пользователя: user_id=%s",
message.from_user.id if message.from_user else None,
)
return allowed
class AdminCallbackFilter(BaseFilter):
async def __call__(self, callback: CallbackQuery) -> bool:
allowed = callback.from_user.id == ADMIN_ID
if not allowed:
logger.warning(
"Отклонен callback от неавторизованного пользователя: user_id=%s data=%s",
callback.from_user.id,
callback.data,
)
return allowed
+1
View File
@@ -0,0 +1 @@
+177
View File
@@ -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("🤵 <b>Админ-панель</b>", 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(
"🤵 <b>Админ-панель</b>", 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"👤 <b>Профиль</b>\n\n"
f"🆔: <code>{user_id}</code>\n"
f"💰 Баланс: <code>{balance}</code> ₽\n"
f"🤖 Ботов: <code>{bot_count}</code>",
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"💲 Текущая цена слота: <code>{await get_bot_slot_price()}</code> ₽\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"👤 <b>Профиль</b>\n\n"
f"🆔: <code>{user_id}</code>\n"
f"💰 Баланс: <code>{balance}</code> ₽\n"
f"🤖 Ботов: <code>{bot_count}</code>",
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"✅ Цена слота обновлена: <code>{price}</code> ₽"
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"✅ Баланс пользователя обновлён: <code>{balance}</code> ₽"
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"👤 <b>Профиль</b>\n\n"
f"🆔: <code>{user_id}</code>\n"
f"💰 Баланс: <code>{balance}</code> ₽\n"
f"🤖 Ботов: <code>{bot_count}</code>",
reply_markup=admin_profile_keyboard(user_id=user_id),
)
await state.clear()
+144
View File
@@ -0,0 +1,144 @@
from aiogram import Router
from aiogram.types import CallbackQuery, Message
from bot.api.asf import get_all_bots
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__)
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)
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"<b>🤖 Ваших ботов:</b> {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(
"Аккаунт ещё загружается. Попробуйте через несколько секунд.", show_alert=True
)
@router.callback_query(lambda c: c.data == "bots")
async def bots_handler(callback: CallbackQuery) -> None:
await stop_twofa_task(callback.message.message_id)
await callback.answer()
response = get_all_bots()
if response.status_code != 200 or not response.json().get("Success"):
await callback.message.edit_text("Не удалось загрузить список ботов")
return
asf_bots = response.json().get("Result", {})
if callback.from_user.id == ADMIN_ID:
await ensure_admin_compatibility(list(asf_bots))
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(
f"<b>🤖 Ваших ботов:</b> {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("bot_") and c.data != "bot_loading"
)
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()
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"))
),
)
@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),
)
@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(
"⌚ Выберите игру для накрутки часов",
reply_markup=games_keyboard(account.id, enabled),
)
+25
View File
@@ -0,0 +1,25 @@
from aiogram import Router
from aiogram.types import CallbackQuery
from aiogram.fsm.context import FSMContext
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
router = Router()
logger = get_logger(__name__)
@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)
await state.set_state(ConsoleFlow.waiting_command)
await callback.answer()
await callback.message.edit_text(
"💻 Консоль ASF\n\nОтправь команду (без !)", reply_markup=console_keyboard()
)
+61
View File
@@ -0,0 +1,61 @@
from aiogram import Router
from aiogram.types import CallbackQuery
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()
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:
account = await account_from_data(callback, "inventory_")
if account is None:
await callback.answer("Бот не найден или вам не принадлежит", show_alert=True); return
await callback.answer()
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_"))
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()
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_"))
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()
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|"))
async def inventory_page(callback: CallbackQuery) -> None:
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()
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)
+282
View File
@@ -0,0 +1,282 @@
import html
from aiogram import Router
from aiogram.types import Message
from aiogram.fsm.context import FSMContext
from bot.api.asf import get_all_bots, send_command
from bot.logging_utils import describe_user, get_logger
from bot.services.redeem import (
extract_keys,
redeem_all_bots_keys,
redeem_single_bot_keys,
)
from bot.services.bot_accounts import (
get_owned_bot,
list_user_activation_bot_accounts,
)
from bot.states import ConsoleFlow, RedeemFlow
from bot.ui.formatters import format_bot_ui, get_asf_status_text
from bot.ui.keyboards import (
main_keyboard,
bot_details_keyboard,
delete_message_keyboard,
)
router = Router()
logger = get_logger(__name__)
@router.message(ConsoleFlow.waiting_command)
async def handle_console_message(message: Message, state: FSMContext):
if not message.text:
await message.answer("Отправьте текстовую команду.")
return
command = message.text.strip()
if not command:
await message.answer("Команда не может быть пустой.")
return
logger.info(
"Пользователь %s отправил консольную команду: %s",
describe_user(message.from_user),
command,
)
try:
await message.delete()
except Exception:
logger.debug(
"Не удалось удалить сообщение с консольной командой от %s",
describe_user(message.from_user),
exc_info=True,
)
try:
response = send_command(f"!{command}")
if response.status_code != 200:
logger.warning(
"ASF вернул HTTP %s на консольную команду '%s' от %s",
response.status_code,
command,
describe_user(message.from_user),
)
text = f"Ошибка HTTP {response.status_code}"
else:
data = response.json()
if not data.get("Success"):
logger.warning(
"ASF вернул неуспешный ответ на консольную команду '%s' от %s",
command,
describe_user(message.from_user),
)
text = "Ошибка ASF"
else:
logger.info(
"Консольная команда '%s' от %s выполнена успешно",
command,
describe_user(message.from_user),
)
result = html.escape(str(data.get("Result", "Нет ответа")))
text = f"<b>Команда:</b> <code>{command}</code>\n\n<b>Ответ:</b>\n<code>{result}</code>"
await message.answer(text, reply_markup=delete_message_keyboard())
except Exception as error:
logger.exception(
"Ошибка при выполнении консольной команды '%s' от %s",
command,
describe_user(message.from_user),
)
await message.answer(f"Ошибка: {error}")
@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(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")
logger.info(
"Пользователь %s отправил ключи для бота %s",
describe_user(message.from_user),
bot_name,
)
if menu_message_id:
try:
response = get_all_bots()
bot_data = response.json().get("Result", {}).get(bot_name)
if bot_data:
await message.bot.edit_message_text(
text=format_bot_ui(bot_data),
reply_markup=bot_details_keyboard(
account_id, bool(bot_data.get("HasMobileAuthenticator"))
),
disable_web_page_preview=True,
chat_id=chat_id,
message_id=menu_message_id,
)
except Exception:
logger.debug(
"Не удалось восстановить меню бота %s после ввода ключей от %s",
bot_name,
describe_user(message.from_user),
exc_info=True,
)
try:
await message.bot.delete_message(
chat_id=message.chat.id, message_id=message.message_id
)
except Exception:
logger.debug(
"Не удалось удалить сообщение с ключами для бота %s от %s",
bot_name,
describe_user(message.from_user),
exc_info=True,
)
keys = extract_keys(message.text or "")
if not keys:
logger.info(
"В сообщении от %s для бота %s не найдено ключей",
describe_user(message.from_user),
bot_name,
)
await message.answer("Ключи не найдены")
await state.clear()
return
logger.info(
"Для бота %s получено %s ключ(ей) от %s",
bot_name,
len(keys),
describe_user(message.from_user),
)
checking = await message.answer(
f"Аккаунт: <code>{bot_name}</code>\nНайдено ключей: {len(keys)}\nНачинаю активацию..."
)
try:
await checking.edit_text(await redeem_single_bot_keys(bot_name, keys))
except Exception as error:
logger.exception(
"Не удалось активировать ключи для бота %s от %s",
bot_name,
describe_user(message.from_user),
)
try:
await checking.edit_text(f"Ошибка: {error}")
except Exception:
await message.answer(f"Ошибка: {error}")
finally:
await state.clear()
@router.message(RedeemFlow.waiting_all_keys)
async def handle_all_bots_redeem(message: Message, state: FSMContext):
state_data = await state.get_data()
chat_id = message.chat.id
menu_message_id = state_data.get("menu_message_id")
logger.info(
"Пользователь %s отправил ключи для активации на всех ботах",
describe_user(message.from_user),
)
if menu_message_id:
try:
await message.bot.edit_message_text(
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,
)
except Exception:
logger.debug(
"Не удалось восстановить главное меню после массовой активации для %s",
describe_user(message.from_user),
exc_info=True,
)
try:
await message.bot.delete_message(
chat_id=message.chat.id, message_id=message.message_id
)
except Exception:
logger.debug(
"Не удалось удалить сообщение с массовой активацией от %s",
describe_user(message.from_user),
exc_info=True,
)
keys = extract_keys(message.text or "")
if not keys:
logger.info(
"В сообщении от %s для массовой активации не найдено ключей",
describe_user(message.from_user),
)
await message.answer("Ключи не найдены")
await state.clear()
return
logger.info(
"Для массовой активации получено %s ключ(ей) от %s",
len(keys),
describe_user(message.from_user),
)
checking = await message.answer(
f"Найдено ключей: {len(keys)}\nНачинаю активацию..."
)
try:
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])
)
except Exception as error:
logger.exception(
"Ошибка при массовой активации ключей от %s",
describe_user(message.from_user),
)
await checking.edit_text(f"Ошибка: {error}")
await state.clear()
+173
View File
@@ -0,0 +1,173 @@
import asyncio
from aiogram import Router
from aiogram.types import CallbackQuery
from aiogram.fsm.context import FSMContext
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
from bot.services.twofa import stop_twofa_task
from bot.ui.formatters import get_asf_status_text, get_asf_status
from bot.ui.keyboards import back_keyboard, confirm_action_keyboard, main_keyboard
router = Router()
logger = get_logger(__name__)
@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)
await state.clear()
await stop_twofa_task(callback.message.message_id)
await callback.answer()
try:
await callback.message.edit_text(
get_asf_status_text(callback.from_user.id),
reply_markup=main_keyboard(callback.from_user.id),
)
logger.info("Пользователь возвращен в главное меню: %s", user)
except Exception as error:
logger.exception("Не удалось вернуть пользователя в главное меню: %s", user)
await callback.message.edit_text(f"Ошибка: {error}")
@router.callback_query(lambda c: c.data == "refresh", AdminCallbackFilter())
async def refresh_handler(callback: CallbackQuery) -> None:
user = describe_user(callback.from_user.id)
logger.info("Запрошено обновление главного экрана: %s", user)
await callback.answer()
try:
await callback.message.edit_text(
get_asf_status_text(callback.from_user.id),
reply_markup=main_keyboard(callback.from_user.id),
)
logger.info("Главный экран обновлен: %s", user)
except Exception as error:
logger.exception("Не удалось обновить главный экран: %s", user)
await callback.message.edit_text(f"Ошибка: {error}")
@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)
await callback.answer()
try:
await callback.message.delete()
logger.info("Сообщение удалено: %s", user)
except Exception:
logger.exception("Не удалось удалить сообщение: %s", user)
@router.callback_query(lambda c: c.data == "restart_asf_confirm", AdminCallbackFilter())
async def restart_asf_confirm(callback: CallbackQuery) -> None:
user = describe_user(callback.from_user.id)
logger.info("Открыто подтверждение перезапуска ASF: %s", user)
await callback.answer()
await callback.message.edit_text(
"♻ Точно перезапустить ASF?\n\nВо время перезапуска IPC будет недоступен несколько секунд.",
reply_markup=confirm_action_keyboard("restart_asf", "admin_panel"),
)
@router.callback_query(lambda c: c.data == "restart_asf", AdminCallbackFilter())
async def restart_asf(callback: CallbackQuery) -> None:
user = describe_user(callback.from_user.id)
logger.info("Подтвержден перезапуск ASF: %s", user)
await callback.answer()
try:
await callback.message.edit_text("♻ ASF перезапускается...")
response = restart_asf_request()
if response.status_code != 200:
logger.warning(
"Перезапуск ASF завершился ошибкой HTTP: %s status=%s",
user,
response.status_code,
)
await callback.message.edit_text(
f"Ошибка HTTP {response.status_code}",
reply_markup=main_keyboard(callback.from_user.id),
)
return
# Проверяем статус ASF до его запуска, не ожидая фиксированные 6 секунд.
status = get_asf_status()
await asyncio.sleep(1)
while status.status_code != 200:
status = get_asf_status()
await asyncio.sleep(1)
await callback.message.edit_text(
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)
except Exception as error:
logger.exception("Не удалось перезапустить ASF: %s", user)
await callback.message.edit_text(
f"Ошибка: {error}",
reply_markup=main_keyboard(callback.from_user.id),
)
@router.callback_query(lambda c: c.data == "plugins", AdminCallbackFilter())
async def plugins_handler(callback: CallbackQuery) -> None:
user = describe_user(callback.from_user.id)
logger.info("Открыт список плагинов ASF: %s", user)
await stop_twofa_task(callback.message.message_id)
await callback.answer()
try:
response = get_plugins()
if response.status_code != 200:
logger.warning(
"Не удалось получить список плагинов: %s status=%s",
user,
response.status_code,
)
await callback.message.edit_text("Ошибка API")
return
data = response.json()
if not data.get("Success"):
logger.warning("ASF вернул ошибку при получении плагинов: %s", user)
await callback.message.edit_text("ASF вернул ошибку")
return
plugins = data.get("Result", [])
if not plugins:
logger.info("Плагины ASF не установлены: %s", user)
text = "Плагины не установлены"
else:
logger.info("Получен список плагинов ASF: %s count=%s", user, len(plugins))
text = "<b>📁 Плагины ASF:</b>\n\n"
for plugin in plugins:
text += (
f"{plugin.get('Name', '???')} ({plugin.get('Version', '???')})\n"
)
await callback.message.edit_text(text.strip(), reply_markup=back_keyboard())
except Exception as error:
logger.exception("Не удалось отобразить список плагинов: %s", user)
await callback.message.edit_text(f"Ошибка: {error}")
+21
View File
@@ -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"👤 <b>Профиль</b>\n\n"
f"🆔: <code>{telegram_id}</code>\n"
f"💰 Баланс: <code>{balance}</code> ₽\n"
f"🤖 Ботов: <code>{bot_count}</code>",
reply_markup=back_keyboard(),
)
+58
View File
@@ -0,0 +1,58 @@
from aiogram import Router
from aiogram.types import CallbackQuery
from aiogram.fsm.context import FSMContext
from bot.logging_utils import describe_user, get_logger
from bot.services.bot_accounts import get_owned_bot
from bot.states import RedeemFlow
from bot.ui.keyboards import back_keyboard
router = Router()
logger = get_logger(__name__)
@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)
await callback.answer()
await callback.message.edit_text(
"Отправьте ключ Steam для активации\n\n"
"Можно отправить сразу несколько ключей.\n\n"
"Пример:\n"
"<code>AAAAA-BBBBB-CCCCC\nDDDDD-EEEEE-FFFFF</code>",
reply_markup=back_keyboard(),
)
await state.set_state(RedeemFlow.waiting_all_keys)
await state.update_data(menu_message_id=callback.message.message_id)
@router.callback_query(lambda c: c.data and c.data.startswith("redeem_bot_"))
async def redeem_bot_menu(callback: CallbackQuery, state: FSMContext) -> None:
user = describe_user(callback.from_user.id)
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
)
await callback.answer()
await callback.message.edit_text(
f"Отправьте ключ Steam для активации на аккаунте:\n<code>{bot_name}</code>\n\n"
"Можно отправить сразу несколько ключей.",
reply_markup=back_keyboard(f"bot_{account.id}"),
)
await state.set_state(RedeemFlow.waiting_bot_keys)
await state.update_data(
bot_name=bot_name, account_id=account.id, menu_message_id=callback.message.message_id
)
+38
View File
@@ -0,0 +1,38 @@
from aiogram import Router
from aiogram.types import Message
from aiogram.filters import Command
from bot.services.users import upsert_user_from_telegram
from bot.ui.keyboards import main_keyboard
from bot.ui.formatters import get_asf_status_text
from bot.logging_utils import describe_user, get_logger
router = Router()
logger = get_logger(__name__)
@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)
logger.info("Получена команда /start: %s", user)
try:
await message.delete()
logger.info("Сообщение /start удалено: %s", user)
except Exception:
logger.exception("Не удалось удалить сообщение /start: %s", user)
try:
await message.answer(
get_asf_status_text(message.from_user.id),
reply_markup=main_keyboard(message.from_user.id),
)
logger.info("Главное меню отправлено: %s", user)
except Exception as error:
logger.exception("Не удалось отправить главное меню: %s", user)
await message.answer(f"Ошибка: {error}")
+207
View File
@@ -0,0 +1,207 @@
import asyncio
from aiogram import Router
from aiogram.types import CallbackQuery
from bot.states import twofa_tasks
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
from bot.ui.keyboards import back_keyboard, confirmations_keyboard
router = Router()
logger = get_logger(__name__)
@router.callback_query(
lambda c: c.data
and c.data.startswith("2fa_")
and not c.data.startswith("2fa_refresh_"),
)
async def twofa_handler(callback: CallbackQuery) -> None:
await callback.answer()
if not callback.data:
logger.debug(
"Колбэк открытия 2FA пришел без data от %s",
describe_user(callback.from_user),
)
return
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",
describe_user(callback.from_user),
bot_name,
)
await stop_twofa_task(message_id)
await callback.message.edit_text(
f"🔐 {bot_name}\n\nЗагрузка 2FA...",
reply_markup=back_keyboard(f"bot_{account.id}"),
)
task = asyncio.create_task(auto_update_2fa(callback.message, bot_name, account.id))
twofa_tasks[message_id] = task
logger.info(
"Запущено фоновое обновление 2FA для бота %s в сообщении %s",
bot_name,
message_id,
)
@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_"),
)
async def confirm_trades(callback: CallbackQuery) -> None:
await callback.answer()
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),
bot_name,
)
try:
status = await accept_confirmations(bot_name)
if status != 200:
logger.warning(
"Не удалось подтвердить сделки для бота %s: HTTP %s",
bot_name,
status,
)
await callback.answer("Ошибка HTTP", show_alert=True)
return
logger.info("Сделки для бота %s подтверждены", bot_name)
await callback.answer("Подтверждено", show_alert=True)
await twofa_handler(callback)
except Exception as error:
logger.exception(
"Ошибка при подтверждении сделок для бота %s",
bot_name,
)
await callback.message.edit_text(f"Ошибка: {error}")
@router.callback_query(
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)
await callback.answer()
if not callback.data:
logger.debug(
"Колбэк списка подтверждений пришел без data от %s",
describe_user(callback.from_user),
)
return
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),
bot_name,
)
try:
confirmations = await get_confirmations(bot_name)
if not confirmations:
logger.info("Для бота %s подтверждений не найдено", bot_name)
text = "Подтверждений нет"
else:
logger.info(
"Для бота %s найдено %s подтверждени(й)",
bot_name,
len(confirmations),
)
text = f"🔐 {bot_name}\n\nПодтверждения:\n\n"
for conf in confirmations:
text += f"- {conf['type_name']}\nID: <code>{conf['id']}</code>\n\n"
await callback.message.edit_text(
text, reply_markup=confirmations_keyboard(account.id)
)
except Exception as error:
logger.exception(
"Ошибка при загрузке списка подтверждений для бота %s",
bot_name,
)
await callback.message.edit_text(f"Ошибка: {error}")
@router.callback_query(
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)
await callback.answer()
if not callback.data:
logger.debug(
"Колбэк подтверждения всех сделок пришел без data от %s",
describe_user(callback.from_user),
)
return
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),
bot_name,
)
try:
status = await accept_confirmations(bot_name)
if status != 200:
logger.warning(
"Не удалось подтвердить все сделки для бота %s: HTTP %s",
bot_name,
status,
)
await callback.answer("Ошибка HTTP", show_alert=True)
return
confirmations = await get_confirmations(bot_name)
if not confirmations:
logger.info("После подтверждения у бота %s не осталось сделок", bot_name)
text = f"🔐 {bot_name}\n\nПодтверждений больше нет"
else:
logger.info(
"После подтверждения у бота %s осталось %s подтверждени(й)",
bot_name,
len(confirmations),
)
text = f"🔐 {bot_name}\n\nПодтверждения:\n\n"
for conf in confirmations:
text += f"- {conf['type_name']}\nID: <code>{conf['id']}</code>\n\n"
await callback.message.edit_text(
text, reply_markup=confirmations_keyboard(account.id)
)
await callback.answer("Все подтверждено", show_alert=True)
except Exception as error:
logger.exception("Ошибка при подтверждении всех сделок для бота %s", bot_name)
await callback.message.edit_text(f"Ошибка: {error}")
+352
View File
@@ -0,0 +1,352 @@
import asyncio
import hashlib
import io
import json
import zipfile
from dataclasses import dataclass
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.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.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] = {}
@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)
balance = await get_user_balance(callback.from_user.id)
slot_price = await get_bot_slot_price()
await callback.message.edit_text(
"Отправьте документы .json или .zip с JSON-конфигурациями.\n\n"
f"💰 Ваш баланс: {balance}\n"
f"💳 Цена одного слота: {slot_price}",
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 должен содержать объект конфигурации")
name = value.get("SteamLogin")
if not isinstance(name, str) or not name.strip():
raise ValueError("отсутствует непустой SteamLogin")
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():
continue
if not name.lower().endswith(".json"):
raise ValueError(f"ZIP содержит неподдерживаемый файл: {name}")
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()
@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)
name = config["SteamLogin"].strip()
digest = _digest(config)
parsed.append((filename, config, digest))
except (ValueError, json.JSONDecodeError, UnicodeDecodeError) as error:
logger.warning(
"Предварительная проверка отклонена: пользователь=%s файл=%s ошибка=%s",
user_id,
filename,
error,
)
errors.append(f"{filename}: {error}")
except Exception as error:
logger.exception(
"Ошибка предварительной проверки: пользователь=%s файл=%s",
user_id,
filename,
)
errors.append(f"{filename}: ошибка проверки ({error})")
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
for filename, config, digest in parsed:
name = config["SteamLogin"].strip()
try:
if name_counts[name] > 1:
raise ValueError("дублируется имя бота в этой загрузке")
if hash_counts[digest] > 1 or await user_hash_exists(user_id, digest):
raise ValueError(
"такая конфигурация уже загружалась или дублируется в загрузке"
)
if await bot_name_exists(name):
raise ValueError("бот с таким именем уже есть в базе данных")
existing = get_bot(name)
if existing.status_code == 200 and existing.json().get("Result"):
raise ValueError("бот с таким именем уже есть в ASF")
candidates.append(_Candidate(filename, config, digest))
logger.info(
"Предварительная проверка пройдена: пользователь=%s файл=%s бот=%s",
user_id,
filename,
name,
)
except (ValueError, json.JSONDecodeError, UnicodeDecodeError) as error:
logger.warning(
"Предварительная проверка отклонена: пользователь=%s файл=%s ошибка=%s",
user_id,
filename,
error,
)
errors.append(f"{filename}: {error}")
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((filename, await _download(message)))
except Exception:
files.append((filename, b""))
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
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
try:
raw = await _download(message)
if filename.lower().endswith(".zip"):
files = _zip_entries(raw)
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("❌ Поддерживаются только документы .json и .zip")
except (zipfile.BadZipFile, ValueError) as error:
await message.answer(f"{filename}: {error}")
finally:
await state.clear()
@router.message(UploadFlow.waiting_upload)
async def upload_non_document(message: Message, state: FSMContext) -> None:
await message.answer(
"❌ Отправьте конфигурацию именно документом .json или архивом .zip"
)
+14
View File
@@ -0,0 +1,14 @@
import logging
from typing import Any
def get_logger(name: str) -> logging.Logger:
return logging.getLogger(name)
def describe_user(user: Any) -> str:
if user is None:
return "пользователь=<unknown>"
user_id = getattr(user, "id", user)
return f"пользователь={user_id}"
+1
View File
@@ -0,0 +1 @@
+169
View File
@@ -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("Сумма должна быть положительным целым числом")
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_rubles=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_rubles) 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("Только администратор может изменять баланс другого пользователя")
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_rubles=amount)
)
await session.commit()
return int(
(
await session.scalar(
select(User.balance_rubles).where(User.id == user.id)
)
)
or 0
)
async def increase_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("Только администратор может изменять баланс другого пользователя")
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_rubles=User.balance_rubles + amount)
)
await session.commit()
return int(
(
await session.scalar(
select(User.balance_rubles).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("Только администратор может изменять баланс другого пользователя")
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_rubles >= amount)
.values(balance_rubles=User.balance_rubles - amount)
)
if result.rowcount != 1:
await session.rollback()
raise InsufficientBalance("Недостаточно рублей на балансе")
await session.commit()
return int(
(
await session.scalar(
select(User.balance_rubles).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_rubles), int(count or 0)
+101
View File
@@ -0,0 +1,101 @@
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 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(
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:
"""Показывает администратору ботов ASF, созданных до добавления владельцев."""
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,
)
+142
View File
@@ -0,0 +1,142 @@
import asyncio
from bot.api.asf import get_all_bots, get_bot, save_bot_config, send_command
from bot.logging_utils import get_logger
logger = get_logger(__name__)
RELOAD_POLL_INTERVAL_SECONDS = 1
RELOAD_TIMEOUT_SECONDS = 20
_reloading_bots: set[str] = set()
def is_bot_reloading(bot_name: str) -> bool:
return bot_name in _reloading_bots
def _is_bot_loaded_and_online(bot_name: str) -> bool:
response = get_all_bots()
if response.status_code != 200:
return False
data = response.json()
bot = data.get("Result", {}).get(bot_name, {})
return bool(
bot.get("BotName")
and bot.get("SteamID")
and bot.get("IsConnectedAndLoggedOn")
)
def is_bot_playing(bot_name: str) -> bool:
logger.info("Проверка, играет ли бот: bot=%s", bot_name)
response = get_all_bots()
if response.status_code != 200:
logger.warning(
"Не удалось проверить состояние игры бота: bot=%s status=%s",
bot_name,
response.status_code,
)
return False
data = response.json()
bot = data.get("Result", {}).get(bot_name, {})
playing = bool(bot.get("PlayingBlocked", False)) or bool(
bot.get("CurrentGamesPlayed", [])
)
logger.info("Состояние игры бота определено: bot=%s playing=%s", bot_name, playing)
return playing
def is_idle_enabled(bot_name: str, app_id: int) -> bool:
logger.info("Проверка статуса idle-игры: bot=%s app_id=%s", bot_name, app_id)
response = get_bot(bot_name)
if response.status_code != 200:
logger.warning(
"Не удалось получить конфиг бота для проверки idle: bot=%s status=%s",
bot_name,
response.status_code,
)
return False
data = response.json()
bot = data.get("Result", {}).get(bot_name, {})
config = bot.get("BotConfig", {})
enabled = app_id in config.get("GamesPlayedWhileIdle", [])
logger.info(
"Статус idle-игры определен: bot=%s app_id=%s enabled=%s",
bot_name,
app_id,
enabled,
)
return enabled
def toggle_idle_game(config: dict, app_id: int) -> tuple[dict, bool]:
idle_games = config.get("GamesPlayedWhileIdle", [])
if app_id in idle_games:
idle_games.remove(app_id)
enabled = False
else:
idle_games.append(app_id)
enabled = True
config["GamesPlayedWhileIdle"] = idle_games
return config, enabled
async def reload_bot(bot_name: str) -> None:
_reloading_bots.add(bot_name)
try:
logger.info("Отправка команды перезагрузки бота: bot=%s", bot_name)
send_command(f"!reload {bot_name}")
for _ in range(RELOAD_TIMEOUT_SECONDS):
await asyncio.sleep(RELOAD_POLL_INTERVAL_SECONDS)
if _is_bot_loaded_and_online(bot_name):
logger.info("Перезагрузка бота завершена: bot=%s", bot_name)
return
logger.warning("Истекло время ожидания перезагрузки бота: bot=%s", bot_name)
except Exception:
logger.exception(
"Не удалось отправить команду перезагрузки бота: bot=%s", bot_name
)
finally:
_reloading_bots.discard(bot_name)
async def update_idle_game(bot_name: str, app_id: int) -> tuple[bool, str, bool]:
logger.info("Изменение настройки idle-игры: bot=%s app_id=%s", bot_name, app_id)
response = get_bot(bot_name)
if response.status_code != 200:
logger.warning(
"Не удалось загрузить конфиг бота для idle: bot=%s status=%s",
bot_name,
response.status_code,
)
return False, "Ошибка API", False
data = response.json()
bot_data = data.get("Result", {}).get(bot_name, {})
config = bot_data.get("BotConfig", {})
config, enabled = toggle_idle_game(config, app_id)
save = save_bot_config(bot_name, config)
if save.status_code != 200:
logger.warning(
"Не удалось сохранить конфиг idle-игры: bot=%s app_id=%s status=%s",
bot_name,
app_id,
save.status_code,
)
return False, "Ошибка сохранения", enabled
_reloading_bots.add(bot_name)
asyncio.create_task(reload_bot(bot_name))
action = "Накрутка часов включена" if enabled else "Накрутка часов выключена"
logger.info(
"Настройка idle-игры изменена: bot=%s app_id=%s enabled=%s",
bot_name,
app_id,
enabled,
)
return True, action, enabled
+143
View File
@@ -0,0 +1,143 @@
from bot.api.asf import get_bot_inventory
from bot.constants import PAGE_SIZE
from bot.logging_utils import get_logger
from bot.ui.formatters import get_inventory_icon
from bot.ui.keyboards import inventory_menu_keyboard, inventory_page_keyboard
logger = get_logger(__name__)
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)
cs2_assets = []
steam_assets = []
try:
cs2_assets = cs2_inventory.get(bot_name, {}).get("Assets", [])
except Exception:
logger.exception("Ошибка чтения CS2-инвентаря: bot=%s", bot_name)
try:
steam_assets = steam_inventory.get(bot_name, {}).get("Assets", [])
except Exception:
logger.exception("Ошибка чтения Steam-инвентаря: bot=%s", bot_name)
if not cs2_assets and not steam_assets:
logger.info("Инвентарь пуст: bot=%s", bot_name)
return (
f"📦 Инвентарь {bot_name} пуст",
inventory_menu_keyboard(account_id, 0, 0),
)
logger.info(
"Меню инвентаря сформировано: bot=%s cs2_items=%s steam_items=%s",
bot_name,
len(cs2_assets),
len(steam_assets),
)
text = (
f"📦 Инвентарь {bot_name}\n\n"
"🔄 — можно трейдить\n"
"💰 — можно продавать\n"
"🔒 — нельзя продавать\n"
"❌ — нельзя трейдить"
)
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, account_id: int
) -> tuple[str, object] | tuple[None, None]:
logger.info(
"Формирование страницы инвентаря: bot=%s type=%s appid=%s contextid=%s page=%s",
bot_name,
inventory_type,
appid,
contextid,
page,
)
inventory = await get_bot_inventory(bot_name, appid, contextid)
if not inventory:
logger.warning(
"Не удалось получить данные для страницы инвентаря: bot=%s type=%s page=%s",
bot_name,
inventory_type,
page,
)
return None, None
bot_inventory = inventory.get(bot_name, {})
assets = bot_inventory.get("Assets", [])
descriptions = bot_inventory.get("Descriptions", [])
if not assets:
logger.info(
"Инвентарь выбранного типа пуст: bot=%s type=%s", bot_name, inventory_type
)
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")))
desc_map[key] = desc
total_pages = (len(assets) + PAGE_SIZE - 1) // PAGE_SIZE
start = page * PAGE_SIZE
end = start + PAGE_SIZE
page_assets = assets[start:end]
lines = []
marketable_count = 0
tradable_count = 0
cards_count = 0
emotes_count = 0
backgrounds_count = 0
gems_count = 0
for asset in page_assets:
key = (str(asset.get("classid")), str(asset.get("instanceid")))
desc = desc_map.get(key, {})
name = desc.get("market_name", "Неизвестно")
amount = asset.get("amount", 1)
tradable = "🔄" if desc.get("tradable") else ""
marketable = "💰" if desc.get("marketable") else "🔒"
icon = get_inventory_icon(desc)
if desc.get("marketable"):
marketable_count += 1
if desc.get("tradable"):
tradable_count += 1
if icon == "🎏":
cards_count += amount
elif icon == "😀":
emotes_count += amount
elif icon == "🖼":
backgrounds_count += amount
elif icon == "💎":
gems_count += amount
lines.append(f"{icon} {tradable}{marketable} {name} x{amount}")
inv_name = "CS2" if appid == 730 else "Steam"
stats = (
f"📦 Предметов: {len(assets)}\n"
f"💰 Можно продать: {marketable_count}\n"
f"🔄 Можно трейдить: {tradable_count}"
)
if appid == 753:
stats += (
f"\n🎏 Карточек: {cards_count}"
f"\n😀 Смайлов: {emotes_count}"
f"\n🖼 Фонов: {backgrounds_count}"
f"\n💎 Самоцветов: {gems_count}"
)
text = (
f"📦 Инвентарь {inv_name} {bot_name}\n"
f"📄 Страница {page + 1}/{total_pages}\n\n"
f"{stats}\n\n" + "\n".join(lines)
)
logger.info(
"Страница инвентаря сформирована: bot=%s type=%s page=%s total_pages=%s items_on_page=%s",
bot_name,
inventory_type,
page,
total_pages,
len(page_assets),
)
return text[:4000], inventory_page_keyboard(
inventory_type, account_id, page, total_pages
)
+211
View File
@@ -0,0 +1,211 @@
import asyncio
import re
from bot.logging_utils import get_logger
from bot.api.asf import get_all_bots, redeem_key
KEY_PATTERN = r"[A-Z0-9]{5}(?:-[A-Z0-9]{5}){2}"
logger = get_logger(__name__)
def extract_keys(text: str) -> list[str]:
keys = re.findall(KEY_PATTERN, text.upper())
unique_keys = list(dict.fromkeys(keys))
logger.info("Из текста извлечены ключи: count=%s", len(unique_keys))
return unique_keys
def parse_redeem_result(result: str, bot_name: str | None = None) -> str:
result_lower = result.lower()
if bot_name:
for line in result.splitlines():
if f"<{bot_name.lower()}>" in line.lower():
result_lower = line.lower()
break
if "ok/nodetail" in result_lower or "ok/" in result_lower:
return "success"
if "ratelimited" in result_lower:
return "rate_limited"
if "alreadypurchased" in result_lower or "already purchased" in result_lower:
return "already_owned"
if "duplicateactivationcode" in result_lower:
return "duplicate"
if "regionlocked" in result_lower:
return "region_locked"
if "invalidactivationcode" in result_lower or "invalid" in result_lower:
return "invalid"
return "unknown"
async def redeem_single_bot_keys(bot_name: str, keys: list[str]) -> str:
logger.info(
"Начата активация ключей для одного бота: bot=%s count=%s",
bot_name,
len(keys),
)
success_count = 0
failed_count = 0
results = []
for key in keys:
logger.info(
"Попытка активации ключа для одного бота: bot=%s key=%s",
bot_name,
key,
)
success, result = await redeem_key(bot_name, key)
await asyncio.sleep(2)
if not success or not result:
failed_count += 1
logger.warning(
"Активация ключа завершилась ошибкой ASF/HTTP: bot=%s key=%s",
bot_name,
key,
)
results.append(f"{key}: ошибка ASF")
continue
status = parse_redeem_result(result)
logger.info(
"Получен результат активации ключа: bot=%s key=%s status=%s",
bot_name,
key,
status,
)
if status == "success":
success_count += 1
results.append(f"{key}: активировано")
elif status == "rate_limited":
failed_count += 1
results.append(f"{key}: лимит запросов")
elif status == "already_owned":
failed_count += 1
results.append(f"⚠️ {key}: игра уже есть")
elif status == "duplicate":
failed_count += 1
results.append(f"{key}: ключ уже использован")
elif status == "region_locked":
failed_count += 1
results.append(f"🌍 {key}: регион лок")
elif status == "invalid":
failed_count += 1
results.append(f"{key}: неверный ключ")
else:
failed_count += 1
results.append(f"{key}: неизвестный ответ")
text = (
f"Результат активации для <code>{bot_name}</code>\n\n"
f"✅ Успешно: {success_count}\n"
f"❌ Ошибок: {failed_count}\n\n" + "\n".join(results[:50])
)
logger.info(
"Завершена активация ключей для одного бота: bot=%s success=%s failed=%s",
bot_name,
success_count,
failed_count,
)
return text[:4000] + ("\n\n...обрезано" if len(text) > 4000 else "")
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())
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
results = []
for key in keys:
activated = False
key_report = []
logger.info("Начата обработка ключа по всем ботам: key=%s", key)
for bot_name in bots:
logger.info(
"Попытка активации ключа на боте: bot=%s key=%s",
bot_name,
key,
)
success, result = await redeem_key(bot_name, key)
await asyncio.sleep(2)
if not success or not result:
logger.warning(
"Активация ключа завершилась ошибкой ASF/HTTP: bot=%s key=%s",
bot_name,
key,
)
key_report.append(f"{bot_name}: ошибка ASF")
continue
status = parse_redeem_result(result, bot_name)
logger.info(
"Получен результат активации ключа: bot=%s key=%s status=%s",
bot_name,
key,
status,
)
if status == "success":
key_report.append(f"{bot_name}: активировано")
success_count += 1
activated = True
break
if status == "rate_limited":
key_report.append(f"{bot_name}: лимит запросов")
continue
if status == "already_owned":
key_report.append(f"⚠️ {bot_name}: игра уже есть")
continue
if status == "duplicate":
key_report.append(f"{bot_name}: ключ уже использован")
break
if status == "region_locked":
key_report.append(f"🌍 {bot_name}: регион лок")
continue
if status == "invalid":
key_report.append(f"{bot_name}: неверный ключ")
break
key_report.append(f"{bot_name}: неизвестный ответ")
if not activated:
failed_count += 1
logger.warning(
"Ключ не удалось активировать ни на одном боте: key=%s",
key,
)
results.append(f"\n🔑 {key}\n" + "\n".join(key_report))
text = (
"Результат активации\n\n"
f"✅ Успешно: {success_count}\n"
f"❌ Ошибок: {failed_count}\n\n" + "\n".join(results[:50])
)
logger.info(
"Завершена массовая активация ключей: success=%s failed=%s",
success_count,
failed_count,
)
return text[:4000] + ("\n\n...обрезано" if len(text) > 4000 else "")
+90
View File
@@ -0,0 +1,90 @@
import asyncio
from bot.states import twofa_tasks
from bot.logging_utils import get_logger
from bot.ui.keyboards import twofa_keyboard
from bot.api.asf import get_2fa_token, get_confirmations
logger = get_logger(__name__)
async def stop_twofa_task(message_id: int) -> None:
task = twofa_tasks.pop(message_id, None)
if task:
logger.info(
"Остановка фоновой задачи обновления 2FA: message_id=%s", message_id
)
task.cancel()
try:
await task
except Exception:
logger.exception(
"Ошибка при завершении фоновой задачи обновления 2FA: message_id=%s",
message_id,
)
async def auto_update_2fa(message, bot_name: str, account_id: int) -> None:
last_code = None
message_id = message.message_id
logger.info(
"Запуск фонового обновления 2FA: bot=%s message_id=%s",
bot_name,
message_id,
)
try:
while True:
code = await get_2fa_token(bot_name)
if code != last_code:
confirmations = await get_confirmations(bot_name)
text = f"🔐 {bot_name}\n\n<code>{code}</code>"
try:
await message.edit_text(
text,
reply_markup=twofa_keyboard(account_id, bool(confirmations)),
)
last_code = code
logger.info(
"Сообщение 2FA обновлено: bot=%s message_id=%s confirmations=%s",
bot_name,
message_id,
len(confirmations),
)
except Exception:
logger.exception(
"Не удалось обновить сообщение 2FA: bot=%s message_id=%s",
bot_name,
message_id,
)
break
await asyncio.sleep(15)
except asyncio.CancelledError:
logger.info(
"Фоновое обновление 2FA отменено: bot=%s message_id=%s",
bot_name,
message_id,
)
except Exception as error:
logger.exception(
"Фоновое обновление 2FA завершилось ошибкой: bot=%s message_id=%s error=%s",
bot_name,
message_id,
error,
)
finally:
twofa_tasks.pop(message_id, None)
logger.info(
"Фоновая задача обновления 2FA завершена: bot=%s message_id=%s",
bot_name,
message_id,
)
+30
View File
@@ -0,0 +1,30 @@
from aiogram.types import User as TelegramUser
from sqlalchemy import select
from bot.config import ADMIN_ID
from bot.db.models import User
from bot.db.session import async_session
async def upsert_user_from_telegram(telegram_user: TelegramUser | None) -> User | None:
if telegram_user is None:
return None
async with async_session() as session:
result = await session.execute(
select(User).where(User.telegram_id == telegram_user.id)
)
user = result.scalar_one_or_none()
if user is None:
user = User(telegram_id=telegram_user.id)
session.add(user)
user.username = telegram_user.username
user.first_name = telegram_user.first_name
user.last_name = telegram_user.last_name
user.is_admin = telegram_user.id == ADMIN_ID
await session.commit()
await session.refresh(user)
return user
+22
View File
@@ -0,0 +1,22 @@
from aiogram.fsm.state import State, StatesGroup
class ConsoleFlow(StatesGroup):
waiting_command = State()
class RedeemFlow(StatesGroup):
waiting_all_keys = State()
waiting_bot_keys = State()
class UploadFlow(StatesGroup):
waiting_upload = State()
class AdminFlow(StatesGroup):
waiting_user_id = State()
waiting_amount = State()
twofa_tasks = {}
+1
View File
@@ -0,0 +1 @@
+220
View File
@@ -0,0 +1,220 @@
import html
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
def get_currency_name(currency_id: int) -> str:
if currency_id == 1:
return "USD"
if currency_id == 3:
return "EUR"
if currency_id == 5:
return "RUB"
if currency_id == 18:
return "UAH"
if currency_id == 6:
return "PLN"
if currency_id == 0:
return ""
return "???"
def get_inventory_icon(desc: dict) -> str:
tags = desc.get("tags", [])
for tag in tags:
category = tag.get("category", "")
name = tag.get("localized_tag_name", "")
if category == "item_class":
if "Trading Card" in name:
return "🎏"
elif "Emoticon" in name:
return "😀"
elif "Profile Background" in name:
return "🖼"
elif "Booster Pack" in name:
return "📦"
elif "Steam Gems" in name:
return "💎"
if "Gift" in name:
return "🎁"
item_type = desc.get("type", "").lower()
if "knife" in item_type:
return "🔪"
elif "pistol" in item_type:
return "🔫"
elif "rifle" in item_type:
return "🎯"
elif "graffiti" in item_type:
return "🎨"
elif "music kit" in item_type:
return "🎵"
return "📦"
def get_uptime(start_time_str: str) -> str:
start_time_str = start_time_str[:26] + "Z"
start_time = datetime.fromisoformat(start_time_str.replace("Z", "+00:00"))
now = datetime.now(timezone.utc)
delta = now - start_time
days = delta.days
hours, remainder = divmod(delta.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
days_measure = f"{days}д" if days > 0 else ""
hours_measure = f" {hours}ч" if hours > 0 else ""
minutes_measure = f" {minutes}м" if minutes > 0 else ""
seconds_measure = f" {seconds}с" if seconds > 0 else ""
return f"{days_measure}{hours_measure}{minutes_measure}{seconds_measure}"
def format_asf(data: dict, 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 user_id == ADMIN_ID:
if current_version != latest_version:
lines.append(f"Доступно обновление: {latest_version}")
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)
def get_bot_status_icon(bot: dict) -> str:
online = bot.get("IsConnectedAndLoggedOn", False)
farming = bot.get("CardsFarmer", {}).get("NowFarming", False)
idle_games = bot.get("BotConfig", {}).get("GamesPlayedWhileIdle", [])
loaded = bool(bot.get("BotName")) and bool(bot.get("SteamID"))
if not loaded:
return "🔄"
if farming:
return "🎴"
if idle_games and online:
return ""
if online:
return "🟢"
return "🔴"
def format_bot_ui(bot: dict) -> str:
online = get_bot_status_icon(bot)
now_farming = bot.get("CardsFarmer", {}).get("NowFarming")
status_line = "🎴 Идет фарм карточек" if now_farming else "🎴 Карточки не фармятся"
time = bot.get("CardsFarmer", {}).get("TimeRemaining", "00:00:00")
games_to_farm = len(bot.get("CardsFarmer", {}).get("GamesToFarm", []))
twofa = "есть" if bot.get("HasMobileAuthenticator") else "нет"
balance = bot.get("WalletBalance", 0) / 100
currency = get_currency_name(bot.get("WalletCurrency", 0))
steam_id = bot.get("SteamID")
nickname = html.escape(str(bot.get("Nickname") or ""))
redeem = bot.get("GamesToRedeemInBackgroundCount", 0)
profile_url = f"https://steamcommunity.com/profiles/{steam_id}"
text = (
f"{online} {bot.get('BotName') or 'Загрузка...'}"
f' (<a href="{profile_url}">{nickname}</a>)\n'
f"{status_line}\n"
)
idle_games = bot.get("BotConfig", {}).get("GamesPlayedWhileIdle", [])
if idle_games:
names = [GAMES.get(game, str(game)) for game in idle_games]
if bot.get("IsConnectedAndLoggedOn"):
idle_text = f"⌚ Накрутка часов: включена ({', '.join(names)})"
else:
idle_text = (
f"⌚ Накрутка часов: включена ({', '.join(names)}) "
"применится после запуска"
)
text += f"{idle_text}\n"
else:
text += "⌚ Накрутка часов: выключена\n"
text += (
f"🆔: <code>{steam_id}</code>\n"
f"🔐 2FA: {twofa}\n\n"
f"💰 Баланс: {balance:.2f} {currency}\n"
)
if redeem > 0:
text += f"\nКлючей в очереди: {redeem}\n"
if now_farming:
text += f"\nОсталось: {time}\n"
if games_to_farm > 0:
text += f"В очереди игр: {games_to_farm}\n"
return text
def get_asf_status_text(user_id: int) -> str:
response = get_asf_status()
if response.status_code != 200:
return f"Ошибка API: {response.status_code}"
data = response.json()
if not data.get("Success"):
return "ASF вернул ошибку"
return f"<b>💻 Панель управления ASF</b>\n\n{format_asf(data, user_id)}"
def get_farm_summary(bots: dict) -> str:
total_games = 0
total_time_seconds = 0
for bot in bots.values():
farmer = bot.get("CardsFarmer", {})
if farmer.get("NowFarming"):
games = farmer.get("CurrentGamesFarming", [])
total_games += len(games)
time_str = farmer.get("TimeRemaining", "00:00:00")
hours, minutes, seconds = map(int, time_str.split(":"))
total_time_seconds += hours * 3600 + minutes * 60 + seconds
if total_games == 0:
return "На аккаунтах нечего фармить"
hours = total_time_seconds // 3600
minutes = (total_time_seconds % 3600) // 60
return (
f"Игр фармится: {total_games}\n"
f"Осталось времени: {hours}ч {minutes}м\n"
f"Карт осталось: ~{total_games * 3}"
)
def format_bot(bot_data: dict) -> str:
return json.dumps(bot_data, indent=2, ensure_ascii=False)
+243
View File
@@ -0,0 +1,243 @@
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from bot.ui.formatters import get_bot_status_icon
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")],
]
if user_id == ADMIN_ID:
keyboard.insert(
0,
[InlineKeyboardButton(text="🆙 Обновить", callback_data="refresh")],
)
keyboard.extend(
[
[
InlineKeyboardButton(
text="🤵 Админ-панель", callback_data="admin_panel"
)
],
],
)
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def back_keyboard(callback_data: str = "back") -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="🔙 Назад", callback_data=callback_data)]
]
)
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|{account_id}|730")],
[InlineKeyboardButton(text="🔙 Назад", callback_data=f"bot_{account_id}")],
]
)
def bots_keyboard(bots: list[tuple[int, str, dict]]) -> InlineKeyboardMarkup:
keyboard = []
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_{account_id}" if loaded else "bot_loading"
keyboard.append(
[InlineKeyboardButton(text=f"{status} {name}", callback_data=callback_data)]
)
keyboard.extend(
[
[InlineKeyboardButton(text="⬆️ Загрузить конфиг", callback_data="upload")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="back")],
]
)
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def bot_details_keyboard(
account_id: int, has_mobile_authenticator: bool
) -> InlineKeyboardMarkup:
keyboard = []
if has_mobile_authenticator:
keyboard.append(
[InlineKeyboardButton(text="🔐 2FA", callback_data=f"2fa_{account_id}")]
)
keyboard.append(
[
InlineKeyboardButton(
text="⌚ Накрутка часов", callback_data=f"games_{account_id}"
)
]
)
keyboard.append(
[
InlineKeyboardButton(
text="🔑 Активировать ключ", callback_data=f"redeem_bot_{account_id}"
)
]
)
keyboard.append(
[
InlineKeyboardButton(
text="📦 Инвентарь", callback_data=f"inventory_{account_id}"
)
]
)
keyboard.append([InlineKeyboardButton(text="🔙 Назад", callback_data="bots")])
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def twofa_keyboard(account_id: int, has_confirmations: bool) -> InlineKeyboardMarkup:
keyboard = []
if has_confirmations:
keyboard.append(
[
InlineKeyboardButton(
text="Подтверждения", callback_data=f"confirm_list_{account_id}"
)
]
)
keyboard.append(
[InlineKeyboardButton(text="🔙 Назад", callback_data=f"bot_{account_id}")]
)
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def confirmations_keyboard(account_id: int) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="Подтвердить всё", callback_data=f"confirm_all_{account_id}"
)
],
[InlineKeyboardButton(text="🔙 Назад", callback_data=f"2fa_{account_id}")],
]
)
def confirm_action_keyboard(
confirm_data: str, cancel_data: str
) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="Да", callback_data=confirm_data)],
[InlineKeyboardButton(text="Нет", callback_data=cancel_data)],
]
)
def inventory_menu_keyboard(
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_{account_id}"
)
]
)
if steam_count:
keyboard.append(
[
InlineKeyboardButton(
text=f"Steam ({steam_count})",
callback_data=f"inv_steam_{account_id}",
)
]
)
keyboard.append(
[InlineKeyboardButton(text="🔙 Назад", callback_data=f"bot_{account_id}")]
)
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def inventory_page_keyboard(
inventory_type: str, account_id: int, page: int, total_pages: int
) -> InlineKeyboardMarkup:
nav = []
if page > 0:
nav.append(
InlineKeyboardButton(
text="",
callback_data=f"invpage|{inventory_type}|{account_id}|{page - 1}",
)
)
if page < total_pages - 1:
nav.append(
InlineKeyboardButton(
text="",
callback_data=f"invpage|{inventory_type}|{account_id}|{page + 1}",
)
)
keyboard = [nav] if nav else []
keyboard.append(
[InlineKeyboardButton(text="🔙 Назад", callback_data=f"inventory_{account_id}")]
)
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def console_keyboard() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_panel")]
]
)
def delete_message_keyboard() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="Удалить", callback_data="delete_msg")]
]
)
+52 -983
View File
File diff suppressed because it is too large Load Diff
+21 -1
View File
@@ -1,5 +1,25 @@
aiofiles==25.1.0
aiogram==3.24.0
aiohappyeyeballs==2.7.1
aiohttp==3.13.3
aiosignal==1.4.0
annotated-types==0.7.0
async-timeout==5.0.1
attrs==26.1.0
certifi==2026.7.22
charset-normalizer==3.4.9
frozenlist==1.8.0
idna==3.18
magic-filter==1.0.12
multidict==6.7.1
propcache==0.5.2
pydantic==2.12.5
pydantic_core==2.41.5
python-dotenv==1.2.1
requests==2.32.5
requests-file==3.0.1
typing-inspection==0.4.2
typing_extensions==4.16.0
urllib3==2.7.0
yarl==1.24.5
SQLAlchemy>=2.0,<3.0
aiosqlite>=0.20,<1.0
-4
View File
@@ -1,4 +0,0 @@
@echo off
title ASFTGCONTROLBOT
python main.py
pause