539 lines
18 KiB
Python
539 lines
18 KiB
Python
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, CRYPTO_PAY_ASSET, 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
|
||
|
||
from runtime import get_cp
|
||
|
||
router = Router(name="admin")
|
||
cp = get_cp()
|
||
|
||
|
||
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>Пользователь</b>\n\n"
|
||
f"🆔 ID: <code>{uid}</code>\n"
|
||
f"👤 @{profile.username or '—'}\n"
|
||
f"📅 Регистрация: {profile.registered or '?'}\n"
|
||
f"💳 Баланс: {profile.balance:.2f} ₽\n"
|
||
f"💰 Всего заработано: {profile.total_earned:.2f} ₽\n"
|
||
f"🍪 Cookie: {profile.cookies_loaded}\n"
|
||
f"👥 Рефералов: {len(profile.referrals)}\n"
|
||
f"👑 Админ: {'✅' if profile.is_admin else '❌'}\n"
|
||
f"🚫 Бан: {'✅ ' + profile.ban_reason if profile.is_banned else '❌'}"
|
||
)
|
||
keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text="➕ Баланс", callback_data=f"u_bal_add_{uid}"
|
||
),
|
||
InlineKeyboardButton(
|
||
text="➖ Баланс", callback_data=f"u_bal_sub_{uid}"
|
||
),
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text="🚫 Разбанить" if profile.is_banned else "🚫 Забанить",
|
||
callback_data=f"u_ban_{uid}",
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text=(
|
||
"👑 Убрать админку" if profile.is_admin else "👑 Выдать админку"
|
||
),
|
||
callback_data=f"u_admin_{uid}",
|
||
)
|
||
],
|
||
[InlineKeyboardButton(text="🔙 Назад", 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>Админ-панель</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>Админ-панель</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"Скупка: {'ВКЛ' if enabled else 'ВЫКЛ'}")
|
||
|
||
|
||
@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"Бот: {'ВКЛ' if enabled else 'ВЫКЛ'}")
|
||
|
||
|
||
@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 пуст", 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"📥 <b>robsec.txt</b>\nСтрок: <b>{lines}</b>\nРазмер: <b>{size} байт</b>",
|
||
)
|
||
await callback.answer("Отправлено")
|
||
except Exception as exc:
|
||
await callback.answer(f"Ошибка: {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="✅ Да, очистить", callback_data="admin_robsec_clear_yes"
|
||
)
|
||
],
|
||
[InlineKeyboardButton(text="🔙 Отмена", callback_data="admin_back")],
|
||
]
|
||
)
|
||
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")
|
||
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 callback.answer("Очищено")
|
||
|
||
|
||
@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} ₽",
|
||
callback_data=f"admin_rate_{key}",
|
||
)
|
||
]
|
||
for key, rate in cfg.rates.items()
|
||
]
|
||
rows.append([InlineKeyboardButton(text="🔙 Назад", callback_data="admin_back")])
|
||
await safe_edit(
|
||
callback,
|
||
"💰 <b>Управление курсами</b>\nВыберите курс для изменения цены:",
|
||
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("Курс не найден", show_alert=True)
|
||
return
|
||
await state.update_data(rate_key=key)
|
||
await state.set_state(Form.admin_edit_rate)
|
||
await safe_edit(
|
||
callback,
|
||
f"💰 Курс: <b>{rate.name}</b>\nТекущая цена: {rate.price:.2f} ₽\n\nОтправьте новую цену числом:",
|
||
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("❌ Введите неотрицательное число.")
|
||
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"✅ Цена <b>{cfg.rates[key].name}</b> = {price:.2f} ₽",
|
||
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"💳 Текущий минимальный вывод: {load_config().min_withdraw:.2f} ₽\nОтправьте новое значение:",
|
||
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("❌ Введите положительное число.")
|
||
return
|
||
update_min_withdraw(value)
|
||
await state.clear()
|
||
await message.answer(
|
||
f"✅ Минимальный вывод: {value:.2f} ₽", 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,
|
||
"🏦 На какую сумму пополнить казну? (в ₽)\nОтправьте число:",
|
||
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("❌ Введите положительное число.")
|
||
return
|
||
await state.clear()
|
||
invoice = await create_treasury_invoice(amount)
|
||
if not invoice:
|
||
await message.answer(
|
||
"❌ Не удалось создать счет CryptoBot.", reply_markup=admin_menu_kb()
|
||
)
|
||
return
|
||
pay_url, invoice_id, amount_asset = invoice
|
||
|
||
keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[InlineKeyboardButton(text="💳 Оплатить", url=pay_url)],
|
||
[
|
||
InlineKeyboardButton(
|
||
text="✅ Проверить оплату",
|
||
callback_data=f"treasury_check_{invoice_id}_{amount}",
|
||
)
|
||
],
|
||
]
|
||
)
|
||
await message.answer(
|
||
f"🏦 Счет на пополнение казны\n\n💰 Сумма: <b>{amount:.2f} ₽</b> (~{amount_asset} {CRYPTO_PAY_ASSET})\n\nПосле оплаты нажмите «Проверить оплату».",
|
||
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("Оплата еще не поступила", show_alert=True)
|
||
return
|
||
|
||
balance = add_treasury(amount_rub)
|
||
await safe_edit(
|
||
callback,
|
||
f"✅ Казна пополнена на {amount_rub:.2f} ₽\n🏦 Всего: {balance:.2f} ₽",
|
||
reply_markup=admin_menu_kb(),
|
||
)
|
||
await callback.answer("Оплачено")
|
||
|
||
|
||
@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,
|
||
"📣 Отправьте сообщение для рассылки (текст или фото с подписью):",
|
||
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"✅ Рассылка завершена. Отправлено: {sent}\n❌ Ошибок: {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,
|
||
"Отправьте ID пользователя или @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("❌ Пользователь не найден.", 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, "Отправьте сумму для начисления:", 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("❌ Введите положительное число.")
|
||
return
|
||
uid = (await state.get_data()).get("target_uid")
|
||
add_balance(uid, amount)
|
||
await state.clear()
|
||
await message.answer(
|
||
f"✅ Пользователю {uid} начислено {amount:.2f} ₽.", 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, "Отправьте сумму для списания:", 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("❌ Введите положительное число.")
|
||
return
|
||
uid = (await state.get_data()).get("target_uid")
|
||
sub_balance(uid, amount)
|
||
await state.clear()
|
||
await message.answer(
|
||
f"✅ У пользователя {uid} списано {amount:.2f} ₽.", 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("Разблокирован")
|
||
else:
|
||
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"),
|
||
)
|
||
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 "Причина не указана").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"Админ: {'ВКЛ' if enabled else 'ВЫКЛ'}")
|
||
card = _user_card(uid)
|
||
if card:
|
||
await safe_edit(callback, card[0], reply_markup=card[1])
|