Compare commits

...

8 Commits

18 changed files with 593 additions and 310 deletions
+5 -2
View File
@@ -1,15 +1,18 @@
# Обязательные поля # Обязательные поля
BOT_TOKEN= BOT_TOKEN=
ADMIN_ID= ADMIN_ID=
CRYPTO_PAY_TOKEN= CRYPTO_PAY_TOKEN=
CRYPTO_PAY_NETWORK=
CRYPTO_PAY_ASSET=USDT
# Бот для логгинга (логи будут в этом боте, можно указать один и тот же бот на главный и логгинга) # Бот для логгинга (логи будут в этом боте, можно указать один и тот же бот на главный и логгинга)
LOG_BOT_TOKEN= LOG_BOT_TOKEN=
# Данные шопа # Данные шопа
SHOP_NAME=KILL UNONY MOM SHOP_NAME=KILL UNONY MOM
SHOP_CHANNEL=https://t.me/username SHOP_CHANNEL=
BOT_USERNAME=username BOT_USERNAME=
# Директории и файлы # Директории и файлы
DATABASE_DIR=Users DATABASE_DIR=Users
+26 -35
View File
@@ -2,51 +2,42 @@
Telegram-бот для проверки Roblox-cookie, получения истории донатов, обновления оплачиваемых cookie и начисления средств продавцам через CryptoBot. Telegram-бот для проверки Roblox-cookie, получения истории донатов, обновления оплачиваемых cookie и начисления средств продавцам через CryptoBot.
## Возможности ## Запуск через .bat (Windows)
- Прием cookie обычным текстом или `.txt`-файлом. Запусти `start.bat` двойным кликом. Скрипт автоматически:
- Параллельная проверка cookie Roblox с поддержкой прокси.
- Расчет оплаты по lifetime- и yearly-ставкам.
- AVG-ценообразование для больших партий донатных cookie.
- Обновление cookie перед сохранением и выплатой.
- Балансы пользователей, вывод средств, реферальная система, статистика и блокировки.
- Админ-панель для управления ставками, магазином, рассылками, казной и пользователями.
## Требования 1. Создаст виртуальное окружение `.venv`
2. Установит зависимости из `requirements.txt`
3. Создаст `.env` из примера (если ещё нет)
4. Запустит бота
- Python 3.10 или новее. ## Запуск через командную строку (вручную)
- Токен Telegram-бота.
- API-токен CryptoBot для выплат и пополнения казны (с соответствующими правами).
- Telegram ID администратора.
Установите зависимости в виртуальное окружение: 1. Установи Python `3.11+`.
2. Поставь зависимости:
```powershell ```bash
python -m venv .venv python3.11 -m pip install -r requirements.txt
.\.venv\Scripts\Activate.ps1
python -m pip install aiogram aiohttp requests curl-cffi
``` ```
В Linux или macOS вместо команды активации PowerShell используйте `source .venv/bin/activate`. 3. Создай `.env` по примеру:
## Настройка ```bash
cp .env.example .env
Скопируйте [.env.example](.env.example) и укажите как минимум:
```text
BOT_TOKEN=токен_telegram_бота
ADMIN_ID=telegram_id_администратора
CRYPTO_PAY_TOKEN=api_токен_cryptobot
``` ```
Приложение получает переменные окружения через `config.py`. 4. Заполни минимум:
## Запуск ```env
BOT_TOKEN=
Запуск бота: ADMIN_ID=
CRYPTO_PAY_TOKEN=
```powershell
python main.py
``` ```
Пользователь может выполнить `/start`, отправить текст с cookie или загрузить `.txt`-файл. Администратор открывает панель управления командой `/admin`. Конфигурация читается только из `.env` или переменных окружения.
5. Запусти бота:
```bash
python3 main.py
```
+13 -4
View File
@@ -5,8 +5,8 @@ from dotenv import load_dotenv
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from pathlib import Path from pathlib import Path
from models.config import BotConfig
from models.stats import BotStats from models.stats import BotStats
from models.config import BotConfig
BASE_DIR = Path(__file__).resolve().parent BASE_DIR = Path(__file__).resolve().parent
@@ -22,6 +22,7 @@ def _env_int(name: str, default: int) -> int:
raw = os.getenv(name) raw = os.getenv(name)
if raw is None or raw == "": if raw is None or raw == "":
return default return default
return int(raw) return int(raw)
@@ -29,6 +30,7 @@ def _env_float(name: str, default: float) -> float:
raw = os.getenv(name) raw = os.getenv(name)
if raw is None or raw == "": if raw is None or raw == "":
return default return default
return float(raw) return float(raw)
@@ -36,21 +38,27 @@ def _env_bool(name: str, default: bool) -> bool:
raw = os.getenv(name) raw = os.getenv(name)
if raw is None or raw == "": if raw is None or raw == "":
return default return default
return raw.strip().lower() in {"1", "true", "yes", "on"} return raw.strip().lower() in {"1", "true", "yes", "on"}
load_dotenv() load_dotenv()
BOT_TOKEN = os.getenv("BOT_TOKEN", "") 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) ADMIN_ID = _env_int("ADMIN_ID", 0)
CRYPTO_PAY_TOKEN = os.getenv("CRYPTO_PAY_TOKEN", "") 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_NAME = os.getenv("SHOP_NAME", "KILL UNONY MOM")
SHOP_CHANNEL = os.getenv("SHOP_CHANNEL", "https://t.me/bot399_start_bot") SHOP_CHANNEL = os.getenv("SHOP_CHANNEL", "")
BOT_USERNAME = os.getenv("BOT_USERNAME", "bot399_start_bot") BOT_USERNAME = os.getenv("BOT_USERNAME", "")
DATABASE_DIR = _resolve_path("DATABASE_DIR", "Users") DATABASE_DIR = _resolve_path("DATABASE_DIR", "Users")
COOKIE_FILES_DIR = _resolve_path("COOKIE_FILES_DIR", "Cookies") COOKIE_FILES_DIR = _resolve_path("COOKIE_FILES_DIR", "Cookies")
CONFIG_FILE = _resolve_path("BOT_CONFIG_FILE", "bot_config.json") CONFIG_FILE = _resolve_path("BOT_CONFIG_FILE", "bot_config.json")
STATS_FILE = _resolve_path("BOT_STATS_FILE", "bot_stats.json") STATS_FILE = _resolve_path("BOT_STATS_FILE", "bot_stats.json")
PROXIES_FILE = _resolve_path("PROXIES_FILE", "proxies.txt") PROXIES_FILE = _resolve_path("PROXIES_FILE", "proxies.txt")
@@ -61,6 +69,7 @@ CONCURRENT_CHECKS = _env_int("CONCURRENT_CHECKS", 50)
REQUEST_TIMEOUT = _env_int("REQUEST_TIMEOUT", 10) REQUEST_TIMEOUT = _env_int("REQUEST_TIMEOUT", 10)
FRESHER_BATCH_SIZE = _env_int("FRESHER_BATCH_SIZE", 10) FRESHER_BATCH_SIZE = _env_int("FRESHER_BATCH_SIZE", 10)
# Инициализация
DATABASE_DIR.mkdir(parents=True, exist_ok=True) DATABASE_DIR.mkdir(parents=True, exist_ok=True)
COOKIE_FILES_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, 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.storage import load_config
from db.users import get from db.users import get
from keyboards.admin import admin_menu_kb from keyboards.admin import admin_menu_kb
@@ -37,7 +37,10 @@ from states.admin import Form
from runtime import get_bot from runtime import get_bot
from utils.logging import safe_edit from utils.logging import safe_edit
from runtime import get_cp
router = Router(name="admin") router = Router(name="admin")
cp = get_cp()
def _admin(user_id: int) -> bool: def _admin(user_id: int) -> bool:
@@ -49,32 +52,42 @@ def _user_card(uid: int) -> tuple[str, InlineKeyboardMarkup] | None:
if not profile: if not profile:
return None return None
text = ( text = (
"<b>User</b>\n\n" "👤 <b>Пользователь</b>\n\n"
f"ID: <code>{uid}</code>\n" f"🆔 ID: <code>{uid}</code>\n"
f"Username: @{profile.username or '-'}\n" f"👤 @{profile.username or ''}\n"
f"Registered: {profile.registered or '-'}\n" f"📅 Регистрация: {profile.registered or '?'}\n"
f"Balance: {profile.balance:.2f} RUB\n" f"💳 Баланс: {profile.balance:.2f} \n"
f"Total earned: {profile.total_earned:.2f} RUB\n" f"💰 Всего заработано: {profile.total_earned:.2f} \n"
f"Cookies: {profile.cookies_loaded}\n" f"🍪 Cookie: {profile.cookies_loaded}\n"
f"Referrals: {len(profile.referrals)}\n" f"👥 Рефералов: {len(profile.referrals)}\n"
f"Admin: {'yes' if profile.is_admin else 'no'}\n" f"👑 Админ: {'' if profile.is_admin else ''}\n"
f"Banned: {'yes: ' + profile.ban_reason if profile.is_banned else 'no'}" f"🚫 Бан: {' ' + profile.ban_reason if profile.is_banned else ''}"
) )
keyboard = InlineKeyboardMarkup( keyboard = InlineKeyboardMarkup(
inline_keyboard=[ inline_keyboard=[
[ [
InlineKeyboardButton(text="Add balance", callback_data=f"u_bal_add_{uid}"), InlineKeyboardButton(
InlineKeyboardButton(text="Subtract balance", callback_data=f"u_bal_sub_{uid}"), 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", InlineKeyboardButton(
callback_data=f"u_ban_{uid}", text="🚫 Разбанить" if profile.is_banned else "🚫 Забанить",
)], callback_data=f"u_ban_{uid}",
[InlineKeyboardButton( )
text="Remove admin" if profile.is_admin else "Grant admin", ],
callback_data=f"u_admin_{uid}", [
)], InlineKeyboardButton(
[InlineKeyboardButton(text="Back", callback_data="admin_back")], text=(
"👑 Убрать админку" if profile.is_admin else "👑 Выдать админку"
),
callback_data=f"u_admin_{uid}",
)
],
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_back")],
] ]
) )
return text, keyboard return text, keyboard
@@ -85,7 +98,9 @@ async def cmd_admin(message: Message, state: FSMContext):
if not _admin(message.from_user.id): if not _admin(message.from_user.id):
return return
await state.clear() 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") @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): if not _admin(callback.from_user.id):
return return
await state.clear() 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() await callback.answer()
@@ -115,7 +130,7 @@ async def admin_toggle_shop(callback: CallbackQuery):
return return
enabled = toggle_shop() enabled = toggle_shop()
await callback.message.edit_reply_markup(reply_markup=admin_menu_kb()) 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") @router.callback_query(F.data == "admin_toggle_bot")
@@ -124,7 +139,7 @@ async def admin_toggle_bot(callback: CallbackQuery):
return return
enabled = toggle_bot() enabled = toggle_bot()
await callback.message.edit_reply_markup(reply_markup=admin_menu_kb()) 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") @router.callback_query(F.data == "admin_robsec_get")
@@ -133,18 +148,18 @@ async def admin_robsec_get(callback: CallbackQuery):
return return
lines, size = robsec_info() lines, size = robsec_info()
if not size: if not size:
await callback.answer("robsec.txt is empty", show_alert=True) await callback.answer("robsec.txt пуст", show_alert=True)
return return
try: try:
bot = get_bot() or callback.message.bot bot = get_bot() or callback.message.bot
await bot.send_document( await bot.send_document(
callback.from_user.id, callback.from_user.id,
FSInputFile(ROBSEC_FILE), 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: 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") @router.callback_query(F.data == "admin_robsec_clear")
@@ -154,11 +169,19 @@ async def admin_robsec_clear_prompt(callback: CallbackQuery):
lines, _ = robsec_info() lines, _ = robsec_info()
keyboard = InlineKeyboardMarkup( keyboard = InlineKeyboardMarkup(
inline_keyboard=[ 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") @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): if not _admin(callback.from_user.id):
return return
clear_robsec() clear_robsec()
await safe_edit(callback, "<b>robsec.txt cleared</b>", reply_markup=admin_menu_kb()) await safe_edit(
await callback.answer("Cleared") callback, "✅ <b>robsec.txt очищен</b>", reply_markup=admin_menu_kb()
)
await callback.answer("Очищено")
@router.callback_query(F.data == "admin_rates") @router.callback_query(F.data == "admin_rates")
@@ -176,13 +201,18 @@ async def admin_rates(callback: CallbackQuery):
return return
cfg = load_config() cfg = load_config()
rows = [ 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() 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( await safe_edit(
callback, callback,
"<b>Rate management</b>\nSelect a rate:", "💰 <b>Управление курсами</b>\nВыберите курс для изменения цены:",
reply_markup=InlineKeyboardMarkup(inline_keyboard=rows), reply_markup=InlineKeyboardMarkup(inline_keyboard=rows),
) )
await callback.answer() await callback.answer()
@@ -196,13 +226,13 @@ async def admin_rate_select(callback: CallbackQuery, state: FSMContext):
cfg = load_config() cfg = load_config()
rate = cfg.rates.get(key) rate = cfg.rates.get(key)
if not rate: if not rate:
await callback.answer("Rate not found", show_alert=True) await callback.answer("Курс не найден", show_alert=True)
return return
await state.update_data(rate_key=key) await state.update_data(rate_key=key)
await state.set_state(Form.admin_edit_rate) await state.set_state(Form.admin_edit_rate)
await safe_edit( await safe_edit(
callback, 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"), reply_markup=back_kb("admin_back"),
) )
@@ -216,7 +246,7 @@ async def admin_edit_rate_save(message: Message, state: FSMContext):
if price < 0: if price < 0:
raise ValueError raise ValueError
except ValueError: except ValueError:
await message.answer("Send a non-negative number.") await message.answer("❌ Введите неотрицательное число.")
return return
data = await state.get_data() data = await state.get_data()
key = data.get("rate_key") key = data.get("rate_key")
@@ -226,7 +256,10 @@ async def admin_edit_rate_save(message: Message, state: FSMContext):
return return
update_rate(key, price) update_rate(key, price)
await state.clear() 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") @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 state.set_state(Form.admin_min_withdraw)
await safe_edit( await safe_edit(
callback, 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"), reply_markup=back_kb("admin_back"),
) )
@@ -250,11 +283,13 @@ async def admin_min_withdraw_save(message: Message, state: FSMContext):
if value <= 0: if value <= 0:
raise ValueError raise ValueError
except ValueError: except ValueError:
await message.answer("Send a positive number.") await message.answer("❌ Введите положительное число.")
return return
update_min_withdraw(value) update_min_withdraw(value)
await state.clear() 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") @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): if not _admin(callback.from_user.id):
return return
await state.set_state(Form.admin_treasury_amount) 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) @router.message(Form.admin_treasury_amount)
@@ -274,22 +313,30 @@ async def admin_treasury_amount(message: Message, state: FSMContext):
if amount <= 0: if amount <= 0:
raise ValueError raise ValueError
except ValueError: except ValueError:
await message.answer("Send a positive number.") await message.answer("❌ Введите положительное число.")
return return
await state.clear() await state.clear()
invoice = await create_treasury_invoice(amount) invoice = await create_treasury_invoice(amount)
if not invoice: 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 return
pay_url, invoice_id, amount_usdt = invoice pay_url, invoice_id, amount_asset = invoice
keyboard = InlineKeyboardMarkup( keyboard = InlineKeyboardMarkup(
inline_keyboard=[ inline_keyboard=[
[InlineKeyboardButton(text="Pay", url=pay_url)], [InlineKeyboardButton(text="💳 Оплатить", url=pay_url)],
[InlineKeyboardButton(text="Check payment", callback_data=f"treasury_check_{invoice_id}_{amount}")], [
InlineKeyboardButton(
text="✅ Проверить оплату",
callback_data=f"treasury_check_{invoice_id}_{amount}",
)
],
] ]
) )
await message.answer( 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", parse_mode="HTML",
reply_markup=keyboard, reply_markup=keyboard,
) )
@@ -299,19 +346,28 @@ async def admin_treasury_amount(message: Message, state: FSMContext):
async def treasury_check(callback: CallbackQuery): async def treasury_check(callback: CallbackQuery):
if not _admin(callback.from_user.id): if not _admin(callback.from_user.id):
return return
_, _, invoice_id, amount = callback.data.split("_", 3) _, _, invoice_id, amount = callback.data.split("_", 3)
try: try:
paid = await check_treasury_invoice(int(invoice_id)) paid = await check_treasury_invoice(int(invoice_id))
amount_rub = float(amount) amount_rub = float(amount)
except (ValueError, TypeError): except (ValueError, TypeError):
paid = False paid = False
amount_rub = 0.0 amount_rub = 0.0
if not paid: if not paid:
await callback.answer("Payment has not arrived yet", show_alert=True) await callback.answer("Оплата еще не поступила", show_alert=True)
return return
balance = add_treasury(amount_rub) 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 safe_edit(
await callback.answer("Paid") callback,
f"✅ Казна пополнена на {amount_rub:.2f}\n🏦 Всего: {balance:.2f}",
reply_markup=admin_menu_kb(),
)
await callback.answer("Оплачено")
@router.callback_query(F.data == "admin_broadcast") @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): if not _admin(callback.from_user.id):
return return
await state.set_state(Form.admin_broadcast) 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) @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, photo_id=message.photo[-1].file_id if message.photo else None,
caption=message.caption, 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") @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): if not _admin(callback.from_user.id):
return return
await state.set_state(Form.admin_find_user) 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) @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()) uid = find_user((message.text or "").strip())
await state.clear() await state.clear()
if uid is None: if uid is None:
await message.answer("User not found.", reply_markup=admin_menu_kb()) await message.answer("❌ Пользователь не найден.", reply_markup=admin_menu_kb())
return return
card = _user_card(uid) card = _user_card(uid)
if card: if card:
@@ -365,7 +432,9 @@ async def u_bal_add(callback: CallbackQuery, state: FSMContext):
uid = int(callback.data.removeprefix("u_bal_add_")) uid = int(callback.data.removeprefix("u_bal_add_"))
await state.update_data(target_uid=uid) await state.update_data(target_uid=uid)
await state.set_state(Form.admin_user_balance_add) 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) @router.message(Form.admin_user_balance_add)
@@ -377,12 +446,14 @@ async def u_bal_add_save(message: Message, state: FSMContext):
if amount <= 0: if amount <= 0:
raise ValueError raise ValueError
except ValueError: except ValueError:
await message.answer("Send a positive number.") await message.answer("❌ Введите положительное число.")
return return
uid = (await state.get_data()).get("target_uid") uid = (await state.get_data()).get("target_uid")
add_balance(uid, amount) add_balance(uid, amount)
await state.clear() 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_")) @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_")) uid = int(callback.data.removeprefix("u_bal_sub_"))
await state.update_data(target_uid=uid) await state.update_data(target_uid=uid)
await state.set_state(Form.admin_user_balance_sub) 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) @router.message(Form.admin_user_balance_sub)
@@ -404,12 +477,14 @@ async def u_bal_sub_save(message: Message, state: FSMContext):
if amount <= 0: if amount <= 0:
raise ValueError raise ValueError
except ValueError: except ValueError:
await message.answer("Send a positive number.") await message.answer("❌ Введите положительное число.")
return return
uid = (await state.get_data()).get("target_uid") uid = (await state.get_data()).get("target_uid")
sub_balance(uid, amount) sub_balance(uid, amount)
await state.clear() 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_")) @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 from services.admin import unban_user
unban_user(uid) unban_user(uid)
await callback.answer("Unbanned") await callback.answer("Разблокирован")
else: else:
await state.update_data(target_uid=uid) await state.update_data(target_uid=uid)
await state.set_state(Form.admin_user_ban_reason) 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 return
card = _user_card(uid) card = _user_card(uid)
if card: if card:
@@ -440,7 +519,7 @@ async def u_ban_reason(message: Message, state: FSMContext):
if not _admin(message.from_user.id): if not _admin(message.from_user.id):
return return
uid = (await state.get_data()).get("target_uid") 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() await state.clear()
card = _user_card(uid) card = _user_card(uid)
if card: if card:
@@ -453,7 +532,7 @@ async def u_admin(callback: CallbackQuery):
return return
uid = int(callback.data.removeprefix("u_admin_")) uid = int(callback.data.removeprefix("u_admin_"))
enabled = toggle_user_admin(uid) 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) card = _user_card(uid)
if card: if card:
await safe_edit(callback, card[0], reply_markup=card[1]) 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: def _welcome_text(cfg) -> str:
text = ( text = (
f"Welcome to <b>{SHOP_NAME}</b>!\n\n" f"💎 Добро пожаловать в <b>{SHOP_NAME}</b>!\n\n"
"You can sell cookies at these rates:\n" "💵 Здесь можно продать куки по следующим ставкам:\n"
) )
for rate in cfg.rates.values(): 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 + ( return text + (
f"\nAVG pricing applies to {cfg.avg_min_cookies}+ cookies with donations.\n" f"\n📊 <b>AVG-система ({cfg.avg_min_price:.2f}-{cfg.avg_max_price:.2f} ₽/шт):</b>\n"
f"The AVG price is capped at {cfg.avg_min_price:.2f}-{cfg.avg_max_price:.2f} RUB per cookie.\n\n" f"При отправке {cfg.avg_min_cookies}+ донатных cookie бот рассчитает средний Lifetime donate и предложит выгодную цену за каждую cookie.\n\n"
"Send a .txt file or paste your cookie text." "🍪 Отправьте .txt-файл или вставьте текст с cookie."
) )
@@ -40,24 +40,24 @@ async def _show_start(
profile = get(user_id) profile = get(user_id)
cfg = load_config() cfg = load_config()
if profile and profile.is_banned: 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 return
if not cfg.bot_enabled and user_id != ADMIN_ID: 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 return
if is_new: if is_new:
bot = get_bot() or message.bot bot = get_bot() or message.bot
await log_admin( await log_admin(
bot, bot,
ADMIN_ID, ADMIN_ID,
f"<b>New user</b>\nID: <code>{user_id}</code>\n" f"🆕 <b>Новый пользователь</b>\nID: <code>{user_id}</code>\n"
f"@{username or '-'}\nReferrer: {referrer or '-'}", f"@{username or 'нет'}\nРеферер: {referrer or ''}",
get_log_bot(), get_log_bot(),
) )
text = _welcome_text(cfg) text = _welcome_text(cfg)
if is_admin(user_id): 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()) 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) register(message.from_user.id, message.from_user.username)
profile = get(message.from_user.id) profile = get(message.from_user.id)
if profile.is_banned: 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 return True
cfg = load_config() cfg = load_config()
if not cfg.bot_enabled and message.from_user.id != ADMIN_ID: 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 return True
if not cfg.shop_enabled and message.from_user.id != ADMIN_ID: 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 True
return False return False
@@ -51,16 +53,21 @@ async def _blocked(message: Message) -> bool:
@router.message(StateFilter(None), F.document) @router.message(StateFilter(None), F.document)
async def handle_document(message: Message): async def handle_document(message: Message):
if is_admin(message.from_user.id): if is_admin(message.from_user.id):
await message.answer("Admin accounts cannot sell cookies. Use /admin.") await message.answer(
"👑 Администраторы не могут продавать cookie. Используйте /admin."
)
return return
if await _blocked(message): if await _blocked(message):
return return
filename = message.document.file_name or "" filename = message.document.file_name or ""
if not filename.lower().endswith(".txt"): if not filename.lower().endswith(".txt"):
await message.answer("Send a .txt file.") await message.answer("❌ Отправьте файл формата .txt.")
return 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: try:
await message.bot.download(message.document, destination=path) await message.bot.download(message.document, destination=path)
text = path.read_text(encoding="utf-8", errors="ignore") 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("/")) @router.message(StateFilter(None), F.text & ~F.text.startswith("/"))
async def handle_text(message: Message): async def handle_text(message: Message):
if is_admin(message.from_user.id): if is_admin(message.from_user.id):
await message.answer("Admin accounts cannot sell cookies. Use /admin.") await message.answer(
"👑 Администраторы не могут продавать cookie. Используйте /admin."
)
return return
if await _blocked(message): if await _blocked(message):
return return
text = message.text or "" text = message.text or ""
if "_|WARNING:-DO-NOT-SHARE-THIS." not in text: 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 return
await process_cookies(message, text) await process_cookies(message, text)
async def process_cookies(message: Message, raw_text: str): async def process_cookies(message: Message, raw_text: str):
cookies, duplicates = parse_cookies_text(raw_text) cookies, duplicates = parse_cookies_text(raw_text)
if not cookies: if not cookies:
await message.answer("No cookies were found in the submitted data.") await message.answer("❌ В отправленных данных cookie не найдены.")
return return
cfg = load_config() cfg = load_config()
proxies = load_proxies() proxies = load_proxies()
total = len(cookies) total = len(cookies)
progress = await message.answer( 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", parse_mode="HTML",
) )
valid = [] valid = []
async with aiohttp.ClientSession() as session: 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): for index, task in enumerate(asyncio.as_completed(tasks), start=1):
try: try:
result = await task result = await task
@@ -110,22 +127,29 @@ async def process_cookies(message: Message, raw_text: str):
if index % 20 == 0 or index == total: if index % 20 == 0 or index == total:
try: try:
await progress.edit_text( 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", parse_mode="HTML",
) )
except Exception: except Exception:
pass pass
if not valid: if not valid:
await progress.edit_text(f"No valid cookies found. Duplicates: {duplicates}") await progress.edit_text(
f"❌ Валидные cookie не найдены. Дубликатов: {duplicates}"
)
return 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 = [] donations = []
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async def get_donation(result): async def get_donation(result):
proxy = random.choice(proxies) if proxies else None 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) year = await get_year_donate(result.cookie, result.user_id, proxy)
return CookieDonationInfo( return CookieDonationInfo(
cookie=result.cookie, 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) priced, avg_used, avg_price = price_cookie_batch(donations, cfg)
if not priced: if not priced:
await progress.edit_text( await progress.edit_text(
f"No cookies matched the rates. Valid: {len(valid)}", f"❌ Ни одна cookie не подошла под курсы. Валидных: {len(valid)}",
reply_markup=back_kb(), reply_markup=back_kb(),
) )
return return
await progress.edit_text( 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", parse_mode="HTML",
) )
fresh_ok = [] fresh_ok = []
fresh_failed = 0 fresh_failed = 0
for offset in range(0, len(priced), FRESHER_BATCH_SIZE): 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): async def refresh(item):
proxy = random.choice(proxies) if proxies else None proxy = random.choice(proxies) if proxies else None
ok, refreshed = await fresh_cookie_async(item.cookie, proxy) ok, refreshed = await fresh_cookie_async(item.cookie, proxy)
return item, ok, refreshed 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"): if ok and isinstance(refreshed, str) and refreshed.startswith("_|WARNING"):
fresh_ok.append((item, refreshed)) fresh_ok.append((item, refreshed))
else: else:
fresh_failed += 1 fresh_failed += 1
try: try:
await progress.edit_text( await progress.edit_text(
f"<b>Refreshing cookies</b>\nProgress: {min(offset + len(batch), len(priced))}/{len(priced)}\n" f"<b>Обновление cookie</b>\nПрогресс: {min(offset + len(batch), len(priced))}/{len(priced)}\n"
f"Refreshed: {len(fresh_ok)}\nFailed: {fresh_failed}", f"✅ Обновлено: {len(fresh_ok)}\n❌ Ошибок: {fresh_failed}",
parse_mode="HTML", parse_mode="HTML",
) )
except Exception: except Exception:
@@ -175,7 +201,7 @@ async def process_cookies(message: Message, raw_text: str):
if not fresh_ok: if not fresh_ok:
await progress.edit_text( 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(), reply_markup=back_kb(),
) )
return return
@@ -184,8 +210,13 @@ async def process_cookies(message: Message, raw_text: str):
with ROBSEC_FILE.open("a", encoding="utf-8") as output: with ROBSEC_FILE.open("a", encoding="utf-8") as output:
output.writelines(f"{cookie}\n" for _, cookie in fresh_ok) 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 = (
user_file.write_text("".join(f"{cookie}\n" for _, cookie in fresh_ok), encoding="utf-8") 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) payout = sum(item.price for item, _ in fresh_ok)
profile = get(message.from_user.id) profile = get(message.from_user.id)
@@ -202,32 +233,34 @@ async def process_cookies(message: Message, raw_text: str):
stats.total_paid_direct += payout stats.total_paid_direct += payout
save_stats(stats) save_stats(stats)
bot = get_bot() or message.bot 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 = ( report = (
"<b>Processing complete</b>\n\n" "<b>Обработка завершена</b>\n\n"
f"Total: {total}\nDuplicates: {duplicates}\nValid: {len(valid)}\n" f"Всего cookie: {total}\nДубликатов: {duplicates}\nВалидных: {len(valid)}\n"
f"Payable: {len(priced)}\nRefreshed: {len(fresh_ok)}\nFailed refresh: {fresh_failed}\n" f"Подошло под курс: {len(priced)}\nОбновлено: {len(fresh_ok)}\nОшибок обновления: {fresh_failed}\n"
f"Method: {method}\n" f"Метод оплаты: {method}\n"
) )
if avg_used: if avg_used:
report += f"AVG price: {avg_price:.2f} RUB\n" report += f"AVG цена за cookie: {avg_price:.2f} \n"
report += f"\nPayout: <b>{payout:.2f} RUB</b>\nBalance: <b>{profile.balance:.2f} RUB</b>" 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 progress.edit_text(report, parse_mode="HTML", reply_markup=main_menu_kb())
await log_admin( await log_admin(
bot, bot,
ADMIN_ID, ADMIN_ID,
f"<b>Cookie purchase</b>\n@{profile.username or '-'} (<code>{profile.user_id}</code>)\n" f"🍪 <b>Новая покупка cookie</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"Всего: {total}, валидных: {len(valid)}, обновлено: {len(fresh_ok)}\nВыплачено: {payout:.2f} ",
get_log_bot(), get_log_bot(),
) )
await log_admin_document( await log_admin_document(
bot, bot,
ADMIN_ID, ADMIN_ID,
str(user_file), 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(), log_bot=get_log_bot(),
) )
user_file.unlink(missing_ok=True) user_file.unlink(missing_ok=True)
+97 -52
View File
@@ -1,11 +1,12 @@
from aiogram import F, Router from aiogram import F, Router
from aiogram.fsm.context import FSMContext 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 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 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 runtime import get_bot, get_log_bot
from services.withdrawals import create_crypto_check from services.withdrawals import create_crypto_check
from utils.logging import log_admin, safe_edit from utils.logging import log_admin, safe_edit
@@ -13,6 +14,10 @@ from utils.logging import log_admin, safe_edit
router = Router(name="user") router = Router(name="user")
class WithdrawState(StatesGroup):
waiting_for_amount = State()
def _get_profile(callback: CallbackQuery): def _get_profile(callback: CallbackQuery):
profile = get(callback.from_user.id) profile = get(callback.from_user.id)
if profile is None: if profile is None:
@@ -27,16 +32,16 @@ async def cb_profile(callback: CallbackQuery, state: FSMContext):
profile = _get_profile(callback) profile = _get_profile(callback)
cfg = load_config() cfg = load_config()
text = ( text = (
"<b>Your profile</b>\n\n" "👤 <b>Ваш профиль</b>\n\n"
f"ID: <code>{profile.user_id}</code>\n" f"ID: <code>{profile.user_id}</code>\n"
f"Registered: {profile.registered}\n\n" f"📅 Дата регистрации: {profile.registered}\n\n"
f"Cookies loaded: <b>{profile.cookies_loaded}</b>\n" f"🍪 Загружено cookie: <b>{profile.cookies_loaded}</b>\n"
f"Total earned: <b>{profile.total_earned:.2f} RUB</b>\n" f"💰 Заработано всего: <b>{profile.total_earned:.2f} </b>\n"
f"Referral earnings: <b>{profile.referral_earned:.2f} RUB</b>\n" f"💸 С рефералов: <b>{profile.referral_earned:.2f} </b>\n"
f"Balance: <b>{profile.balance:.2f} RUB</b>\n\n" f"💳 Текущий баланс: <b>{profile.balance:.2f} </b>\n\n"
f"Referral link:\nhttps://t.me/{BOT_USERNAME}?start=ref_{profile.user_id}\n\n" f"🔗 Ваша реферальная ссылка:\nhttps://t.me/{BOT_USERNAME}?start=ref_{profile.user_id}\n\n"
f"Referral payout: {cfg.referral_percent}%\n" f"💡 Реферальный процент: {cfg.referral_percent}%\n"
f"Minimum withdrawal: {cfg.min_withdraw:.2f} RUB" f"💳 Минимальный вывод: {cfg.min_withdraw:.2f} "
) )
await safe_edit(callback, text, reply_markup=back_kb()) await safe_edit(callback, text, reply_markup=back_kb())
await callback.answer() await callback.answer()
@@ -47,11 +52,11 @@ async def cb_stats(callback: CallbackQuery, state: FSMContext):
await state.clear() await state.clear()
stats = load_stats() stats = load_stats()
text = ( text = (
"<b>Global statistics</b>\n\n" "📈 <b>Общая статистика</b>\n\n"
f"Users: <b>{stats.total_users}</b>\n" f"👥 Пользователей: <b>{stats.total_users}</b>\n"
f"Cookies: <b>{stats.total_cookies}</b>\n" f"🍪 Всего cookie: <b>{stats.total_cookies}</b>\n"
f"Direct payouts: <b>{stats.total_paid_direct:.2f} RUB</b>\n" f"💰 Прямых выплат: <b>{stats.total_paid_direct:.2f} </b>\n"
f"Referral payouts: <b>{stats.total_paid_referral:.2f} RUB</b>" f"💸 Реферальных выплат: <b>{stats.total_paid_referral:.2f} </b>"
) )
await safe_edit(callback, text, reply_markup=back_kb()) await safe_edit(callback, text, reply_markup=back_kb())
await callback.answer() await callback.answer()
@@ -61,12 +66,12 @@ async def cb_stats(callback: CallbackQuery, state: FSMContext):
async def cb_rates(callback: CallbackQuery, state: FSMContext): async def cb_rates(callback: CallbackQuery, state: FSMContext):
await state.clear() await state.clear()
cfg = load_config() cfg = load_config()
text = "<b>Current rates</b>\n\n" text = "💰 <b>Актуальные курсы</b>\n\n"
for rate in cfg.rates.values(): for rate in cfg.rates.values():
text += f"- {rate.name}: {rate.price:.2f} RUB\n" text += f"🔸 {rate.name}: {rate.price:.2f} \n"
text += ( text += (
f"\nAVG: {cfg.avg_min_cookies}+ donation cookies, " f"\n📊 <b>AVG-система (от {cfg.avg_min_cookies}+ донатных cookie):</b>\n"
f"{cfg.avg_min_price:.2f}-{cfg.avg_max_price:.2f} RUB per cookie." f"Цена за cookie: {cfg.avg_min_price:.2f}-{cfg.avg_max_price:.2f} ."
) )
await safe_edit(callback, text, reply_markup=back_kb()) await safe_edit(callback, text, reply_markup=back_kb())
await callback.answer() await callback.answer()
@@ -77,51 +82,91 @@ async def cb_withdraw(callback: CallbackQuery, state: FSMContext):
await state.clear() await state.clear()
profile = _get_profile(callback) profile = _get_profile(callback)
cfg = load_config() cfg = load_config()
if profile.balance < cfg.min_withdraw: if profile.balance < cfg.min_withdraw:
await safe_edit( await safe_edit(
callback, callback,
f"Insufficient balance: <b>{profile.balance:.2f} RUB</b>\n" f"❌ Недостаточно средств. Баланс: <b>{profile.balance:.2f} </b>\n"
f"Minimum: <b>{cfg.min_withdraw:.2f} RUB</b>\n" f"📉 Минимум для вывода: <b>{cfg.min_withdraw:.2f} </b>\n"
f"Treasury: <b>{cfg.treasury:.2f} RUB</b>", f"🏦 Казна: <b>{cfg.treasury:.2f} </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(), reply_markup=back_kb(),
) )
await callback.answer() await callback.answer()
return 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( await safe_edit(
callback, callback,
f"Check created for <b>{amount:.2f} RUB</b>.\n" f"💳 <b>Вывод средств</b>\n\n"
f"<a href='{check_url}'>Activate check</a>", f"Ваш баланс: <b>{profile.balance:.2f}</b>\n"
f"Минимум: <b>{cfg.min_withdraw:.2f} ₽</b>\n\n"
f"Введите сумму для вывода:",
reply_markup=back_kb(), 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( await log_admin(
bot, bot,
ADMIN_ID, ADMIN_ID,
f"<b>Withdrawal</b>\n@{profile.username or '-'} (<code>{profile.user_id}</code>)\n" f"💸 <b>Вывод средств</b>\n@{profile.username or ''} (<code>{profile.user_id}</code>)\n"
f"Amount: {amount:.2f} RUB\nCheck: {check_url}", f"Сумма: {amount:.2f} \nЧек: {check_url}",
get_log_bot(), get_log_bot(),
) )
+10 -10
View File
@@ -8,26 +8,26 @@ def admin_menu_kb() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup( return InlineKeyboardMarkup(
inline_keyboard=[ inline_keyboard=[
[InlineKeyboardButton( [InlineKeyboardButton(
text=f"Shop: {'ON' if cfg.shop_enabled else 'OFF'}", text=f"🛒 Скупка: {'✅ ВКЛ' if cfg.shop_enabled else '❌ ВЫКЛ'}",
callback_data="admin_toggle_shop", callback_data="admin_toggle_shop",
)], )],
[InlineKeyboardButton( [InlineKeyboardButton(
text=f"Bot: {'ON' if cfg.bot_enabled else 'OFF'}", text=f"🤖 Бот: {'✅ ВКЛ' if cfg.bot_enabled else '❌ ВЫКЛ'}",
callback_data="admin_toggle_bot", callback_data="admin_toggle_bot",
)], )],
[InlineKeyboardButton(text="Users", callback_data="admin_users")], [InlineKeyboardButton(text="👥 Пользователи", callback_data="admin_users")],
[InlineKeyboardButton(text="Rates", callback_data="admin_rates")], [InlineKeyboardButton(text="💰 Управление курсами", callback_data="admin_rates")],
[InlineKeyboardButton(text="Broadcast", callback_data="admin_broadcast")], [InlineKeyboardButton(text="📣 Рассылка", callback_data="admin_broadcast")],
[InlineKeyboardButton( [InlineKeyboardButton(
text=f"Minimum withdrawal: {cfg.min_withdraw:.2f} RUB", text=f"💳 Мин. вывод: {cfg.min_withdraw:.2f} ",
callback_data="admin_min_withdraw", callback_data="admin_min_withdraw",
)], )],
[InlineKeyboardButton( [InlineKeyboardButton(
text=f"Treasury: {cfg.treasury:.2f} RUB | Top up", text=f"🏦 Казна: {cfg.treasury:.2f} ₽ | Пополнить",
callback_data="admin_treasury", callback_data="admin_treasury",
)], )],
[InlineKeyboardButton(text="Download robsec.txt", callback_data="admin_robsec_get")], [InlineKeyboardButton(text="📥 Выгрузить robsec.txt", callback_data="admin_robsec_get")],
[InlineKeyboardButton(text="Clear robsec.txt", callback_data="admin_robsec_clear")], [InlineKeyboardButton(text="🗑 Очистить robsec.txt", callback_data="admin_robsec_clear")],
[InlineKeyboardButton(text="Close", callback_data="admin_close")], [InlineKeyboardButton(text="❌ Закрыть", callback_data="admin_close")],
] ]
) )
+17 -6
View File
@@ -7,14 +7,25 @@ def main_menu_kb() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup( return InlineKeyboardMarkup(
inline_keyboard=[ inline_keyboard=[
[ [
InlineKeyboardButton(text="Profile", callback_data="profile"), InlineKeyboardButton(text="👤 Профиль", callback_data="profile"),
InlineKeyboardButton(text="Statistics", callback_data="stats"), InlineKeyboardButton(text="📈 Статистика", callback_data="stats"),
], ],
[ [
InlineKeyboardButton(text="Rates", callback_data="rates"), InlineKeyboardButton(text="💰 Курсы", callback_data="rates"),
InlineKeyboardButton(text="Withdraw", callback_data="withdraw"), 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: def back_kb(callback_data: str = "back_main") -> InlineKeyboardMarkup:
return InlineKeyboardMarkup( return InlineKeyboardMarkup(
inline_keyboard=[ 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 import logging
from datetime import datetime from datetime import datetime
from aiosend import CryptoPay, TESTNET, MAINNET
from aiogram import Bot, Dispatcher from aiogram import Bot, Dispatcher
from aiogram.enums import ParseMode from aiogram.enums import ParseMode
from aiogram.fsm.storage.memory import MemoryStorage from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.client.default import DefaultBotProperties 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 db.storage import load_config, load_stats
from handlers import admin, start, upload, user 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 from utils.logging import log_admin
async def main() -> None: async def main() -> None:
defaults = DefaultBotProperties(parse_mode=ParseMode.HTML) defaults = DefaultBotProperties(parse_mode=ParseMode.HTML)
bot = Bot(token=BOT_TOKEN, default=defaults) bot = Bot(token=BOT_TOKEN, default=defaults)
log_bot = Bot(token=LOG_BOT_TOKEN, default=defaults) if LOG_BOT_TOKEN else None 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) set_bots(bot, log_bot)
dispatcher = Dispatcher(storage=MemoryStorage()) dp = Dispatcher(storage=MemoryStorage())
dispatcher.include_router(start.router)
dispatcher.include_router(user.router) dp.include_router(start.router)
dispatcher.include_router(admin.router) dp.include_router(user.router)
dispatcher.include_router(upload.router) dp.include_router(admin.router)
dp.include_router(upload.router)
load_config() load_config()
load_stats() load_stats()
await log_admin( await log_admin(
bot, bot,
ADMIN_ID, 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, log_bot,
) )
try: try:
await dispatcher.start_polling(bot) await dp.start_polling(bot)
finally: finally:
if log_bot: if log_bot:
await log_bot.session.close() await log_bot.session.close()
await bot.session.close() await bot.session.close()
@@ -51,4 +72,5 @@ if __name__ == "__main__":
logging.StreamHandler(), logging.StreamHandler(),
], ],
) )
asyncio.run(main()) asyncio.run(main())
BIN
View File
Binary file not shown.
+11
View File
@@ -1,9 +1,11 @@
from __future__ import annotations from __future__ import annotations
from aiogram import Bot from aiogram import Bot
from aiosend import CryptoPay
_bot: Bot | None = None _bot: Bot | None = None
_log_bot: Bot | None = None _log_bot: Bot | None = None
_cp: CryptoPay | None = None
def set_bots(bot: Bot, log_bot: Bot | None = 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: def get_log_bot() -> Bot | None:
return _log_bot 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 from __future__ import annotations
import asyncio
import os import os
import asyncio
from aiogram.enums import ParseMode 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 config import ADMIN_ID, ROBSEC_FILE
from db.storage import load_config, save_config, load_stats, save_stats
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 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: def is_admin(user_id: int) -> bool:
if user_id == ADMIN_ID: if user_id == ADMIN_ID:
return True return True
profile = get(user_id) profile = get(user_id)
return bool(profile and profile.is_admin) return bool(profile and profile.is_admin)
@@ -25,6 +24,7 @@ def is_admin(user_id: int) -> bool:
def toggle_shop() -> bool: def toggle_shop() -> bool:
cfg = load_config() cfg = load_config()
cfg.shop_enabled = not cfg.shop_enabled cfg.shop_enabled = not cfg.shop_enabled
save_config(cfg) save_config(cfg)
return cfg.shop_enabled return cfg.shop_enabled
@@ -32,6 +32,7 @@ def toggle_shop() -> bool:
def toggle_bot() -> bool: def toggle_bot() -> bool:
cfg = load_config() cfg = load_config()
cfg.bot_enabled = not cfg.bot_enabled cfg.bot_enabled = not cfg.bot_enabled
save_config(cfg) save_config(cfg)
return cfg.bot_enabled return cfg.bot_enabled
@@ -52,6 +53,7 @@ def update_min_withdraw(value: float) -> None:
def add_treasury(amount_rub: float) -> float: def add_treasury(amount_rub: float) -> float:
cfg = load_config() cfg = load_config()
cfg.treasury += amount_rub cfg.treasury += amount_rub
save_config(cfg) save_config(cfg)
return cfg.treasury return cfg.treasury
@@ -59,6 +61,7 @@ def add_treasury(amount_rub: float) -> float:
def subtract_treasury(amount_rub: float) -> float: def subtract_treasury(amount_rub: float) -> float:
cfg = load_config() cfg = load_config()
cfg.treasury = max(0.0, cfg.treasury - amount_rub) cfg.treasury = max(0.0, cfg.treasury - amount_rub)
save_config(cfg) save_config(cfg)
return cfg.treasury return cfg.treasury
@@ -99,8 +102,12 @@ async def send_user_card(target, uid: int) -> None:
kb = InlineKeyboardMarkup( kb = InlineKeyboardMarkup(
inline_keyboard=[ inline_keyboard=[
[ [
InlineKeyboardButton(text=" Баланс", callback_data=f"u_bal_add_{uid}"), InlineKeyboardButton(
InlineKeyboardButton(text=" Баланс", callback_data=f"u_bal_sub_{uid}"), text=" Баланс", callback_data=f"u_bal_add_{uid}"
),
InlineKeyboardButton(
text=" Баланс", callback_data=f"u_bal_sub_{uid}"
),
], ],
[ [
InlineKeyboardButton( InlineKeyboardButton(
@@ -110,7 +117,9 @@ async def send_user_card(target, uid: int) -> None:
], ],
[ [
InlineKeyboardButton( InlineKeyboardButton(
text="👑 Убрать админку" if profile.is_admin else "👑 Выдать админку", text=(
"👑 Убрать админку" if profile.is_admin else "👑 Выдать админку"
),
callback_data=f"u_admin_{uid}", 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) 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 ok = 0
fail = 0 fail = 0
for uid in all_users(): for uid in all_users():
try: try:
if photo_id: 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: 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 ok += 1
except Exception: except Exception:
fail += 1 fail += 1
@@ -143,6 +161,7 @@ def find_user(query: str) -> int | None:
if query.isdigit(): if query.isdigit():
uid = int(query) uid = int(query)
return uid if get(uid) else None return uid if get(uid) else None
return find_by_username(query) return find_by_username(query)
@@ -150,6 +169,7 @@ def set_balance(uid: int, value: float) -> None:
profile = get(uid) profile = get(uid)
if not profile: if not profile:
return return
profile.balance = value profile.balance = value
save(uid, profile) save(uid, profile)
@@ -158,6 +178,7 @@ def add_balance(uid: int, amount: float) -> None:
profile = get(uid) profile = get(uid)
if not profile: if not profile:
return return
profile.balance += amount profile.balance += amount
save(uid, profile) save(uid, profile)
@@ -166,6 +187,7 @@ def sub_balance(uid: int, amount: float) -> None:
profile = get(uid) profile = get(uid)
if not profile: if not profile:
return return
profile.balance = max(0.0, profile.balance - amount) profile.balance = max(0.0, profile.balance - amount)
save(uid, profile) save(uid, profile)
@@ -174,6 +196,7 @@ def toggle_user_admin(uid: int) -> bool:
profile = get(uid) profile = get(uid)
if not profile: if not profile:
return False return False
profile.is_admin = not profile.is_admin profile.is_admin = not profile.is_admin
save(uid, profile) save(uid, profile)
return profile.is_admin return profile.is_admin
@@ -183,6 +206,7 @@ def ban_user(uid: int, reason: str) -> None:
profile = get(uid) profile = get(uid)
if not profile: if not profile:
return return
profile.is_banned = True profile.is_banned = True
profile.ban_reason = reason profile.ban_reason = reason
save(uid, profile) save(uid, profile)
@@ -192,6 +216,7 @@ def unban_user(uid: int) -> None:
profile = get(uid) profile = get(uid)
if not profile: if not profile:
return return
profile.is_banned = False profile.is_banned = False
profile.ban_reason = "" profile.ban_reason = ""
save(uid, profile) save(uid, profile)
+11 -5
View File
@@ -1,8 +1,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import logging
import random import random
import asyncio
import aiohttp import aiohttp
import requests as plain_requests import requests as plain_requests
@@ -39,7 +38,9 @@ def _extract_new_cookie(response):
return cookie return cookie
except Exception: except Exception:
pass 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: if ".ROBLOSECURITY=" in sc:
try: try:
return sc.split(".ROBLOSECURITY=")[1].split(";")[0] 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): async def fresh_cookie_async(cookie, proxy=None):
loop = asyncio.get_event_loop() 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: 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, proxy=f"http://{proxy}" if proxy else None,
timeout=aiohttp.ClientTimeout(total=REQUEST_TIMEOUT), timeout=aiohttp.ClientTimeout(total=REQUEST_TIMEOUT),
) as response: ) 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() data = await response.json()
donate = data.get("purchasesTotal", 0) donate = data.get("purchasesTotal", 0)
if donate != 0: if donate != 0:
+39 -57
View File
@@ -1,82 +1,64 @@
from __future__ import annotations from __future__ import annotations
import aiohttp
import logging import logging
from runtime import get_cp
from config import CRYPTO_PAY_TOKEN from config import CRYPTO_PAY_ASSET
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: async def create_crypto_check(amount_rub: float) -> str | None:
try: try:
headers = {"Crypto-Pay-API-Token": CRYPTO_PAY_TOKEN} cp = get_cp()
async with aiohttp.ClientSession() as session:
usdt_rub = await _get_usdt_rub_rate(session) amount_asset = await cp.exchange(
amount_usdt = round(amount_rub / usdt_rub, 4) amount=amount_rub, source="RUB", target=CRYPTO_PAY_ASSET
async with session.post( )
"https://testnet-pay.crypt.bot/api/createCheck",
headers=headers, createdCheck = await cp.create_check(
json={"asset": "USDT", "amount": str(amount_usdt)}, amount=amount_asset, asset=CRYPTO_PAY_ASSET
) as response: )
data = await response.json() check_image_url = await createdCheck.get_image(fiat="RUB")
if data.get("ok"):
return data["result"]["bot_check_url"] return createdCheck.bot_check_url, check_image_url
except Exception as exc: except Exception as exc:
logging.error("crypto check: %s", exc) logging.error("crypto check: %s", exc)
return None return None
async def create_treasury_invoice(amount_rub: float) -> tuple[str, int, float] | None: async def create_treasury_invoice(amount_rub: float) -> tuple[str, int, float] | None:
try: try:
headers = {"Crypto-Pay-API-Token": CRYPTO_PAY_TOKEN} cp = get_cp()
async with aiohttp.ClientSession() as session:
usdt_rub = await _get_usdt_rub_rate(session) createdInvoice = await cp.create_invoice(
amount_usdt = round(amount_rub / usdt_rub, 4) amount=amount_rub,
async with session.post( currency_type="fiat",
"https://testnet-pay.crypt.bot/api/createInvoice", accepted_assets=[CRYPTO_PAY_ASSET],
headers=headers, fiat="RUB",
json={ )
"asset": "USDT",
"amount": str(amount_usdt), amount_asset = round(
"description": f"Treasury +{amount_rub} RUB", await cp.exchange(amount=amount_rub, source="RUB", target=CRYPTO_PAY_ASSET),
"payload": f"treasury_{amount_rub}", 2,
}, )
) as response:
data = await response.json() return (createdInvoice.pay_url, int(createdInvoice.invoice_id), amount_asset)
if data.get("ok"):
result = data["result"]
return result["pay_url"], int(result["invoice_id"]), amount_usdt
except Exception as exc: except Exception as exc:
logging.error("create_treasury_invoice: %s", exc) logging.error("create_treasury_invoice: %s", exc)
return None return None
async def check_treasury_invoice(invoice_id: int) -> bool: async def check_treasury_invoice(invoice_id: int) -> bool:
try: try:
headers = {"Crypto-Pay-API-Token": CRYPTO_PAY_TOKEN} cp = get_cp()
async with aiohttp.ClientSession() as session:
async with session.get( paidInvoice = await cp.get_invoice(invoice_id)
f"https://testnet-pay.crypt.bot/api/getInvoices?invoice_ids={invoice_id}", return paidInvoice.status == "paid"
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: except Exception as exc:
logging.error("check_treasury_invoice: %s", exc) logging.error("check_treasury_invoice: %s", exc)
return False 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: try:
target = log_bot or bot target = log_bot or bot
await target.send_message(admin_id, text, parse_mode=ParseMode.HTML) await target.send_message(admin_id, text, parse_mode=ParseMode.HTML)
except Exception as exc: except Exception as exc:
logging.error("log_admin: %s", 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: try:
target = log_bot or bot target = log_bot or bot
await target.send_document( await target.send_document(