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
+3 -2
View File
@@ -4,14 +4,15 @@ ADMIN_ID=
CRYPTO_PAY_TOKEN= CRYPTO_PAY_TOKEN=
CRYPTO_PAY_NETWORK= 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
+9 -5
View File
@@ -5,8 +5,8 @@ from dotenv import load_dotenv
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from pathlib import Path from pathlib import Path
from models.config import BotConfig
from models.stats import BotStats from models.stats import BotStats
from models.config import BotConfig
BASE_DIR = Path(__file__).resolve().parent BASE_DIR = Path(__file__).resolve().parent
@@ -22,6 +22,7 @@ def _env_int(name: str, default: int) -> int:
raw = os.getenv(name) raw = os.getenv(name)
if raw is None or raw == "": if raw is None or raw == "":
return default return default
return int(raw) return int(raw)
@@ -29,6 +30,7 @@ def _env_float(name: str, default: float) -> float:
raw = os.getenv(name) raw = os.getenv(name)
if raw is None or raw == "": if raw is None or raw == "":
return default return default
return float(raw) return float(raw)
@@ -36,21 +38,23 @@ 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_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")
+24 -7
View File
@@ -11,7 +11,7 @@ from aiogram.types import (
Message, Message,
) )
from config import ADMIN_ID, ROBSEC_FILE from config import ADMIN_ID, CRYPTO_PAY_ASSET, ROBSEC_FILE
from db.storage import load_config from db.storage import load_config
from db.users import get from db.users import get
from keyboards.admin import admin_menu_kb from keyboards.admin import admin_menu_kb
@@ -37,7 +37,10 @@ from states.admin import Form
from runtime import get_bot from runtime import get_bot
from utils.logging import safe_edit from utils.logging import safe_edit
from runtime import get_cp
router = Router(name="admin") router = Router(name="admin")
cp = get_cp()
def _admin(user_id: int) -> bool: def _admin(user_id: int) -> bool:
@@ -78,7 +81,9 @@ def _user_card(uid: int) -> tuple[str, InlineKeyboardMarkup] | 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}",
) )
], ],
@@ -184,7 +189,9 @@ 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 очищен</b>", reply_markup=admin_menu_kb()) await safe_edit(
callback, "✅ <b>robsec.txt очищен</b>", reply_markup=admin_menu_kb()
)
await callback.answer("Очищено") await callback.answer("Очищено")
@@ -315,7 +322,8 @@ async def admin_treasury_amount(message: Message, state: FSMContext):
"❌ Не удалось создать счет CryptoBot.", reply_markup=admin_menu_kb() "❌ Не удалось создать счет 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="💳 Оплатить", url=pay_url)], [InlineKeyboardButton(text="💳 Оплатить", url=pay_url)],
@@ -328,7 +336,7 @@ async def admin_treasury_amount(message: Message, state: FSMContext):
] ]
) )
await message.answer( 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", parse_mode="HTML",
reply_markup=keyboard, reply_markup=keyboard,
) )
@@ -338,16 +346,21 @@ 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("Оплата еще не поступила", show_alert=True) await callback.answer("Оплата еще не поступила", show_alert=True)
return return
balance = add_treasury(amount_rub) balance = add_treasury(amount_rub)
await safe_edit( await safe_edit(
callback, callback,
@@ -392,7 +405,9 @@ async def admin_users(callback: CallbackQuery, state: FSMContext):
return return
await state.set_state(Form.admin_find_user) await state.set_state(Form.admin_find_user)
await safe_edit( 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.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( await safe_edit(
callback, "Отправьте причину блокировки:", reply_markup=back_kb("admin_back") callback,
"Отправьте причину блокировки:",
reply_markup=back_kb("admin_back"),
) )
return return
card = _user_card(uid) card = _user_card(uid)
+46 -13
View File
@@ -36,7 +36,9 @@ 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"❌ Вы заблокированы. Причина: {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:
@@ -51,7 +53,9 @@ 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("👑 Администраторы не могут продавать cookie. Используйте /admin.") await message.answer(
"👑 Администраторы не могут продавать cookie. Используйте /admin."
)
return return
if await _blocked(message): if await _blocked(message):
return return
@@ -60,7 +64,10 @@ async def handle_document(message: Message):
await message.answer("❌ Отправьте файл формата .txt.") 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,14 +79,22 @@ 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("👑 Администраторы не могут продавать cookie. Используйте /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("👋 Используйте меню ниже или отправьте текст с cookie.", 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)
@@ -99,7 +114,9 @@ async def process_cookies(message: Message, raw_text: str):
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
@@ -117,15 +134,22 @@ async def process_cookies(message: Message, raw_text: str):
pass pass
if not valid: if not valid:
await progress.edit_text(f"❌ Валидные cookie не найдены. Дубликатов: {duplicates}") await progress.edit_text(
f"❌ Валидные cookie не найдены. Дубликатов: {duplicates}"
)
return 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 = [] 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,
@@ -159,7 +183,9 @@ async def process_cookies(message: Message, raw_text: str):
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:
@@ -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,7 +233,9 @@ 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 "📋 Поштучный" method = "📊 AVG-система" if avg_used else "📋 Поштучный"
report = ( report = (
+64 -19
View File
@@ -1,11 +1,12 @@
from aiogram import F, Router from aiogram import F, Router
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery from aiogram.types import LinkPreviewOptions, CallbackQuery, Message
from aiogram.fsm.state import State, StatesGroup
from config import ADMIN_ID, BOT_USERNAME from config import ADMIN_ID, BOT_USERNAME
from db.storage import load_config, load_stats from db.storage import load_config, load_stats, save_config
from db.users import get, register, save from db.users import get, register, save
from keyboards.user import back_kb from keyboards.user import back_kb, activate_kb
from runtime import get_bot, get_log_bot from runtime import get_bot, get_log_bot
from services.withdrawals import create_crypto_check from services.withdrawals import create_crypto_check
from utils.logging import log_admin, safe_edit from utils.logging import log_admin, safe_edit
@@ -13,6 +14,10 @@ from utils.logging import log_admin, safe_edit
router = Router(name="user") router = Router(name="user")
class WithdrawState(StatesGroup):
waiting_for_amount = State()
def _get_profile(callback: CallbackQuery): def _get_profile(callback: CallbackQuery):
profile = get(callback.from_user.id) profile = get(callback.from_user.id)
if profile is None: if profile is None:
@@ -77,6 +82,7 @@ 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,
@@ -87,37 +93,76 @@ async def cb_withdraw(callback: CallbackQuery, state: FSMContext):
) )
await callback.answer() await callback.answer()
return return
if profile.balance > cfg.treasury:
await safe_edit( await safe_edit(
callback, callback,
f"⚠️ Недостаточно средств в казне: <b>{cfg.treasury:.2f} ₽</b>.", 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(), reply_markup=back_kb(),
) )
await state.set_state(WithdrawState.waiting_for_amount)
await callback.answer() 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 return
await callback.answer("⏳ Создаю чек...") if amount <= 0:
await safe_edit(callback, " Создаю CryptoBot-чек, подождите...") await message.answer(" Сумма должна быть больше нуля.")
amount = profile.balance return
check_url = await create_crypto_check(amount)
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: if not check_url:
await safe_edit(callback, "❌ Не удалось создать чек. Попробуйте позже.", reply_markup=back_kb()) await message.answer(
"❌ Не удалось создать чек. Попробуйте позже.",
reply_markup=back_kb(),
)
return return
profile.balance = 0.0 profile.balance -= amount
save(profile.user_id, profile) save(profile.user_id, profile)
cfg.treasury = max(0.0, cfg.treasury - amount) cfg.treasury = max(0.0, cfg.treasury - amount)
from db.storage import save_config
save_config(cfg) save_config(cfg)
await safe_edit(
callback, await message.answer(
f" Чек создан на сумму <b>{amount:.2f} ₽</b>.\n" text=f"🦋 Чек на <b>{amount:.2f} ₽</b>.",
f"<a href='{check_url}'>Активировать чек</a>", reply_markup=activate_kb(url=check_url, amount=amount),
reply_markup=back_kb(), link_preview_options=LinkPreviewOptions(
disable_web_page_preview=True, url=check_image_url, show_above_text=True
),
) )
bot = get_bot() or callback.message.bot
bot = get_bot() or message.bot
await log_admin( await log_admin(
bot, bot,
ADMIN_ID, ADMIN_ID,
+11
View File
@@ -19,6 +19,17 @@ def main_menu_kb() -> InlineKeyboardMarkup:
) )
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)]
]
)
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=[
+9 -8
View File
@@ -4,22 +4,23 @@ from datetime import datetime
from aiosend import CryptoPay, TESTNET, MAINNET 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 ( from config import (
ADMIN_ID,
BOT_TOKEN, BOT_TOKEN,
CRYPTO_PAY_NETWORK,
CRYPTO_PAY_TOKEN,
LOG_BOT_TOKEN, LOG_BOT_TOKEN,
ADMIN_ID,
SHOP_NAME, 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
@@ -28,11 +29,13 @@ async def main() -> None:
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
set_bots(bot, log_bot)
cp_net = TESTNET if CRYPTO_PAY_NETWORK.lower() == "testnet" else MAINNET cp_net = TESTNET if CRYPTO_PAY_NETWORK.lower() == "testnet" else MAINNET
cp = CryptoPay(token=CRYPTO_PAY_TOKEN, network=cp_net) cp = CryptoPay(token=CRYPTO_PAY_TOKEN, network=cp_net)
set_cp(cp)
set_bots(bot, log_bot)
dp = Dispatcher(storage=MemoryStorage()) dp = Dispatcher(storage=MemoryStorage())
dp.include_router(start.router) dp.include_router(start.router)
@@ -52,13 +55,11 @@ async def main() -> None:
try: try:
await dp.start_polling(bot) await dp.start_polling(bot)
await cp.start_polling(bot)
finally: finally:
if log_bot: if log_bot:
await log_bot.session.close() await log_bot.session.close()
await cp.session.close()
await bot.session.close() await bot.session.close()
+11
View File
@@ -1,9 +1,11 @@
from __future__ import annotations from __future__ import annotations
from aiogram import Bot from aiogram import Bot
from aiosend import CryptoPay
_bot: Bot | None = None _bot: Bot | None = None
_log_bot: Bot | None = None _log_bot: Bot | None = None
_cp: CryptoPay | None = None
def set_bots(bot: Bot, log_bot: Bot | None = None) -> None: def set_bots(bot: Bot, log_bot: Bot | None = None) -> None:
@@ -18,3 +20,12 @@ def get_bot() -> Bot | None:
def get_log_bot() -> Bot | None: def get_log_bot() -> Bot | None:
return _log_bot return _log_bot
def set_cp(cp: CryptoPay | None = None) -> None:
global _cp
_cp = cp
def get_cp() -> CryptoPay | None:
return _cp
+39 -14
View File
@@ -1,23 +1,22 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import os import os
import asyncio
from aiogram.enums import ParseMode from aiogram.enums import ParseMode
from aiogram.types import FSInputFile, InlineKeyboardButton, InlineKeyboardMarkup from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from config import ADMIN_ID, BOT_USERNAME, ROBSEC_FILE from config import ADMIN_ID, ROBSEC_FILE
from db.storage import load_config, save_config, load_stats, save_stats
from utils.logging import safe_edit
from db.storage import load_config, save_config
from db.users import all_users, get, save, find_by_username from db.users import all_users, get, save, find_by_username
from keyboards.admin import admin_menu_kb
from models.user import UserProfile
from runtime import get_log_bot
from utils.logging import log_admin, log_admin_document, safe_edit
def is_admin(user_id: int) -> bool: def is_admin(user_id: int) -> bool:
if user_id == ADMIN_ID: if user_id == ADMIN_ID:
return True return True
profile = get(user_id) profile = get(user_id)
return bool(profile and profile.is_admin) return bool(profile and profile.is_admin)
@@ -25,6 +24,7 @@ def is_admin(user_id: int) -> bool:
def toggle_shop() -> bool: def toggle_shop() -> bool:
cfg = load_config() cfg = load_config()
cfg.shop_enabled = not cfg.shop_enabled cfg.shop_enabled = not cfg.shop_enabled
save_config(cfg) save_config(cfg)
return cfg.shop_enabled return cfg.shop_enabled
@@ -32,6 +32,7 @@ def toggle_shop() -> bool:
def toggle_bot() -> bool: def toggle_bot() -> bool:
cfg = load_config() cfg = load_config()
cfg.bot_enabled = not cfg.bot_enabled cfg.bot_enabled = not cfg.bot_enabled
save_config(cfg) save_config(cfg)
return cfg.bot_enabled return cfg.bot_enabled
@@ -52,6 +53,7 @@ def update_min_withdraw(value: float) -> None:
def add_treasury(amount_rub: float) -> float: def add_treasury(amount_rub: float) -> float:
cfg = load_config() cfg = load_config()
cfg.treasury += amount_rub cfg.treasury += amount_rub
save_config(cfg) save_config(cfg)
return cfg.treasury return cfg.treasury
@@ -59,6 +61,7 @@ def add_treasury(amount_rub: float) -> float:
def subtract_treasury(amount_rub: float) -> float: def subtract_treasury(amount_rub: float) -> float:
cfg = load_config() cfg = load_config()
cfg.treasury = max(0.0, cfg.treasury - amount_rub) cfg.treasury = max(0.0, cfg.treasury - amount_rub)
save_config(cfg) save_config(cfg)
return cfg.treasury return cfg.treasury
@@ -99,8 +102,12 @@ async def send_user_card(target, uid: int) -> None:
kb = InlineKeyboardMarkup( kb = InlineKeyboardMarkup(
inline_keyboard=[ inline_keyboard=[
[ [
InlineKeyboardButton(text=" Баланс", callback_data=f"u_bal_add_{uid}"), InlineKeyboardButton(
InlineKeyboardButton(text=" Баланс", callback_data=f"u_bal_sub_{uid}"), text=" Баланс", callback_data=f"u_bal_add_{uid}"
),
InlineKeyboardButton(
text=" Баланс", callback_data=f"u_bal_sub_{uid}"
),
], ],
[ [
InlineKeyboardButton( InlineKeyboardButton(
@@ -110,7 +117,9 @@ async def send_user_card(target, uid: int) -> None:
], ],
[ [
InlineKeyboardButton( InlineKeyboardButton(
text="👑 Убрать админку" if profile.is_admin else "👑 Выдать админку", text=(
"👑 Убрать админку" if profile.is_admin else "👑 Выдать админку"
),
callback_data=f"u_admin_{uid}", callback_data=f"u_admin_{uid}",
) )
], ],
@@ -123,15 +132,24 @@ async def send_user_card(target, uid: int) -> None:
await safe_edit(target, text, reply_markup=kb) await safe_edit(target, text, reply_markup=kb)
async def broadcast(bot, text: str | None = None, photo_id: str | None = None, caption: str | None = None) -> tuple[int, int]: async def broadcast(
bot,
text: str | None = None,
photo_id: str | None = None,
caption: str | None = None,
) -> tuple[int, int]:
ok = 0 ok = 0
fail = 0 fail = 0
for uid in all_users(): for uid in all_users():
try: try:
if photo_id: if photo_id:
await bot.send_photo(uid, photo_id, caption=caption or "", parse_mode=ParseMode.HTML) await bot.send_photo(
uid, photo_id, caption=caption or "", parse_mode=ParseMode.HTML
)
else: else:
await bot.send_message(uid, text or caption or "-", parse_mode=ParseMode.HTML) await bot.send_message(
uid, text or caption or "-", parse_mode=ParseMode.HTML
)
ok += 1 ok += 1
except Exception: except Exception:
fail += 1 fail += 1
@@ -143,6 +161,7 @@ def find_user(query: str) -> int | None:
if query.isdigit(): if query.isdigit():
uid = int(query) uid = int(query)
return uid if get(uid) else None return uid if get(uid) else None
return find_by_username(query) return find_by_username(query)
@@ -150,6 +169,7 @@ def set_balance(uid: int, value: float) -> None:
profile = get(uid) profile = get(uid)
if not profile: if not profile:
return return
profile.balance = value profile.balance = value
save(uid, profile) save(uid, profile)
@@ -158,6 +178,7 @@ def add_balance(uid: int, amount: float) -> None:
profile = get(uid) profile = get(uid)
if not profile: if not profile:
return return
profile.balance += amount profile.balance += amount
save(uid, profile) save(uid, profile)
@@ -166,6 +187,7 @@ def sub_balance(uid: int, amount: float) -> None:
profile = get(uid) profile = get(uid)
if not profile: if not profile:
return return
profile.balance = max(0.0, profile.balance - amount) profile.balance = max(0.0, profile.balance - amount)
save(uid, profile) save(uid, profile)
@@ -174,6 +196,7 @@ def toggle_user_admin(uid: int) -> bool:
profile = get(uid) profile = get(uid)
if not profile: if not profile:
return False return False
profile.is_admin = not profile.is_admin profile.is_admin = not profile.is_admin
save(uid, profile) save(uid, profile)
return profile.is_admin return profile.is_admin
@@ -183,6 +206,7 @@ def ban_user(uid: int, reason: str) -> None:
profile = get(uid) profile = get(uid)
if not profile: if not profile:
return return
profile.is_banned = True profile.is_banned = True
profile.ban_reason = reason profile.ban_reason = reason
save(uid, profile) save(uid, profile)
@@ -192,6 +216,7 @@ def unban_user(uid: int) -> None:
profile = get(uid) profile = get(uid)
if not profile: if not profile:
return return
profile.is_banned = False profile.is_banned = False
profile.ban_reason = "" profile.ban_reason = ""
save(uid, profile) save(uid, profile)
+38 -57
View File
@@ -1,82 +1,63 @@
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)
+4 -1
View File
@@ -8,11 +8,14 @@ async def log_admin(bot, admin_id: int, text: str, log_bot=None) -> None:
try: try:
target = log_bot or bot target = log_bot or bot
await target.send_message(admin_id, text, parse_mode=ParseMode.HTML) await target.send_message(admin_id, text, parse_mode=ParseMode.HTML)
except Exception as exc: except Exception as exc:
logging.error("log_admin: %s", exc) logging.error("log_admin: %s", exc)
async def log_admin_document(bot, admin_id: int, path: str, caption: str | None = None, log_bot=None) -> None: async def log_admin_document(
bot, admin_id: int, path: str, caption: str | None = None, log_bot=None
) -> None:
try: try:
target = log_bot or bot target = log_bot or bot
await target.send_document( await target.send_document(