Compare commits

...

9 Commits

18 changed files with 595 additions and 327 deletions
+5 -2
View File
@@ -1,15 +1,18 @@
# Обязательные поля
BOT_TOKEN=
ADMIN_ID=
CRYPTO_PAY_TOKEN=
CRYPTO_PAY_NETWORK=
CRYPTO_PAY_ASSET=USDT
# Бот для логгинга (логи будут в этом боте, можно указать один и тот же бот на главный и логгинга)
LOG_BOT_TOKEN=
# Данные шопа
SHOP_NAME=KILL UNONY MOM
SHOP_CHANNEL=https://t.me/username
BOT_USERNAME=username
SHOP_CHANNEL=
BOT_USERNAME=
# Директории и файлы
DATABASE_DIR=Users
+23 -52
View File
@@ -2,71 +2,42 @@
Telegram-бот для проверки Roblox-cookie, получения истории донатов, обновления оплачиваемых cookie и начисления средств продавцам через CryptoBot.
## Возможности
## Запуск через .bat (Windows)
- Прием cookie обычным текстом или `.txt`-файлом.
- Параллельная проверка cookie Roblox с поддержкой прокси.
- Расчет оплаты по lifetime- и yearly-ставкам.
- AVG-ценообразование для больших партий донатных cookie.
- Обновление cookie перед сохранением и выплатой.
- Балансы пользователей, вывод средств, реферальная система, статистика и блокировки.
- Админ-панель для управления ставками, магазином, рассылками, казной и пользователями.
Запусти `start.bat` двойным кликом. Скрипт автоматически:
## Требования
1. Создаст виртуальное окружение `.venv`
2. Установит зависимости из `requirements.txt`
3. Создаст `.env` из примера (если ещё нет)
4. Запустит бота
- Python 3.10 или новее.
- Токен Telegram-бота.
- API-токен CryptoBot для выплат и пополнения казны.
- Telegram ID администратора.
## Запуск через командную строку (вручную)
Установите зависимости в виртуальное окружение:
1. Установи Python `3.11+`.
2. Поставь зависимости:
```powershell
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install aiogram aiohttp requests curl-cffi
```bash
python3.11 -m pip install -r requirements.txt
```
В Linux или macOS вместо команды активации PowerShell используйте `source .venv/bin/activate`.
3. Создай `.env` по примеру:
## Настройка
Скопируйте [.env.example](.env.example) и укажите как минимум:
```text
BOT_TOKEN=токен_telegram_бота
ADMIN_ID=telegram_id_администратора
CRYPTO_PAY_TOKEN=api_токен_cryptobot
```bash
cp .env.example .env
```
Приложение получает переменные окружения через `config.py`. Файлы `.env` автоматически не загружаются, поэтому передавайте переменные через shell, менеджер процессов, контейнер или другой загрузчик окружения.
4. Заполни минимум:
Пример для PowerShell:
```powershell
$env:BOT_TOKEN = "токен_telegram_бота"
$env:ADMIN_ID = "123456789"
$env:CRYPTO_PAY_TOKEN = "api_токен_cryptobot"
python main.py
```env
BOT_TOKEN=
ADMIN_ID=
CRYPTO_PAY_TOKEN=
```
Основные локальные пути по умолчанию:
Конфигурация читается только из `.env` или переменных окружения.
- `Users/` - записи пользователей.
- `Cookies/` - временные файлы cookie.
- `bot_config.json` - ставки, казна и переключатели функций.
- `bot_stats.json` - общая статистика.
- `proxies.txt` - необязательный список прокси.
- `robsec.txt` - сохраненные обновленные cookie.
5. Запусти бота:
Эти runtime-файлы исключены из Git. Не добавляйте в репозиторий токены, CryptoBot-токены, Roblox-cookie, записи пользователей или учетные данные прокси.
## Запуск
Запустите polling:
```powershell
python main.py
```bash
python3 main.py
```
Пользователь может выполнить `/start`, отправить текст с cookie или загрузить `.txt`-файл. Администратор открывает панель управления командой `/admin`.
+18 -4
View File
@@ -1,9 +1,12 @@
import os
from dotenv import load_dotenv
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from models.config import BotConfig
from models.stats import BotStats
from models.config import BotConfig
BASE_DIR = Path(__file__).resolve().parent
@@ -19,6 +22,7 @@ def _env_int(name: str, default: int) -> int:
raw = os.getenv(name)
if raw is None or raw == "":
return default
return int(raw)
@@ -26,6 +30,7 @@ def _env_float(name: str, default: float) -> float:
raw = os.getenv(name)
if raw is None or raw == "":
return default
return float(raw)
@@ -33,19 +38,27 @@ 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"}
load_dotenv()
BOT_TOKEN = os.getenv("BOT_TOKEN", "")
LOG_BOT_TOKEN = os.getenv("LOG_BOT_TOKEN", "") or None
LOG_BOT_TOKEN = os.getenv("LOG_BOT_TOKEN", BOT_TOKEN) or None
ADMIN_ID = _env_int("ADMIN_ID", 0)
CRYPTO_PAY_TOKEN = os.getenv("CRYPTO_PAY_TOKEN", "")
CRYPTO_PAY_NETWORK = os.getenv("CRYPTO_PAY_NETWORK", "MAINNET")
CRYPTO_PAY_ASSET = os.getenv("CRYPTO_PAY_ASSET", "USDT")
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")
SHOP_CHANNEL = os.getenv("SHOP_CHANNEL", "")
BOT_USERNAME = os.getenv("BOT_USERNAME", "")
DATABASE_DIR = _resolve_path("DATABASE_DIR", "Users")
COOKIE_FILES_DIR = _resolve_path("COOKIE_FILES_DIR", "Cookies")
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")
@@ -56,6 +69,7 @@ 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)
+148 -69
View File
@@ -11,7 +11,7 @@ from aiogram.types import (
Message,
)
from config import ADMIN_ID, ROBSEC_FILE
from config import ADMIN_ID, CRYPTO_PAY_ASSET, ROBSEC_FILE
from db.storage import load_config
from db.users import get
from keyboards.admin import admin_menu_kb
@@ -37,7 +37,10 @@ from states.admin import Form
from runtime import get_bot
from utils.logging import safe_edit
from runtime import get_cp
router = Router(name="admin")
cp = get_cp()
def _admin(user_id: int) -> bool:
@@ -49,32 +52,42 @@ def _user_card(uid: int) -> tuple[str, InlineKeyboardMarkup] | None:
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'}"
"👤 <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"🍪 Cookie: {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 ''}"
)
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=" Баланс", callback_data=f"u_bal_add_{uid}"
),
InlineKeyboardButton(
text=" Баланс", 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")],
[
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")],
]
)
return text, keyboard
@@ -85,7 +98,9 @@ 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())
await message.answer(
"🛠 <b>Админ-панель</b>", parse_mode="HTML", reply_markup=admin_menu_kb()
)
@router.callback_query(F.data == "admin_close")
@@ -105,7 +120,7 @@ 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 safe_edit(callback, "🛠 <b>Админ-панель</b>", reply_markup=admin_menu_kb())
await callback.answer()
@@ -115,7 +130,7 @@ async def admin_toggle_shop(callback: CallbackQuery):
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'}")
await callback.answer(f"Скупка: {'ВКЛ' if enabled else 'ВЫКЛ'}")
@router.callback_query(F.data == "admin_toggle_bot")
@@ -124,7 +139,7 @@ async def admin_toggle_bot(callback: CallbackQuery):
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'}")
await callback.answer(f"Бот: {'ВКЛ' if enabled else 'ВЫКЛ'}")
@router.callback_query(F.data == "admin_robsec_get")
@@ -133,18 +148,18 @@ async def admin_robsec_get(callback: CallbackQuery):
return
lines, size = robsec_info()
if not size:
await callback.answer("robsec.txt is empty", show_alert=True)
await callback.answer("robsec.txt пуст", 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",
caption=f"📥 <b>robsec.txt</b>\nСтрок: <b>{lines}</b>\nРазмер: <b>{size} байт</b>",
)
await callback.answer("Sent")
await callback.answer("Отправлено")
except Exception as exc:
await callback.answer(f"Error: {exc}", show_alert=True)
await callback.answer(f"Ошибка: {exc}", show_alert=True)
@router.callback_query(F.data == "admin_robsec_clear")
@@ -154,11 +169,19 @@ async def admin_robsec_clear_prompt(callback: CallbackQuery):
lines, _ = robsec_info()
keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="Yes, clear", callback_data="admin_robsec_clear_yes")],
[InlineKeyboardButton(text="Cancel", callback_data="admin_back")],
[
InlineKeyboardButton(
text="✅ Да, очистить", callback_data="admin_robsec_clear_yes"
)
],
[InlineKeyboardButton(text="🔙 Отмена", callback_data="admin_back")],
]
)
await safe_edit(callback, f"Clear robsec.txt? Current lines: <b>{lines}</b>", reply_markup=keyboard)
await safe_edit(
callback,
f"🗑 <b>Очистить robsec.txt?</b>\n\nСтрок сейчас: <b>{lines}</b>\n\n⚠️ Данные будут удалены безвозвратно!",
reply_markup=keyboard,
)
@router.callback_query(F.data == "admin_robsec_clear_yes")
@@ -166,8 +189,10 @@ 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")
await safe_edit(
callback, "✅ <b>robsec.txt очищен</b>", reply_markup=admin_menu_kb()
)
await callback.answer("Очищено")
@router.callback_query(F.data == "admin_rates")
@@ -176,13 +201,18 @@ async def admin_rates(callback: CallbackQuery):
return
cfg = load_config()
rows = [
[InlineKeyboardButton(text=f"{rate.name}: {rate.price:.2f} RUB", callback_data=f"admin_rate_{key}")]
[
InlineKeyboardButton(
text=f"{rate.name}: {rate.price:.2f}",
callback_data=f"admin_rate_{key}",
)
]
for key, rate in cfg.rates.items()
]
rows.append([InlineKeyboardButton(text="Back", callback_data="admin_back")])
rows.append([InlineKeyboardButton(text="🔙 Назад", callback_data="admin_back")])
await safe_edit(
callback,
"<b>Rate management</b>\nSelect a rate:",
"💰 <b>Управление курсами</b>\nВыберите курс для изменения цены:",
reply_markup=InlineKeyboardMarkup(inline_keyboard=rows),
)
await callback.answer()
@@ -196,13 +226,13 @@ async def admin_rate_select(callback: CallbackQuery, state: FSMContext):
cfg = load_config()
rate = cfg.rates.get(key)
if not rate:
await callback.answer("Rate not found", show_alert=True)
await callback.answer("Курс не найден", 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:",
f"💰 Курс: <b>{rate.name}</b>\nТекущая цена: {rate.price:.2f} \n\nОтправьте новую цену числом:",
reply_markup=back_kb("admin_back"),
)
@@ -216,7 +246,7 @@ async def admin_edit_rate_save(message: Message, state: FSMContext):
if price < 0:
raise ValueError
except ValueError:
await message.answer("Send a non-negative number.")
await message.answer("❌ Введите неотрицательное число.")
return
data = await state.get_data()
key = data.get("rate_key")
@@ -226,7 +256,10 @@ async def admin_edit_rate_save(message: Message, state: FSMContext):
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())
await message.answer(
f"✅ Цена <b>{cfg.rates[key].name}</b> = {price:.2f}",
reply_markup=admin_menu_kb(),
)
@router.callback_query(F.data == "admin_min_withdraw")
@@ -236,7 +269,7 @@ async def admin_min_withdraw(callback: CallbackQuery, state: FSMContext):
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:",
f"💳 Текущий минимальный вывод: {load_config().min_withdraw:.2f} \nОтправьте новое значение:",
reply_markup=back_kb("admin_back"),
)
@@ -250,11 +283,13 @@ async def admin_min_withdraw_save(message: Message, state: FSMContext):
if value <= 0:
raise ValueError
except ValueError:
await message.answer("Send a positive number.")
await message.answer("❌ Введите положительное число.")
return
update_min_withdraw(value)
await state.clear()
await message.answer(f"Minimum withdrawal updated to {value:.2f} RUB", reply_markup=admin_menu_kb())
await message.answer(
f"✅ Минимальный вывод: {value:.2f}", reply_markup=admin_menu_kb()
)
@router.callback_query(F.data == "admin_treasury")
@@ -262,7 +297,11 @@ 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"))
await safe_edit(
callback,
"🏦 На какую сумму пополнить казну? (в ₽)\nОтправьте число:",
reply_markup=back_kb("admin_back"),
)
@router.message(Form.admin_treasury_amount)
@@ -274,22 +313,30 @@ async def admin_treasury_amount(message: Message, state: FSMContext):
if amount <= 0:
raise ValueError
except ValueError:
await message.answer("Send a positive number.")
await message.answer("❌ Введите положительное число.")
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())
await message.answer(
"❌ Не удалось создать счет CryptoBot.", reply_markup=admin_menu_kb()
)
return
pay_url, invoice_id, amount_usdt = invoice
pay_url, invoice_id, amount_asset = invoice
keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="Pay", url=pay_url)],
[InlineKeyboardButton(text="Check payment", callback_data=f"treasury_check_{invoice_id}_{amount}")],
[InlineKeyboardButton(text="💳 Оплатить", url=pay_url)],
[
InlineKeyboardButton(
text="✅ Проверить оплату",
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.",
f"🏦 Счет на пополнение казны\n\n💰 Сумма: <b>{amount:.2f} </b> (~{amount_asset} {CRYPTO_PAY_ASSET})\n\nПосле оплаты нажмите «Проверить оплату».",
parse_mode="HTML",
reply_markup=keyboard,
)
@@ -299,19 +346,28 @@ async def admin_treasury_amount(message: Message, state: FSMContext):
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)
await callback.answer("Оплата еще не поступила", 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")
await safe_edit(
callback,
f"✅ Казна пополнена на {amount_rub:.2f}\n🏦 Всего: {balance:.2f}",
reply_markup=admin_menu_kb(),
)
await callback.answer("Оплачено")
@router.callback_query(F.data == "admin_broadcast")
@@ -319,7 +375,11 @@ 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"))
await safe_edit(
callback,
"📣 Отправьте сообщение для рассылки (текст или фото с подписью):",
reply_markup=back_kb("admin_back"),
)
@router.message(Form.admin_broadcast)
@@ -333,7 +393,10 @@ async def admin_broadcast_send(message: Message, state: FSMContext):
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())
await message.answer(
f"✅ Рассылка завершена. Отправлено: {sent}\n❌ Ошибок: {failed}",
reply_markup=admin_menu_kb(),
)
@router.callback_query(F.data == "admin_users")
@@ -341,7 +404,11 @@ 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"))
await safe_edit(
callback,
"Отправьте ID пользователя или @username.",
reply_markup=back_kb("admin_back"),
)
@router.message(Form.admin_find_user)
@@ -351,7 +418,7 @@ async def admin_find_user(message: Message, state: FSMContext):
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())
await message.answer("❌ Пользователь не найден.", reply_markup=admin_menu_kb())
return
card = _user_card(uid)
if card:
@@ -365,7 +432,9 @@ async def u_bal_add(callback: CallbackQuery, state: FSMContext):
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"))
await safe_edit(
callback, "Отправьте сумму для начисления:", reply_markup=back_kb("admin_back")
)
@router.message(Form.admin_user_balance_add)
@@ -377,12 +446,14 @@ async def u_bal_add_save(message: Message, state: FSMContext):
if amount <= 0:
raise ValueError
except ValueError:
await message.answer("Send a positive number.")
await message.answer("❌ Введите положительное число.")
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())
await message.answer(
f"✅ Пользователю {uid} начислено {amount:.2f} ₽.", reply_markup=admin_menu_kb()
)
@router.callback_query(F.data.startswith("u_bal_sub_"))
@@ -392,7 +463,9 @@ async def u_bal_sub(callback: CallbackQuery, state: FSMContext):
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"))
await safe_edit(
callback, "Отправьте сумму для списания:", reply_markup=back_kb("admin_back")
)
@router.message(Form.admin_user_balance_sub)
@@ -404,12 +477,14 @@ async def u_bal_sub_save(message: Message, state: FSMContext):
if amount <= 0:
raise ValueError
except ValueError:
await message.answer("Send a positive number.")
await message.answer("❌ Введите положительное число.")
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())
await message.answer(
f"У пользователя {uid} списано {amount:.2f} ₽.", reply_markup=admin_menu_kb()
)
@router.callback_query(F.data.startswith("u_ban_"))
@@ -424,11 +499,15 @@ async def u_ban(callback: CallbackQuery, state: FSMContext):
from services.admin import unban_user
unban_user(uid)
await callback.answer("Unbanned")
await callback.answer("Разблокирован")
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"))
await safe_edit(
callback,
"Отправьте причину блокировки:",
reply_markup=back_kb("admin_back"),
)
return
card = _user_card(uid)
if card:
@@ -440,7 +519,7 @@ 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())
ban_user(uid, (message.text or "Причина не указана").strip())
await state.clear()
card = _user_card(uid)
if card:
@@ -453,7 +532,7 @@ async def u_admin(callback: CallbackQuery):
return
uid = int(callback.data.removeprefix("u_admin_"))
enabled = toggle_user_admin(uid)
await callback.answer(f"Admin: {'ON' if enabled else 'OFF'}")
await callback.answer(f"Админ: {'ВКЛ' if enabled else 'ВЫКЛ'}")
card = _user_card(uid)
if card:
await safe_edit(callback, card[0], reply_markup=card[1])
+11 -11
View File
@@ -16,15 +16,15 @@ 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"
f"💎 Добро пожаловать в <b>{SHOP_NAME}</b>!\n\n"
"💵 Здесь можно продать куки по следующим ставкам:\n"
)
for rate in cfg.rates.values():
text += f"- {rate.name}: {rate.price:.2f} RUB\n"
text += f"🔸 {rate.name}: {rate.price:.2f} \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."
f"\n📊 <b>AVG-система ({cfg.avg_min_price:.2f}-{cfg.avg_max_price:.2f} ₽/шт):</b>\n"
f"При отправке {cfg.avg_min_cookies}+ донатных cookie бот рассчитает средний Lifetime donate и предложит выгодную цену за каждую cookie.\n\n"
"🍪 Отправьте .txt-файл или вставьте текст с cookie."
)
@@ -40,24 +40,24 @@ async def _show_start(
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 '-'}")
await message.answer(f"❌ Вы заблокированы. Причина: {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.")
await message.answer("🚫 Бот временно отключен администратором.")
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 '-'}",
f"🆕 <b>Новый пользователь</b>\nID: <code>{user_id}</code>\n"
f"@{username or 'нет'}\nРеферер: {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>"
text += "\n\n👑 <i>Вы администратор. Продажа cookie отключена. Используйте /admin.</i>"
await message.answer(text, parse_mode="HTML", reply_markup=main_menu_kb())
+68 -35
View File
@@ -36,14 +36,16 @@ async def _blocked(message: Message) -> bool:
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 '-'}")
await message.answer(
f"❌ Вы заблокированы. Причина: {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.")
await message.answer("🚫 Бот временно отключен.")
return True
if not cfg.shop_enabled and message.from_user.id != ADMIN_ID:
await message.answer("Cookie buying is temporarily disabled.")
await message.answer("🚫 Скупка cookie временно отключена.")
return True
return False
@@ -51,16 +53,21 @@ async def _blocked(message: Message) -> bool:
@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.")
await message.answer(
"👑 Администраторы не могут продавать cookie. Используйте /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.")
await message.answer("❌ Отправьте файл формата .txt.")
return
path = COOKIE_FILES_DIR / f"{message.from_user.id}_{random.randint(100000, 999999)}.txt"
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")
@@ -72,34 +79,44 @@ async def handle_document(message: Message):
@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.")
await message.answer(
"👑 Администраторы не могут продавать cookie. Используйте /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())
await message.answer(
"👋 Используйте меню ниже или отправьте текст с cookie.",
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.")
await message.answer("❌ В отправленных данных cookie не найдены.")
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...",
f"<b>Начинаю обработку cookie</b>\nВсего: {total}\nДубликатов удалено: {duplicates}\nЭтап: проверка...",
parse_mode="HTML",
)
valid = []
async with aiohttp.ClientSession() as session:
tasks = [check_cookie_with_retry(session, cookie, proxies) for cookie in cookies]
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
@@ -110,22 +127,29 @@ async def process_cookies(message: Message, raw_text: str):
if index % 20 == 0 or index == total:
try:
await progress.edit_text(
f"<b>Validation</b>\nProgress: {index}/{total}\nValid: {len(valid)}",
f"<b>Проверка cookie</b>\nПрогресс: {index}/{total}\n✅ Валидных: {len(valid)}",
parse_mode="HTML",
)
except Exception:
pass
if not valid:
await progress.edit_text(f"No valid cookies found. Duplicates: {duplicates}")
await progress.edit_text(
f"❌ Валидные cookie не найдены. Дубликатов: {duplicates}"
)
return
await progress.edit_text(f"<b>Checking donations</b>\nValid cookies: {len(valid)}", parse_mode="HTML")
await progress.edit_text(
f"⏳ <b>Проверяю донаты</b>\nВалидных cookie: {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)
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,
@@ -140,34 +164,36 @@ async def process_cookies(message: Message, raw_text: str):
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)}",
f"❌ Ни одна cookie не подошла под курсы. Валидных: {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'}",
f"<b>Обновляю cookie</b>\nК оплате: {len(priced)}\nМетод: {'AVG' if avg_used else 'поштучный'}",
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]
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)):
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}",
f"<b>Обновление cookie</b>\nПрогресс: {min(offset + len(batch), len(priced))}/{len(priced)}\n"
f"✅ Обновлено: {len(fresh_ok)}\n❌ Ошибок: {fresh_failed}",
parse_mode="HTML",
)
except Exception:
@@ -175,7 +201,7 @@ async def process_cookies(message: Message, raw_text: str):
if not fresh_ok:
await progress.edit_text(
f"No cookies could be refreshed. Valid: {len(valid)}, failed: {fresh_failed}",
f"❌ Не удалось обновить ни одной cookie. Валидных: {len(valid)}, ошибок: {fresh_failed}",
reply_markup=back_kb(),
)
return
@@ -184,8 +210,13 @@ async def process_cookies(message: Message, raw_text: str):
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")
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)
@@ -202,32 +233,34 @@ async def process_cookies(message: Message, raw_text: str):
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)
await apply_referral_payout(
bot, profile.user_id, profile.username, payout, cfg.referral_percent
)
method = "AVG" if avg_used else "per-rate"
method = "📊 AVG-система" if avg_used else "📋 Поштучный"
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"
"<b>Обработка завершена</b>\n\n"
f"Всего cookie: {total}\nДубликатов: {duplicates}\nВалидных: {len(valid)}\n"
f"Подошло под курс: {len(priced)}\nОбновлено: {len(fresh_ok)}\nОшибок обновления: {fresh_failed}\n"
f"Метод оплаты: {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>"
report += f"AVG цена за cookie: {avg_price:.2f} \n"
report += f"\n💰 Начислено: <b>{payout:.2f} </b>\n💳 Баланс: <b>{profile.balance:.2f} </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",
f"🍪 <b>Новая покупка cookie</b>\n@{profile.username or ''} (<code>{profile.user_id}</code>)\n"
f"Всего: {total}, валидных: {len(valid)}, обновлено: {len(fresh_ok)}\nВыплачено: {payout:.2f} ",
get_log_bot(),
)
await log_admin_document(
bot,
ADMIN_ID,
str(user_file),
caption=f"robsec from <code>{profile.user_id}</code>",
caption=f"robsec от <code>{profile.user_id}</code>",
log_bot=get_log_bot(),
)
user_file.unlink(missing_ok=True)
+97 -52
View File
@@ -1,11 +1,12 @@
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery
from aiogram.types import LinkPreviewOptions, CallbackQuery, Message
from aiogram.fsm.state import State, StatesGroup
from config import ADMIN_ID, BOT_USERNAME
from db.storage import load_config, load_stats
from db.storage import load_config, load_stats, save_config
from db.users import get, register, save
from keyboards.user import back_kb
from keyboards.user import back_kb, activate_kb
from runtime import get_bot, get_log_bot
from services.withdrawals import create_crypto_check
from utils.logging import log_admin, safe_edit
@@ -13,6 +14,10 @@ from utils.logging import log_admin, safe_edit
router = Router(name="user")
class WithdrawState(StatesGroup):
waiting_for_amount = State()
def _get_profile(callback: CallbackQuery):
profile = get(callback.from_user.id)
if profile is None:
@@ -27,16 +32,16 @@ async def cb_profile(callback: CallbackQuery, state: FSMContext):
profile = _get_profile(callback)
cfg = load_config()
text = (
"<b>Your profile</b>\n\n"
"👤 <b>Ваш профиль</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"
f"📅 Дата регистрации: {profile.registered}\n\n"
f"🍪 Загружено cookie: <b>{profile.cookies_loaded}</b>\n"
f"💰 Заработано всего: <b>{profile.total_earned:.2f} </b>\n"
f"💸 С рефералов: <b>{profile.referral_earned:.2f} </b>\n"
f"💳 Текущий баланс: <b>{profile.balance:.2f} </b>\n\n"
f"🔗 Ваша реферальная ссылка:\nhttps://t.me/{BOT_USERNAME}?start=ref_{profile.user_id}\n\n"
f"💡 Реферальный процент: {cfg.referral_percent}%\n"
f"💳 Минимальный вывод: {cfg.min_withdraw:.2f} "
)
await safe_edit(callback, text, reply_markup=back_kb())
await callback.answer()
@@ -47,11 +52,11 @@ 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>"
"📈 <b>Общая статистика</b>\n\n"
f"👥 Пользователей: <b>{stats.total_users}</b>\n"
f"🍪 Всего cookie: <b>{stats.total_cookies}</b>\n"
f"💰 Прямых выплат: <b>{stats.total_paid_direct:.2f} </b>\n"
f"💸 Реферальных выплат: <b>{stats.total_paid_referral:.2f} </b>"
)
await safe_edit(callback, text, reply_markup=back_kb())
await callback.answer()
@@ -61,12 +66,12 @@ async def cb_stats(callback: CallbackQuery, state: FSMContext):
async def cb_rates(callback: CallbackQuery, state: FSMContext):
await state.clear()
cfg = load_config()
text = "<b>Current rates</b>\n\n"
text = "💰 <b>Актуальные курсы</b>\n\n"
for rate in cfg.rates.values():
text += f"- {rate.name}: {rate.price:.2f} RUB\n"
text += f"🔸 {rate.name}: {rate.price:.2f} \n"
text += (
f"\nAVG: {cfg.avg_min_cookies}+ donation cookies, "
f"{cfg.avg_min_price:.2f}-{cfg.avg_max_price:.2f} RUB per cookie."
f"\n📊 <b>AVG-система (от {cfg.avg_min_cookies}+ донатных cookie):</b>\n"
f"Цена за cookie: {cfg.avg_min_price:.2f}-{cfg.avg_max_price:.2f} ."
)
await safe_edit(callback, text, reply_markup=back_kb())
await callback.answer()
@@ -77,51 +82,91 @@ 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>.",
f"❌ Недостаточно средств. Баланс: <b>{profile.balance:.2f} </b>\n"
f"📉 Минимум для вывода: <b>{cfg.min_withdraw:.2f} </b>\n"
f"🏦 Казна: <b>{cfg.treasury:.2f} </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>",
f"💳 <b>Вывод средств</b>\n\n"
f"Ваш баланс: <b>{profile.balance:.2f}</b>\n"
f"Минимум: <b>{cfg.min_withdraw:.2f} ₽</b>\n\n"
f"Введите сумму для вывода:",
reply_markup=back_kb(),
disable_web_page_preview=True,
)
bot = get_bot() or callback.message.bot
await state.set_state(WithdrawState.waiting_for_amount)
await callback.answer()
@router.message(WithdrawState.waiting_for_amount)
async def process_withdraw_amount(message: Message, state: FSMContext):
profile = _get_profile(message)
cfg = load_config()
try:
amount = float(message.text)
except ValueError:
await message.answer("❌ Введите корректную сумму числом.")
return
if amount <= 0:
await message.answer("❌ Сумма должна быть больше нуля.")
return
if amount < cfg.min_withdraw:
await message.answer(f"❌ Минимум для вывода: <b>{cfg.min_withdraw:.2f} ₽</b>.")
return
if amount > profile.balance:
await message.answer(
f"❌ Недостаточно средств. Ваш баланс: <b>{profile.balance:.2f} ₽</b>."
)
return
if amount > cfg.treasury:
await message.answer(
f"❌ Недостаточно средств в казне: <b>{cfg.treasury:.2f} ₽</b>."
)
return
await state.clear()
await message.answer("⏳ Создаю CryptoBot-чек, подождите...")
check_url, check_image_url = await create_crypto_check(amount)
if not check_url:
await message.answer(
"❌ Не удалось создать чек. Попробуйте позже.",
reply_markup=back_kb(),
)
return
profile.balance -= amount
save(profile.user_id, profile)
cfg.treasury = max(0.0, cfg.treasury - amount)
save_config(cfg)
await message.answer(
text=f"🦋 Чек на <b>{amount:.2f} ₽</b>.",
reply_markup=activate_kb(url=check_url, amount=amount),
link_preview_options=LinkPreviewOptions(
url=check_image_url, show_above_text=True
),
)
bot = get_bot() or 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}",
f"💸 <b>Вывод средств</b>\n@{profile.username or ''} (<code>{profile.user_id}</code>)\n"
f"Сумма: {amount:.2f} \nЧек: {check_url}",
get_log_bot(),
)
+10 -10
View File
@@ -8,26 +8,26 @@ def admin_menu_kb() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(
text=f"Shop: {'ON' if cfg.shop_enabled else 'OFF'}",
text=f"🛒 Скупка: {'✅ ВКЛ' if cfg.shop_enabled else '❌ ВЫКЛ'}",
callback_data="admin_toggle_shop",
)],
[InlineKeyboardButton(
text=f"Bot: {'ON' if cfg.bot_enabled else 'OFF'}",
text=f"🤖 Бот: {'✅ ВКЛ' if cfg.bot_enabled else '❌ ВЫКЛ'}",
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="👥 Пользователи", callback_data="admin_users")],
[InlineKeyboardButton(text="💰 Управление курсами", callback_data="admin_rates")],
[InlineKeyboardButton(text="📣 Рассылка", callback_data="admin_broadcast")],
[InlineKeyboardButton(
text=f"Minimum withdrawal: {cfg.min_withdraw:.2f} RUB",
text=f"💳 Мин. вывод: {cfg.min_withdraw:.2f} ",
callback_data="admin_min_withdraw",
)],
[InlineKeyboardButton(
text=f"Treasury: {cfg.treasury:.2f} RUB | Top up",
text=f"🏦 Казна: {cfg.treasury:.2f} ₽ | Пополнить",
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")],
[InlineKeyboardButton(text="📥 Выгрузить robsec.txt", callback_data="admin_robsec_get")],
[InlineKeyboardButton(text="🗑 Очистить robsec.txt", callback_data="admin_robsec_clear")],
[InlineKeyboardButton(text="❌ Закрыть", callback_data="admin_close")],
]
)
+17 -6
View File
@@ -7,14 +7,25 @@ def main_menu_kb() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Profile", callback_data="profile"),
InlineKeyboardButton(text="Statistics", callback_data="stats"),
InlineKeyboardButton(text="👤 Профиль", callback_data="profile"),
InlineKeyboardButton(text="📈 Статистика", callback_data="stats"),
],
[
InlineKeyboardButton(text="Rates", callback_data="rates"),
InlineKeyboardButton(text="Withdraw", callback_data="withdraw"),
InlineKeyboardButton(text="💰 Курсы", callback_data="rates"),
InlineKeyboardButton(text="💳 Вывод", callback_data="withdraw"),
],
[InlineKeyboardButton(text="Channel", url=SHOP_CHANNEL)],
[InlineKeyboardButton(text="📢 Канал", url=SHOP_CHANNEL)],
]
)
def activate_kb(url: str, amount: float) -> InlineKeyboardMarkup:
if not url:
return back_kb()
return InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text=f"Получить {amount:.2f}", url=url)]
]
)
@@ -22,6 +33,6 @@ def main_menu_kb() -> InlineKeyboardMarkup:
def back_kb(callback_data: str = "back_main") -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="Back", callback_data=callback_data)]
[InlineKeyboardButton(text="🔙 Назад", callback_data=callback_data)]
]
)
+31 -9
View File
@@ -2,43 +2,64 @@ import asyncio
import logging
from datetime import datetime
from aiosend import CryptoPay, TESTNET, MAINNET
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 config import (
BOT_TOKEN,
LOG_BOT_TOKEN,
ADMIN_ID,
SHOP_NAME,
CRYPTO_PAY_TOKEN,
CRYPTO_PAY_NETWORK,
)
from db.storage import load_config, load_stats
from handlers import admin, start, upload, user
from runtime import set_bots
from runtime import set_bots, set_cp
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
cp_net = TESTNET if CRYPTO_PAY_NETWORK.lower() == "testnet" else MAINNET
cp = CryptoPay(token=CRYPTO_PAY_TOKEN, network=cp_net)
set_cp(cp)
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)
dp = Dispatcher(storage=MemoryStorage())
dp.include_router(start.router)
dp.include_router(user.router)
dp.include_router(admin.router)
dp.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')}",
f"🟢 <b>{SHOP_NAME}</b> запущен\n{datetime.now().strftime('%d.%m.%Y %H:%M:%S')}",
log_bot,
)
try:
await dispatcher.start_polling(bot)
await dp.start_polling(bot)
finally:
if log_bot:
await log_bot.session.close()
await bot.session.close()
@@ -51,4 +72,5 @@ if __name__ == "__main__":
logging.StreamHandler(),
],
)
asyncio.run(main())
BIN
View File
Binary file not shown.
+11
View File
@@ -1,9 +1,11 @@
from __future__ import annotations
from aiogram import Bot
from aiosend import CryptoPay
_bot: Bot | None = None
_log_bot: Bot | None = None
_cp: CryptoPay | None = None
def set_bots(bot: Bot, log_bot: Bot | None = None) -> None:
@@ -18,3 +20,12 @@ def get_bot() -> Bot | None:
def get_log_bot() -> Bot | None:
return _log_bot
def set_cp(cp: CryptoPay | None = None) -> None:
global _cp
_cp = cp
def get_cp() -> CryptoPay | None:
return _cp
+39 -14
View File
@@ -1,23 +1,22 @@
from __future__ import annotations
import asyncio
import os
import asyncio
from aiogram.enums import ParseMode
from aiogram.types import FSInputFile, InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from config import ADMIN_ID, BOT_USERNAME, ROBSEC_FILE
from db.storage import load_config, save_config, load_stats, save_stats
from config import ADMIN_ID, ROBSEC_FILE
from utils.logging import safe_edit
from db.storage import load_config, save_config
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)
@@ -25,6 +24,7 @@ def is_admin(user_id: int) -> bool:
def toggle_shop() -> bool:
cfg = load_config()
cfg.shop_enabled = not cfg.shop_enabled
save_config(cfg)
return cfg.shop_enabled
@@ -32,6 +32,7 @@ def toggle_shop() -> bool:
def toggle_bot() -> bool:
cfg = load_config()
cfg.bot_enabled = not cfg.bot_enabled
save_config(cfg)
return cfg.bot_enabled
@@ -52,6 +53,7 @@ def update_min_withdraw(value: float) -> None:
def add_treasury(amount_rub: float) -> float:
cfg = load_config()
cfg.treasury += amount_rub
save_config(cfg)
return cfg.treasury
@@ -59,6 +61,7 @@ def add_treasury(amount_rub: float) -> float:
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
@@ -99,8 +102,12 @@ async def send_user_card(target, uid: int) -> None:
kb = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text=" Баланс", callback_data=f"u_bal_add_{uid}"),
InlineKeyboardButton(text=" Баланс", callback_data=f"u_bal_sub_{uid}"),
InlineKeyboardButton(
text=" Баланс", callback_data=f"u_bal_add_{uid}"
),
InlineKeyboardButton(
text=" Баланс", callback_data=f"u_bal_sub_{uid}"
),
],
[
InlineKeyboardButton(
@@ -110,7 +117,9 @@ async def send_user_card(target, uid: int) -> None:
],
[
InlineKeyboardButton(
text="👑 Убрать админку" if profile.is_admin else "👑 Выдать админку",
text=(
"👑 Убрать админку" if profile.is_admin else "👑 Выдать админку"
),
callback_data=f"u_admin_{uid}",
)
],
@@ -123,15 +132,24 @@ async def send_user_card(target, uid: int) -> None:
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]:
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)
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)
await bot.send_message(
uid, text or caption or "-", parse_mode=ParseMode.HTML
)
ok += 1
except Exception:
fail += 1
@@ -143,6 +161,7 @@ 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)
@@ -150,6 +169,7 @@ def set_balance(uid: int, value: float) -> None:
profile = get(uid)
if not profile:
return
profile.balance = value
save(uid, profile)
@@ -158,6 +178,7 @@ def add_balance(uid: int, amount: float) -> None:
profile = get(uid)
if not profile:
return
profile.balance += amount
save(uid, profile)
@@ -166,6 +187,7 @@ 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)
@@ -174,6 +196,7 @@ 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
@@ -183,6 +206,7 @@ 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)
@@ -192,6 +216,7 @@ def unban_user(uid: int) -> None:
profile = get(uid)
if not profile:
return
profile.is_banned = False
profile.ban_reason = ""
save(uid, profile)
+11 -5
View File
@@ -1,8 +1,7 @@
from __future__ import annotations
import asyncio
import logging
import random
import asyncio
import aiohttp
import requests as plain_requests
@@ -39,7 +38,9 @@ def _extract_new_cookie(response):
return cookie
except Exception:
pass
sc = response.headers.get("set-cookie", "") or response.headers.get("Set-Cookie", "")
sc = response.headers.get("set-cookie", "") or response.headers.get(
"Set-Cookie", ""
)
if ".ROBLOSECURITY=" in sc:
try:
return sc.split(".ROBLOSECURITY=")[1].split(";")[0]
@@ -132,7 +133,9 @@ def _sync_fresh_cookie(cookie, proxy=None, timeout=15):
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)
return await loop.run_in_executor(
FRESHER_POOL, _sync_fresh_cookie, cookie, proxy, 15
)
async def check_cookie_basic(session, cookie, proxy=None) -> CookieValidationResult:
@@ -234,7 +237,10 @@ async def get_year_donate(cookie, user_id, proxy=None) -> int:
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":
if (
response.status == 200
and response.content_type == "application/json"
):
data = await response.json()
donate = data.get("purchasesTotal", 0)
if donate != 0:
+39 -57
View File
@@ -1,82 +1,64 @@
from __future__ import annotations
import aiohttp
import logging
from runtime import get_cp
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
from config import CRYPTO_PAY_ASSET
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"]
cp = get_cp()
amount_asset = await cp.exchange(
amount=amount_rub, source="RUB", target=CRYPTO_PAY_ASSET
)
createdCheck = await cp.create_check(
amount=amount_asset, asset=CRYPTO_PAY_ASSET
)
check_image_url = await createdCheck.get_image(fiat="RUB")
return createdCheck.bot_check_url, check_image_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
cp = get_cp()
createdInvoice = await cp.create_invoice(
amount=amount_rub,
currency_type="fiat",
accepted_assets=[CRYPTO_PAY_ASSET],
fiat="RUB",
)
amount_asset = round(
await cp.exchange(amount=amount_rub, source="RUB", target=CRYPTO_PAY_ASSET),
2,
)
return (createdInvoice.pay_url, int(createdInvoice.invoice_id), amount_asset)
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")
cp = get_cp()
paidInvoice = await cp.get_invoice(invoice_id)
return paidInvoice.status == "paid"
except Exception as exc:
logging.error("check_treasury_invoice: %s", exc)
return False
+2
View File
@@ -0,0 +1,2 @@
@echo off
powershell -ExecutionPolicy Bypass -File "%~dp0example-start.ps1"
+61
View File
@@ -0,0 +1,61 @@
$Host.UI.RawUI.WindowTitle = "Roblox Buyer Bot"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
Set-Location $ScriptDir
# Проверка Python
if (-not (Get-Command python -ErrorAction SilentlyContinue)) {
Write-Host "[ERROR] Python не найден. Установи Python 3.11+ с https://python.org" -ForegroundColor Red
exit 1
}
# Создание .venv если нет
if (-not (Test-Path ".venv")) {
Write-Host "[INFO] Создание виртуального окружения..."
python -m venv .venv
}
# Активация
& ".venv\Scripts\Activate.ps1"
# Установка зависимостей
Write-Host "[INFO] Установка зависимостей..."
python -m pip install --upgrade pip
pip install -r requirements.txt
cls
# Создание .env если нет
if (-not (Test-Path ".env")) {
Write-Host "[INFO] Создание .env из примера..."
Copy-Item ".env.example" ".env"
Write-Host "[!] Заполни BOT_TOKEN, CRYPTO_PAY_TOKEN и ADMIN_ID в файле .env" -ForegroundColor Yellow
Write-Host "[!] Открываю .env для редактирования..." -ForegroundColor Yellow
notepad .env
Read-Host "Нажми Enter после сохранения .env"
}
cls
# Проверка BOT_TOKEN
$envContent = Get-Content ".env" -Raw
if ($envContent -notmatch "(?m)^BOT_TOKEN=\S+") {
Write-Host "[ERROR] BOT_TOKEN не заполнен в .env" -ForegroundColor Red
Write-Host "[!] Открываю .env для редактирования..." -ForegroundColor Yellow
notepad .env
Read-Host "Нажми Enter после сохранения .env"
}
# Проверка CRYPTO_PAY_TOKEN
$envContent = Get-Content ".env" -Raw
if ($envContent -notmatch "(?m)^CRYPTO_PAY_TOKEN=\S+") {
Write-Host "[ERROR] CRYPTO_PAY_TOKEN не заполнен в .env" -ForegroundColor Red
Write-Host "[!] Открываю .env для редактирования..." -ForegroundColor Yellow
notepad .env
Read-Host "Нажми Enter после сохранения .env"
}
cls
# Запуск бота
Write-Host "[INFO] Запуск бота..." -ForegroundColor Green
python main.py
+4 -1
View File
@@ -8,11 +8,14 @@ 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:
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(