diff --git a/handlers/admin/clusters/clusters_handler.py b/handlers/admin/clusters/clusters_handler.py
index 1c153731..a169d8b2 100644
--- a/handlers/admin/clusters/clusters_handler.py
+++ b/handlers/admin/clusters/clusters_handler.py
@@ -12,7 +12,7 @@ from aiogram.types import CallbackQuery, Message
from py3xui import AsyncApi
from backup import create_backup_and_send_to_admins
-from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, TOTAL_GB
+from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, TOTAL_GB, USE_COUNTRY_SELECTION
from database import check_unique_server_name, get_servers, update_key_expiry
from filters.admin import IsAdminFilter
from handlers.keys.key_utils import create_client_on_server, create_key_on_cluster, renew_key_in_cluster
@@ -39,6 +39,10 @@ class AdminClusterStates(StatesGroup):
waiting_for_server_name = State()
waiting_for_subscription_url = State()
waiting_for_days_input = State()
+ waiting_for_new_cluster_name = State()
+ waiting_for_new_server_name = State()
+ waiting_for_server_transfer = State()
+ waiting_for_cluster_transfer = State()
@router.callback_query(
@@ -538,3 +542,289 @@ async def handle_days_input(message: Message, state: FSMContext, session: Any):
await message.answer("❌ Произошла ошибка при продлении времени.")
finally:
await state.clear()
+
+
+@router.callback_query(AdminClusterCallback.filter(F.action == "rename"), IsAdminFilter())
+async def handle_rename_cluster(callback_query: CallbackQuery, callback_data: AdminClusterCallback, state: FSMContext):
+ cluster_name = callback_data.data
+ await state.update_data(old_cluster_name=cluster_name)
+
+ text = (
+ f"✏️ Введите новое имя для кластера '{cluster_name}':\n\n"
+ "▸ Имя должно быть уникальным.\n"
+ "▸ Имя не должно превышать 12 символов.\n\n"
+ "📌 Пример: new_cluster"
+ )
+
+ await callback_query.message.edit_text(
+ text=text,
+ reply_markup=build_admin_back_kb("clusters"),
+ )
+ await state.set_state(AdminClusterStates.waiting_for_new_cluster_name)
+
+
+@router.message(AdminClusterStates.waiting_for_new_cluster_name, IsAdminFilter())
+async def handle_new_cluster_name_input(message: Message, state: FSMContext, session: Any):
+ if not message.text:
+ await message.answer(
+ text="❌ Имя кластера не может быть пустым! Попробуйте снова.",
+ reply_markup=build_admin_back_kb("clusters"),
+ )
+ return
+
+ new_cluster_name = message.text.strip()
+ if len(new_cluster_name) > 12:
+ await message.answer(
+ text="❌ Имя кластера не должно превышать 12 символов! Попробуйте снова.",
+ reply_markup=build_admin_back_kb("clusters"),
+ )
+ return
+
+ user_data = await state.get_data()
+ old_cluster_name = user_data.get("old_cluster_name")
+
+ conn = await asyncpg.connect(DATABASE_URL)
+ try:
+ existing_cluster = await conn.fetchval(
+ "SELECT cluster_name FROM servers WHERE cluster_name = $1 LIMIT 1",
+ new_cluster_name
+ )
+ if existing_cluster:
+ await message.answer(
+ text=f"❌ Кластер с именем '{new_cluster_name}' уже существует. Введите другое имя.",
+ reply_markup=build_admin_back_kb("clusters"),
+ )
+ return
+
+ keys_count = await conn.fetchval(
+ "SELECT COUNT(*) FROM keys WHERE server_id = $1",
+ old_cluster_name
+ )
+
+ async with conn.transaction():
+ await conn.execute(
+ "UPDATE servers SET cluster_name = $1 WHERE cluster_name = $2",
+ new_cluster_name,
+ old_cluster_name
+ )
+
+ if keys_count > 0:
+ await conn.execute(
+ "UPDATE keys SET server_id = $1 WHERE server_id = $2",
+ new_cluster_name,
+ old_cluster_name
+ )
+
+ await message.answer(
+ text=f"✅ Название кластера успешно изменено с '{old_cluster_name}' на '{new_cluster_name}'!\n\n⚠️ Не забудьте сделать \"Синхронизацию\".",
+ reply_markup=build_admin_back_kb("clusters"),
+ )
+ except Exception as e:
+ logger.error(f"Ошибка при смене имени кластера {old_cluster_name} на {new_cluster_name}: {e}")
+ await message.answer(
+ text=f"❌ Произошла ошибка при смене имени кластера: {e}",
+ reply_markup=build_admin_back_kb("clusters"),
+ )
+ finally:
+ await conn.close()
+ await state.clear()
+
+
+@router.callback_query(AdminServerCallback.filter(F.action == "rename"), IsAdminFilter())
+async def handle_rename_server(callback_query: CallbackQuery, callback_data: AdminServerCallback, state: FSMContext):
+ old_server_name = callback_data.data
+
+ servers = await get_servers()
+ cluster_name = None
+ for c_name, server_list in servers.items():
+ for server in server_list:
+ if server["server_name"] == old_server_name:
+ cluster_name = c_name
+ break
+ if cluster_name:
+ break
+
+ if not cluster_name:
+ await callback_query.message.edit_text(
+ text=f"❌ Не удалось найти кластер для сервера '{old_server_name}'.",
+ reply_markup=build_admin_back_kb("clusters"),
+ )
+ return
+
+ await state.update_data(old_server_name=old_server_name, cluster_name=cluster_name)
+
+ text = (
+ f"✏️ Введите новое имя для сервера '{old_server_name}' в кластере '{cluster_name}':\n\n"
+ "▸ Имя должно быть уникальным в пределах кластера.\n"
+ "▸ Имя не должно превышать 12 символов.\n\n"
+ "📌 Пример: new_server"
+ )
+
+ await callback_query.message.edit_text(
+ text=text,
+ reply_markup=build_admin_back_kb("clusters"),
+ )
+ await state.set_state(AdminClusterStates.waiting_for_new_server_name)
+
+
+@router.message(AdminClusterStates.waiting_for_new_server_name, IsAdminFilter())
+async def handle_new_server_name_input(message: Message, state: FSMContext, session: Any):
+ if not message.text:
+ await message.answer(
+ text="❌ Имя сервера не может быть пустым! Попробуйте снова.",
+ reply_markup=build_admin_back_kb("clusters"),
+ )
+ return
+
+ new_server_name = message.text.strip()
+ if len(new_server_name) > 12:
+ await message.answer(
+ text="❌ Имя сервера не должно превышать 12 символов! Попробуйте снова.",
+ reply_markup=build_admin_back_kb("clusters"),
+ )
+ return
+
+ user_data = await state.get_data()
+ old_server_name = user_data.get("old_server_name")
+ cluster_name = user_data.get("cluster_name")
+
+ conn = await asyncpg.connect(DATABASE_URL)
+ try:
+ existing_server = await conn.fetchval(
+ "SELECT server_name FROM servers WHERE cluster_name = $1 AND server_name = $2 LIMIT 1",
+ cluster_name,
+ new_server_name
+ )
+ if existing_server:
+ await message.answer(
+ text=f"❌ Сервер с именем '{new_server_name}' уже существует в кластере '{cluster_name}'. Введите другое имя.",
+ reply_markup=build_admin_back_kb("clusters"),
+ )
+ return
+
+ keys_count = await conn.fetchval(
+ "SELECT COUNT(*) FROM keys WHERE server_id = $1",
+ old_server_name
+ )
+
+ async with conn.transaction():
+ await conn.execute(
+ "UPDATE servers SET server_name = $1 WHERE cluster_name = $2 AND server_name = $3",
+ new_server_name,
+ cluster_name,
+ old_server_name
+ )
+
+ if keys_count > 0:
+ await conn.execute(
+ "UPDATE keys SET server_id = $1 WHERE server_id = $2",
+ new_server_name,
+ old_server_name
+ )
+
+ # Формируем текст сообщения с учетом USE_COUNTRY_SELECTION
+ base_text = f"✅ Название сервера успешно изменено с '{old_server_name}' на '{new_server_name}' в кластере '{cluster_name}'!"
+ sync_reminder = "\n\n⚠️ Не забудьте сделать \"Синхронизацию\"."
+ final_text = base_text + (sync_reminder if USE_COUNTRY_SELECTION else "")
+
+ await message.answer(
+ text=final_text,
+ reply_markup=build_admin_back_kb("clusters"),
+ )
+ except Exception as e:
+ logger.error(f"Ошибка при смене имени сервера {old_server_name} на {new_server_name}: {e}")
+ await message.answer(
+ text=f"❌ Произошла ошибка при смене имени сервера: {e}",
+ reply_markup=build_admin_back_kb("clusters"),
+ )
+ finally:
+ await conn.close()
+ await state.clear()
+
+
+@router.callback_query(F.data.startswith("transfer_to_server|"))
+async def handle_server_transfer(callback_query: CallbackQuery, state: FSMContext):
+ data = callback_query.data.split("|")
+ new_server_name = data[1]
+ old_server_name = data[2]
+
+ user_data = await state.get_data()
+ cluster_name = user_data.get("cluster_name")
+
+ conn = await asyncpg.connect(DATABASE_URL)
+ try:
+ async with conn.transaction():
+ await conn.execute(
+ "UPDATE keys SET server_id = $1 WHERE server_id = $2",
+ new_server_name,
+ old_server_name
+ )
+
+ await conn.execute(
+ "DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2",
+ cluster_name,
+ old_server_name
+ )
+
+ base_text = f"✅ Ключи успешно перенесены на сервер '{new_server_name}', сервер '{old_server_name}' удален!"
+ sync_reminder = "\n\n⚠️ Не забудьте сделать \"Синхронизацию\"."
+ final_text = base_text + (sync_reminder if USE_COUNTRY_SELECTION else "")
+
+ await callback_query.message.edit_text(
+ text=final_text,
+ reply_markup=build_admin_back_kb("clusters"),
+ )
+ except Exception as e:
+ logger.error(f"Ошибка при переносе ключей на сервер {new_server_name}: {e}")
+ await callback_query.message.edit_text(
+ text=f"❌ Произошла ошибка при переносе ключей: {e}",
+ reply_markup=build_admin_back_kb("clusters"),
+ )
+ finally:
+ await conn.close()
+ await state.clear()
+
+
+@router.callback_query(F.data.startswith("transfer_to_cluster|"))
+async def handle_cluster_transfer(callback_query: CallbackQuery, state: FSMContext):
+ data = callback_query.data.split("|")
+ new_cluster_name = data[1]
+ old_cluster_name = data[2]
+ old_server_name = data[3]
+
+ user_data = await state.get_data()
+ cluster_name = user_data.get("cluster_name")
+
+ conn = await asyncpg.connect(DATABASE_URL)
+ try:
+ async with conn.transaction():
+ await conn.execute(
+ "UPDATE keys SET server_id = $1 WHERE server_id = $2",
+ new_cluster_name,
+ old_server_name
+ )
+ await conn.execute(
+ "UPDATE keys SET server_id = $1 WHERE server_id = $2",
+ new_cluster_name,
+ old_cluster_name
+ )
+
+ await conn.execute(
+ "DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2",
+ cluster_name,
+ old_server_name
+ )
+
+ await callback_query.message.edit_text(
+ text=f"✅ Ключи успешно перенесены в кластер '{new_cluster_name}', сервер '{old_server_name}' и кластер '{old_cluster_name}' удалены!\n\n⚠️ Не забудьте сделать \"Синхронизацию\".",
+ reply_markup=build_admin_back_kb("clusters"),
+ )
+ except Exception as e:
+ logger.error(f"Ошибка при переносе ключей в кластер {new_cluster_name}: {e}")
+ await callback_query.message.edit_text(
+ text=f"❌ Произошла ошибка при переносе ключей: {e}",
+ reply_markup=build_admin_back_kb("clusters"),
+ )
+ finally:
+ await conn.close()
+ await state.clear()
diff --git a/handlers/admin/clusters/keyboard.py b/handlers/admin/clusters/keyboard.py
index a2397d52..e267fa8b 100644
--- a/handlers/admin/clusters/keyboard.py
+++ b/handlers/admin/clusters/keyboard.py
@@ -93,6 +93,12 @@ def build_cluster_management_kb(cluster_name: str) -> InlineKeyboardMarkup:
callback_data=AdminClusterCallback(action="add_time", data=cluster_name).pack(),
)
)
+ builder.row(
+ InlineKeyboardButton(
+ text="✏️ Сменить название",
+ callback_data=AdminClusterCallback(action="rename", data=cluster_name).pack(),
+ )
+ )
builder.row(
InlineKeyboardButton(
text="🔙 Назад",
diff --git a/handlers/admin/servers/keyboard.py b/handlers/admin/servers/keyboard.py
index d0a7cbea..e74036ce 100644
--- a/handlers/admin/servers/keyboard.py
+++ b/handlers/admin/servers/keyboard.py
@@ -15,14 +15,7 @@ def build_manage_server_kb(server_name: str, cluster_name: str) -> InlineKeyboar
builder = InlineKeyboardBuilder()
builder.button(text="🗑️ Удалить", callback_data=AdminServerCallback(action="delete", data=server_name).pack())
+ builder.button(text="✏️ Сменить название", callback_data=AdminServerCallback(action="rename", data=server_name).pack())
builder.button(text=BACK, callback_data=AdminClusterCallback(action="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=AdminServerCallback(action="delete_confirm", data=server_name).pack())
- builder.button(text=BACK, callback_data=AdminServerCallback(action="manage", data=server_name).pack())
- builder.adjust(1)
- return builder.as_markup()
diff --git a/handlers/admin/servers/servers_handler.py b/handlers/admin/servers/servers_handler.py
index 849b5c92..db23c632 100644
--- a/handlers/admin/servers/servers_handler.py
+++ b/handlers/admin/servers/servers_handler.py
@@ -1,7 +1,10 @@
from typing import Any
from aiogram import F, Router, types
-from aiogram.types import CallbackQuery
+from aiogram.fsm.context import FSMContext
+from aiogram.types import CallbackQuery, InlineKeyboardButton
+from aiogram.utils.keyboard import InlineKeyboardBuilder
+from handlers.buttons import BACK
from database import delete_server, get_servers
from filters.admin import IsAdminFilter
@@ -9,7 +12,6 @@ from filters.admin import IsAdminFilter
from ..panel.keyboard import build_admin_back_kb
from .keyboard import (
AdminServerCallback,
- build_delete_server_kb,
build_manage_server_kb,
)
@@ -47,23 +49,139 @@ async def handle_server_manage(callback_query: CallbackQuery, callback_data: Adm
@router.callback_query(AdminServerCallback.filter(F.action == "delete"), IsAdminFilter())
-async def handle_server_delete(callback_query: CallbackQuery, callback_data: AdminServerCallback):
- server_name = callback_data.data
-
- await callback_query.message.edit_text(
- text=f"🗑️ Вы уверены, что хотите удалить сервер {server_name}?",
- reply_markup=build_delete_server_kb(server_name),
- )
-
-
-@router.callback_query(AdminServerCallback.filter(F.action == "delete_confirm"), IsAdminFilter())
-async def handle_server_delete_confirm(
- callback_query: types.CallbackQuery, callback_data: AdminServerCallback, session: Any
+async def process_callback_delete_server(
+ callback_query: CallbackQuery, callback_data: AdminServerCallback, state: FSMContext, session: Any
):
+ from ..clusters.clusters_handler import AdminClusterStates
+
server_name = callback_data.data
- await delete_server(server_name, session)
+ servers = await get_servers(session)
+ cluster_name = None
+ for c_name, server_list in servers.items():
+ for server in server_list:
+ if server["server_name"] == server_name:
+ cluster_name = c_name
+ break
+ if cluster_name:
+ break
- await callback_query.message.edit_text(
- text=f"🗑️ Сервер {server_name} успешно удален.", reply_markup=build_admin_back_kb("clusters")
+ if not cluster_name:
+ await callback_query.message.edit_text(
+ text=f"❌ Не удалось найти кластер для сервера '{server_name}'.",
+ reply_markup=build_admin_back_kb("clusters"),
+ )
+ return
+
+ keys_count = await session.fetchval(
+ "SELECT COUNT(*) FROM keys WHERE server_id = $1",
+ server_name
)
+
+ if keys_count > 0:
+ await state.update_data(server_name=server_name, cluster_name=cluster_name)
+
+ all_servers = await session.fetch(
+ """
+ SELECT server_name, (SELECT COUNT(*) FROM keys WHERE server_id = servers.server_name) as key_count
+ FROM servers
+ WHERE server_name != $1
+ """,
+ server_name
+ )
+
+ if all_servers:
+ builder = InlineKeyboardBuilder()
+ for server in all_servers:
+ builder.row(
+ InlineKeyboardButton(
+ text=f"{server['server_name']} ({server['key_count']})",
+ callback_data=f"transfer_to_server|{server['server_name']}|{server_name}"
+ )
+ )
+ builder.row(
+ InlineKeyboardButton(
+ text=BACK,
+ callback_data=AdminServerCallback(action="manage", data=server_name).pack()
+ )
+ )
+
+ await callback_query.message.edit_text(
+ text=f"⚠️ На сервере '{server_name}' есть {keys_count} ключей. Выберите сервер для переноса ключей:",
+ reply_markup=builder.as_markup(),
+ )
+ await state.set_state(AdminClusterStates.waiting_for_server_transfer)
+ return
+
+ remaining_servers = await session.fetchval(
+ "SELECT COUNT(*) FROM servers WHERE cluster_name = $1 AND server_name != $2",
+ cluster_name,
+ server_name
+ )
+
+ if remaining_servers == 0:
+ other_clusters = await session.fetch(
+ "SELECT DISTINCT cluster_name FROM servers WHERE cluster_name != $1",
+ cluster_name
+ )
+
+ if other_clusters:
+ cluster_keys_count = await session.fetchval(
+ "SELECT COUNT(*) FROM keys WHERE server_id = $1",
+ cluster_name
+ )
+
+ if cluster_keys_count > 0:
+ await state.update_data(server_name=server_name, cluster_name=cluster_name)
+
+ all_clusters = await session.fetch(
+ """
+ SELECT cluster_name, (SELECT COUNT(*) FROM keys WHERE server_id = servers.cluster_name) as key_count
+ FROM servers
+ WHERE cluster_name != $1
+ GROUP BY cluster_name
+ """,
+ cluster_name
+ )
+
+ builder = InlineKeyboardBuilder()
+ for cluster in all_clusters:
+ builder.row(
+ InlineKeyboardButton(
+ text=f"{cluster['cluster_name']} ({cluster['key_count']})",
+ callback_data=f"transfer_to_cluster|{cluster['cluster_name']}|{cluster_name}|{server_name}"
+ )
+ )
+ builder.row(
+ InlineKeyboardButton(
+ text=BACK,
+ callback_data=AdminServerCallback(action="manage", data=server_name).pack()
+ )
+ )
+
+ await callback_query.message.edit_text(
+ text=f"⚠️ Это последний сервер в кластере '{cluster_name}'. На кластере есть {cluster_keys_count} ключей. Выберите кластер для переноса ключей:",
+ reply_markup=builder.as_markup(),
+ )
+ await state.set_state(AdminClusterStates.waiting_for_cluster_transfer)
+ return
+
+ await session.execute(
+ "DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2",
+ cluster_name,
+ server_name
+ )
+ await callback_query.message.edit_text(
+ text=f"✅ Сервер '{server_name}' удален. Кластер '{cluster_name}' также удален, так как в нем не осталось серверов.",
+ reply_markup=build_admin_back_kb("clusters"),
+ )
+ else:
+ await session.execute(
+ "DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2",
+ cluster_name,
+ server_name
+ )
+ await callback_query.message.edit_text(
+ text=f"✅ Сервер '{server_name}' удален.",
+ reply_markup=build_admin_back_kb("clusters"),
+ )
diff --git a/handlers/admin/stats/stats_handler.py b/handlers/admin/stats/stats_handler.py
index b2efe1df..8b7355ec 100644
--- a/handlers/admin/stats/stats_handler.py
+++ b/handlers/admin/stats/stats_handler.py
@@ -41,6 +41,16 @@ async def handle_stats(callback_query: CallbackQuery, session: Any):
"SELECT COALESCE(SUM(amount), 0) FROM payments WHERE created_at >= date_trunc('month', CURRENT_DATE)"
)
)
+ total_payments_last_month = int(
+ await session.fetchval(
+ """
+ SELECT COALESCE(SUM(amount), 0)
+ FROM payments
+ WHERE created_at >= date_trunc('month', CURRENT_DATE - interval '1 month')
+ AND created_at < date_trunc('month', CURRENT_DATE)
+ """
+ )
+ )
total_payments_all_time = int(await session.fetchval("SELECT COALESCE(SUM(amount), 0) FROM payments"))
all_keys = await session.fetch("SELECT created_at, expiry_time FROM keys")
@@ -120,6 +130,7 @@ async def handle_stats(callback_query: CallbackQuery, session: Any):
f"├ 📅 За день: {total_payments_today} ₽\n"
f"├ 📆 За неделю: {total_payments_week} ₽\n"
f"├ 📆 За месяц: {total_payments_month} ₽\n"
+ f"├ 📆 За прошлый месяц: {total_payments_last_month} ₽\n"
f"└ 🏦 Всего: {total_payments_all_time} ₽\n\n"
f"🔥 Горящие лиды: {hot_leads_count} (платили, но не продлили)\n\n"
f"⏱️ Последнее обновление: {update_time}"
diff --git a/handlers/coupons.py b/handlers/coupons.py
index 8632bd97..119e05e8 100644
--- a/handlers/coupons.py
+++ b/handlers/coupons.py
@@ -60,7 +60,9 @@ async def process_coupon_code(message: Message, state: FSMContext, session: Any)
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
await message.answer(activation_result, reply_markup=builder.as_markup())
- await state.clear()
+
+ if activation_result.startswith(COUPON_ACTIVATED_SUCCESS_MSG.split('.')[0]):
+ await state.clear()
async def activate_coupon(user_id: int, coupon_code: str, session: Any):
diff --git a/handlers/profile.py b/handlers/profile.py
index 82fe9ea3..8fe644b0 100644
--- a/handlers/profile.py
+++ b/handlers/profile.py
@@ -1,9 +1,11 @@
import html
+from io import BytesIO
import os
-from typing import Any, Optional
-
import asyncpg
+import qrcode
+
+from typing import Any, Optional
from aiogram import F, Router
from aiogram.enums import ParseMode
@@ -48,6 +50,7 @@ from handlers.buttons import (
MAIN_MENU,
MY_SUBS,
PAYMENT,
+ QR,
TOP_FIVE,
)
from handlers.texts import BALANCE_HISTORY_HEADER, BALANCE_MANAGEMENT_TEXT, INVITE_TEXT_NON_INLINE, TOP_REFERRALS_TEXT
@@ -250,6 +253,7 @@ async def invite_handler(callback_query_or_message: Message | CallbackQuery):
else:
invite_text = INVITE_TEXT_NON_INLINE.format(referral_link=referral_link)
builder.button(text=INVITE, switch_inline_query=invite_text)
+ builder.button(text=QR, callback_data=f"show_referral_qr|{chat_id}")
if TOP_REFERRAL_BUTTON:
builder.button(text=TOP_FIVE, callback_data="top_referrals")
builder.button(text=MAIN_MENU, callback_data="profile")
@@ -290,6 +294,43 @@ async def inline_referral_handler(inline_query: InlineQuery):
await inline_query.answer(results=results, cache_time=86400, is_personal=True)
+@router.callback_query(F.data.startswith("show_referral_qr|"))
+async def show_referral_qr(callback_query: CallbackQuery):
+ try:
+ chat_id = callback_query.data.split("|")[1]
+ referral_link = get_referral_link(chat_id)
+
+ qr = qrcode.QRCode(version=1, box_size=10, border=4)
+ qr.add_data(referral_link)
+ qr.make(fit=True)
+
+ img = qr.make_image(fill_color="black", back_color="white")
+ buffer = BytesIO()
+ img.save(buffer, format="PNG")
+ buffer.seek(0)
+
+ qr_path = f"/tmp/qrcode_referral_{chat_id}.png"
+ with open(qr_path, "wb") as f:
+ f.write(buffer.read())
+
+ builder = InlineKeyboardBuilder()
+ builder.row(InlineKeyboardButton(text=BACK, callback_data="invite"))
+ builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
+
+ await edit_or_send_message(
+ target_message=callback_query.message,
+ text="📷 Ваш QR-код для реферальной ссылки.",
+ reply_markup=builder.as_markup(),
+ media_path=qr_path,
+ )
+
+ os.remove(qr_path)
+
+ except Exception as e:
+ logger.error(f"Ошибка при генерации QR-кода для реферальной ссылки: {e}", exc_info=True)
+ await callback_query.message.answer("❌ Произошла ошибка при создании QR-кода.")
+
+
@router.callback_query(F.data == "top_referrals")
async def top_referrals_handler(callback_query: CallbackQuery):
conn = await asyncpg.connect(DATABASE_URL)