Add modular Roblox buyer bot and documentation

This commit is contained in:
2026-07-20 06:24:45 +05:00
commit c9413efd6a
29 changed files with 2607 additions and 0 deletions
+459
View File
@@ -0,0 +1,459 @@
from __future__ import annotations
from aiogram import F, Router
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import (
CallbackQuery,
FSInputFile,
InlineKeyboardButton,
InlineKeyboardMarkup,
Message,
)
from config import ADMIN_ID, ROBSEC_FILE
from db.storage import load_config
from db.users import get
from keyboards.admin import admin_menu_kb
from keyboards.user import back_kb
from services.admin import (
add_balance,
add_treasury,
ban_user,
broadcast,
clear_robsec,
find_user,
is_admin,
robsec_info,
sub_balance,
toggle_bot,
toggle_shop,
toggle_user_admin,
update_min_withdraw,
update_rate,
)
from services.withdrawals import check_treasury_invoice, create_treasury_invoice
from states.admin import Form
from runtime import get_bot
from utils.logging import safe_edit
router = Router(name="admin")
def _admin(user_id: int) -> bool:
return user_id == ADMIN_ID or is_admin(user_id)
def _user_card(uid: int) -> tuple[str, InlineKeyboardMarkup] | None:
profile = get(uid)
if not profile:
return None
text = (
"<b>User</b>\n\n"
f"ID: <code>{uid}</code>\n"
f"Username: @{profile.username or '-'}\n"
f"Registered: {profile.registered or '-'}\n"
f"Balance: {profile.balance:.2f} RUB\n"
f"Total earned: {profile.total_earned:.2f} RUB\n"
f"Cookies: {profile.cookies_loaded}\n"
f"Referrals: {len(profile.referrals)}\n"
f"Admin: {'yes' if profile.is_admin else 'no'}\n"
f"Banned: {'yes: ' + profile.ban_reason if profile.is_banned else 'no'}"
)
keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Add balance", callback_data=f"u_bal_add_{uid}"),
InlineKeyboardButton(text="Subtract balance", callback_data=f"u_bal_sub_{uid}"),
],
[InlineKeyboardButton(
text="Unban" if profile.is_banned else "Ban",
callback_data=f"u_ban_{uid}",
)],
[InlineKeyboardButton(
text="Remove admin" if profile.is_admin else "Grant admin",
callback_data=f"u_admin_{uid}",
)],
[InlineKeyboardButton(text="Back", callback_data="admin_back")],
]
)
return text, keyboard
@router.message(Command("admin"))
async def cmd_admin(message: Message, state: FSMContext):
if not _admin(message.from_user.id):
return
await state.clear()
await message.answer("<b>Admin panel</b>", parse_mode="HTML", reply_markup=admin_menu_kb())
@router.callback_query(F.data == "admin_close")
async def admin_close(callback: CallbackQuery, state: FSMContext):
if not _admin(callback.from_user.id):
return
await state.clear()
try:
await callback.message.delete()
except Exception:
pass
await callback.answer()
@router.callback_query(F.data == "admin_back")
async def admin_back(callback: CallbackQuery, state: FSMContext):
if not _admin(callback.from_user.id):
return
await state.clear()
await safe_edit(callback, "<b>Admin panel</b>", reply_markup=admin_menu_kb())
await callback.answer()
@router.callback_query(F.data == "admin_toggle_shop")
async def admin_toggle_shop(callback: CallbackQuery):
if not _admin(callback.from_user.id):
return
enabled = toggle_shop()
await callback.message.edit_reply_markup(reply_markup=admin_menu_kb())
await callback.answer(f"Shop: {'ON' if enabled else 'OFF'}")
@router.callback_query(F.data == "admin_toggle_bot")
async def admin_toggle_bot(callback: CallbackQuery):
if not _admin(callback.from_user.id):
return
enabled = toggle_bot()
await callback.message.edit_reply_markup(reply_markup=admin_menu_kb())
await callback.answer(f"Bot: {'ON' if enabled else 'OFF'}")
@router.callback_query(F.data == "admin_robsec_get")
async def admin_robsec_get(callback: CallbackQuery):
if not _admin(callback.from_user.id):
return
lines, size = robsec_info()
if not size:
await callback.answer("robsec.txt is empty", show_alert=True)
return
try:
bot = get_bot() or callback.message.bot
await bot.send_document(
callback.from_user.id,
FSInputFile(ROBSEC_FILE),
caption=f"robsec.txt\nLines: {lines}\nSize: {size} bytes",
)
await callback.answer("Sent")
except Exception as exc:
await callback.answer(f"Error: {exc}", show_alert=True)
@router.callback_query(F.data == "admin_robsec_clear")
async def admin_robsec_clear_prompt(callback: CallbackQuery):
if not _admin(callback.from_user.id):
return
lines, _ = robsec_info()
keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="Yes, clear", callback_data="admin_robsec_clear_yes")],
[InlineKeyboardButton(text="Cancel", callback_data="admin_back")],
]
)
await safe_edit(callback, f"Clear robsec.txt? Current lines: <b>{lines}</b>", reply_markup=keyboard)
@router.callback_query(F.data == "admin_robsec_clear_yes")
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 cleared</b>", reply_markup=admin_menu_kb())
await callback.answer("Cleared")
@router.callback_query(F.data == "admin_rates")
async def admin_rates(callback: CallbackQuery):
if not _admin(callback.from_user.id):
return
cfg = load_config()
rows = [
[InlineKeyboardButton(text=f"{rate.name}: {rate.price:.2f} RUB", callback_data=f"admin_rate_{key}")]
for key, rate in cfg.rates.items()
]
rows.append([InlineKeyboardButton(text="Back", callback_data="admin_back")])
await safe_edit(
callback,
"<b>Rate management</b>\nSelect a rate:",
reply_markup=InlineKeyboardMarkup(inline_keyboard=rows),
)
await callback.answer()
@router.callback_query(F.data.startswith("admin_rate_"))
async def admin_rate_select(callback: CallbackQuery, state: FSMContext):
if not _admin(callback.from_user.id):
return
key = callback.data.removeprefix("admin_rate_")
cfg = load_config()
rate = cfg.rates.get(key)
if not rate:
await callback.answer("Rate not found", show_alert=True)
return
await state.update_data(rate_key=key)
await state.set_state(Form.admin_edit_rate)
await safe_edit(
callback,
f"Rate: <b>{rate.name}</b>\nCurrent price: {rate.price:.2f} RUB\n\nSend a new price:",
reply_markup=back_kb("admin_back"),
)
@router.message(Form.admin_edit_rate)
async def admin_edit_rate_save(message: Message, state: FSMContext):
if not _admin(message.from_user.id):
return
try:
price = float((message.text or "").replace(",", "."))
if price < 0:
raise ValueError
except ValueError:
await message.answer("Send a non-negative number.")
return
data = await state.get_data()
key = data.get("rate_key")
cfg = load_config()
if key not in cfg.rates:
await state.clear()
return
update_rate(key, price)
await state.clear()
await message.answer(f"Rate updated: {cfg.rates[key].name} = {price:.2f} RUB", reply_markup=admin_menu_kb())
@router.callback_query(F.data == "admin_min_withdraw")
async def admin_min_withdraw(callback: CallbackQuery, state: FSMContext):
if not _admin(callback.from_user.id):
return
await state.set_state(Form.admin_min_withdraw)
await safe_edit(
callback,
f"Current minimum withdrawal: {load_config().min_withdraw:.2f} RUB\nSend a new value:",
reply_markup=back_kb("admin_back"),
)
@router.message(Form.admin_min_withdraw)
async def admin_min_withdraw_save(message: Message, state: FSMContext):
if not _admin(message.from_user.id):
return
try:
value = float((message.text or "").replace(",", "."))
if value <= 0:
raise ValueError
except ValueError:
await message.answer("Send a positive number.")
return
update_min_withdraw(value)
await state.clear()
await message.answer(f"Minimum withdrawal updated to {value:.2f} RUB", reply_markup=admin_menu_kb())
@router.callback_query(F.data == "admin_treasury")
async def admin_treasury(callback: CallbackQuery, state: FSMContext):
if not _admin(callback.from_user.id):
return
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"))
@router.message(Form.admin_treasury_amount)
async def admin_treasury_amount(message: Message, state: FSMContext):
if not _admin(message.from_user.id):
return
try:
amount = float((message.text or "").replace(",", "."))
if amount <= 0:
raise ValueError
except ValueError:
await message.answer("Send a positive number.")
return
await state.clear()
invoice = await create_treasury_invoice(amount)
if not invoice:
await message.answer("CryptoBot invoice creation failed.", reply_markup=admin_menu_kb())
return
pay_url, invoice_id, amount_usdt = invoice
keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="Pay", url=pay_url)],
[InlineKeyboardButton(text="Check payment", callback_data=f"treasury_check_{invoice_id}_{amount}")],
]
)
await message.answer(
f"Treasury invoice: <b>{amount:.2f} RUB</b> (~{amount_usdt} USDT)\nPay it and check the status.",
parse_mode="HTML",
reply_markup=keyboard,
)
@router.callback_query(F.data.startswith("treasury_check_"))
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("Payment has not arrived yet", show_alert=True)
return
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 callback.answer("Paid")
@router.callback_query(F.data == "admin_broadcast")
async def admin_broadcast_prompt(callback: CallbackQuery, state: FSMContext):
if not _admin(callback.from_user.id):
return
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"))
@router.message(Form.admin_broadcast)
async def admin_broadcast_send(message: Message, state: FSMContext):
if not _admin(message.from_user.id):
return
await state.clear()
sent, failed = await broadcast(
message.bot,
text=message.text,
photo_id=message.photo[-1].file_id if message.photo else None,
caption=message.caption,
)
await message.answer(f"Broadcast complete. Sent: {sent}, failed: {failed}.", reply_markup=admin_menu_kb())
@router.callback_query(F.data == "admin_users")
async def admin_users(callback: CallbackQuery, state: FSMContext):
if not _admin(callback.from_user.id):
return
await state.set_state(Form.admin_find_user)
await safe_edit(callback, "Send a user ID or @username.", reply_markup=back_kb("admin_back"))
@router.message(Form.admin_find_user)
async def admin_find_user(message: Message, state: FSMContext):
if not _admin(message.from_user.id):
return
uid = find_user((message.text or "").strip())
await state.clear()
if uid is None:
await message.answer("User not found.", reply_markup=admin_menu_kb())
return
card = _user_card(uid)
if card:
await message.answer(card[0], parse_mode="HTML", reply_markup=card[1])
@router.callback_query(F.data.startswith("u_bal_add_"))
async def u_bal_add(callback: CallbackQuery, state: FSMContext):
if not _admin(callback.from_user.id):
return
uid = int(callback.data.removeprefix("u_bal_add_"))
await state.update_data(target_uid=uid)
await state.set_state(Form.admin_user_balance_add)
await safe_edit(callback, "Send the amount to add:", reply_markup=back_kb("admin_back"))
@router.message(Form.admin_user_balance_add)
async def u_bal_add_save(message: Message, state: FSMContext):
if not _admin(message.from_user.id):
return
try:
amount = float((message.text or "").replace(",", "."))
if amount <= 0:
raise ValueError
except ValueError:
await message.answer("Send a positive number.")
return
uid = (await state.get_data()).get("target_uid")
add_balance(uid, amount)
await state.clear()
await message.answer(f"Added {amount:.2f} RUB to {uid}.", reply_markup=admin_menu_kb())
@router.callback_query(F.data.startswith("u_bal_sub_"))
async def u_bal_sub(callback: CallbackQuery, state: FSMContext):
if not _admin(callback.from_user.id):
return
uid = int(callback.data.removeprefix("u_bal_sub_"))
await state.update_data(target_uid=uid)
await state.set_state(Form.admin_user_balance_sub)
await safe_edit(callback, "Send the amount to subtract:", reply_markup=back_kb("admin_back"))
@router.message(Form.admin_user_balance_sub)
async def u_bal_sub_save(message: Message, state: FSMContext):
if not _admin(message.from_user.id):
return
try:
amount = float((message.text or "").replace(",", "."))
if amount <= 0:
raise ValueError
except ValueError:
await message.answer("Send a positive number.")
return
uid = (await state.get_data()).get("target_uid")
sub_balance(uid, amount)
await state.clear()
await message.answer(f"Subtracted {amount:.2f} RUB from {uid}.", reply_markup=admin_menu_kb())
@router.callback_query(F.data.startswith("u_ban_"))
async def u_ban(callback: CallbackQuery, state: FSMContext):
if not _admin(callback.from_user.id):
return
uid = int(callback.data.removeprefix("u_ban_"))
profile = get(uid)
if not profile:
return
if profile.is_banned:
from services.admin import unban_user
unban_user(uid)
await callback.answer("Unbanned")
else:
await state.update_data(target_uid=uid)
await state.set_state(Form.admin_user_ban_reason)
await safe_edit(callback, "Send the ban reason:", reply_markup=back_kb("admin_back"))
return
card = _user_card(uid)
if card:
await safe_edit(callback, card[0], reply_markup=card[1])
@router.message(Form.admin_user_ban_reason)
async def u_ban_reason(message: Message, state: FSMContext):
if not _admin(message.from_user.id):
return
uid = (await state.get_data()).get("target_uid")
ban_user(uid, (message.text or "No reason").strip())
await state.clear()
card = _user_card(uid)
if card:
await message.answer(card[0], parse_mode="HTML", reply_markup=card[1])
@router.callback_query(F.data.startswith("u_admin_"))
async def u_admin(callback: CallbackQuery):
if not _admin(callback.from_user.id):
return
uid = int(callback.data.removeprefix("u_admin_"))
enabled = toggle_user_admin(uid)
await callback.answer(f"Admin: {'ON' if enabled else 'OFF'}")
card = _user_card(uid)
if card:
await safe_edit(callback, card[0], reply_markup=card[1])
+95
View File
@@ -0,0 +1,95 @@
from aiogram import F, Router
from aiogram.filters import CommandStart
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
from config import ADMIN_ID, BOT_USERNAME, SHOP_NAME
from db.storage import load_config
from db.users import get, register
from keyboards.user import main_menu_kb
from services.admin import is_admin
from runtime import get_bot, get_log_bot
from utils.logging import log_admin
router = Router(name="start")
def _welcome_text(cfg) -> str:
text = (
f"Welcome to <b>{SHOP_NAME}</b>!\n\n"
"You can sell cookies at these rates:\n"
)
for rate in cfg.rates.values():
text += f"- {rate.name}: {rate.price:.2f} RUB\n"
return text + (
f"\nAVG pricing applies to {cfg.avg_min_cookies}+ cookies with donations.\n"
f"The AVG price is capped at {cfg.avg_min_price:.2f}-{cfg.avg_max_price:.2f} RUB per cookie.\n\n"
"Send a .txt file or paste your cookie text."
)
async def _show_start(
message: Message,
state: FSMContext,
user_id: int,
username: str | None,
referrer: int | None = None,
) -> None:
await state.clear()
is_new = register(user_id, username, referrer)
profile = get(user_id)
cfg = load_config()
if profile and profile.is_banned:
await message.answer(f"You are banned. Reason: {profile.ban_reason or '-'}")
return
if not cfg.bot_enabled and user_id != ADMIN_ID:
await message.answer("The bot is temporarily disabled by an administrator.")
return
if is_new:
bot = get_bot() or message.bot
await log_admin(
bot,
ADMIN_ID,
f"<b>New user</b>\nID: <code>{user_id}</code>\n"
f"@{username or '-'}\nReferrer: {referrer or '-'}",
get_log_bot(),
)
text = _welcome_text(cfg)
if is_admin(user_id):
text += "\n\n<b>Admin accounts cannot sell cookies. Use /admin.</b>"
await message.answer(text, parse_mode="HTML", reply_markup=main_menu_kb())
@router.message(CommandStart())
async def cmd_start(message: Message, state: FSMContext):
argument = (message.text or "").split(maxsplit=1)
referrer = None
if len(argument) > 1 and argument[1].startswith("ref_"):
try:
referrer = int(argument[1][4:])
except ValueError:
referrer = None
await _show_start(
message,
state,
message.from_user.id,
message.from_user.username,
referrer,
)
@router.callback_query(F.data == "back_main")
async def cb_back_main(callback: CallbackQuery, state: FSMContext):
await state.clear()
try:
await callback.message.delete()
except Exception:
pass
await _show_start(
callback.message,
state,
callback.from_user.id,
callback.from_user.username,
)
await callback.answer()
+233
View File
@@ -0,0 +1,233 @@
from __future__ import annotations
import asyncio
import random
import aiohttp
from aiogram import F, Router
from aiogram.filters import StateFilter
from aiogram.fsm.context import FSMContext
from aiogram.types import Message
from config import ADMIN_ID, COOKIE_FILES_DIR, FRESHER_BATCH_SIZE, ROBSEC_FILE
from db.storage import load_config, load_proxies, load_stats, save_stats
from db.users import get, register, save
from keyboards.user import back_kb, main_menu_kb
from models.pricing import CookieDonationInfo
from runtime import get_bot, get_log_bot
from services.admin import is_admin
from services.pricing import price_cookie_batch
from services.referrals import apply_referral_payout
from services.roblox import (
check_cookie_with_retry,
fresh_cookie_async,
get_all_time_donate,
get_year_donate,
)
from utils.logging import log_admin, log_admin_document
from utils.parsing import parse_cookies_text
router = Router(name="upload")
async def _blocked(message: Message) -> bool:
profile = get(message.from_user.id)
if profile is None:
register(message.from_user.id, message.from_user.username)
profile = get(message.from_user.id)
if profile.is_banned:
await message.answer(f"You are banned. Reason: {profile.ban_reason or '-'}")
return True
cfg = load_config()
if not cfg.bot_enabled and message.from_user.id != ADMIN_ID:
await message.answer("The bot is temporarily disabled.")
return True
if not cfg.shop_enabled and message.from_user.id != ADMIN_ID:
await message.answer("Cookie buying is temporarily disabled.")
return True
return False
@router.message(StateFilter(None), F.document)
async def handle_document(message: Message):
if is_admin(message.from_user.id):
await message.answer("Admin accounts cannot sell cookies. Use /admin.")
return
if await _blocked(message):
return
filename = message.document.file_name or ""
if not filename.lower().endswith(".txt"):
await message.answer("Send a .txt file.")
return
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")
finally:
path.unlink(missing_ok=True)
await process_cookies(message, text)
@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("Admin accounts cannot sell cookies. Use /admin.")
return
if await _blocked(message):
return
text = message.text or ""
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())
return
await process_cookies(message, text)
async def process_cookies(message: Message, raw_text: str):
cookies, duplicates = parse_cookies_text(raw_text)
if not cookies:
await message.answer("No cookies were found in the submitted data.")
return
cfg = load_config()
proxies = load_proxies()
total = len(cookies)
progress = await message.answer(
f"<b>Processing cookies</b>\nTotal: {total}\nDuplicates removed: {duplicates}\nStage: validation...",
parse_mode="HTML",
)
valid = []
async with aiohttp.ClientSession() as session:
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
if result.status == "valid":
valid.append(result)
except Exception:
pass
if index % 20 == 0 or index == total:
try:
await progress.edit_text(
f"<b>Validation</b>\nProgress: {index}/{total}\nValid: {len(valid)}",
parse_mode="HTML",
)
except Exception:
pass
if not valid:
await progress.edit_text(f"No valid cookies found. Duplicates: {duplicates}")
return
await progress.edit_text(f"<b>Checking donations</b>\nValid cookies: {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)
year = await get_year_donate(result.cookie, result.user_id, proxy)
return CookieDonationInfo(
cookie=result.cookie,
user_id=result.user_id,
username=result.username,
all_time=all_time,
year=year,
)
donations = await asyncio.gather(*(get_donation(result) for result in valid))
priced, avg_used, avg_price = price_cookie_batch(donations, cfg)
if not priced:
await progress.edit_text(
f"No cookies matched the rates. Valid: {len(valid)}",
reply_markup=back_kb(),
)
return
await progress.edit_text(
f"<b>Refreshing cookies</b>\nPayable: {len(priced)}\nMethod: {'AVG' if avg_used else 'per-rate'}",
parse_mode="HTML",
)
fresh_ok = []
fresh_failed = 0
for offset in range(0, len(priced), 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)):
if ok and isinstance(refreshed, str) and refreshed.startswith("_|WARNING"):
fresh_ok.append((item, refreshed))
else:
fresh_failed += 1
try:
await progress.edit_text(
f"<b>Refreshing cookies</b>\nProgress: {min(offset + len(batch), len(priced))}/{len(priced)}\n"
f"Refreshed: {len(fresh_ok)}\nFailed: {fresh_failed}",
parse_mode="HTML",
)
except Exception:
pass
if not fresh_ok:
await progress.edit_text(
f"No cookies could be refreshed. Valid: {len(valid)}, failed: {fresh_failed}",
reply_markup=back_kb(),
)
return
ROBSEC_FILE.parent.mkdir(parents=True, exist_ok=True)
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")
payout = sum(item.price for item, _ in fresh_ok)
profile = get(message.from_user.id)
if profile is None:
register(message.from_user.id, message.from_user.username)
profile = get(message.from_user.id)
profile.balance += payout
profile.total_earned += payout
profile.cookies_loaded += len(fresh_ok)
save(profile.user_id, profile)
stats = load_stats()
stats.total_cookies += len(fresh_ok)
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)
method = "AVG" if avg_used else "per-rate"
report = (
"<b>Processing complete</b>\n\n"
f"Total: {total}\nDuplicates: {duplicates}\nValid: {len(valid)}\n"
f"Payable: {len(priced)}\nRefreshed: {len(fresh_ok)}\nFailed refresh: {fresh_failed}\n"
f"Method: {method}\n"
)
if avg_used:
report += f"AVG price: {avg_price:.2f} RUB\n"
report += f"\nPayout: <b>{payout:.2f} RUB</b>\nBalance: <b>{profile.balance:.2f} RUB</b>"
await progress.edit_text(report, parse_mode="HTML", reply_markup=main_menu_kb())
await log_admin(
bot,
ADMIN_ID,
f"<b>Cookie purchase</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",
get_log_bot(),
)
await log_admin_document(
bot,
ADMIN_ID,
str(user_file),
caption=f"robsec from <code>{profile.user_id}</code>",
log_bot=get_log_bot(),
)
user_file.unlink(missing_ok=True)
+127
View File
@@ -0,0 +1,127 @@
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery
from config import ADMIN_ID, BOT_USERNAME
from db.storage import load_config, load_stats
from db.users import get, register, save
from keyboards.user import back_kb
from runtime import get_bot, get_log_bot
from services.withdrawals import create_crypto_check
from utils.logging import log_admin, safe_edit
router = Router(name="user")
def _get_profile(callback: CallbackQuery):
profile = get(callback.from_user.id)
if profile is None:
register(callback.from_user.id, callback.from_user.username)
profile = get(callback.from_user.id)
return profile
@router.callback_query(F.data == "profile")
async def cb_profile(callback: CallbackQuery, state: FSMContext):
await state.clear()
profile = _get_profile(callback)
cfg = load_config()
text = (
"<b>Your profile</b>\n\n"
f"ID: <code>{profile.user_id}</code>\n"
f"Registered: {profile.registered}\n\n"
f"Cookies loaded: <b>{profile.cookies_loaded}</b>\n"
f"Total earned: <b>{profile.total_earned:.2f} RUB</b>\n"
f"Referral earnings: <b>{profile.referral_earned:.2f} RUB</b>\n"
f"Balance: <b>{profile.balance:.2f} RUB</b>\n\n"
f"Referral link:\nhttps://t.me/{BOT_USERNAME}?start=ref_{profile.user_id}\n\n"
f"Referral payout: {cfg.referral_percent}%\n"
f"Minimum withdrawal: {cfg.min_withdraw:.2f} RUB"
)
await safe_edit(callback, text, reply_markup=back_kb())
await callback.answer()
@router.callback_query(F.data == "stats")
async def cb_stats(callback: CallbackQuery, state: FSMContext):
await state.clear()
stats = load_stats()
text = (
"<b>Global statistics</b>\n\n"
f"Users: <b>{stats.total_users}</b>\n"
f"Cookies: <b>{stats.total_cookies}</b>\n"
f"Direct payouts: <b>{stats.total_paid_direct:.2f} RUB</b>\n"
f"Referral payouts: <b>{stats.total_paid_referral:.2f} RUB</b>"
)
await safe_edit(callback, text, reply_markup=back_kb())
await callback.answer()
@router.callback_query(F.data == "rates")
async def cb_rates(callback: CallbackQuery, state: FSMContext):
await state.clear()
cfg = load_config()
text = "<b>Current rates</b>\n\n"
for rate in cfg.rates.values():
text += f"- {rate.name}: {rate.price:.2f} RUB\n"
text += (
f"\nAVG: {cfg.avg_min_cookies}+ donation cookies, "
f"{cfg.avg_min_price:.2f}-{cfg.avg_max_price:.2f} RUB per cookie."
)
await safe_edit(callback, text, reply_markup=back_kb())
await callback.answer()
@router.callback_query(F.data == "withdraw")
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,
f"Insufficient balance: <b>{profile.balance:.2f} RUB</b>\n"
f"Minimum: <b>{cfg.min_withdraw:.2f} RUB</b>\n"
f"Treasury: <b>{cfg.treasury:.2f} RUB</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(),
)
await callback.answer()
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(
callback,
f"Check created for <b>{amount:.2f} RUB</b>.\n"
f"<a href='{check_url}'>Activate check</a>",
reply_markup=back_kb(),
disable_web_page_preview=True,
)
bot = get_bot() or callback.message.bot
await log_admin(
bot,
ADMIN_ID,
f"<b>Withdrawal</b>\n@{profile.username or '-'} (<code>{profile.user_id}</code>)\n"
f"Amount: {amount:.2f} RUB\nCheck: {check_url}",
get_log_bot(),
)