Admin update

This commit is contained in:
hteppl
2025-01-15 22:05:32 +03:00
21 changed files with 2085 additions and 1121 deletions
+12 -2
View File
@@ -2,16 +2,26 @@ __all__ = ("router",)
from aiogram import Router
from .admin_backups import router as backups_router
from .admin_bans import router as bans_router
from .admin_coupons import router as coupons_router
from .admin_panel import router as panel_router
from .admin_restart import router as restart_router
from .admin_sender import router as sender_router
from .admin_servers import router as servers_router
from .admin_user_editor import router as user_editor_router
from .admin_stats import router as stats_router
from .admin_users import router as users_router
router = Router(name="admins_main_router")
router.include_routers(
panel_router,
servers_router,
users_router,
stats_router,
backups_router,
sender_router,
coupons_router,
user_editor_router,
restart_router,
bans_router,
)
+30
View File
@@ -0,0 +1,30 @@
from aiogram import F, Router
from aiogram.types import CallbackQuery
from backup import backup_database
from filters.admin import IsAdminFilter
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
router = Router()
@router.callback_query(
AdminPanelCallback.filter(F.action == "backups"),
IsAdminFilter(),
)
async def handle_backup(
callback_query: CallbackQuery
):
kb = build_admin_back_kb("management")
await callback_query.message.edit_text(
text="💾 Инициализация резервного копирования базы данных...",
reply_markup=kb
)
await backup_database()
await callback_query.message.edit_text(
text="✅ Резервная копия успешно создана и отправлена администратору.",
reply_markup=kb
)
+107
View File
@@ -0,0 +1,107 @@
from typing import Any
from aiogram import F, Router
from aiogram.types import CallbackQuery, BufferedInputFile
from database import delete_user_data
from filters.admin import IsAdminFilter
from keyboards.admin.bans_kb import build_bans_kb
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
router = Router()
@router.callback_query(
AdminPanelCallback.filter(F.action == "bans"),
IsAdminFilter(),
)
async def handle_bans(
callback_query: CallbackQuery
):
text = (
"🚫 Заблокировавшие бота"
"\n\nЗдесь можно просматривать и удалять пользователей, которые забанили вашего бота!"
)
await callback_query.message.edit_text(
text=text,
reply_markup=build_bans_kb(),
)
@router.callback_query(
AdminPanelCallback.filter(F.action == "bans_export"),
IsAdminFilter(),
)
async def handle_bans_export(
callback_query: CallbackQuery,
session: Any
):
kb = build_admin_back_kb("management")
try:
banned_users = await session.fetch("SELECT tg_id, blocked_at FROM blocked_users")
import csv
import io
csv_output = io.StringIO()
writer = csv.writer(csv_output)
writer.writerow(["tg_id", "blocked_at"])
for user in banned_users:
writer.writerow([user["tg_id"], user["blocked_at"]])
csv_output.seek(0)
document = BufferedInputFile(
file=csv_output.getvalue().encode("utf-8"), filename="banned_users.csv"
)
await callback_query.message.answer_document(
document=document,
caption="📥 Экспорт пользователей, заблокировавших бота в CSV",
)
except Exception as e:
await callback_query.message.answer(
text=f"❗ Произошла ошибка при экспорте: {e}",
reply_markup=kb,
)
@router.callback_query(
AdminPanelCallback.filter(F.action == "bans_delete_banned"),
IsAdminFilter(),
)
async def handle_bans_delete_banned(
callback_query: CallbackQuery,
session: Any
):
kb = build_admin_back_kb("bans")
try:
blocked_users = await session.fetch("SELECT tg_id FROM blocked_users")
blocked_ids = [record["tg_id"] for record in blocked_users]
if not blocked_ids:
await callback_query.message.answer(
text="📂 Нет заблокировавших пользователей для удаления.",
reply_markup=kb,
)
return
for tg_id in blocked_ids:
await delete_user_data(session, tg_id)
await session.execute(
"DELETE FROM blocked_users WHERE tg_id = ANY($1)", blocked_ids
)
await callback_query.message.answer(
text=f"🗑️ Удалены данные о {len(blocked_ids)} пользователях и связанных записях.",
reply_markup=kb,
)
except Exception as e:
await callback_query.message.answer(
text=f"❗ Произошла ошибка при удалении записей: {e}",
reply_markup=kb,
)
+123 -110
View File
@@ -3,56 +3,136 @@ from typing import Any
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from database import create_coupon, delete_coupon_from_db, get_all_coupons
from database import delete_coupon_from_db, get_all_coupons, create_coupon
from filters.admin import IsAdminFilter
from keyboards.admin.coupons_kb import build_coupons_kb, build_coupons_list_kb, AdminCouponDeleteCallback
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
from logger import logger
router = Router()
class AdminCouponsState(StatesGroup):
waiting_for_coupon_data = State()
router = Router()
@router.callback_query(F.data == "coupons_editor", IsAdminFilter())
@router.callback_query(
AdminPanelCallback.filter(F.action == "coupons"),
IsAdminFilter(),
)
async def show_coupon_management_menu(
callback_query: types.CallbackQuery, state: FSMContext
callback_query: types.CallbackQuery,
):
await state.clear()
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text=" Создать купон", callback_data="create_coupon")
)
builder.row(InlineKeyboardButton(text="Купоны", callback_data="coupons"))
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin"))
await callback_query.message.answer(
"🛠 Меню управления купонами:", reply_markup=builder.as_markup()
await callback_query.message.edit_text(
text="🛠 Меню управления купонами:",
reply_markup=build_coupons_kb()
)
@router.callback_query(F.data == "coupons", IsAdminFilter())
async def show_coupon_list(callback_query: types.CallbackQuery, session: Any):
@router.callback_query(
AdminPanelCallback.filter(F.action == "coupons_create"),
IsAdminFilter(),
)
async def handle_create_coupon(
callback_query: types.CallbackQuery,
state: FSMContext
):
text = (
"🎫 <b>Введите данные для создания купона в формате:</b>\n\n"
"📝 <i>код</i> 💰 <i>сумма</i> 🔢 <i>лимит</i>\n\n"
"Пример: <b>'COUPON1 50 5'</b> 👈\n\n"
)
await callback_query.message.edit_text(
text=text,
reply_markup=build_admin_back_kb("coupons"),
)
await state.set_state(AdminCouponsState.waiting_for_coupon_data)
@router.message(
AdminCouponsState.waiting_for_coupon_data,
IsAdminFilter()
)
async def process_coupon_data(
message: types.Message,
state: FSMContext,
session: Any
):
text = message.text.strip()
parts = text.split()
kb = build_admin_back_kb("coupons")
if len(parts) != 3:
text = (
"❌ <b>Некорректный формат!</b> 📝 Пожалуйста, введите данные в формате:\n"
"🏷️ <b>код</b> 💰 <b>сумма</b> 🔢 <b>лимит</b>\n"
"Пример: <b>'COUPON1 50 5'</b> 👈"
)
await message.answer(
text=text,
reply_markup=kb,
)
return
try:
coupon_code = parts[0]
coupon_amount = float(parts[1])
usage_limit = int(parts[2])
except ValueError:
text = (
"⚠️ <b>Проверьте правильность введенных данных!</b>\n"
"💱 Сумма должна быть числом, а лимит — целым числом."
)
await message.answer(
text=text,
reply_markup=kb,
)
return
try:
await create_coupon(coupon_code, coupon_amount, usage_limit, session)
text = (
f"✅ Купон с кодом <b>{coupon_code}</b> успешно создан!\n"
f"💰 Сумма: <b>{coupon_amount} рублей</b> \n"
f"🔢 Лимит использования: <b>{usage_limit} раз</b>"
)
await message.answer(
text=text,
reply_markup=kb
)
await state.clear()
except Exception as e:
logger.error(f"Ошибка при создании купона: {e}")
@router.callback_query(
AdminPanelCallback.filter(F.action == "coupons_list"),
IsAdminFilter(),
)
async def show_coupon_list(
callback_query: types.CallbackQuery,
session: Any
):
try:
coupons = await get_all_coupons(session)
if not coupons:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")
)
await callback_query.message.answer(
"❌ На данный момент нет доступных купонов. 🚫\nВы можете вернуться в меню управления. 🔙",
reply_markup=builder.as_markup(),
await callback_query.message.edit_text(
text="❌ На данный момент нет доступных купонов!",
reply_markup=build_admin_back_kb("coupons"),
)
return
kb = build_coupons_list_kb(coupons)
coupon_list = "📜 Список всех купонов:\n\n"
builder = InlineKeyboardBuilder()
for coupon in coupons:
coupon_list += (
@@ -62,27 +142,25 @@ async def show_coupon_list(callback_query: types.CallbackQuery, session: Any):
f"✅ <b>Использовано:</b> {coupon['usage_count']} раз\n\n"
)
builder.row(
InlineKeyboardButton(
text=f"❌ Удалить {coupon['code']}",
callback_data=f"delete_coupon_{coupon['code']}",
)
)
builder.row(
InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")
)
await callback_query.message.answer(
coupon_list, reply_markup=builder.as_markup()
await callback_query.message.edit_text(
text=coupon_list,
reply_markup=kb
)
except Exception as e:
logger.error(f"Ошибка при получении списка купонов: {e}")
@router.callback_query(F.data.startswith("delete_coupon_"), IsAdminFilter())
async def handle_delete_coupon(callback_query: types.CallbackQuery, session: Any):
coupon_code = callback_query.data[len("delete_coupon_") :]
@router.callback_query(
AdminCouponDeleteCallback.filter(),
IsAdminFilter(),
)
async def handle_delete_coupon(
callback_query: types.CallbackQuery,
callback_data: AdminCouponDeleteCallback,
session: Any
):
coupon_code = callback_data.coupon_code
try:
result = await delete_coupon_from_db(coupon_code, session)
@@ -90,75 +168,10 @@ async def handle_delete_coupon(callback_query: types.CallbackQuery, session: Any
if result:
await show_coupon_list(callback_query, session)
else:
await callback_query.message.answer(
f"❌ Купон с кодом <b>{coupon_code}</b> не найден.",
await callback_query.message.edit_text(
text=f"❌ Купон с кодом <b>{coupon_code}</b> не найден.",
)
await show_coupon_list(callback_query, session)
except Exception as e:
logger.error(f"Ошибка при удалении купона: {e}")
@router.callback_query(F.data == "create_coupon", IsAdminFilter())
async def handle_create_coupon(callback_query: types.CallbackQuery, state: FSMContext):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor"))
await callback_query.message.answer(
"🎫 <b>Введите данные для создания купона в формате:</b>\n\n"
"📝 <i>код</i> 💰 <i>сумма</i> 🔢 <i>лимит</i>\n\n"
"Пример: <b>'COUPON1 50 5'</b> 👈\n\n",
reply_markup=builder.as_markup(),
)
await state.set_state(AdminCouponsState.waiting_for_coupon_data)
@router.message(AdminCouponsState.waiting_for_coupon_data, IsAdminFilter())
async def process_coupon_data(message: types.Message, state: FSMContext, session: Any):
text = message.text.strip()
parts = text.split()
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor"))
if len(parts) != 3:
await message.answer(
"❌ <b>Некорректный формат!</b> 📝 Пожалуйста, введите данные в формате:\n"
"🏷️ <b>код</b> 💰 <b>сумма</b> 🔢 <b>лимит</b>\n"
"Пример: <b>'COUPON1 50 5'</b> 👈",
reply_markup=builder.as_markup(),
)
return
try:
coupon_code = parts[0]
coupon_amount = float(parts[1])
usage_limit = int(parts[2])
except ValueError:
await message.answer(
"⚠️ <b>Проверьте правильность введенных данных!</b>\n"
"💱 Сумма должна быть числом, 🔢 а лимит — целым числом.",
reply_markup=builder.as_markup(),
)
return
try:
await create_coupon(coupon_code, coupon_amount, usage_limit, session)
result_message = (
f"✅ Купон с кодом <b>{coupon_code}</b> успешно создан! 🎉\n"
f"Сумма: <b>{coupon_amount} рублей</b> 💰\n"
f"Лимит использования: <b>{usage_limit} раз</b> 🔢."
)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")
)
await message.answer(result_message, reply_markup=builder.as_markup())
await state.clear()
except Exception as e:
logger.error(f"Ошибка при создании купона: {e}")
+30 -605
View File
@@ -1,627 +1,52 @@
import subprocess
from datetime import datetime
from io import BytesIO
from typing import Any
import asyncpg
from aiogram import F, Router, types
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import BufferedInputFile, CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.types import CallbackQuery
from backup import backup_database
from bot import bot
from config import DATABASE_URL
from database import delete_user_data
from filters.admin import IsAdminFilter
from logger import logger
from keyboards.admin.panel_kb import build_panel_kb, AdminPanelCallback, build_management_kb
router = Router()
class UserEditorState(StatesGroup):
waiting_for_tg_id = State()
displaying_user_info = State()
waiting_for_restart_confirmation = State()
waiting_for_message = State()
@router.callback_query(F.data == "admin", IsAdminFilter())
@router.callback_query(
AdminPanelCallback.filter(F.action == "admin"),
IsAdminFilter()
)
async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMContext):
await handle_admin_message(callback_query.message, state)
@router.message(Command("admin"), F.data == "admin", IsAdminFilter())
async def handle_admin_message(message: types.Message, state: FSMContext):
await state.clear()
BOT_VERSION = "4.0.0-preAlpha(14)"
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="📊 Статистика пользователей", callback_data="user_stats"
)
)
builder.row(
InlineKeyboardButton(
text="👥 Управление пользователями", callback_data="user_editor"
)
)
builder.row(
InlineKeyboardButton(
text="🖥️ Управление серверами", callback_data="servers_editor"
)
)
builder.row(
InlineKeyboardButton(
text="🎟️ Управление купонами", callback_data="coupons_editor"
)
)
builder.row(
InlineKeyboardButton(text="📢 Массовая рассылка", callback_data="send_to")
)
builder.row(
InlineKeyboardButton(text="🤖 Управление Ботом", callback_data="bot_management")
)
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await message.answer(
f"🤖 Панель администратора\n\nВерсия бота: <b>{BOT_VERSION}</b>",
reply_markup=builder.as_markup()
)
@router.callback_query(F.data == "bot_management")
async def handle_bot_management(callback_query: types.CallbackQuery):
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="💾 Создать резервную копию", callback_data="backups")
)
builder.row(
InlineKeyboardButton(text="🔄 Перезагрузить бота", callback_data="restart_bot")
)
builder.row(InlineKeyboardButton(text="🚫 Баны", callback_data="ban_user"))
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="admin"))
await callback_query.message.answer(
"🤖 Управление ботом",
reply_markup=builder.as_markup(),
)
@router.callback_query(F.data == "user_stats", IsAdminFilter())
async def user_stats_menu(callback_query: CallbackQuery, session: Any):
try:
total_users = await session.fetchval("SELECT COUNT(*) FROM users")
total_keys = await session.fetchval("SELECT COUNT(*) FROM keys")
total_referrals = await session.fetchval("SELECT COUNT(*) FROM referrals")
total_payments_today = await session.fetchval(
"SELECT COALESCE(SUM(amount), 0) FROM payments WHERE created_at >= CURRENT_DATE"
)
total_payments_week = await session.fetchval(
"SELECT COALESCE(SUM(amount), 0) FROM payments WHERE created_at >= date_trunc('week', CURRENT_DATE)"
)
total_payments_month = await session.fetchval(
"SELECT COALESCE(SUM(amount), 0) FROM payments WHERE created_at >= date_trunc('month', CURRENT_DATE)"
)
total_payments_all_time = await session.fetchval(
"SELECT COALESCE(SUM(amount), 0) FROM payments"
)
registrations_today = await session.fetchval(
"SELECT COUNT(*) FROM users WHERE created_at >= CURRENT_DATE"
)
registrations_week = await session.fetchval(
"SELECT COUNT(*) FROM users WHERE created_at >= date_trunc('week', CURRENT_DATE)"
)
registrations_month = await session.fetchval(
"SELECT COUNT(*) FROM users WHERE created_at >= date_trunc('month', CURRENT_DATE)"
)
users_updated_today = await session.fetchval(
"SELECT COUNT(*) FROM users WHERE updated_at >= CURRENT_DATE"
)
active_keys = await session.fetchval(
"SELECT COUNT(*) FROM keys WHERE expiry_time > $1",
int(datetime.utcnow().timestamp() * 1000),
)
expired_keys = total_keys - active_keys
stats_message = (
f"📊 <b>Подробная статистика проекта:</b>\n\n"
f"👥 Пользователи:\n"
f" 📅 За день: <b>{registrations_today}</b>\n"
f" 📆 За неделю: <b>{registrations_week}</b>\n"
f" 📆 За месяц: <b>{registrations_month}</b>\n"
f" 🌐 За все время: <b>{total_users}</b>\n\n"
f"🌟 Активные пользователи:\n"
f" 🌟 Активных сегодня: <b>{users_updated_today}</b>\n\n"
f"👥 Рефералы:\n"
f" 🤝 Всего привлечено: <b>{total_referrals}</b>\n\n"
f"🔑 Ключи:\n"
f" 🌈 Всего сгенерировано: <b>{total_keys}</b>\n"
f" ✅ Действующих: <b>{active_keys}</b>\n"
f" ❌ Просроченных: <b>{expired_keys}</b>\n\n"
f"💰 Финансовая статистика:\n"
f" 📅 За день: <b>{total_payments_today} ₽</b>\n"
f" 📆 За неделю: <b>{total_payments_week} ₽</b>\n"
f" 📆 За месяц: <b>{total_payments_month} ₽</b>\n"
f" 🏦 За все время: <b>{total_payments_all_time} ₽</b>\n"
)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="🔄 Обновить", callback_data="user_stats")
)
builder.row(
InlineKeyboardButton(
text="📥 Выгрузить пользователей в CSV",
callback_data="export_users_csv",
)
)
builder.row(
InlineKeyboardButton(
text="📥 Выгрузить оплаты в CSV", callback_data="export_payments_csv"
)
)
builder.row(
InlineKeyboardButton(text="🔙 Вернуться в меню", callback_data="admin")
)
await callback_query.message.answer(
stats_message, reply_markup=builder.as_markup()
)
except Exception as e:
logger.error(f"Error in user_stats_menu: {e}")
@router.callback_query(F.data == "export_users_csv", IsAdminFilter())
async def export_users_csv(callback_query: CallbackQuery, session: Any):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_stats"))
try:
users = await session.fetch(
"""
SELECT
u.tg_id,
u.username,
u.first_name,
u.last_name,
u.language_code,
u.is_bot,
c.balance,
c.trial
FROM users u
LEFT JOIN connections c ON u.tg_id = c.tg_id
"""
)
if not users:
await callback_query.message.answer(
"📭 Нет пользователей для экспорта.", reply_markup=builder.as_markup()
)
return
csv_data = (
"tg_id,username,first_name,last_name,language_code,is_bot,balance,trial\n"
)
for user in users:
csv_data += f"{user['tg_id']},{user['username']},{user['first_name']},{user['last_name']},{user['language_code']},{user['is_bot']},{user['balance']},{user['trial']}\n"
file_name = BytesIO(csv_data.encode("utf-8-sig"))
file_name.seek(0)
file = BufferedInputFile(file_name.getvalue(), filename="users_export.csv")
await callback_query.message.answer_document(
file,
caption="📥 Экспорт пользователей в CSV",
reply_markup=builder.as_markup(),
)
file_name.close()
except Exception as e:
logger.error(f"Ошибка при экспорте пользователей в CSV: {e}")
await callback_query.message.answer(
"❗ Произошла ошибка при экспорте пользователей.",
reply_markup=builder.as_markup(),
)
@router.callback_query(F.data == "export_payments_csv", IsAdminFilter())
async def export_payments_csv(callback_query: CallbackQuery, session: Any):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_stats"))
try:
payments = await session.fetch(
"""
SELECT
u.tg_id,
u.username,
u.first_name,
u.last_name,
p.amount,
p.payment_system,
p.status,
p.created_at
FROM users u
JOIN payments p ON u.tg_id = p.tg_id
"""
)
if not payments:
await callback_query.message.answer(
"📭 Нет платежей для экспорта.", reply_markup=builder.as_markup()
)
return
csv_data = "tg_id,username,first_name,last_name,amount,payment_system,status,created_at\n" # Заголовки CSV
for payment in payments:
csv_data += f"{payment['tg_id']},{payment['username']},{payment['first_name']},{payment['last_name']},{payment['amount']},{payment['payment_system']},{payment['status']},{payment['created_at']}\n"
file_name = BytesIO(csv_data.encode("utf-8-sig"))
file_name.seek(0)
file = BufferedInputFile(file_name.getvalue(), filename="payments_export.csv")
await callback_query.message.answer_document(
file, caption="📥 Экспорт платежей в CSV", reply_markup=builder.as_markup()
)
file_name.close()
except Exception as e:
logger.error(f"Ошибка при экспорте платежей в CSV: {e}")
await callback_query.message.answer(
"❗ Произошла ошибка при экспорте платежей.",
reply_markup=builder.as_markup(),
)
@router.callback_query(F.data == "send_to", IsAdminFilter())
async def handle_send_to_all(callback_query: CallbackQuery, state: FSMContext):
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="📢 Отправить всем", callback_data="send_to_all")
)
builder.row(
InlineKeyboardButton(
text="📢 Отправить с подпиской", callback_data="send_to_subscribed"
)
)
builder.row(
InlineKeyboardButton(
text="📢 Отправить без подписки", callback_data="send_to_unsubscribed"
)
)
builder.row(
InlineKeyboardButton(
text="📢 Рассылка по кластеру", callback_data="send_to_cluster"
)
)
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin"))
await callback_query.message.answer(
"✍️ Выберите группу пользователей и введите текст сообщения для рассылки:",
reply_markup=builder.as_markup(),
)
@router.callback_query(F.data == "send_to_all", IsAdminFilter())
async def handle_send_to_all(callback_query: CallbackQuery, state: FSMContext):
await state.update_data(send_to="all")
await callback_query.message.answer(
"✍️ Введите текст сообщения для рассылки всем пользователям:"
)
await state.set_state(UserEditorState.waiting_for_message)
@router.callback_query(F.data == "send_to_subscribed", IsAdminFilter())
async def handle_send_to_subscribed(callback_query: CallbackQuery, state: FSMContext):
await state.update_data(send_to="subscribed")
await callback_query.message.answer(
"✍️ Введите текст сообщения для рассылки пользователям с активной подпиской:"
)
await state.set_state(UserEditorState.waiting_for_message)
@router.callback_query(F.data == "send_to_unsubscribed", IsAdminFilter())
async def handle_send_to_unsubscribed(callback_query: CallbackQuery, state: FSMContext):
await state.update_data(send_to="unsubscribed")
await callback_query.message.answer(
"✍️ Введите текст сообщения для рассылки пользователям без активной подписки:"
)
await state.set_state(UserEditorState.waiting_for_message)
@router.callback_query(F.data == "send_to_cluster", IsAdminFilter())
async def handle_send_to_cluster(
callback_query: CallbackQuery, state: FSMContext, session: Any
):
clusters = await session.fetch("SELECT DISTINCT cluster_name FROM servers")
builder = InlineKeyboardBuilder()
for cluster in clusters:
builder.row(
InlineKeyboardButton(
text=f"🌐 {cluster['cluster_name']}",
callback_data=f"send_cluster|{cluster['cluster_name']}",
)
)
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="send_to"))
await callback_query.message.answer(
"✍️ Выберите кластер для рассылки сообщений:",
reply_markup=builder.as_markup(),
)
@router.callback_query(F.data.startswith("send_cluster|"), IsAdminFilter())
async def handle_send_cluster(callback_query: CallbackQuery, state: FSMContext):
cluster_name = callback_query.data.split("|")[1]
await state.update_data(send_to="cluster", cluster_name=cluster_name)
await callback_query.message.answer(
f"✍️ Введите текст сообщения для рассылки пользователям кластера <b>{cluster_name}</b>:"
)
await state.set_state(UserEditorState.waiting_for_message)
@router.message(UserEditorState.waiting_for_message, IsAdminFilter())
async def process_message_to_all(
message: types.Message, state: FSMContext, session: Any
):
text_message = message.text
try:
state_data = await state.get_data()
send_to = state_data.get("send_to", "all")
if send_to == "all":
tg_ids = await session.fetch("SELECT DISTINCT tg_id FROM connections")
elif send_to == "subscribed":
tg_ids = await session.fetch(
"""
SELECT DISTINCT c.tg_id
FROM connections c
JOIN keys k ON c.tg_id = k.tg_id
WHERE k.expiry_time > $1
""",
int(datetime.utcnow().timestamp() * 1000),
)
elif send_to == "unsubscribed":
tg_ids = await session.fetch(
"""
SELECT c.tg_id
FROM connections c
LEFT JOIN keys k ON c.tg_id = k.tg_id
GROUP BY c.tg_id
HAVING COUNT(k.tg_id) = 0 OR MAX(k.expiry_time) <= $1
""",
int(datetime.utcnow().timestamp() * 1000),
)
elif send_to == "cluster":
cluster_name = state_data.get("cluster_name")
tg_ids = await session.fetch(
"""
SELECT DISTINCT c.tg_id
FROM connections c
JOIN keys k ON c.tg_id = k.tg_id
JOIN servers s ON k.server_id = s.cluster_name
WHERE s.cluster_name = $1
""",
cluster_name,
)
total_users = len(tg_ids)
success_count = 0
error_count = 0
for record in tg_ids:
tg_id = record["tg_id"]
try:
await bot.send_message(chat_id=tg_id, text=text_message)
success_count += 1
except Exception as e:
error_count += 1
logger.error(
f"❌ Ошибка при отправке сообщения пользователю {tg_id}: {e}"
)
await message.answer(
f"📤 Рассылка завершена:\n"
f"👥 Всего пользователей: {total_users}\n"
f"✅ Успешно отправлено: {success_count}\n"
f"❌ Не доставлено: {error_count}"
)
except Exception as e:
logger.error(f"❗ Ошибка при подключении к базе данных: {e}")
await state.clear()
@router.callback_query(F.data == "backups", IsAdminFilter())
async def handle_backup(callback_query: CallbackQuery, state: FSMContext):
await callback_query.message.answer(
"💾 Инициализация резервного копирования базы данных..."
)
await backup_database()
await callback_query.message.answer(
"✅ Резервная копия успешно создана и отправлена администратору."
)
@router.callback_query(F.data == "restart_bot", IsAdminFilter())
async def handle_restart(callback_query: CallbackQuery, state: FSMContext):
await state.set_state(UserEditorState.waiting_for_restart_confirmation)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="✅ Да, перезапустить", callback_data="confirm_restart"
),
InlineKeyboardButton(text="❌ Нет, отмена", callback_data="admin"),
)
builder.row(InlineKeyboardButton(text="🔙 Вернуться в меню", callback_data="admin"))
await callback_query.message.answer(
"🤔 Вы уверены, что хотите перезапустить бота?",
reply_markup=builder.as_markup(),
await callback_query.message.edit_text(
text="🤖 Панель администратора",
reply_markup=build_panel_kb()
)
@router.callback_query(
F.data == "confirm_restart",
UserEditorState.waiting_for_restart_confirmation,
IsAdminFilter(),
F.data == "admin",
IsAdminFilter()
)
async def confirm_restart_bot(callback_query: CallbackQuery, state: FSMContext):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Вернуться в меню", callback_data="admin"))
try:
subprocess.run(
["sudo", "systemctl", "restart", "bot.service"],
check=True,
capture_output=True,
text=True,
)
await state.clear()
await callback_query.message.answer(
"🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup()
)
except subprocess.CalledProcessError:
await callback_query.message.answer(
"🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup()
)
except Exception as e:
await callback_query.message.answer(
f"⚠️ Ошибка при перезагрузке бота: {e.stderr}",
reply_markup=builder.as_markup(),
)
async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMContext):
await handle_admin_message(callback_query.message, state)
@router.callback_query(F.data == "user_editor", IsAdminFilter())
async def user_editor_menu(callback_query: CallbackQuery):
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔍 Поиск по названию ключа",
callback_data="search_by_key_name",
)
)
builder.row(
InlineKeyboardButton(
text="🆔 Поиск по Telegram ID", callback_data="search_by_tg_id"
)
)
builder.row(
InlineKeyboardButton(
text="🌐 Поиск по Username", callback_data="search_by_username"
)
)
builder.row(InlineKeyboardButton(text="🔙 Вернуться назад", callback_data="admin"))
await callback_query.message.answer(
"👇 Выберите способ поиска пользователя:", reply_markup=builder.as_markup()
@router.message(
Command("admin"),
IsAdminFilter()
)
async def handle_admin_message(message: types.Message, state: FSMContext):
await state.clear()
await message.answer(
text="🤖 Панель администратора",
reply_markup=build_panel_kb()
)
@router.callback_query(F.data == "ban_user")
async def handle_ban_user(callback_query: types.CallbackQuery):
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="📄 Выгрузить в CSV", callback_data="export_to_csv")
@router.callback_query(
AdminPanelCallback.filter(F.action == "management"),
IsAdminFilter()
)
async def handle_management(callback_query: CallbackQuery):
await callback_query.message.edit_text(
text="🤖 Управление ботом",
reply_markup=build_management_kb(),
)
builder.row(
InlineKeyboardButton(
text="🗑️ Удалить из БД", callback_data="delete_banned_users"
)
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management"))
await callback_query.message.answer(
"🚫 Заблокировавшие бота\n\n"
"Здесь можно просматривать и удалять пользователей, которые забанили вашего бота!",
reply_markup=builder.as_markup(),
)
@router.callback_query(F.data == "export_to_csv")
async def export_banned_users_to_csv(callback_query: types.CallbackQuery):
conn = await asyncpg.connect(DATABASE_URL)
try:
banned_users = await conn.fetch("SELECT tg_id, blocked_at FROM blocked_users")
import csv
import io
csv_output = io.StringIO()
writer = csv.writer(csv_output)
writer.writerow(["tg_id", "blocked_at"])
for user in banned_users:
writer.writerow([user["tg_id"], user["blocked_at"]])
csv_output.seek(0)
document = BufferedInputFile(
file=csv_output.getvalue().encode("utf-8"), filename="banned_users.csv"
)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management")
)
await callback_query.message.answer_document(
document=document,
caption="📄 Список заблокировавших бота пользователей",
reply_markup=builder.as_markup(),
)
except Exception as e:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management")
)
await callback_query.message.answer(
text=f"Ошибка при выгрузке CSV: {e}",
reply_markup=builder.as_markup(),
)
finally:
await conn.close()
@router.callback_query(F.data == "delete_banned_users")
async def delete_banned_users(callback_query: types.CallbackQuery):
conn = await asyncpg.connect(DATABASE_URL)
try:
blocked_users = await conn.fetch("SELECT tg_id FROM blocked_users")
blocked_ids = [record["tg_id"] for record in blocked_users]
if not blocked_ids:
await callback_query.message.answer(
"📂 Нет заблокировавших пользователей для удаления."
)
return
for tg_id in blocked_ids:
await delete_user_data(conn, tg_id)
await conn.execute(
"DELETE FROM blocked_users WHERE tg_id = ANY($1)", blocked_ids
)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management")
)
await callback_query.message.answer(
text=f"🗑️ Удалено данные о {len(blocked_ids)} пользователях и связанных записях.",
reply_markup=builder.as_markup(),
)
except Exception as e:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management")
)
await callback_query.message.answer(
text=f"Ошибка при удалении записей: {e}",
reply_markup=builder.as_markup(),
)
finally:
await conn.close()
+49
View File
@@ -0,0 +1,49 @@
import subprocess
from aiogram import F, Router
from aiogram.types import CallbackQuery
from filters.admin import IsAdminFilter
from keyboards.admin.panel_kb import build_restart_kb, AdminPanelCallback, build_admin_back_kb
router = Router()
@router.callback_query(
AdminPanelCallback.filter(F.action == "restart"),
IsAdminFilter(),
)
async def handle_restart(callback_query: CallbackQuery):
await callback_query.message.edit_text(
text="🤔 Вы уверены, что хотите перезагрузить бота?",
reply_markup=build_restart_kb(),
)
@router.callback_query(
AdminPanelCallback.filter(F.action == "restart_confirm"),
IsAdminFilter(),
)
async def confirm_restart_bot(callback_query: CallbackQuery):
kb = build_admin_back_kb()
try:
subprocess.run(
["sudo", "systemctl", "restart", "bot.service"],
check=True,
capture_output=True,
text=True,
)
await callback_query.message.edit_text(
text="🔄 Бот успешно перезагружен!",
reply_markup=kb
)
except subprocess.CalledProcessError:
await callback_query.message.edit_text(
text="🔄 Бот успешно перезагружен!",
reply_markup=kb
)
except Exception as e:
await callback_query.message.edit_text(
text=f"⚠️ Ошибка при перезагрузке бота: {e.stderr}",
reply_markup=kb
)
+112
View File
@@ -0,0 +1,112 @@
from datetime import datetime
from typing import Any
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery
from filters.admin import IsAdminFilter
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
from keyboards.admin.sender_kb import build_sender_kb, AdminSenderCallback
from logger import logger
router = Router()
class AdminSender(StatesGroup):
waiting_for_message = State()
@router.callback_query(
AdminPanelCallback.filter(F.action == "sender"),
IsAdminFilter(),
)
async def handle_sender(
callback_query: CallbackQuery
):
await callback_query.message.edit_text(
text="✍️ Выберите группу пользователей для рассылки:",
reply_markup=build_sender_kb(),
)
@router.callback_query(
AdminSenderCallback.filter(),
IsAdminFilter(),
)
async def handle_sender_callback(
callback_query: CallbackQuery,
callback_data: AdminSenderCallback,
state: FSMContext
):
await callback_query.message.edit_text(
text="✍️ Введите текст сообщения для рассылки:",
reply_markup=build_admin_back_kb("sender"),
)
await state.update_data(type=callback_data.type)
await state.set_state(AdminSender.waiting_for_message)
@router.message(
AdminSender.waiting_for_message,
IsAdminFilter(),
)
async def process_message(
message: types.Message,
state: FSMContext,
session: Any
):
text_message = message.text
try:
state_data = await state.get_data()
send_to = state_data.get("type", "all")
if send_to == "subscribed":
tg_ids = await session.fetch("""
SELECT DISTINCT c.tg_id
FROM connections c
JOIN keys k ON c.tg_id = k.tg_id
WHERE k.expiry_time > $1
""", int(datetime.utcnow().timestamp() * 1000))
elif send_to == "unsubscribed":
tg_ids = await session.fetch("""
SELECT c.tg_id
FROM connections c
LEFT JOIN keys k ON c.tg_id = k.tg_id
GROUP BY c.tg_id
HAVING COUNT(k.tg_id) = 0 OR MAX(k.expiry_time) <= $1
""", int(datetime.utcnow().timestamp() * 1000))
else:
tg_ids = await session.fetch("SELECT DISTINCT tg_id FROM connections")
total_users = len(tg_ids)
success_count = 0
for record in tg_ids:
tg_id = record["tg_id"]
try:
await message.bot.send_message(
chat_id=tg_id,
text=text_message
)
success_count += 1
except Exception:
pass
text = (
f"📤 Рассылка завершена!"
f"\n\n👥 Всего пользователей: {total_users}"
f"\n✅ Доставлено: {success_count}"
f"\n❌ Не доставлено: {total_users - success_count}"
)
await message.answer(
text=text,
reply_markup=build_admin_back_kb("stats")
)
except Exception as e:
logger.error(f"❗ Ошибка при подключении к базе данных: {e}")
await state.clear()
+257 -384
View File
@@ -1,24 +1,22 @@
import asyncio
import asyncpg
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from py3xui import AsyncApi
from backup import create_backup_and_send_to_admins
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, SUPERNODE
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL
from database import check_unique_server_name, get_servers_from_db
from filters.admin import IsAdminFilter
from handlers.keys.key_utils import create_key_on_cluster
from logger import logger
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
from keyboards.admin.servers_kb import build_cancel_kb, build_manage_server_kb, \
build_delete_server_kb, \
build_manage_cluster_kb, build_clusters_editor_kb, AdminServerEditorCallback
router = Router()
class UserEditorState(StatesGroup):
class AdminServersEditor(StatesGroup):
waiting_for_cluster_name = State()
waiting_for_api_url = State()
waiting_for_inbound_id = State()
@@ -26,112 +24,103 @@ class UserEditorState(StatesGroup):
waiting_for_subscription_url = State()
@router.callback_query(F.data == "servers_editor", IsAdminFilter())
async def handle_servers_editor(callback_query: types.CallbackQuery):
@router.callback_query(
AdminPanelCallback.filter(F.action == "servers"),
IsAdminFilter(),
)
async def handle_servers_editor(
callback_query: types.CallbackQuery
):
servers = await get_servers_from_db()
builder = InlineKeyboardBuilder()
for cluster_name, cluster_servers in servers.items():
builder.row(
InlineKeyboardButton(
text=f"⚙️ {cluster_name}", callback_data=f"manage_cluster|{cluster_name}"
)
)
builder.row(
InlineKeyboardButton(text=" Добавить кластер", callback_data="add_cluster")
)
builder.row(InlineKeyboardButton(text="🔙 Назад в админку", callback_data="admin"))
await callback_query.message.answer(
text = (
"<b>🔧 Управление кластерами</b>\n\n"
"<i>📌 Здесь вы можете добавить новый кластер.</i>\n\n"
"<i>🌐 <b>Кластеры</b> — это пространство серверов, в пределах которого создается подписка.</i>\n"
"💡 Если вы хотите выдавать по 1 серверу, то добавьте всего 1 сервер в кластер.\n\n"
"<i>⚠️ <b>Важно:</b> Кластеры удаляются автоматически, если удалить все серверы внутри них.</i>\n\n",
"<i>⚠️ <b>Важно:</b> Кластеры удаляются автоматически, если удалить все серверы внутри них.</i>\n\n"
)
await callback_query.message.edit_text(
text=text,
parse_mode="HTML",
reply_markup=builder.as_markup(),
reply_markup=build_clusters_editor_kb(servers),
)
@router.callback_query(F.data == "add_cluster", IsAdminFilter())
async def handle_add_cluster(callback_query: types.CallbackQuery, state: FSMContext):
await callback_query.message.answer(
@router.callback_query(
AdminPanelCallback.filter(F.action == "clusters_add"),
IsAdminFilter(),
)
async def handle_add_cluster(
callback_query: types.CallbackQuery,
state: FSMContext
):
text = (
"🔧 <b>Введите имя нового кластера:</b>\n\n"
"<b>Имя кластера должно быть уникальным и на английском языке.</b>\n"
"<i>Пример:</i> <code>cluster2</code> или <code>us_east_1</code>",
"<b>Имя кластера должно быть уникальным!</b>\n"
"<i>Пример:</i> <code>cluster1</code> или <code>us_east_1</code>"
)
await callback_query.message.edit_text(
text=text,
parse_mode="HTML",
)
await state.set_state(UserEditorState.waiting_for_cluster_name)
await state.set_state(AdminServersEditor.waiting_for_cluster_name)
@router.message(UserEditorState.waiting_for_cluster_name, IsAdminFilter())
async def handle_cluster_name_input(message: types.Message, state: FSMContext):
cluster_name = message.text.strip()
if cluster_name == "❌ Отменить":
await state.clear()
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔧 Управление кластерами", callback_data="servers_editor"
)
)
@router.message(
AdminServersEditor.waiting_for_cluster_name,
IsAdminFilter()
)
async def handle_cluster_name_input(
message: types.Message,
state: FSMContext
):
if not message.text:
await message.answer(
"Процесс создания кластера отменен. Вы вернулись в меню управления серверами.",
reply_markup=builder.as_markup(),
text="❌ Имя кластера не может быть пустым. Попробуйте снова."
)
return
if not cluster_name:
await message.answer("❌ Имя кластера не может быть пустым. Попробуйте снова.")
return
cluster_name = message.text.strip()
await state.update_data(cluster_name=cluster_name)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")
)
await message.answer(
text = (
f"<b>Введите имя сервера для кластера {cluster_name}:</b>\n\n"
"Рекомендуется указать локацию сервера в имени.\n\n"
"<i>Пример:</i> <code>server-asia</code>, <code>server-europe</code>",
parse_mode="HTML",
reply_markup=builder.as_markup(),
"<i>Пример:</i> <code>server-frankfurt1</code>, <code>fra1</code>"
)
await state.set_state(UserEditorState.waiting_for_server_name)
await message.edit_text(
text=text,
parse_mode="HTML",
reply_markup=build_admin_back_kb("servers"),
)
await state.set_state(AdminServersEditor.waiting_for_server_name)
@router.message(UserEditorState.waiting_for_server_name, IsAdminFilter())
async def handle_server_name_input(message: types.Message, state: FSMContext):
@router.message(
AdminServersEditor.waiting_for_server_name,
IsAdminFilter()
)
async def handle_server_name_input(
message: types.Message,
state: FSMContext
):
if not message.text:
await message.answer(
text="❌ Имя сервера не может быть пустым. Попробуйте снова."
)
return
server_name = message.text.strip()
if server_name == "❌ Отменить":
await state.clear()
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔧 Управление кластерами", callback_data="servers_editor"
)
)
if not await check_unique_server_name(server_name):
await message.answer(
"Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.",
reply_markup=builder.as_markup(),
)
return
if not server_name:
await message.answer("❌ Имя сервера не может быть пустым. Попробуйте снова.")
return
server_unique = await check_unique_server_name(server_name)
if not server_unique:
await message.answer(
"❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя."
text="❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя."
)
return
@@ -139,124 +128,109 @@ async def handle_server_name_input(message: types.Message, state: FSMContext):
cluster_name = user_data.get("cluster_name")
await state.update_data(server_name=server_name)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")
)
await message.answer(
text = (
f"<b>Введите API URL для сервера {server_name} в кластере {cluster_name}:</b>\n\n"
"API URL должен быть в следующем формате:\n\n"
"<code>https://your_domain:port/panel_path</code>\n\n"
"URL должен быть без слэша на конце!\n",
parse_mode="HTML",
reply_markup=builder.as_markup(),
"URL должен быть без слэша на конце!\n"
)
await state.set_state(UserEditorState.waiting_for_api_url)
await message.edit_text(
text=text,
parse_mode="HTML",
reply_markup=build_admin_back_kb("servers"),
)
await state.set_state(AdminServersEditor.waiting_for_api_url)
@router.message(UserEditorState.waiting_for_api_url, IsAdminFilter())
async def handle_api_url_input(message: types.Message, state: FSMContext):
api_url = message.text.strip()
if api_url == "❌ Отменить":
await state.clear()
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔧 Управление кластерами", callback_data="servers_editor"
)
)
@router.message(
AdminServersEditor.waiting_for_api_url,
IsAdminFilter()
)
async def handle_api_url_input(
message: types.Message,
state: FSMContext
):
if not message.text or not message.text.strip().startswith("https://"):
await message.answer(
"Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.",
reply_markup=builder.as_markup(),
)
return
if not api_url.startswith("https://"):
await message.answer(
"❌ API URL должен начинаться с <code>https://</code>. Попробуйте снова.",
text="❌ API URL должен начинаться с <code>https://</code>. Попробуйте снова.",
parse_mode="HTML",
)
return
api_url = api_url.rstrip("/")
api_url = message.text.strip().rstrip("/")
user_data = await state.get_data()
cluster_name = user_data.get("cluster_name")
server_name = user_data.get("server_name")
await state.update_data(api_url=api_url)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")
)
await message.answer(
text = (
f"<b>Введите subscription_url для сервера {server_name} в кластере {cluster_name}:</b>\n\n"
"Subscription URL должен быть в следующем формате:\n\n"
"<code>https://your_domain:port_sub/sub_path</code>\n\n"
"URL должен быть без слэша и имени клиента на конце!\n"
"Его можно увидеть в панели 3x-ui в информации о клиенте.",
parse_mode="HTML",
reply_markup=builder.as_markup(),
"Его можно увидеть в панели 3x-ui в информации о клиенте."
)
await state.set_state(UserEditorState.waiting_for_subscription_url)
await message.edit_text(
text=text,
parse_mode="HTML",
reply_markup=build_cancel_kb(),
)
await state.set_state(AdminServersEditor.waiting_for_subscription_url)
@router.message(UserEditorState.waiting_for_subscription_url, IsAdminFilter())
async def handle_subscription_url_input(message: types.Message, state: FSMContext):
subscription_url = message.text.strip()
if subscription_url == "❌ Отменить":
await state.clear()
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔧 Управление кластерами", callback_data="servers_editor"
)
)
@router.message(
AdminServersEditor.waiting_for_subscription_url,
IsAdminFilter()
)
async def handle_subscription_url_input(
message: types.Message,
state: FSMContext
):
if not message.text or not message.text.strip().startswith("https://"):
await message.answer(
"Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.",
reply_markup=builder.as_markup(),
)
return
if not subscription_url.startswith("https://"):
await message.answer(
"❌ subscription_url должен начинаться с <code>https://</code>. Попробуйте снова.",
text="❌ subscription_url должен начинаться с <code>https://</code>. Попробуйте снова.",
parse_mode="HTML",
)
return
subscription_url = subscription_url.rstrip("/")
subscription_url = message.text.strip().rstrip("/")
user_data = await state.get_data()
cluster_name = user_data.get("cluster_name")
server_name = user_data.get("server_name")
await state.update_data(subscription_url=subscription_url)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")
)
await message.answer(
text = (
f"<b>Введите inbound_id для сервера {server_name} в кластере {cluster_name}:</b>\n\n"
"Это номер подключения vless в вашей панели 3x-ui. Обычно это <b>1</b> при чистой настройке по гайду.\n\n",
parse_mode="HTML",
reply_markup=builder.as_markup(),
"Это номер подключения vless в вашей панели 3x-ui. Обычно это <b>1</b> при чистой настройке по гайду.\n\n"
)
await state.set_state(UserEditorState.waiting_for_inbound_id)
await message.edit_text(
text=text,
parse_mode="HTML",
reply_markup=build_admin_back_kb("servers"),
)
await state.set_state(AdminServersEditor.waiting_for_inbound_id)
@router.message(UserEditorState.waiting_for_inbound_id, IsAdminFilter())
async def handle_inbound_id_input(message: types.Message, state: FSMContext):
@router.message(
AdminServersEditor.waiting_for_inbound_id,
IsAdminFilter()
)
async def handle_inbound_id_input(
message: types.Message,
state: FSMContext
):
inbound_id = message.text.strip()
if not inbound_id.isdigit():
await message.answer(
"❌ inbound_id должен быть числовым значением. Попробуйте снова."
text="❌ inbound_id должен быть числовым значением. Попробуйте снова."
)
return
@@ -280,156 +254,62 @@ async def handle_inbound_id_input(message: types.Message, state: FSMContext):
)
await conn.close()
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔙 Назад к кластерам", callback_data="servers_editor"
)
)
await message.answer(
f"✅ Кластер {cluster_name} и сервер {server_name} успешно добавлены!",
reply_markup=builder.as_markup(),
await message.edit_text(
text=f"✅ Кластер {cluster_name} и сервер {server_name} успешно добавлены!",
reply_markup=build_admin_back_kb("servers"),
)
await state.clear()
@router.callback_query(F.data.startswith("manage_cluster|"), IsAdminFilter())
async def handle_manage_cluster(callback_query: types.CallbackQuery, state: FSMContext):
cluster_name = callback_query.data.split("|")[1]
@router.callback_query(
AdminServerEditorCallback.filter(F.action == "clusters_manage"),
IsAdminFilter()
)
async def handle_manage_cluster(
callback_query: types.CallbackQuery,
callback_data: AdminServerEditorCallback,
):
cluster_name = callback_data.data
servers = await get_servers_from_db()
cluster_servers = servers.get(cluster_name, [])
builder = InlineKeyboardBuilder()
for server in cluster_servers:
builder.row(
InlineKeyboardButton(
text=f"🌍 {server['server_name']}",
callback_data=f"manage_server|{server['server_name']}",
)
)
builder.row(
InlineKeyboardButton(
text=" Добавить сервер", callback_data=f"add_server|{cluster_name}"
)
)
builder.row(
InlineKeyboardButton(
text="🌐 Доступность серверов",
callback_data=f"server_availability|{cluster_name}",
)
)
builder.row(
InlineKeyboardButton(
text="💾 Создать бэкап кластера",
callback_data=f"backup_cluster|{cluster_name}",
)
)
builder.row(
InlineKeyboardButton(
text="🔄 Синхронизировать",
callback_data=f"sync_cluster|{cluster_name}",
)
)
builder.row(
InlineKeyboardButton(
text="🔙 Назад в управление кластерами", callback_data="servers_editor"
)
)
await callback_query.message.answer(
f"🔧 Управление серверами для кластера {cluster_name}",
reply_markup=builder.as_markup(),
await callback_query.message.edit_text(
text=f"🔧 Управление серверами для кластера {cluster_name}",
reply_markup=build_manage_cluster_kb(cluster_servers, cluster_name),
)
@router.callback_query(F.data.startswith("sync_cluster|"), IsAdminFilter())
async def sync_cluster_handler(callback_query: types.CallbackQuery):
"""Обработчик для синхронизации ключей на всех серверах выбранного кластера."""
cluster_name = callback_query.data.split("|")[1]
conn = await asyncpg.connect(DATABASE_URL)
try:
query_keys = """
SELECT tg_id, client_id, email, expiry_time
FROM keys
WHERE server_id = $1
"""
keys_to_sync = await conn.fetch(query_keys, cluster_name)
if not keys_to_sync:
await callback_query.message.answer(
f"❌ Нет ключей для синхронизации в кластере {cluster_name}.",
reply_markup=InlineKeyboardBuilder()
.row(
InlineKeyboardButton(
text="🔙 Назад", callback_data="servers_editor"
)
)
.as_markup(),
)
return
servers = await get_servers_from_db()
cluster_servers = servers.get(cluster_name, [])
for key in keys_to_sync:
for server_info in cluster_servers:
try:
await create_key_on_cluster(
cluster_name,
key["tg_id"],
key["client_id"],
key["email"],
key["expiry_time"],
)
await asyncio.sleep(0.6)
except Exception as e:
logger.error(f"Ошибка при добавлении ключа {key['client_id']} в кластер {cluster_name}: {e}")
await callback_query.message.answer(
f"✅ Ключи успешно синхронизированы для кластера {cluster_name}.",
reply_markup=InlineKeyboardBuilder()
.row(InlineKeyboardButton(text="🔙 Назад", callback_data="servers_editor"))
.as_markup(),
)
except Exception as e:
logger.error(f"Ошибка синхронизации ключей в кластере {cluster_name}: {e}")
await callback_query.message.answer(
f"❌ Произошла ошибка при синхронизации: {e}",
reply_markup=InlineKeyboardBuilder()
.row(InlineKeyboardButton(text="🔙 Назад", callback_data="servers_editor"))
.as_markup(),
)
finally:
await conn.close()
@router.callback_query(F.data.startswith("server_availability|"), IsAdminFilter())
async def handle_check_server_availability(callback_query: types.CallbackQuery):
cluster_name = callback_query.data.split("|")[1]
@router.callback_query(
AdminServerEditorCallback.filter(F.action == "servers_availability"),
IsAdminFilter()
)
async def handle_check_server_availability(
callback_query: types.CallbackQuery,
callback_data: AdminServerEditorCallback
):
cluster_name = callback_data.data
servers = await get_servers_from_db()
cluster_servers = servers.get(cluster_name, [])
if not cluster_servers:
await callback_query.answer(f"Кластер '{cluster_name}' не содержит серверов.")
await callback_query.message.answer(
text=f"Кластер '{cluster_name}' не содержит серверов."
)
return
in_progress_message = await callback_query.message.answer(
text = (
f"🖥️ Проверка доступности серверов для кластера {cluster_name}.\n\n"
"Это может занять до 1 минуты, пожалуйста, подождите..."
)
availability_message = (
in_progress_message = await callback_query.message.answer(
text=text
)
text = (
f"🖥️ Проверка доступности серверов для кластера {cluster_name} завершена:\n\n"
)
@@ -442,97 +322,84 @@ async def handle_check_server_availability(callback_query: types.CallbackQuery):
await xui.login()
online_users = len(await xui.client.online())
availability_message += (
text += (
f"🌍 {server['server_name']}: {online_users} активных пользователей.\n"
)
except Exception as e:
availability_message += f"{server['server_name']}: Не удалось получить информацию. Ошибка: {e}\n"
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}"
)
)
text += f"{server['server_name']}: Не удалось получить информацию. Ошибка: {e}\n"
await in_progress_message.edit_text(
availability_message, reply_markup=builder.as_markup()
text=text,
reply_markup=build_admin_back_kb("servers")
)
await callback_query.answer()
@router.callback_query(F.data.startswith("manage_server|"), IsAdminFilter())
async def handle_manage_server(callback_query: types.CallbackQuery, state: FSMContext):
server_name = callback_query.data.split("|")[1]
@router.callback_query(
AdminServerEditorCallback.filter(F.action == "servers_manage"),
IsAdminFilter()
)
async def handle_manage_server(
callback_query: types.CallbackQuery,
callback_data: AdminServerEditorCallback
):
server_name = callback_data.data
servers = await get_servers_from_db()
server = None
cluster_name = None
for cluster, cluster_servers in servers.items():
server = next(
(s for s in cluster_servers if s["server_name"] == server_name), None
)
if server:
cluster_name = cluster
break
cluster_name, server = next(
((c, s) for c, cs in servers.items()
for s in cs if s["server_name"] == server_name),
(None, None)
)
if server:
api_url = server["api_url"]
subscription_url = server["subscription_url"]
inbound_id = server["inbound_id"]
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🗑️ Удалить", callback_data=f"delete_server|{server_name}"
)
)
builder.row(
InlineKeyboardButton(
text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}"
)
)
await callback_query.message.answer(
text = (
f"<b>🔧 Информация о сервере {server_name}:</b>\n\n"
f"<b>📡 API URL:</b> {api_url}\n"
f"<b>🌐 Subscription URL:</b> {subscription_url}\n"
f"<b>🔑 Inbound ID:</b> {inbound_id}",
f"<b>🔑 Inbound ID:</b> {inbound_id}"
)
await callback_query.message.edit_text(
text=text,
parse_mode="HTML",
reply_markup=builder.as_markup(),
reply_markup=build_manage_server_kb(server_name, cluster_name),
)
else:
await callback_query.message.answer("❌ Сервер не найден.")
await callback_query.message.edit_text(
text="❌ Сервер не найден."
)
@router.callback_query(F.data.startswith("delete_server|"), IsAdminFilter())
async def handle_delete_server(callback_query: types.CallbackQuery, state: FSMContext):
server_name = callback_query.data.split("|")[1]
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="✅ Да", callback_data=f"confirm_delete_server|{server_name}"
),
InlineKeyboardButton(
text="❌ Нет", callback_data=f"manage_server|{server_name}"
),
)
await callback_query.message.answer(
f"🗑️ Вы уверены, что хотите удалить сервер {server_name}?",
reply_markup=builder.as_markup(),
)
@router.callback_query(F.data.startswith("confirm_delete_server|"), IsAdminFilter())
async def handle_confirm_delete_server(
callback_query: types.CallbackQuery, state: FSMContext
@router.callback_query(
AdminServerEditorCallback.filter(F.action == "servers_delete"),
IsAdminFilter()
)
async def handle_delete_server(
callback_query: types.CallbackQuery,
callback_data: AdminServerEditorCallback
):
server_name = callback_query.data.split("|")[1]
server_name = callback_data.data
await callback_query.message.edit_text(
text=f"🗑️ Вы уверены, что хотите удалить сервер {server_name}?",
reply_markup=build_delete_server_kb(server_name),
)
@router.callback_query(
AdminServerEditorCallback.filter(F.action == "servers_delete_confirm"),
IsAdminFilter()
)
async def handle_confirm_delete_server(
callback_query: types.CallbackQuery,
callback_data: AdminServerEditorCallback
):
server_name = callback_data.data
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute(
@@ -543,43 +410,49 @@ async def handle_confirm_delete_server(
)
await conn.close()
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔙 Назад в управление кластерами", callback_data="servers_editor"
)
)
await callback_query.message.answer(
f"🗑️ Сервер {server_name} успешно удален.", reply_markup=builder.as_markup()
await callback_query.message.edit_text(
text=f"🗑️ Сервер {server_name} успешно удален.",
reply_markup=build_admin_back_kb("servers")
)
@router.callback_query(F.data.startswith("add_server|"), IsAdminFilter())
async def handle_add_server(callback_query: types.CallbackQuery, state: FSMContext):
cluster_name = callback_query.data.split("|")[1]
@router.callback_query(
AdminServerEditorCallback.filter(F.action == "servers_add"),
IsAdminFilter()
)
async def handle_add_server(
callback_query: types.CallbackQuery,
callback_data: AdminServerEditorCallback,
state: FSMContext
):
cluster_name = callback_data.data
await state.update_data(cluster_name=cluster_name)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")
)
await callback_query.message.answer(
text = (
f"<b>Введите имя сервера для кластера {cluster_name}:</b>\n\n"
"Рекомендуется указать локацию сервера в имени.\n\n"
"<i>Пример:</i> <code>server-asia</code>, <code>server-europe</code>",
parse_mode="HTML",
reply_markup=builder.as_markup(),
"<i>Пример:</i> <code>server-asia</code>, <code>server-europe</code>"
)
await state.set_state(UserEditorState.waiting_for_server_name)
await callback_query.message.edit_text(
text=text,
parse_mode="HTML",
reply_markup=build_admin_back_kb("servers"),
)
await state.set_state(AdminServersEditor.waiting_for_server_name)
@router.callback_query(F.data.startswith("backup_cluster|"), IsAdminFilter())
async def handle_backup_cluster(callback_query: types.CallbackQuery):
cluster_name = callback_query.data.split("|")[1]
@router.callback_query(
AdminServerEditorCallback.filter(F.action == "clusters_backup"),
IsAdminFilter()
)
async def handle_backup_cluster(
callback_query: types.CallbackQuery,
callback_data: AdminServerEditorCallback,
):
cluster_name = callback_data.data
servers = await get_servers_from_db()
cluster_servers = servers.get(cluster_name, [])
@@ -592,13 +465,13 @@ async def handle_backup_cluster(callback_query: types.CallbackQuery):
)
await create_backup_and_send_to_admins(xui)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="servers_editor"))
await callback_query.message.answer(
text = (
f"<b>Бэкап для кластера {cluster_name} был успешно создан и отправлен администраторам!</b>\n\n"
f"🔔 <i>Бэкапы отправлены в боты панелей.</i>",
parse_mode="HTML",
reply_markup=builder.as_markup(),
f"🔔 <i>Бэкапы отправлены в боты панелей.</i>"
)
await callback_query.message.edit_text(
text=text,
parse_mode="HTML",
reply_markup=build_admin_back_kb("servers"),
)
await callback_query.answer()
+194
View File
@@ -0,0 +1,194 @@
from datetime import datetime
from io import BytesIO
from typing import Any
from aiogram import F, Router
from aiogram.types import CallbackQuery, BufferedInputFile
from filters.admin import IsAdminFilter
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
from keyboards.admin.stats_kb import build_stats_kb
from logger import logger
router = Router()
@router.callback_query(
AdminPanelCallback.filter(F.action == "stats"),
IsAdminFilter(),
)
async def user_stats_menu(callback_query: CallbackQuery, session: Any):
try:
total_users = await session.fetchval("SELECT COUNT(*) FROM users")
total_keys = await session.fetchval("SELECT COUNT(*) FROM keys")
total_referrals = await session.fetchval("SELECT COUNT(*) FROM referrals")
total_payments_today = await session.fetchval(
"SELECT COALESCE(SUM(amount), 0) FROM payments WHERE created_at >= CURRENT_DATE"
)
total_payments_week = await session.fetchval(
"SELECT COALESCE(SUM(amount), 0) FROM payments WHERE created_at >= date_trunc('week', CURRENT_DATE)"
)
total_payments_month = await session.fetchval(
"SELECT COALESCE(SUM(amount), 0) FROM payments WHERE created_at >= date_trunc('month', CURRENT_DATE)"
)
total_payments_all_time = await session.fetchval(
"SELECT COALESCE(SUM(amount), 0) FROM payments"
)
registrations_today = await session.fetchval(
"SELECT COUNT(*) FROM users WHERE created_at >= CURRENT_DATE"
)
registrations_week = await session.fetchval(
"SELECT COUNT(*) FROM users WHERE created_at >= date_trunc('week', CURRENT_DATE)"
)
registrations_month = await session.fetchval(
"SELECT COUNT(*) FROM users WHERE created_at >= date_trunc('month', CURRENT_DATE)"
)
users_updated_today = await session.fetchval(
"SELECT COUNT(*) FROM users WHERE updated_at >= CURRENT_DATE"
)
active_keys = await session.fetchval(
"SELECT COUNT(*) FROM keys WHERE expiry_time > $1",
int(datetime.utcnow().timestamp() * 1000),
)
expired_keys = total_keys - active_keys
stats_message = (
f"📊 <b>Подробная статистика проекта:</b>\n\n"
f"👥 Пользователи:\n"
f" 📅 За день: <b>{registrations_today}</b>\n"
f" 📆 За неделю: <b>{registrations_week}</b>\n"
f" 📆 За месяц: <b>{registrations_month}</b>\n"
f" 🌐 За все время: <b>{total_users}</b>\n\n"
f"🌟 Активные пользователи:\n"
f" 🌟 Активных сегодня: <b>{users_updated_today}</b>\n\n"
f"👥 Рефералы:\n"
f" 🤝 Всего привлечено: <b>{total_referrals}</b>\n\n"
f"🔑 Ключи:\n"
f" 🌈 Всего сгенерировано: <b>{total_keys}</b>\n"
f" ✅ Действующих: <b>{active_keys}</b>\n"
f" ❌ Просроченных: <b>{expired_keys}</b>\n\n"
f"💰 Финансовая статистика:\n"
f" 📅 За день: <b>{total_payments_today} ₽</b>\n"
f" 📆 За неделю: <b>{total_payments_week} ₽</b>\n"
f" 📆 За месяц: <b>{total_payments_month} ₽</b>\n"
f" 🏦 За все время: <b>{total_payments_all_time} ₽</b>\n"
)
await callback_query.message.edit_text(
text=stats_message,
reply_markup=build_stats_kb()
)
except Exception as e:
logger.error(f"Error in user_stats_menu: {e}")
@router.callback_query(
AdminPanelCallback.filter(F.action == "stats_export_users_csv"),
IsAdminFilter(),
)
async def export_users_csv(callback_query: CallbackQuery, session: Any):
kb = build_admin_back_kb("stats")
try:
users = await session.fetch(
"""
SELECT
u.tg_id,
u.username,
u.first_name,
u.last_name,
u.language_code,
u.is_bot,
c.balance,
c.trial
FROM users u
LEFT JOIN connections c ON u.tg_id = c.tg_id
"""
)
if not users:
await callback_query.message.edit_text(
text="📭 Нет пользователей для экспорта.",
reply_markup=kb
)
return
csv_data = "tg_id,username,first_name,last_name,language_code,is_bot,balance,trial\n" # Заголовки CSV
for user in users:
csv_data += f"{user['tg_id']},{user['username']},{user['first_name']},{user['last_name']},{user['language_code']},{user['is_bot']},{user['balance']},{user['trial']}\n"
file_name = BytesIO(csv_data.encode("utf-8-sig"))
file_name.seek(0)
file = BufferedInputFile(file_name.getvalue(), filename="users_export.csv")
await callback_query.message.answer_document(
document=file,
caption="📥 Экспорт пользователей в CSV"
)
file_name.close()
except Exception as e:
logger.error(f"Ошибка при экспорте пользователей в CSV: {e}")
await callback_query.message.edit_text(
text=f"❗ Произошла ошибка при экспорте: {e}",
reply_markup=kb
)
@router.callback_query(
AdminPanelCallback.filter(F.action == "stats_export_payments_csv"),
IsAdminFilter(),
)
async def export_payments_csv(callback_query: CallbackQuery, session: Any):
kb = build_admin_back_kb("stats")
try:
payments = await session.fetch(
"""
SELECT
u.tg_id,
u.username,
u.first_name,
u.last_name,
p.amount,
p.payment_system,
p.status,
p.created_at
FROM users u
JOIN payments p ON u.tg_id = p.tg_id
"""
)
if not payments:
await callback_query.message.edit_text(
text="📭 Нет платежей для экспорта.",
reply_markup=kb
)
return
csv_data = "tg_id,username,first_name,last_name,amount,payment_system,status,created_at\n" # Заголовки CSV
for payment in payments:
csv_data += f"{payment['tg_id']},{payment['username']},{payment['first_name']},{payment['last_name']},{payment['amount']},{payment['payment_system']},{payment['status']},{payment['created_at']}\n"
file_name = BytesIO(csv_data.encode("utf-8-sig"))
file_name.seek(0)
file = BufferedInputFile(file_name.getvalue(), filename="payments_export.csv")
await callback_query.message.answer_document(
document=file,
caption="📥 Экспорт платежей в CSV"
)
file_name.close()
except Exception as e:
logger.error(f"Ошибка при экспорте платежей в CSV: {e}")
await callback_query.message.edit_text(
text=f"❗ Произошла ошибка при экспорте: {e}",
reply_markup=kb
)
+2 -4
View File
@@ -2,14 +2,13 @@ import asyncio
from datetime import datetime
from typing import Any
import pytz
from aiogram import Bot, F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import TOTAL_GB
from database import (
delete_user_data,
get_client_id_by_email,
@@ -497,9 +496,8 @@ async def handle_expiry_time_input(
try:
expiry_time_str = message.text
moscow_tz = pytz.timezone("Europe/Moscow")
expiry_time = int(
datetime.strptime(expiry_time_str, "%Y-%m-%d %H:%M:%S").replace(tzinfo=moscow_tz).timestamp() * 1000
datetime.strptime(expiry_time_str, "%Y-%m-%d %H:%M:%S").timestamp() * 1000
)
client_id = await get_client_id_by_email(email)
+622
View File
@@ -0,0 +1,622 @@
import asyncio
from datetime import datetime
from typing import Any
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery
from config import TOTAL_GB
from database import delete_user_data, get_client_id_by_email, get_servers_from_db, restore_trial, update_key_expiry
from filters.admin import IsAdminFilter
from handlers.keys.key_utils import (
delete_key_from_cluster,
delete_key_from_db,
renew_key_in_cluster,
)
from handlers.utils import sanitize_key_name
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
from keyboards.admin.users_kb import build_user_edit_kb, build_key_edit_kb, build_key_delete_kb, \
build_user_delete_kb, AdminUserEditorCallback, build_editor_kb
from logger import logger
router = Router()
class UserEditorState(StatesGroup):
# search
waiting_for_user_data = State()
waiting_for_key_name = State()
# updating data
waiting_for_new_balance = State()
waiting_for_expiry_time = State()
waiting_for_message_text = State()
@router.callback_query(
AdminPanelCallback.filter(F.action == "search_user"),
IsAdminFilter(),
)
async def handle_users_search(
callback_query: CallbackQuery,
state: FSMContext
):
text = (
"🔍 Введите ID или Username пользователя для поиска:"
"\n\n🆔 ID - числовой айди"
"\n📝 Username - юзернейм пользователя"
)
await state.set_state(UserEditorState.waiting_for_user_data)
await callback_query.message.edit_text(
text=text,
reply_markup=build_admin_back_kb()
)
@router.callback_query(
AdminPanelCallback.filter(F.action == "search_key"),
IsAdminFilter(),
)
async def handle_users_search_key(
callback_query: CallbackQuery,
state: FSMContext
):
await state.set_state(UserEditorState.waiting_for_key_name)
await callback_query.message.edit_text(
text="🔑 Введите имя ключа для поиска:",
reply_markup=build_admin_back_kb()
)
@router.message(
UserEditorState.waiting_for_user_data,
IsAdminFilter()
)
async def handle_user_data_input(
message: types.Message,
state: FSMContext,
session: Any
):
kb = build_admin_back_kb()
if not message.text:
await message.answer(
text="🚫 Пожалуйста, отправьте текстовое сообщение.",
reply_markup=kb
)
return
if message.text.isdigit():
tg_id = int(message.text)
else:
# Удаление '@' символа в начале сообщения
username = message.text.strip().lstrip('@')
# Удаление начала ссылки на профиль
username = username.replace('https://t.me/', '')
user = await session.fetchrow(
"SELECT tg_id FROM users WHERE username = $1", username
)
if not user:
await message.answer(
text="🚫 Пользователь с указанным Username не найден!",
reply_markup=kb,
)
return
tg_id = user["tg_id"]
await process_user_search(message, state, session, tg_id)
@router.message(
UserEditorState.waiting_for_key_name,
IsAdminFilter()
)
async def handle_key_name_input(
message: types.Message,
state: FSMContext,
session: Any
):
kb = build_admin_back_kb()
if not message.text:
await message.answer(
text="🚫 Пожалуйста, отправьте текстовое сообщение.",
reply_markup=kb
)
return
key_name = sanitize_key_name(message.text)
key_details = await get_key_details(key_name, session)
if not key_details:
await message.answer(
text="🚫 Пользователь с указанным именем ключа не найден.",
reply_markup=kb
)
return
await process_user_search(message, state, session, key_details["tg_id"])
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_send_message"),
IsAdminFilter(),
)
async def handle_send_message(
callback_query: types.CallbackQuery,
callback_data: AdminUserEditorCallback,
state: FSMContext
):
tg_id = callback_data.tg_id
await callback_query.message.edit_text(
text="✉️ Введите текст сообщения, которое вы хотите отправить пользователю:",
reply_markup=build_editor_kb(tg_id)
)
await state.update_data(tg_id=tg_id)
await state.set_state(UserEditorState.waiting_for_message_text)
@router.message(
UserEditorState.waiting_for_message_text,
IsAdminFilter()
)
async def handle_message_text_input(
message: types.Message,
state: FSMContext
):
data = await state.get_data()
tg_id = data.get("tg_id")
try:
await message.bot.send_message(
chat_id=tg_id,
text=message.text
)
await message.answer(
text="✅ Сообщение успешно отправлено.",
reply_markup=build_editor_kb(tg_id)
)
except Exception as e:
await message.answer(
text=f"❌ Не удалось отправить сообщение: {e}",
reply_markup=build_editor_kb(tg_id)
)
await state.clear()
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_trial_restore"),
IsAdminFilter(),
)
async def handle_restore_trial(
callback_query: types.CallbackQuery,
callback_data: AdminUserEditorCallback,
session: Any
):
tg_id = callback_data.tg_id
await restore_trial(tg_id, session)
await callback_query.message.edit_text(
text="✅ Триал успешно восстановлен!",
reply_markup=build_editor_kb(tg_id)
)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_balance_change"),
IsAdminFilter()
)
async def process_balance_change(
callback_query: CallbackQuery,
callback_data: AdminUserEditorCallback,
state: FSMContext
):
await state.update_data(tg_id=callback_data.tg_id)
await callback_query.message.edit_text(
text="💸 Введите новую сумму баланса:",
reply_markup=build_editor_kb(callback_data.tg_id)
)
await state.set_state(UserEditorState.waiting_for_new_balance)
@router.message(
UserEditorState.waiting_for_new_balance,
IsAdminFilter()
)
async def handle_new_balance_input(
message: types.Message,
state: FSMContext,
session: Any
):
user_data = await state.get_data()
tg_id = user_data.get("tg_id")
if not message.text.isdigit() or int(message.text) < 0:
await message.answer(
text="❌ Пожалуйста, введите корректную сумму для изменения баланса.",
reply_markup=build_editor_kb(tg_id),
)
return
new_balance = int(message.text)
await session.execute(
"UPDATE connections SET balance = $1 WHERE tg_id = $2",
new_balance,
tg_id,
)
await message.answer(
text=f"✅ Баланс успешно изменен на <b>{new_balance}</b>",
reply_markup=build_admin_back_kb()
)
await state.clear()
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_key_edit"),
IsAdminFilter()
)
async def process_key_edit(
callback_query: CallbackQuery,
callback_data: AdminUserEditorCallback,
session: Any
):
email = callback_data.data
key_details = await get_key_details(email, session)
if not key_details:
await callback_query.message.edit_text(
text="🚫 Информация о ключе не найдена.",
reply_markup=build_editor_kb(callback_data.tg_id),
)
return
text = (
f"🔑 Ключ: <code>{key_details['key']}</code>\n"
f"⏰ Дата истечения: <b>{key_details['expiry_date']}</b>\n"
f"💰 Баланс пользователя: <b>{key_details['balance']}</b>\n"
f"🌐 Кластер: <b>{key_details['server_name']}</b>"
)
await callback_query.message.edit_text(
text=text,
reply_markup=build_key_edit_kb(key_details, email)
)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_change_expiry"),
IsAdminFilter()
)
async def prompt_expiry_change(
callback_query: CallbackQuery,
callback_data: AdminUserEditorCallback,
state: FSMContext
):
email = callback_data.data
await callback_query.message.edit_text(
text=f"⏳ Введите новое время истечения для ключа <b>{email}</b> в формате <code>YYYY-MM-DD HH:MM:SS</code>:"
)
await state.update_data(tg_id=callback_data.tg_id, email=email)
await state.set_state(UserEditorState.waiting_for_expiry_time)
@router.message(
UserEditorState.waiting_for_expiry_time,
IsAdminFilter()
)
async def handle_expiry_time_input(
message: types.Message,
state: FSMContext,
session: Any
):
user_data = await state.get_data()
email = user_data.get("email")
try:
expiry_time = int(
datetime.strptime(message.text, "%Y-%m-%d %H:%M:%S").timestamp() * 1000
)
client_id = await get_client_id_by_email(email)
if client_id is None:
await message.edit_text(
text=f"🚫 Клиент с Email {email} не найден. 🔍",
reply_markup=build_admin_back_kb(),
)
await state.clear()
return
server_id = await session.fetchrow(
"SELECT server_id FROM keys WHERE client_id = $1", client_id
)
if not server_id:
await message.edit_text(
text="🚫 Клиент не найден в базе данных. 🔍",
reply_markup=build_admin_back_kb(),
)
await state.clear()
return
clusters = await get_servers_from_db()
async def update_key_on_all_servers():
tasks = [
asyncio.create_task(
renew_key_in_cluster(
cluster_name,
email,
client_id,
expiry_time,
total_gb=TOTAL_GB,
)
)
for cluster_name in clusters
]
await asyncio.gather(*tasks)
await update_key_on_all_servers()
await update_key_expiry(client_id, expiry_time)
response_message = f"✅ Время истечения ключа для клиента {client_id} ({email}) успешно обновлено на всех серверах."
await message.edit_text(
text=response_message,
reply_markup=build_admin_back_kb()
)
except ValueError:
tg_id = user_data.get("tg_id")
await message.edit_text(
text="❌ Пожалуйста, используйте формат: YYYY-MM-DD HH:MM:SS.",
reply_markup=build_editor_kb(tg_id),
)
except Exception as e:
logger.error(e)
await state.clear()
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_delete_key"),
IsAdminFilter()
)
async def process_callback_delete_key(
callback_query: types.CallbackQuery,
callback_data: AdminUserEditorCallback,
session: Any
):
email = callback_data.data
client_id = await session.fetchval(
"SELECT client_id FROM keys WHERE email = $1", email
)
if client_id is None:
await callback_query.message.edit_text(
text="🚫 Ключ не найден!",
reply_markup=build_editor_kb(callback_data.tg_id)
)
return
await callback_query.message.edit_text(
text="❓ Вы уверены, что хотите удалить ключ?",
reply_markup=build_key_delete_kb(callback_data.tg_id, client_id)
)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_delete_key_confirm"),
IsAdminFilter()
)
async def process_callback_confirm_delete(
callback_query: types.CallbackQuery,
callback_data: AdminUserEditorCallback,
session: Any
):
client_id = callback_data.data
record = await session.fetchrow(
"SELECT email FROM keys WHERE client_id = $1", client_id
)
kb = build_editor_kb(callback_data.tg_id)
if record:
clusters = await get_servers_from_db()
async def delete_key_from_servers(email, client_id):
tasks = []
for cluster_name, cluster_servers in clusters.items():
for server in cluster_servers:
tasks.append(
delete_key_from_cluster(cluster_name, email, client_id)
)
await asyncio.gather(*tasks)
await delete_key_from_servers(record["email"], client_id)
await delete_key_from_db(client_id, session)
await callback_query.message.edit_text(
text="✅ Ключ успешно удален.",
reply_markup=kb
)
else:
await callback_query.message.edit_text(
text="🚫 Ключ не найден или уже удален.",
reply_markup=kb
)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_delete_user"),
IsAdminFilter()
)
async def confirm_delete_user(
callback_query: types.CallbackQuery,
callback_data: AdminUserEditorCallback
):
tg_id = callback_data.tg_id
await callback_query.message.edit_text(
text=f"❗️ Вы уверены, что хотите удалить пользователя с ID {tg_id}?",
reply_markup=build_user_delete_kb(tg_id)
)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_delete_user_confirm"),
IsAdminFilter()
)
async def delete_user(
callback_query: types.CallbackQuery,
callback_data: AdminUserEditorCallback,
session: Any
):
tg_id = callback_data.tg_id
key_records = await session.fetch("SELECT email, client_id FROM keys WHERE tg_id = $1", tg_id)
async def delete_keys_from_servers():
try:
tasks = []
for email, client_id in key_records:
servers = await get_servers_from_db()
for cluster_id, cluster in servers.items():
tasks.append(delete_key_from_cluster(cluster_id, email, client_id))
await asyncio.gather(*tasks)
except Exception as e:
logger.error(f"Ошибка при удалении ключей с серверов для пользователя {tg_id}: {e}")
await delete_keys_from_servers()
try:
await delete_user_data(session, tg_id)
await callback_query.message.edit_text(
text=f"🗑️ Пользователь с ID {tg_id} был удален.",
reply_markup=build_editor_kb(callback_data.tg_id)
)
except Exception as e:
logger.error(f"Ошибка при удалении данных из базы данных для пользователя {tg_id}: {e}")
await callback_query.message.edit_text(
text=f"❌ Произошла ошибка при удалении пользователя с ID {tg_id}. Попробуйте снова."
)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_editor"),
IsAdminFilter()
)
async def handle_users_editor(
callback_query: types.CallbackQuery,
callback_data: AdminUserEditorCallback,
state: FSMContext,
session: Any
):
await process_user_search(
callback_query.message,
state,
session,
callback_data.tg_id,
callback_data.data == "edit"
)
async def process_user_search(
message: types.Message,
state: FSMContext,
session: Any,
tg_id: int,
edit: bool = False
) -> None:
await state.clear()
username = await session.fetchval(
"SELECT username FROM users WHERE tg_id = $1", tg_id
)
balance = await session.fetchval(
"SELECT balance FROM connections WHERE tg_id = $1", tg_id
)
if balance is None:
await message.answer(
text="🚫 Пользователь с указанным ID не найден!",
reply_markup=build_admin_back_kb(),
)
return
key_records = await session.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id)
referral_count = await session.fetchval(
"SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id
)
text = (
f"📊 Информация о пользователе:\n\n"
f"🆔 ID пользователя: <b>{tg_id}</b>\n"
f"👤 Логин пользователя: <b>@{username}</b>\n"
f"💰 Баланс: <b>{balance}</b>\n"
f"👥 Количество рефералов: <b>{referral_count}</b>\n"
f"🔑 Ключи (для редактирования нажмите на ключ):"
)
if edit:
await message.edit_text(
text=text,
reply_markup=build_user_edit_kb(tg_id, key_records)
)
else:
await message.answer(
text=text,
reply_markup=build_user_edit_kb(tg_id, key_records)
)
async def get_key_details(email, session):
record = await session.fetchrow(
"""
SELECT k.key, k.expiry_time, k.server_id, c.tg_id, c.balance
FROM keys k
JOIN connections c ON k.tg_id = c.tg_id
WHERE k.email = $1
""",
email,
)
if not record:
return None
servers = await get_servers_from_db()
cluster_name = "Неизвестный кластер"
for cluster_name, cluster_servers in servers.items():
if any(
server["inbound_id"] == record["server_id"] for server in cluster_servers
):
cluster_name = cluster_name
break
expiry_date = datetime.utcfromtimestamp(record["expiry_time"] / 1000)
current_date = datetime.utcnow()
time_left = expiry_date - current_date
if time_left.total_seconds() <= 0:
days_left_message = "<b>Ключ истек.</b>"
elif time_left.days > 0:
days_left_message = f"Осталось дней: <b>{time_left.days}</b>"
else:
hours_left = time_left.seconds // 3600
days_left_message = f"Осталось часов: <b>{hours_left}</b>"
return {
"key": record["key"],
"expiry_date": expiry_date.strftime("%d %B %Y года"),
"days_left_message": days_left_message,
"server_name": cluster_name,
"balance": record["balance"],
"tg_id": record["tg_id"],
}
View File
View File
+21
View File
@@ -0,0 +1,21 @@
from aiogram.types import InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from keyboards.admin.panel_kb import build_admin_back_btn, AdminPanelCallback
def build_bans_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text="📄 Выгрузить в CSV",
callback_data=AdminPanelCallback(action="bans_export").pack()
)
builder.button(
text="🗑️ Удалить из БД",
callback_data=AdminPanelCallback(action="bans_delete_banned").pack()
)
builder.row(
build_admin_back_btn("management")
)
builder.adjust(1)
return builder.as_markup()
+43
View File
@@ -0,0 +1,43 @@
from aiogram.filters.callback_data import CallbackData
from aiogram.types import InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_btn
class AdminCouponDeleteCallback(CallbackData, prefix="admin_coupon_delete"):
coupon_code: str
def build_coupons_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text=" Создать купон",
callback_data=AdminPanelCallback(action="coupons_create").pack()
)
builder.button(
text="Купоны",
callback_data=AdminPanelCallback(action="coupons_list").pack()
)
builder.row(
build_admin_back_btn()
)
return builder.as_markup()
def build_coupons_list_kb(coupons: list) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
for coupon in coupons:
coupon_code = coupon["code"]
builder.button(
text=f"❌ Удалить {coupon_code}",
callback_data=AdminCouponDeleteCallback(
coupon_code=coupon_code
).pack(),
)
builder.row(
build_admin_back_btn("coupons")
)
return builder.as_markup()
+103
View File
@@ -0,0 +1,103 @@
from aiogram.filters.callback_data import CallbackData
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
class AdminPanelCallback(CallbackData, prefix="admin_panel"):
action: str
def build_panel_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text="👤 Поиск пользователя",
callback_data=AdminPanelCallback(action="search_user").pack()
)
builder.button(
text="🔑 Поиск по названию ключа",
callback_data=AdminPanelCallback(action="search_key").pack()
)
builder.row(
InlineKeyboardButton(
text="🖥️ Серверы",
callback_data=AdminPanelCallback(action="servers").pack()
),
InlineKeyboardButton(
text="🎟️ Купоны",
callback_data=AdminPanelCallback(action="coupons").pack()
)
)
builder.button(
text="📢 Рассылка",
callback_data=AdminPanelCallback(action="sender").pack()
)
builder.row(
InlineKeyboardButton(
text="📊 Статистика",
callback_data=AdminPanelCallback(action="stats").pack()
),
InlineKeyboardButton(
text="🤖 Управление",
callback_data=AdminPanelCallback(action="management").pack()
)
)
builder.button(
text="Личный кабинет",
callback_data="profile"
)
builder.adjust(1, 1, 2, 1, 2, 1)
return builder.as_markup()
def build_management_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text="💾 Создать резервную копию",
callback_data=AdminPanelCallback(action="backups").pack()
)
builder.button(
text="🚫 Заблокировавшие бота",
callback_data=AdminPanelCallback(action="bans").pack()
)
builder.button(
text="🔄 Перезагрузить бота",
callback_data=AdminPanelCallback(action="restart").pack()
)
builder.row(
build_admin_back_btn()
)
builder.adjust(1)
return builder.as_markup()
def build_restart_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text="✅ Да, перезагрузить",
callback_data=AdminPanelCallback(action="restart_confirm").pack()
)
builder.row(
build_admin_back_btn()
)
builder.adjust(1)
return builder.as_markup()
def build_admin_back_kb(action: str = "admin") -> InlineKeyboardMarkup:
return build_admin_singleton_kb("🔙 Назад", action)
def build_admin_singleton_kb(text: str, action: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.row(
build_admin_btn(text, action)
)
return builder.as_markup()
def build_admin_back_btn(action: str = "admin") -> InlineKeyboardMarkup:
return build_admin_btn("🔙 Назад", action)
def build_admin_btn(text: str, action: str) -> InlineKeyboardButton:
return InlineKeyboardButton(text=text, callback_data=AdminPanelCallback(action=action).pack())
+36
View File
@@ -0,0 +1,36 @@
from aiogram.filters.callback_data import CallbackData
from aiogram.types import InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from keyboards.admin.panel_kb import build_admin_back_btn
class AdminSenderCallback(CallbackData, prefix="admin_sender"):
type: str
def build_sender_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text="👥 Все пользователи",
callback_data=AdminSenderCallback(
type="all"
).pack()
)
builder.button(
text="✅ Пользователи с подпиской",
callback_data=AdminSenderCallback(
type="subscribed"
).pack()
)
builder.button(
text="❌ Пользователи без подписки",
callback_data=AdminSenderCallback(
type="unsubscribed"
).pack()
)
builder.row(
build_admin_back_btn()
)
builder.adjust(1)
return builder.as_markup()
+125
View File
@@ -0,0 +1,125 @@
from aiogram.filters.callback_data import CallbackData
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_btn
class AdminServerEditorCallback(CallbackData, prefix="admin_servers"):
action: str
data: str
def build_clusters_editor_kb(servers: dict) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
for cluster_name in servers:
builder.button(
text=f"⚙️ {cluster_name}",
callback_data=AdminServerEditorCallback(
action="clusters_manage",
data=cluster_name
).pack()
)
builder.button(
text=" Добавить кластер",
callback_data=AdminPanelCallback(action="clusters_add").pack()
)
builder.row(
build_admin_back_btn()
)
builder.adjust(1)
return builder.as_markup()
def build_manage_cluster_kb(cluster_servers, cluster_name) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
for server in cluster_servers:
builder.button(
text=f"🌍 {server['server_name']}",
callback_data=AdminServerEditorCallback(
action="servers_manage",
data=server["server_name"]
).pack()
)
builder.button(
text=" Добавить сервер",
callback_data=AdminServerEditorCallback(
action="servers_add",
data=cluster_name
).pack()
)
builder.button(
text="🌐 Доступность серверов",
callback_data=AdminServerEditorCallback(
action="servers_availability",
data=cluster_name
).pack()
)
builder.button(
text="💾 Создать бэкап кластера",
callback_data=AdminServerEditorCallback(
action="clusters_backup",
data=cluster_name
).pack()
)
builder.row(
build_admin_back_btn("servers")
)
builder.adjust(1)
return builder.as_markup()
def build_manage_server_kb(server_name: str, cluster_name: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text="🗑️ Удалить",
callback_data=AdminServerEditorCallback(
action="servers_delete",
data=server_name
).pack()
)
builder.button(
text="🔙 Назад",
callback_data=AdminServerEditorCallback(
action="clusters_manage",
data=cluster_name
).pack()
)
builder.adjust(1)
return builder.as_markup()
def build_delete_server_kb(server_name: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text="✅ Да",
callback_data=AdminServerEditorCallback(
action="servers_delete_confirm",
data=server_name
).pack()
)
builder.button(
text="🔙 Назад",
callback_data=AdminServerEditorCallback(
action="servers_manage",
data=server_name
).pack()
)
builder.adjust(1)
return builder.as_markup()
def build_cancel_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="❌ Отменить",
callback_data="servers"
)
)
builder.adjust(1)
return builder.as_markup()
+25
View File
@@ -0,0 +1,25 @@
from aiogram.types import InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from keyboards.admin.panel_kb import build_admin_back_btn, AdminPanelCallback
def build_stats_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text="🔄 Обновить",
callback_data=AdminPanelCallback(action="stats").pack()
)
builder.button(
text="📥 Выгрузить пользователей в CSV",
callback_data=AdminPanelCallback(action="stats_export_users_csv").pack()
)
builder.button(
text="📥 Выгрузить оплаты в CSV",
callback_data=AdminPanelCallback(action="stats_export_payments_csv").pack()
)
builder.row(
build_admin_back_btn()
)
builder.adjust(1)
return builder.as_markup()
+151
View File
@@ -0,0 +1,151 @@
from aiogram.filters.callback_data import CallbackData
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from keyboards.admin.panel_kb import build_admin_back_btn
class AdminUserEditorCallback(CallbackData, prefix="admin_users"):
action: str
tg_id: int
data: str | None = None
def build_user_edit_kb(tg_id: int, key_records: list) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
for record in key_records:
email = record["email"]
builder.button(
text=f"🔑 {email}",
callback_data=AdminUserEditorCallback(
action="users_key_edit",
tg_id=tg_id,
data=str(email)
).pack()
)
builder.button(
text="💸 Изменить баланс",
callback_data=AdminUserEditorCallback(
action="users_balance_change",
tg_id=tg_id
).pack()
)
builder.button(
text="✉️ Сообщение",
callback_data=AdminUserEditorCallback(
action="users_send_message",
tg_id=tg_id
).pack()
)
builder.button(
text="🔄 Восстановить триал",
callback_data=AdminUserEditorCallback(
action="users_trial_restore",
tg_id=tg_id
).pack()
)
builder.button(
text="❌ Удалить клиента",
callback_data=AdminUserEditorCallback(
action="users_delete_user",
tg_id=tg_id
).pack()
)
builder.row(
build_editor_btn("🔄 Обновить", tg_id, edit=True)
)
builder.row(
build_admin_back_btn()
)
builder.adjust(1)
return builder.as_markup()
def build_user_delete_kb(tg_id: int):
builder = InlineKeyboardBuilder()
builder.button(
text="❌ Да, удалить!",
callback_data=AdminUserEditorCallback(
action="users_delete_user_confirm",
tg_id=tg_id
).pack()
)
builder.row(
build_editor_back_btn(tg_id, True)
)
builder.adjust(1)
return builder.as_markup()
def build_key_edit_kb(key_details: dict, email: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text="⏳ Изменить время истечения",
callback_data=AdminUserEditorCallback(
action="users_change_expiry",
data=email,
tg_id=key_details["tg_id"]
).pack()
)
builder.button(
text="❌ Удалить ключ",
callback_data=AdminUserEditorCallback(
action="users_delete_key",
data=email,
tg_id=key_details["tg_id"]
).pack()
)
builder.row(
build_editor_back_btn(key_details["tg_id"], True)
)
builder.adjust(1)
return builder.as_markup()
def build_key_delete_kb(tg_id: int, client_id: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="✅ Да, удалить",
callback_data=AdminUserEditorCallback(
action="users_delete_key_confirm",
data=client_id,
tg_id=tg_id
).pack()
)
)
builder.row(
build_editor_back_btn(tg_id)
)
builder.adjust(1)
return builder.as_markup()
def build_editor_kb(tg_id: int, edit: bool = False) -> InlineKeyboardMarkup:
return build_editor_singleton_kb("🔙 Назад", tg_id, edit)
def build_editor_singleton_kb(text: str, tg_id: int, edit: bool = False) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.row(
build_editor_btn(text, tg_id, edit)
)
return builder.as_markup()
def build_editor_back_btn(tg_id: int, edit: bool = False) -> InlineKeyboardButton:
return build_editor_btn("🔙 Назад", tg_id, edit)
def build_editor_btn(text: str, tg_id: int, edit: bool = False) -> InlineKeyboardButton:
return InlineKeyboardButton(
text=text,
callback_data=AdminUserEditorCallback(
action="users_editor",
data="" if edit else "edit",
tg_id=tg_id
).pack()
)
+43 -16
View File
@@ -4,25 +4,52 @@ from typing import Any
from aiogram import BaseMiddleware
from aiogram.types import CallbackQuery, Message, TelegramObject
from keyboards.admin.coupons_kb import AdminCouponDeleteCallback
from keyboards.admin.panel_kb import AdminPanelCallback
from keyboards.admin.sender_kb import AdminSenderCallback
from keyboards.admin.servers_kb import AdminServerEditorCallback
from keyboards.admin.users_kb import AdminUserEditorCallback
pass_callbacks = [
AdminPanelCallback,
AdminCouponDeleteCallback,
AdminSenderCallback,
AdminServerEditorCallback,
AdminUserEditorCallback,
]
class DeleteMessageMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: dict[str, Any],
self,
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: dict[str, Any],
) -> Any:
if isinstance(event, (Message, CallbackQuery)):
if isinstance(event, Message):
if not event.text or not event.text.startswith("/start"):
try:
await event.bot.delete_message(
event.chat.id, event.message_id - 1
)
except Exception:
pass
await event.delete()
elif isinstance(event, CallbackQuery):
await event.answer()
if isinstance(event, Message):
if (
not event.text
or not event.text.startswith("/start")
):
try:
await event.bot.delete_message(
event.chat.id, event.message_id - 1
)
except Exception:
pass
await event.delete()
if isinstance(event, CallbackQuery):
await event.answer()
if not await self._check_callbacks(event):
await event.message.delete()
return await handler(event, data)
@staticmethod
async def _check_callbacks(event: CallbackQuery) -> bool:
for callback in pass_callbacks:
if await callback.filter()(event):
return True
return False