forked from FOSS/ASF-Control-Bot
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ff9b3cc8ef |
@@ -0,0 +1,4 @@
|
||||
API_TOKEN = "" #Токен бота Телеграм
|
||||
ADMIN_ID = 0 # Ваш Тг Ид
|
||||
ASF_URL = "http://127.0.0.1:1242/Api/ASF"
|
||||
ASF_PASSWORD = "0000" # Пароль от IPC ASF
|
||||
@@ -1,9 +0,0 @@
|
||||
#Токен бота Телеграм
|
||||
BOT_TOKEN=
|
||||
|
||||
# Ваш Тг Ид
|
||||
ADMIN_ID=
|
||||
|
||||
# конфиг ASF
|
||||
ASF_URL=http://127.0.0.1:1242
|
||||
ASF_PASSWORD=0000
|
||||
-193
@@ -1,193 +0,0 @@
|
||||
# ---> 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
|
||||
@@ -0,0 +1,14 @@
|
||||
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 +0,0 @@
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
-236
@@ -1,236 +0,0 @@
|
||||
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", ""))
|
||||
@@ -1,13 +0,0 @@
|
||||
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")
|
||||
@@ -1,5 +0,0 @@
|
||||
PAGE_SIZE = 40
|
||||
GAMES = {730: "CS2", 440: "TF2", 570: "Dota 2"}
|
||||
|
||||
appid = 753
|
||||
contextid = 6
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
@@ -1,8 +0,0 @@
|
||||
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)
|
||||
@@ -1,59 +0,0 @@
|
||||
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")
|
||||
@@ -1,6 +0,0 @@
|
||||
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)
|
||||
@@ -1,34 +0,0 @@
|
||||
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 +0,0 @@
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
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()
|
||||
@@ -1,144 +0,0 @@
|
||||
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),
|
||||
)
|
||||
@@ -1,25 +0,0 @@
|
||||
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()
|
||||
)
|
||||
@@ -1,61 +0,0 @@
|
||||
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)
|
||||
@@ -1,282 +0,0 @@
|
||||
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()
|
||||
@@ -1,173 +0,0 @@
|
||||
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}")
|
||||
@@ -1,21 +0,0 @@
|
||||
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(),
|
||||
)
|
||||
@@ -1,58 +0,0 @@
|
||||
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
|
||||
)
|
||||
@@ -1,38 +0,0 @@
|
||||
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}")
|
||||
@@ -1,207 +0,0 @@
|
||||
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}")
|
||||
@@ -1,352 +0,0 @@
|
||||
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"
|
||||
)
|
||||
@@ -1,14 +0,0 @@
|
||||
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 +0,0 @@
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
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)
|
||||
@@ -1,101 +0,0 @@
|
||||
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,
|
||||
)
|
||||
@@ -1,142 +0,0 @@
|
||||
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
|
||||
@@ -1,143 +0,0 @@
|
||||
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
|
||||
)
|
||||
@@ -1,211 +0,0 @@
|
||||
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 "")
|
||||
@@ -1,90 +0,0 @@
|
||||
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,
|
||||
)
|
||||
@@ -1,30 +0,0 @@
|
||||
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
|
||||
@@ -1,22 +0,0 @@
|
||||
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 +0,0 @@
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
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)
|
||||
@@ -1,243 +0,0 @@
|
||||
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")]
|
||||
]
|
||||
)
|
||||
+1
-21
@@ -1,25 +1,5 @@
|
||||
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
|
||||
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
|
||||
requests-file==3.0.1
|
||||
|
||||
Reference in New Issue
Block a user