Compare commits
8 Commits
cea7da5d2b
...
16da3ea9eb
| Author | SHA1 | Date | |
|---|---|---|---|
| 16da3ea9eb | |||
| f07d588ae1 | |||
| 69e5574ead | |||
| ee7414fb74 | |||
| 8ae14f415f | |||
| 8d4d75f7fd | |||
| 5e407dc1a8 | |||
| 4adf9aa32e |
+5
-2
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
```
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)]
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
Binary file not shown.
+11
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
@echo off
|
||||||
|
powershell -ExecutionPolicy Bypass -File "%~dp0example-start.ps1"
|
||||||
@@ -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
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user