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
|
aiogram==3.24.0
|
||||||
aiohappyeyeballs==2.7.1
|
|
||||||
aiohttp==3.13.3
|
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
|
python-dotenv==1.2.1
|
||||||
requests==2.32.5
|
requests==2.32.5
|
||||||
typing-inspection==0.4.2
|
requests-file==3.0.1
|
||||||
typing_extensions==4.16.0
|
|
||||||
urllib3==2.7.0
|
|
||||||
yarl==1.24.5
|
|
||||||
SQLAlchemy>=2.0,<3.0
|
|
||||||
aiosqlite>=0.20,<1.0
|
|
||||||
|
|||||||
Reference in New Issue
Block a user