This commit is contained in:
Zakhar Izmaylov
2025-01-29 07:01:01 +03:00
27 changed files with 2708 additions and 988 deletions
+53 -44
View File
@@ -9,73 +9,64 @@ from config import ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER, PG_HOST, P
from logger import logger
async def backup_database():
from bot import bot
async def backup_database() -> Exception | None:
backup_file_path, exception = _create_database_backup()
if exception:
logger.error(f"Ошибка при создании бэкапа базы данных: {exception}")
return exception
try:
if backup_file_path := _create_database_backup():
await _send_backup_to_admin(bot, backup_file_path)
_cleanup_old_backups()
await _send_backup_to_admins(backup_file_path)
exception = _cleanup_old_backups()
if exception:
logger.error(f"Ошибка при удалении старых бэкапов базы данных: {exception}")
return exception
return None
except Exception as e:
logger.error(f"Ошибка при создании или отправке бэкапа: {e}")
logger.error(f"Ошибка при отправке бэкапа базы данных: {e}")
return e
def _create_database_backup():
USER = DB_USER
HOST = PG_HOST
PORT = PG_PORT
BACKUP_DIR = BACK_DIR
DATE = datetime.now().strftime("%Y-%m-%d-%H%M%S")
def _create_database_backup() -> (str | None, Exception | None):
date_formatted = datetime.now().strftime("%Y-%m-%d-%H%M%S")
if not os.path.exists(BACKUP_DIR):
os.makedirs(BACKUP_DIR)
if not os.path.exists(BACK_DIR):
os.makedirs(BACK_DIR)
BACKUP_FILE = f"{BACKUP_DIR}/{DB_NAME}-backup-{DATE}.sql"
os.environ["PGPASSWORD"] = DB_PASSWORD
filename = os.path.join(BACK_DIR, f"{DB_NAME}-backup-{date_formatted}.sql")
try:
os.environ["PGPASSWORD"] = DB_PASSWORD
subprocess.run(
[
"pg_dump",
"-U",
USER,
DB_USER,
"-h",
HOST,
PG_HOST,
"-p",
PORT,
PG_PORT,
"-F",
"c",
"-f",
BACKUP_FILE,
filename,
DB_NAME,
],
check=True,
)
logger.info(f"Бэкап базы данных создан: {BACKUP_FILE}")
return BACKUP_FILE
logger.info(f"Бэкап базы данных создан: {filename}")
return filename, None
except subprocess.CalledProcessError as e:
logger.error(f"Ошибка при создании бэкапа базы данных: {e}")
return None
return None, e
finally:
del os.environ["PGPASSWORD"]
async def _send_backup_to_admin(bot, backup_file_path):
try:
async with aiofiles.open(backup_file_path, "rb") as backup_file:
backup_file_data = await backup_file.read()
backup_input_file = BufferedInputFile(backup_file_data, filename=os.path.basename(backup_file_path))
admin_ids: int | list[int] = ADMIN_ID
if isinstance(admin_ids, list):
for id in admin_ids:
await bot.send_document(id, backup_input_file)
logger.info(f"Бэкап базы данных отправлен админу: {id}")
except Exception as e:
logger.error(f"Ошибка при отправке бэкапа в Telegram: {e}")
def _cleanup_old_backups():
def _cleanup_old_backups() -> None | Exception:
try:
subprocess.run(
[
@@ -95,12 +86,30 @@ def _cleanup_old_backups():
check=True,
)
logger.info("Старые бэкапы удалены.")
return None
except subprocess.CalledProcessError as e:
logger.error(f"Ошибка при удалении старых бэкапов: {e}")
except Exception as e:
logger.error(f"Ошибка при удалении старых бэкапов: {e}")
return e
async def create_backup_and_send_to_admins(xui):
async def create_backup_and_send_to_admins(xui) -> None:
await xui.login()
await xui.database.export()
async def _send_backup_to_admins(backup_file_path: str) -> None:
try:
from bot import bot
with open(backup_file_path, "rb") as backup_file:
backup_input_file = BufferedInputFile(
file=backup_file.read(),
filename=os.path.basename(backup_file_path)
)
admin_ids = ADMIN_ID if isinstance(ADMIN_ID, list) else [ADMIN_ID]
for admin_id in admin_ids:
await bot.send_document(
chat_id=admin_id,
document=backup_input_file
)
logger.info(f"Бэкап базы данных отправлен админу: {admin_id}")
except Exception as e:
logger.error(f"Ошибка при отправке бэкапа в Telegram: {e}")
+2
View File
@@ -18,6 +18,8 @@ bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTM
storage = MemoryStorage()
dp = Dispatcher(bot=bot, storage=storage)
version = "4.0.0-preAlpha(14-dev)"
register_middleware(dp)
dp.message.filter(IsPrivateFilter())
+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,
)
+35
View File
@@ -0,0 +1,35 @@
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_backups(
callback_query: CallbackQuery
):
kb = build_admin_back_kb("management")
await callback_query.message.edit_text(
text="💾 Инициализация резервного копирования базы данных...",
reply_markup=kb
)
exception = await backup_database()
if exception:
text = f"❌ Ошибка при создании резервной копии: {exception}"
else:
text = "✅ Резервная копия успешно создана и отправлена администраторам."
await callback_query.message.edit_text(
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,
)
+128 -103
View File
@@ -3,33 +3,124 @@ 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, get_all_coupons
from database import delete_coupon, 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(
AdminPanelCallback.filter(F.action == "coupons"),
IsAdminFilter(),
)
async def handle_coupons(
callback_query: types.CallbackQuery,
):
await callback_query.message.edit_text(
text="🛠 Меню управления купонами:",
reply_markup=build_coupons_kb()
)
@router.callback_query(F.data == "coupons_editor", IsAdminFilter())
async def show_coupon_management_menu(callback_query: types.CallbackQuery, state: FSMContext):
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())
@router.callback_query(
AdminPanelCallback.filter(F.action == "coupons_create"),
IsAdminFilter(),
)
async def handle_coupons_create(
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.callback_query(F.data.startswith("coupons"), IsAdminFilter())
async def show_coupon_list(callback_query: types.CallbackQuery, session: Any):
@router.message(
AdminCouponsState.waiting_for_coupon_data,
IsAdminFilter()
)
async def handle_coupon_data_input(
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 handle_coupons_list(
callback_query: types.CallbackQuery,
session: Any
):
try:
page = int(callback_query.data.split(":")[1]) if ":" in callback_query.data else 1
per_page = 10
@@ -39,16 +130,14 @@ async def show_coupon_list(callback_query: types.CallbackQuery, session: Any):
current_page = result["current_page"]
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
coupon_list = f"📜 Список купонов (страница {current_page} из {total_pages}):\n\n"
builder = InlineKeyboardBuilder()
kb = build_coupons_list_kb(coupons)
coupon_list = "📜 Список всех купонов:\n\n"
for coupon in coupons:
coupon_list += (
@@ -57,102 +146,38 @@ async def show_coupon_list(callback_query: types.CallbackQuery, session: Any):
f"🔢 <b>Лимит использования:</b> {coupon['usage_limit']} раз\n"
f"✅ <b>Использовано:</b> {coupon['usage_count']} раз\n\n"
)
builder.row(
InlineKeyboardButton(
text=f"❌ Удалить {coupon['code']}", callback_data=f"delete_coupon_{coupon['code']}"
)
)
if current_page > 1:
builder.row(InlineKeyboardButton(text="⬅️ Предыдущая", callback_data=f"coupons:{current_page - 1}"))
if current_page < total_pages:
builder.row(InlineKeyboardButton(text="➡️ Следующая", callback_data=f"coupons:{current_page + 1}"))
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}")
await callback_query.message.answer("Произошла ошибка при получении списка купонов.")
@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_coupon_delete(
callback_query: types.CallbackQuery,
callback_data: AdminCouponDeleteCallback,
session: Any
):
coupon_code = callback_data.coupon_code
try:
result = await delete_coupon(coupon_code, session)
if result:
await show_coupon_list(callback_query, session)
await handle_coupons_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)
await handle_coupons_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}")
+38 -483
View File
@@ -1,508 +1,63 @@
import subprocess
from datetime import datetime
from io import BytesIO
from typing import Any
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 database import delete_user_data, delete_blocked_user
from bot import version
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-Alpha(21)"
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()
text = (
"🤖 Панель администратора"
f"\n📌 Версия бота: {version}"
)
@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:
# Создаем кнопку для личного кабинета
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await bot.send_message(chat_id=tg_id, text=text_message, reply_markup=builder.as_markup())
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=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",
)
@router.message(
Command("admin"),
IsAdminFilter()
)
async def handle_admin_message(message: types.Message, state: FSMContext):
text = (
"🤖 Панель администратора"
f"\n📌 Версия бота: {version}"
)
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.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"))
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(),
await state.clear()
await message.answer(
text=text,
reply_markup=build_panel_kb()
)
@router.callback_query(F.data == "export_to_csv")
async def export_banned_users_to_csv(callback_query: types.CallbackQuery, session: Any):
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")
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(),
)
@router.callback_query(F.data == "delete_banned_users")
async def delete_banned_users(callback_query: types.CallbackQuery, session: Any):
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("📂 Нет заблокировавших пользователей для удаления.")
return
for tg_id in blocked_ids:
await delete_user_data(session, tg_id)
await delete_blocked_user(blocked_ids, session)
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(),
)
@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(),
)
+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 handle_restart_confirm(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 handle_message_input(
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()
+274 -298
View File
@@ -1,25 +1,22 @@
import asyncio
from typing import Any
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
from database import create_server, check_unique_server_name, delete_server, get_keys_by_server, get_servers
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_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()
@@ -27,196 +24,213 @@ 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, session: Any):
servers = await get_servers(session)
@router.callback_query(
AdminPanelCallback.filter(F.action == "servers"),
IsAdminFilter(),
)
async def handle_servers(
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",
reply_markup=builder.as_markup(),
"<i>⚠️ <b>Важно:</b> Кластеры удаляются автоматически, если удалить все серверы внутри них.</i>\n\n"
)
await callback_query.message.edit_text(
text=text,
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_clusters_add(
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 state.set_state(UserEditorState.waiting_for_cluster_name)
await callback_query.message.edit_text(
text=text,
reply_markup=build_admin_back_kb("servers")
)
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="❌ Имя кластера не может быть пустым. Попробуйте снова.",
reply_markup=build_admin_back_kb("servers")
)
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"))
text = (
f"<b>Введите имя сервера для кластера {cluster_name}:</b>\n\n"
"Рекомендуется указать локацию и номер сервера в имени.\n\n"
"<i>Пример:</i> <code>de1</code>, <code>fra1</code>, <code>fi2</code>"
)
await message.answer(
f"<b>Введите имя сервера для кластера {cluster_name}:</b>\n\n"
"Рекомендуется указать локацию сервера в имени.\n\n"
"<i>Пример:</i> <code>server-asia</code>, <code>server-europe</code>",
reply_markup=builder.as_markup(),
text=text,
reply_markup=build_admin_back_kb("servers"),
)
await state.set_state(UserEditorState.waiting_for_server_name)
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, session: Any):
server_name = message.text.strip()
if server_name == "❌ Отменить":
await state.clear()
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔧 Управление кластерами", callback_data="servers_editor"))
@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(
"Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.",
reply_markup=builder.as_markup(),
text="❌ Имя сервера не может быть пустым. Попробуйте снова.",
reply_markup=build_admin_back_kb("servers")
)
return
if not server_name:
await message.answer("❌ Имя сервера не может быть пустым. Попробуйте снова.")
return
server_name = message.text.strip()
server_unique = await check_unique_server_name(server_name, session)
if not server_unique:
await message.answer("❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя.")
if not await check_unique_server_name(server_name):
await message.answer(
text="❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя.",
reply_markup=build_admin_back_kb("servers")
)
return
user_data = await state.get_data()
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",
reply_markup=builder.as_markup(),
"URL должен быть без слэша на конце!\n"
)
await state.set_state(UserEditorState.waiting_for_api_url)
await message.answer(
text=text,
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(),
text="❌ API URL должен начинаться с <code>https://</code>. Попробуйте снова.",
reply_markup=build_admin_back_kb("servers")
)
return
if not api_url.startswith("https://"):
await message.answer(
"❌ API URL должен начинаться с <code>https://</code>. Попробуйте снова.",
)
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 в информации о клиенте.",
reply_markup=builder.as_markup(),
"Его можно увидеть в панели 3x-ui в информации о клиенте."
)
await state.set_state(UserEditorState.waiting_for_subscription_url)
await message.answer(
text=text,
reply_markup=build_admin_back_kb("servers"),
)
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(),
text="❌ subscription_url должен начинаться с <code>https://</code>. Попробуйте снова.",
reply_markup=build_admin_back_kb("servers")
)
return
if not subscription_url.startswith("https://"):
await message.answer(
"❌ subscription_url должен начинаться с <code>https://</code>. Попробуйте снова.",
)
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"))
text = (
f"<b>Введите inbound_id для сервера {server_name} в кластере {cluster_name}:</b>\n\n"
"Это номер подключения vless в вашей панели 3x-ui. Обычно это <b>1</b> при чистой настройке по гайду.\n\n"
)
await message.answer(
f"<b>Введите inbound_id для сервера {server_name} в кластере {cluster_name}:</b>\n\n"
"Это номер подключения vless в вашей панели 3x-ui. Обычно это <b>1</b> при чистой настройке по гайду.\n\n",
reply_markup=builder.as_markup(),
text=text,
reply_markup=build_admin_back_kb("servers"),
)
await state.set_state(UserEditorState.waiting_for_inbound_id)
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, session: Any):
@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 должен быть числовым значением. Попробуйте снова.")
await message.answer(
text="❌ inbound_id должен быть числовым значением. Попробуйте снова.",
reply_markup=build_admin_back_kb("servers")
)
return
user_data = await state.get_data()
@@ -225,141 +239,78 @@ async def handle_inbound_id_input(message: types.Message, state: FSMContext, ses
api_url = user_data.get("api_url")
subscription_url = user_data.get("subscription_url")
await create_server(
cluster_name=cluster_name,
server_name=server_name,
api_url=api_url,
subscription_url=subscription_url,
inbound_id=inbound_id,
session=session,
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute(
"""
INSERT INTO servers (cluster_name, server_name, api_url, subscription_url, inbound_id)
VALUES ($1, $2, $3, $4, $5)
""",
cluster_name,
server_name,
api_url,
subscription_url,
inbound_id,
)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад к кластерам", callback_data="servers_editor"))
await conn.close()
await message.answer(
f"✅ Кластер {cluster_name} и сервер {server_name} успешно добавлены!",
reply_markup=builder.as_markup(),
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, session: Any):
cluster_name = callback_query.data.split("|")[1]
@router.callback_query(
AdminServerEditorCallback.filter(F.action == "clusters_manage"),
IsAdminFilter()
)
async def handle_clusters_manage(
callback_query: types.CallbackQuery,
callback_data: AdminServerEditorCallback,
):
cluster_name = callback_data.data
servers = await get_servers(session)
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, session: Any):
"""Обработчик для синхронизации ключей на всех серверах выбранного кластера."""
cluster_name = callback_query.data.split("|")[1]
try:
keys_to_sync = await get_keys_by_server(None, cluster_name, session)
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(session)
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(),
)
@router.callback_query(F.data.startswith("server_availability|"), IsAdminFilter())
async def handle_check_server_availability(callback_query: types.CallbackQuery, session: Any):
cluster_name = callback_query.data.split("|")[1]
@router.callback_query(
AdminServerEditorCallback.filter(F.action == "servers_availability"),
IsAdminFilter()
)
async def handle_servers_availability(
callback_query: types.CallbackQuery,
callback_data: AdminServerEditorCallback
):
cluster_name = callback_data.data
servers = await get_servers(session)
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 = f"🖥️ Проверка доступности серверов для кластера {cluster_name} завершена:\n\n"
in_progress_message = await callback_query.message.answer(
text=text
)
text = (
f"🖥️ Проверка доступности серверов для кластера {cluster_name} завершена:\n\n"
)
for server in cluster_servers:
xui = AsyncApi(server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD)
@@ -368,103 +319,128 @@ async def handle_check_server_availability(callback_query: types.CallbackQuery,
await xui.login()
online_users = len(await xui.client.online())
availability_message += f"🌍 {server['server_name']}: {online_users} активных пользователей.\n"
text += (
f"🌍 {server['server_name']}: {online_users} активных пользователей.\n"
)
except Exception as e:
availability_message += f"{server['server_name']}: Не удалось получить информацию. Ошибка: {e}\n"
text += f"{server['server_name']}: Не удалось получить информацию. Ошибка: {e}\n"
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"manage_cluster|{cluster_name}"))
await in_progress_message.edit_text(availability_message, reply_markup=builder.as_markup())
await callback_query.answer()
await in_progress_message.edit_text(
text=text,
reply_markup=build_admin_back_kb("servers")
)
@router.callback_query(F.data.startswith("manage_server|"), IsAdminFilter())
async def handle_manage_server(callback_query: types.CallbackQuery, state: FSMContext, session: Any):
server_name = callback_query.data.split("|")[1]
@router.callback_query(
AdminServerEditorCallback.filter(F.action == "servers_manage"),
IsAdminFilter()
)
async def handle_servers_manage(
callback_query: types.CallbackQuery,
callback_data: AdminServerEditorCallback
):
server_name = callback_data.data
servers = await get_servers_from_db()
servers = await get_servers(session)
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}",
reply_markup=builder.as_markup(),
f"<b>🔑 Inbound ID:</b> {inbound_id}"
)
await callback_query.message.edit_text(
text=text,
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]
@router.callback_query(
AdminServerEditorCallback.filter(F.action == "servers_delete"),
IsAdminFilter()
)
async def handle_servers_delete(
callback_query: types.CallbackQuery,
callback_data: AdminServerEditorCallback
):
server_name = callback_data.data
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(),
await callback_query.message.edit_text(
text=f"🗑️ Вы уверены, что хотите удалить сервер {server_name}?",
reply_markup=build_delete_server_kb(server_name),
)
@router.callback_query(F.data.startswith("confirm_delete_server|"), IsAdminFilter())
async def handle_confirm_delete_server(callback_query: types.CallbackQuery, state: FSMContext, session: Any):
server_name = callback_query.data.split("|")[1]
@router.callback_query(
AdminServerEditorCallback.filter(F.action == "servers_delete_confirm"),
IsAdminFilter()
)
async def handle_servers_delete_confirm(
callback_query: types.CallbackQuery,
callback_data: AdminServerEditorCallback
):
server_name = callback_data.data
await delete_server(server_name, session)
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_servers_add(
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>",
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,
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, session: Any):
cluster_name = callback_query.data.split("|")[1]
@router.callback_query(
AdminServerEditorCallback.filter(F.action == "clusters_backup"),
IsAdminFilter()
)
async def handle_clusters_backup(
callback_query: types.CallbackQuery,
callback_data: AdminServerEditorCallback,
):
cluster_name = callback_data.data
servers = await get_servers(session)
cluster_servers = servers.get(cluster_name, [])
@@ -477,12 +453,12 @@ async def handle_backup_cluster(callback_query: types.CallbackQuery, session: An
)
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>",
reply_markup=builder.as_markup(),
f"🔔 <i>Бэкапы отправлены в боты панелей.</i>"
)
await callback_query.message.edit_text(
text=text,
reply_markup=build_admin_back_kb("servers"),
)
await callback_query.answer()
+138
View File
@@ -0,0 +1,138 @@
from datetime import datetime
from typing import Any
from aiogram import F, Router
from aiogram.types import CallbackQuery
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
from utils.csv_export import export_payments_csv, export_users_csv
router = Router()
@router.callback_query(
AdminPanelCallback.filter(F.action == "stats"),
IsAdminFilter(),
)
async def handle_stats(
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 handle_export_users_csv(
callback_query: CallbackQuery,
session: Any
):
kb = build_admin_back_kb("stats")
try:
export = await export_users_csv(session)
await callback_query.message.answer_document(
document=export,
caption="📥 Экспорт пользователей в CSV"
)
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 handle_export_payments_csv(
callback_query: CallbackQuery,
session: Any
):
kb = build_admin_back_kb("stats")
try:
export = await export_payments_csv(session)
await callback_query.message.answer_document(
document=export,
caption="📥 Экспорт платежей в CSV"
)
except Exception as e:
logger.error(f"Ошибка при экспорте платежей в CSV: {e}")
await callback_query.message.edit_text(
text=f"❗ Произошла ошибка при экспорте: {e}",
reply_markup=kb
)
+1
View File
@@ -590,3 +590,4 @@ async def delete_user(callback_query: types.CallbackQuery, session: Any):
await callback_query.message.answer(
f"❌ Произошла ошибка при удалении пользователя с ID {tg_id}. Попробуйте снова."
)
+886
View File
@@ -0,0 +1,886 @@
import asyncio
from datetime import datetime, timezone
from typing import Any
from aiogram import F, Router, types
from aiogram.exceptions import TelegramBadRequest
from aiogram.filters.callback_data import CallbackData
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, update_subscription,
)
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, build_users_balance_kb, \
build_users_balance_change_kb, build_user_key_kb, build_users_key_expiry_kb, AdminUserKeyEditorCallback, \
build_users_key_show_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_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_search_user(
callback_query: CallbackQuery,
state: FSMContext
):
text = (
"<b>🔍 Поиск пользователя</b>"
"\n\n📌 Введите ID, Username или перешлите сообщение пользователя."
"\n\n🆔 ID - числовой айди"
"\n📝 Username - юзернейм пользователя"
"\n\n<i>✉️ Для поиска, вы можете просто переслать сообщение от пользователя.</i>"
)
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_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 message.forward_from:
tg_id = message.forward_from.id
await process_user_search(message, state, session, tg_id)
return
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_trial_restore(
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_edit"),
IsAdminFilter()
)
async def handle_balance_change(
callback_query: CallbackQuery,
callback_data: AdminUserEditorCallback,
session: Any
):
tg_id = callback_data.tg_id
records = await session.fetch("""
SELECT amount, payment_system, status, created_at
FROM payments
WHERE tg_id = $1
ORDER BY created_at DESC
LIMIT 5
""", tg_id)
balance = await get_user_balance(tg_id, session)
text = (
f"<b>💵 Изменение баланса</b>"
f"\n\n🆔 ID: <b>{tg_id}</b>"
f"\n💰 Баланс: <b>{balance}Р</b>"
f"\n📊 Последние операции (5):"
)
if records:
for record in records:
amount = record["amount"]
payment_system = record["payment_system"]
status = record["status"]
date = record["created_at"].strftime("%Y-%m-%d %H:%M:%S")
text += (
f"\n<blockquote>💸 Сумма: {amount} | {payment_system}"
f"\n📌 Статус: {status}"
f"\n⏳ Дата: {date}</blockquote>"
)
else:
text += "\n <i>🚫 Отсутствуют</i>"
await callback_query.message.edit_text(
text=text,
reply_markup=build_users_balance_kb(tg_id)
)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_balance_add"),
IsAdminFilter()
)
async def handle_balance_add(
callback_query: CallbackQuery,
callback_data: AdminUserEditorCallback,
state: FSMContext,
session: Any
):
tg_id = callback_data.tg_id
amount = callback_data.data
if amount:
await add_user_balance(tg_id, int(amount), session)
await handle_balance_change(callback_query, callback_data, session)
return
await state.update_data(tg_id=tg_id, op_type="add")
await state.set_state(UserEditorState.waiting_for_balance)
await callback_query.message.edit_text(
text="✍️ Введите сумму, которую хотите добавить на баланс пользователя:",
reply_markup=build_users_balance_change_kb(tg_id)
)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_balance_take"),
IsAdminFilter()
)
async def handle_balance_take(
callback_query: CallbackQuery,
callback_data: AdminUserEditorCallback,
state: FSMContext
):
tg_id = callback_data.tg_id
await state.update_data(tg_id=tg_id, op_type="take")
await state.set_state(UserEditorState.waiting_for_balance)
await callback_query.message.edit_text(
text="✍️ Введите сумму, которую хотите вычесть из баланса пользователя:",
reply_markup=build_users_balance_change_kb(tg_id)
)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_balance_set"),
IsAdminFilter()
)
async def handle_balance_set(
callback_query: CallbackQuery,
callback_data: AdminUserEditorCallback,
state: FSMContext
):
tg_id = callback_data.tg_id
await state.update_data(tg_id=tg_id, op_type="set")
await state.set_state(UserEditorState.waiting_for_balance)
await callback_query.message.edit_text(
text="✍️ Введите баланс, который хотите установить пользователю:",
reply_markup=build_users_balance_change_kb(tg_id)
)
@router.message(
UserEditorState.waiting_for_balance,
IsAdminFilter()
)
async def handle_balance_input(
message: types.Message,
state: FSMContext,
session: Any
):
data = await state.get_data()
tg_id = data.get("tg_id")
op_type = data.get("op_type")
if not message.text.isdigit() or int(message.text) < 0:
await message.answer(
text="🚫 Пожалуйста, введите корректную сумму!",
reply_markup=build_users_balance_change_kb(tg_id)
)
return
amount = int(message.text)
if op_type == "add":
text = f"✅ К балансу пользователя добавлено <b>{amount}Р</b>"
await add_user_balance(tg_id, amount, session)
elif op_type == "take":
text = f"✅ Из баланса пользователя было вычтено <b>{amount}Р</b>"
await add_user_balance(tg_id, -amount, session)
else:
text = f"✅ Баланс пользователя изменен на <b>{amount}Р</b>"
await set_user_balance(tg_id, amount, session)
await message.answer(
text=text,
reply_markup=build_users_balance_change_kb(tg_id)
)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_key_edit"),
IsAdminFilter()
)
async def handle_key_edit(
callback_query: CallbackQuery,
callback_data: CallbackData,
session: Any,
update: bool = False
):
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"<b>🔑 Информация о ключе</b>"
f"\n\n<code>{key_details['key']}</code>"
f"\n\n⏰ Дата истечения: <b>{key_details['expiry_date']} (UTC)</b>"
f"\n🌐 Кластер: <b>{key_details['cluster_name']}</b>"
f"\n🆔 ID клиента: <b>{key_details['tg_id']}</b>"
)
if not update or not callback_data.edit:
await callback_query.message.edit_text(
text=text,
reply_markup=build_key_edit_kb(key_details, email)
)
else:
await callback_query.message.edit_text(
text=text,
reply_markup=build_users_key_expiry_kb(callback_data.tg_id, email)
)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_expiry_edit"),
IsAdminFilter()
)
async def handle_change_expiry(
callback_query: CallbackQuery,
callback_data: AdminUserEditorCallback
):
tg_id = callback_data.tg_id
email = callback_data.data
await callback_query.message.edit_reply_markup(
reply_markup=build_users_key_expiry_kb(tg_id, email)
)
@router.callback_query(
AdminUserKeyEditorCallback.filter(F.action == "add"),
IsAdminFilter()
)
async def handle_expiry_add(
callback_query: CallbackQuery,
callback_data: AdminUserKeyEditorCallback,
state: FSMContext,
session: Any
):
tg_id = callback_data.tg_id
email = callback_data.data
month = callback_data.month
key_details = await get_key_details(email, session)
if not key_details:
await callback_query.message.edit_text(
text="🚫 Информация о ключе не найдена.",
reply_markup=build_editor_kb(tg_id),
)
return
if month:
await change_expiry_time(key_details["expiry_time"] + month * 30 * 24 * 3600 * 1000, email, session)
await handle_key_edit(callback_query, callback_data, session, True)
return
await state.update_data(tg_id=tg_id, email=email, op_type="add")
await state.set_state(UserEditorState.waiting_for_expiry_time)
await callback_query.message.edit_text(
text="✍️ Введите количество дней, которое хотите добавить к времени действия ключа:",
reply_markup=build_users_key_show_kb(tg_id, email)
)
@router.callback_query(
AdminUserKeyEditorCallback.filter(F.action == "take"),
IsAdminFilter()
)
async def handle_expiry_take(
callback_query: CallbackQuery,
callback_data: AdminUserKeyEditorCallback,
state: FSMContext
):
tg_id = callback_data.tg_id
email = callback_data.data
await state.update_data(tg_id=tg_id, email=email, op_type="take")
await state.set_state(UserEditorState.waiting_for_expiry_time)
await callback_query.message.edit_text(
text="✍️ Введите количество дней, которое хотите вычесть из времени действия ключа:",
reply_markup=build_users_key_show_kb(tg_id, email)
)
@router.callback_query(
AdminUserKeyEditorCallback.filter(F.action == "set"),
IsAdminFilter()
)
async def handle_expiry_set(
callback_query: CallbackQuery,
callback_data: AdminUserKeyEditorCallback,
state: FSMContext
):
tg_id = callback_data.tg_id
email = callback_data.data
await state.update_data(tg_id=tg_id, email=email, op_type="set")
await state.set_state(UserEditorState.waiting_for_expiry_time)
text = (
"✍️ Введите новое время действия ключа:"
"\n\n📌 Формат: <b>год-месяц-день час:минута</b>"
"\n Пример: 2025-02-09 09:01"
)
await callback_query.message.edit_text(
text=text,
reply_markup=build_users_key_show_kb(tg_id, email)
)
@router.message(
UserEditorState.waiting_for_expiry_time,
IsAdminFilter()
)
async def handle_expiry_time_input(
message: types.Message,
state: FSMContext,
session: Any
):
data = await state.get_data()
tg_id = data.get("tg_id")
email = data.get("email")
op_type = data.get("op_type")
if op_type != "set" and (not message.text.isdigit() or int(message.text) < 0):
await message.answer(
text="🚫 Пожалуйста, введите корректное количество дней!",
reply_markup=build_users_key_show_kb(tg_id, email)
)
return
key_details = await get_key_details(email, session)
if not key_details:
await message.answer(
text="🚫 Информация о ключе не найдена.",
reply_markup=build_editor_kb(tg_id),
)
return
if op_type == "add":
days = int(message.text)
text = f"✅ Ко времени действия ключа добавлено <b>{days} дн.</b>"
await change_expiry_time(key_details["expiry_time"] + days * 24 * 3600 * 1000, email, session)
elif op_type == "take":
days = int(message.text)
text = f"✅ Из времени действия ключа вычтено <b>{days} дн.</b>"
await change_expiry_time(key_details["expiry_time"] - days * 24 * 3600 * 1000, email, session)
else:
try:
expiry_time = int(
datetime.strptime(message.text, "%Y-%m-%d %H:%M").timestamp() * 1000
)
text = f"✅ Время действия ключа изменено на <b>{message.text}</b>"
await change_expiry_time(expiry_time, email, session)
except ValueError:
text = f"🚫 Пожалуйста, используйте корректный формат даты!"
except Exception as e:
text = f"❗ Произошла ошибка во время изменения времени действия ключа: {e}"
await message.answer(
text=text,
reply_markup=build_users_key_show_kb(tg_id, email)
)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_update_key"),
IsAdminFilter()
)
async def handle_update_key(
callback_query: CallbackQuery,
callback_data: AdminUserEditorCallback,
session: Any
):
tg_id = callback_data.tg_id
email = callback_data.data
try:
await update_subscription(tg_id, email, session)
await handle_key_edit(callback_query, callback_data, session)
except TelegramBadRequest:
pass
except Exception as e:
logger.error(f"Ошибка при обновлении ключа {email} администратором: {e}")
await callback_query.message.answer(
text=f"❗ Произошла ошибка при обновлении ключа: {e}",
reply_markup=build_user_key_kb(tg_id, email)
)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_delete_key"),
IsAdminFilter()
)
async def handle_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, email)
)
@router.callback_query(
AdminUserEditorCallback.filter(F.action == "users_delete_key_confirm"),
IsAdminFilter()
)
async def handle_delete_key_confirm(
callback_query: types.CallbackQuery,
callback_data: AdminUserEditorCallback,
session: Any
):
email = callback_data.data
record = await session.fetchrow(
"SELECT client_id FROM keys WHERE email = $1", email
)
kb = build_editor_kb(callback_data.tg_id)
if record:
client_id = record["client_id"]
clusters = await get_servers_from_db()
async def delete_key_from_servers():
tasks = []
for cluster_name, cluster_servers in clusters.items():
for _ in cluster_servers:
tasks.append(
delete_key_from_cluster(cluster_name, email, client_id)
)
await asyncio.gather(*tasks)
await delete_key_from_servers()
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 handle_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 handle_delete_user_confirm(
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_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.edit
)
async def process_user_search(
message: types.Message,
state: FSMContext,
session: Any,
tg_id: int,
edit: bool = False
) -> None:
await state.clear()
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
username = await session.fetchval(
"SELECT username FROM users WHERE tg_id = $1", tg_id
)
key_records = await session.fetch(
"SELECT email, expiry_time 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"<b>📊 Информация о пользователе</b>"
f"\n\n🆔 ID: <b>{tg_id}</b>"
f"\n📄 Логин: <b>@{username}</b>"
f"\n💰 Баланс: <b>{balance}</b>"
f"\n👥 Количество рефералов: <b>{referral_count}</b>"
)
kb = build_user_edit_kb(tg_id, key_records)
if edit:
try:
await message.edit_text(
text=text,
reply_markup=kb
)
except TelegramBadRequest:
pass
else:
await message.answer(
text=text,
reply_markup=kb
)
async def get_key_details(email, session):
record = await session.fetchrow(
"""
SELECT k.client_id, 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
cluster_name = record["server_id"]
expiry_date = datetime.fromtimestamp(record["expiry_time"] / 1000, tz=timezone.utc)
return {
"client_id": record["client_id"],
"balance": record["balance"],
"tg_id": record["tg_id"],
"key": record["key"],
"cluster_name": cluster_name,
"expiry_time": record["expiry_time"],
"expiry_date": expiry_date.strftime("%d %B %Y года %H:%M"),
}
async def change_expiry_time(expiry_time: int, email: str, session: Any) -> Exception | None:
client_id = await get_client_id_by_email(email)
if client_id is None:
return ValueError(f"User with email {email} was not found")
server_id = await session.fetchrow(
"SELECT server_id FROM keys WHERE client_id = $1", client_id
)
if not server_id:
return ValueError(f"User with client_id {server_id} was not found")
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)
async def get_user_balance(tg_id: int, session: Any) -> float:
try:
return await session.fetchval(
"SELECT balance FROM connections WHERE tg_id = $1", tg_id,
)
except Exception as e:
logger.error(f"Ошибка при получении баланса для пользователя {tg_id}: {e}")
return -1
async def add_user_balance(tg_id: int, balance: int, session: Any) -> None:
try:
await session.execute(
"UPDATE connections SET balance = balance + $1 WHERE tg_id = $2",
balance, tg_id,
)
except Exception as e:
logger.error(f"Ошибка при добавлении баланса для пользователя {tg_id}: {e}")
async def set_user_balance(tg_id: int, balance: int, session: Any) -> None:
try:
await session.execute(
"UPDATE connections SET balance = $1 WHERE tg_id = $2",
balance, tg_id,
)
except Exception as e:
logger.error(f"Ошибка при установке баланса для пользователя {tg_id}: {e}")
+55 -2
View File
@@ -1,10 +1,12 @@
import asyncio
from typing import Any
from py3xui import AsyncApi
from client import ClientConfig, add_client, delete_client, extend_client_key
from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, SUPERNODE, TOTAL_GB
from database import get_servers
from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, SUPERNODE, TOTAL_GB, PUBLIC_LINK
from database import get_servers, store_key
from handlers.utils import get_least_loaded_cluster
from logger import logger
@@ -103,6 +105,7 @@ async def create_client_on_server(
await asyncio.sleep(0.7)
async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, total_gb):
try:
servers = await get_servers()
@@ -232,3 +235,53 @@ async def update_key_on_cluster(tg_id, client_id, email, expiry_time, cluster_id
except Exception as e:
logger.error(f"Ошибка при обновлении ключа на серверах кластера {cluster_id} для {client_id}: {e}")
raise e
async def update_subscription(tg_id: int, email: str, session: Any) -> None:
record = await session.fetchrow(
"""
SELECT k.key, k.expiry_time, k.email, k.server_id, k.client_id
FROM keys k
WHERE k.tg_id = $1 AND k.email = $2
""",
tg_id,
email,
)
if not record:
raise ValueError(f"The key {email} does not exist in database")
expiry_time = record["expiry_time"]
client_id = record["client_id"]
public_link = f"{PUBLIC_LINK}{email}/{tg_id}"
await session.execute(
"""
DELETE FROM keys
WHERE tg_id = $1 AND email = $2
""",
tg_id,
email,
)
least_loaded_cluster_id = await get_least_loaded_cluster()
await asyncio.gather(
update_key_on_cluster(
tg_id,
client_id,
email,
expiry_time,
least_loaded_cluster_id,
)
)
await store_key(
tg_id,
client_id,
email,
expiry_time,
public_link,
server_id=least_loaded_cluster_id,
session=session,
)
+15 -42
View File
@@ -10,6 +10,7 @@ import pytz
from aiogram import F, Router, types
from aiogram.types import BufferedInputFile, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from handlers.payments.yookassa_pay import process_custom_amount_input
from bot import bot
from config import (
@@ -51,10 +52,9 @@ from handlers.keys.key_utils import (
delete_key_from_cluster,
delete_key_from_db,
renew_key_in_cluster,
update_key_on_cluster,
update_subscription,
)
from handlers.payments.robokassa_pay import handle_custom_amount_input
from handlers.payments.yookassa_pay import process_custom_amount_input
from handlers.texts import (
DISCOUNTS,
KEY_NOT_FOUND_MSG,
@@ -62,7 +62,7 @@ from handlers.texts import (
SUCCESS_RENEWAL_MSG,
key_message,
)
from handlers.utils import get_least_loaded_cluster, handle_error
from handlers.utils import handle_error
from logger import logger
locale.setlocale(locale.LC_TIME, "ru_RU.UTF-8")
@@ -73,7 +73,7 @@ router = Router()
@router.callback_query(F.data == "view_keys")
@router.message(F.text == "/subs")
async def process_callback_or_message_view_keys(
callback_query_or_message: types.Message | types.CallbackQuery, session: Any
callback_query_or_message: types.Message | types.CallbackQuery, session: Any
):
if isinstance(callback_query_or_message, types.CallbackQuery):
chat_id = callback_query_or_message.message.chat.id
@@ -252,43 +252,15 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session
async def process_callback_update_subscription(callback_query: types.CallbackQuery, session: Any):
tg_id = callback_query.message.chat.id
email = callback_query.data.split("|")[1]
try:
record = await get_key_details(email, session)
if record:
expiry_time = record["expiry_time"]
client_id = record["client_id"]
public_link = f"{PUBLIC_LINK}{email}/{tg_id}"
await delete_key(client_id, session)
least_loaded_cluster_id = await get_least_loaded_cluster()
await asyncio.gather(
update_key_on_cluster(
tg_id,
client_id,
email,
expiry_time,
least_loaded_cluster_id,
)
)
await store_key(
tg_id,
client_id,
email,
expiry_time,
public_link,
server_id=least_loaded_cluster_id,
session=session,
)
await process_callback_view_key(callback_query, session)
else:
await callback_query.message.answer("<b>Ключ не найден в базе данных.</b>")
await update_subscription(tg_id, email, session)
await process_callback_view_key(callback_query, session)
except Exception as e:
await handle_error(tg_id, callback_query, f"Ошибка при обновлении подписки: {e}")
logger.error(f"Ошибка при обновлении ключа {email} пользователем: {e}")
await handle_error(
tg_id, callback_query, f"Ошибка при обновлении подписки: {e}"
)
@router.callback_query(F.data.startswith("delete_key|"))
@@ -363,7 +335,9 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery, sessio
@router.callback_query(F.data.startswith("confirm_delete|"))
async def process_callback_confirm_delete(callback_query: types.CallbackQuery, session: Any):
async def process_callback_confirm_delete(
callback_query: types.CallbackQuery, session: Any
):
email = callback_query.data.split("|")[1]
try:
record = await get_key_details(email, session)
@@ -439,8 +413,7 @@ async def process_callback_renew_plan(callback_query: types.CallbackQuery, sessi
required_amount = cost - balance
logger.info(
f"[RENEW] Пользователю {tg_id} не хватает {required_amount}₽. Запуск доплаты через {USE_NEW_PAYMENT_FLOW}"
)
f"[RENEW] Пользователю {tg_id} не хватает {required_amount}₽. Запуск доплаты через {USE_NEW_PAYMENT_FLOW}")
await create_temporary_data(
session,
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()
+113
View File
@@ -0,0 +1,113 @@
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 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()
+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()
+317
View File
@@ -0,0 +1,317 @@
from datetime import datetime, timezone
from aiogram.filters.callback_data import CallbackData
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import RENEWAL_PRICES
from keyboards.admin.panel_kb import build_admin_back_btn
class AdminUserEditorCallback(CallbackData, prefix="admin_users"):
action: str
tg_id: int
data: str | int | None = None
edit: bool = False
class AdminUserKeyEditorCallback(CallbackData, prefix="admin_users_key"):
action: str
tg_id: int
data: str
month: int | None = None
def build_user_edit_kb(tg_id: int, key_records: list) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
current_time = datetime.now(tz=timezone.utc)
for record in key_records:
email = record["email"]
expiry = datetime.fromtimestamp(record["expiry_time"] / 1000, tz=timezone.utc)
days = (expiry - current_time).days
builder.button(
text=f"🔑 {email} ({'<1' if days < 1 else days} дн.)",
callback_data=AdminUserEditorCallback(
action="users_key_edit",
tg_id=tg_id,
data=str(email)
).pack()
)
builder.button(
text="✉️ Сообщение",
callback_data=AdminUserEditorCallback(
action="users_send_message",
tg_id=tg_id
).pack()
)
builder.button(
text="💸 Изменить баланс",
callback_data=AdminUserEditorCallback(
action="users_balance_edit",
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_users_balance_change_kb(tg_id: int) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text="🔙 Назад", # todo: fix magic text was set
callback_data=AdminUserEditorCallback(
action="users_balance_edit",
tg_id=tg_id
).pack()
)
return builder.as_markup()
def build_users_balance_kb(tg_id: int) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
for month, amount in RENEWAL_PRICES.items():
builder.button(
text=f"+ {amount}Р ({month} мес.)",
callback_data=AdminUserEditorCallback(
action="users_balance_add",
tg_id=tg_id,
data=amount
).pack()
)
builder.button(
text=f"- {amount}Р ({month} мес.)",
callback_data=AdminUserEditorCallback(
action="users_balance_add",
tg_id=tg_id,
data=-amount
).pack()
)
builder.button(
text="💵 Добавить",
callback_data=AdminUserEditorCallback(
action="users_balance_add",
tg_id=tg_id
).pack()
)
builder.button(
text="💵 Вычесть",
callback_data=AdminUserEditorCallback(
action="users_balance_take",
tg_id=tg_id
).pack()
)
builder.button(
text="💵 Установить баланс",
callback_data=AdminUserEditorCallback(
action="users_balance_set",
tg_id=tg_id
).pack()
)
builder.row(
build_editor_back_btn(tg_id, True)
)
builder.adjust(2, 2, 2, 2, 2, 1)
return builder.as_markup()
def build_users_key_show_kb(tg_id: int, email: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text="🔙 Назад", # todo: fix magic text was set
callback_data=AdminUserEditorCallback(
action="users_key_edit",
tg_id=tg_id,
data=email,
edit=True
).pack()
)
return builder.as_markup()
def build_users_key_expiry_kb(tg_id: int, email: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
for month in RENEWAL_PRICES.keys():
month = int(month)
builder.button(
text=f"+ {month} мес.",
callback_data=AdminUserKeyEditorCallback(
action="add",
tg_id=tg_id,
data=email,
month=month
).pack()
)
builder.button(
text=f"- {month} мес.",
callback_data=AdminUserKeyEditorCallback(
action="add",
tg_id=tg_id,
data=email,
month=-month
).pack()
)
builder.button(
text="⏳ Добавить дни",
callback_data=AdminUserKeyEditorCallback(
action="add",
tg_id=tg_id,
data=email
).pack()
)
builder.button(
text="⏳ Вычесть дни",
callback_data=AdminUserKeyEditorCallback(
action="take",
tg_id=tg_id,
data=email
).pack()
)
builder.button(
text="⏳ Установить дату истечения",
callback_data=AdminUserKeyEditorCallback(
action="set",
tg_id=tg_id,
data=email
).pack()
)
builder.button(
text="🔙 Назад", # todo: fix magic text was set
callback_data=AdminUserEditorCallback(
action="users_key_edit",
tg_id=tg_id,
data=email
).pack()
)
builder.adjust(2, 2, 2, 2, 2, 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_user_key_kb(tg_id: int, email: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text=f"🔙 Назад",
callback_data=AdminUserEditorCallback(
action="users_key_edit",
tg_id=tg_id,
data=email
).pack()
)
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_expiry_edit",
data=email,
tg_id=key_details["tg_id"]
).pack()
)
builder.button(
text="🔄 Перевыпустить",
callback_data=AdminUserEditorCallback(
action="users_update_key",
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, email: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="✅ Да, удалить",
callback_data=AdminUserEditorCallback(
action="users_delete_key_confirm",
data=email,
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",
tg_id=tg_id,
edit=edit
).pack()
)
+44 -14
View File
@@ -4,23 +4,53 @@ 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, AdminUserKeyEditorCallback
pass_callbacks = [
AdminPanelCallback,
AdminCouponDeleteCallback,
AdminSenderCallback,
AdminServerEditorCallback,
AdminUserEditorCallback,
AdminUserKeyEditorCallback,
]
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
View File
+101
View File
@@ -0,0 +1,101 @@
from io import StringIO
from typing import Any
from aiogram.types import BufferedInputFile
async def export_users_csv(session: Any) -> BufferedInputFile:
# Получение данных о платежах пользователя
query = """
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
"""
users = await session.fetch(query)
buffer = StringIO()
buffer.write("tg_id,username,first_name,last_name,language_code,is_bot,balance,trial\n")
# Запись данных
for user in users:
buffer.write(
f"{user['tg_id']},{user['username']},{user['first_name']},{user['last_name']},"
f"{user['language_code']},{user['is_bot']},{user['balance']},{user['trial']}\n"
)
# Перемещение указателя в начало для чтения
buffer.seek(0)
return BufferedInputFile(
file=buffer.getvalue().encode("utf-8-sig"),
filename="users_export.csv"
)
async def export_payments_csv(session: Any) -> BufferedInputFile:
# Получение данных о всех платежах
query = """
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
"""
payments = await session.fetch(query)
return _export_payments_csv(payments, "payments_export.csv")
async def export_user_payments_csv(tg_id: int, session: Any) -> BufferedInputFile:
# Получение данных о платежах пользователя
query = """
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
WHERE u.tg_id = $1
"""
payments = await session.fetch(query, tg_id)
return _export_payments_csv(payments, f"payments_export_{tg_id}.csv")
def _export_payments_csv(payments: list, filename: str) -> BufferedInputFile:
# Формирование CSV данных через StringIO
buffer = StringIO()
buffer.write("tg_id,username,first_name,last_name,amount,payment_system,status,created_at\n")
# Запись данных
for payment in payments:
buffer.write(
f"{payment['tg_id']},{payment['username']},{payment['first_name']},{payment['last_name']},"
f"{payment['amount']},{payment['payment_system']},{payment['status']},{payment['created_at']}\n"
)
# Перемещение указателя в начало для чтения
buffer.seek(0)
return BufferedInputFile(
file=buffer.getvalue().encode("utf-8-sig"),
filename=filename
)