diff --git a/database.py b/database.py
index a51ceb17..ad91821f 100644
--- a/database.py
+++ b/database.py
@@ -915,12 +915,12 @@ async def get_tg_id_by_client_id(client_id: str):
async def upsert_user(
- tg_id: int,
- username: str = None,
- first_name: str = None,
- last_name: str = None,
- language_code: str = None,
- is_bot: bool = False,
+ tg_id: int,
+ username: str = None,
+ first_name: str = None,
+ last_name: str = None,
+ language_code: str = None,
+ is_bot: bool = False,
):
"""
Обновляет или вставляет информацию о пользователе в базу данных.
@@ -1117,3 +1117,12 @@ async def get_servers_from_db():
)
return servers
+
+
+async def delete_user_data(session: Any, tg_id: int):
+ await session.execute("DELETE FROM gifts WHERE sender_tg_id = $1 OR recipient_tg_id = $1", tg_id)
+ await session.execute("DELETE FROM payments WHERE tg_id = $1", tg_id)
+ await session.execute("DELETE FROM users WHERE tg_id = $1", tg_id)
+ await session.execute("DELETE FROM connections WHERE tg_id = $1", tg_id)
+ await session.execute("DELETE FROM keys WHERE tg_id = $1", tg_id)
+ await session.execute("DELETE FROM referrals WHERE referrer_tg_id = $1", tg_id)
diff --git a/handlers/__init__.py b/handlers/__init__.py
index 613d08f9..5037c650 100644
--- a/handlers/__init__.py
+++ b/handlers/__init__.py
@@ -22,7 +22,6 @@ router.include_routers(
donate_router,
coupons_router,
notifications_router,
-
payments_router,
keys_router,
instructions_router,
diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py
index d579cb5f..7a9bd856 100644
--- a/handlers/admin/admin_user_editor.py
+++ b/handlers/admin/admin_user_editor.py
@@ -5,11 +5,11 @@ 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, InlineKeyboardButton
+from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import TOTAL_GB
-from database import get_client_id_by_email, get_servers_from_db, restore_trial, update_key_expiry
+from database import get_client_id_by_email, get_servers_from_db, restore_trial, update_key_expiry, delete_user_data
from filters.admin import IsAdminFilter
from handlers.keys.key_utils import delete_key_from_cluster, delete_key_from_db, renew_key_in_cluster
from handlers.utils import sanitize_key_name
@@ -86,7 +86,7 @@ async def handle_username_input(message: types.Message, state: FSMContext, sessi
callback_data=f"restore_trial_{tg_id}",
)
)
-
+ builder.row(InlineKeyboardButton(text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}"))
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
user_info = (
@@ -499,20 +499,9 @@ async def handle_user_info(callback_query: types.CallbackQuery, state: FSMContex
for (email,) in key_records:
builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}"))
- builder.row(
- InlineKeyboardButton(
- text="📝 Изменить баланс",
- callback_data=f"change_balance_{tg_id}",
- )
- )
-
- builder.row(
- InlineKeyboardButton(
- text="🔄 Восстановить пробник",
- callback_data=f"restore_trial_{tg_id}",
- )
- )
-
+ builder.row(InlineKeyboardButton(text="📝 Изменить баланс", callback_data=f"change_balance_{tg_id}"))
+ builder.row(InlineKeyboardButton(text="🔄 Восстановить пробник", callback_data=f"restore_trial_{tg_id}"))
+ builder.row(InlineKeyboardButton(text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}"))
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
user_info = (
@@ -523,5 +512,56 @@ async def handle_user_info(callback_query: types.CallbackQuery, state: FSMContex
f"👥 Количество рефералов: {referral_count}\n"
f"🔑 Ключи (для редактирования нажмите на ключ):"
)
+
await callback_query.message.answer(user_info, reply_markup=builder.as_markup())
await state.set_state(UserEditorState.displaying_user_info)
+
+
+@router.callback_query(F.data.startswith("confirm_delete_user_"), IsAdminFilter())
+async def confirm_delete_user(callback_query: types.CallbackQuery, state: FSMContext, session: Any):
+ tg_id = int(callback_query.data.split("_")[3])
+
+ confirmation_markup = InlineKeyboardMarkup(
+ row_width=2,
+ inline_keyboard=[
+ [InlineKeyboardButton(text="✅ Подтвердить", callback_data=f"delete_user_{tg_id}")],
+ [InlineKeyboardButton(text="❌ Отменить", callback_data="user_editor")],
+ ],
+ )
+
+ await callback_query.message.answer(
+ f"Вы уверены, что хотите удалить пользователя с ID {tg_id}?", reply_markup=confirmation_markup
+ )
+
+
+@router.callback_query(F.data.startswith("delete_user_"), IsAdminFilter())
+async def delete_user(callback_query: types.CallbackQuery, session: Any):
+ tg_id = int(callback_query.data.split("_")[2])
+
+ 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)
+
+ back_button = InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")
+ keyboard = InlineKeyboardMarkup(inline_keyboard=[[back_button]])
+
+ await callback_query.message.answer(f"🗑️ Пользователь с ID {tg_id} был удален.", reply_markup=keyboard)
+ except Exception as e:
+ logger.error(f"Ошибка при удалении данных из базы данных для пользователя {tg_id}: {e}")
+ await callback_query.message.answer(
+ f"❌ Произошла ошибка при удалении пользователя с ID {tg_id}. Попробуйте снова."
+ )
diff --git a/handlers/keys/subscriptions.py b/handlers/keys/subscriptions.py
index 45b0d3fc..0f735199 100644
--- a/handlers/keys/subscriptions.py
+++ b/handlers/keys/subscriptions.py
@@ -8,6 +8,9 @@ import asyncpg
from config import DATABASE_URL, TRANSITION_DATE_STR
from database import get_servers_from_db
from logger import logger
+import urllib.parse
+
+from config import PROJECT_NAME, NEWS_MESSAGE
async def fetch_url_content(url, tg_id):
@@ -67,7 +70,7 @@ async def handle_old_subscription(request):
conn = await asyncpg.connect(DATABASE_URL)
try:
- key_info = await conn.fetchrow("SELECT created_at FROM keys WHERE email = $1", email)
+ key_info = await conn.fetchrow("SELECT created_at, cluster_name FROM keys WHERE email = $1", email)
if not key_info:
logger.warning(f"Клиент с email {email} не найден в базе.")
@@ -77,7 +80,8 @@ async def handle_old_subscription(request):
)
created_at_ms = key_info["created_at"]
- logger.info(f"Значение created_at для клиента с email {email}: {created_at_ms}")
+ cluster_name = key_info["cluster_name"]
+ logger.info(f"Значение created_at для клиента с email {email}: {created_at_ms}, кластер: {cluster_name}")
created_at_datetime = datetime.utcfromtimestamp(created_at_ms / 1000)
logger.info(f"Время создания клиента в формате datetime (UTC): {created_at_datetime}")
@@ -90,22 +94,24 @@ async def handle_old_subscription(request):
)
servers = await get_servers_from_db()
+ cluster_servers = servers.get(cluster_name, [])
+ logger.info(f"Сервера в кластере: {cluster_servers}")
urls = []
- for cluster_name, cluster_servers in servers.items():
- for server in cluster_servers:
- server_subscription_url = f"{server['subscription_url']}/{email}"
- urls.append(server_subscription_url)
+ for server in cluster_servers:
+ server_subscription_url = f"{server['subscription_url']}/{email}"
+ urls.append(server_subscription_url)
combined_subscriptions = await combine_unique_lines(urls, email, "")
base64_encoded = base64.b64encode("\n".join(combined_subscriptions).encode("utf-8")).decode("utf-8")
+ encoded_project_name = urllib.parse.quote(f"{PROJECT_NAME}\n{NEWS_MESSAGE}")
headers = {
"Content-Type": "text/plain; charset=utf-8",
"Content-Disposition": "inline",
"profile-update-interval": "7",
- "profile-title": email,
+ "profile-title": encoded_project_name,
}
logger.info(f"Возвращаем объединенные подписки для email: {email}")
@@ -130,7 +136,7 @@ async def handle_new_subscription(request):
conn = await asyncpg.connect(DATABASE_URL)
try:
- client_data = await conn.fetchrow("SELECT tg_id FROM keys WHERE email = $1", email)
+ client_data = await conn.fetchrow("SELECT tg_id, server_id FROM keys WHERE email = $1", email)
if not client_data:
logger.warning(f"Клиент с email {email} не найден в базе.")
@@ -140,6 +146,7 @@ async def handle_new_subscription(request):
)
stored_tg_id = client_data["tg_id"]
+ cluster_name = client_data["server_id"]
if str(tg_id) != str(stored_tg_id):
logger.warning(f"Неверный tg_id для клиента с email {email}.")
@@ -147,16 +154,17 @@ async def handle_new_subscription(request):
text="❌ Неверные данные. Получите свой ключ в боте.",
status=403,
)
+
finally:
await conn.close()
servers = await get_servers_from_db()
+ cluster_servers = servers.get(cluster_name, [])
urls = []
- for cluster_name, cluster_servers in servers.items():
- for server in cluster_servers:
- server_subscription_url = f"{server['subscription_url']}/{email}"
- urls.append(server_subscription_url)
+ for server in cluster_servers:
+ server_subscription_url = f"{server['subscription_url']}/{email}"
+ urls.append(server_subscription_url)
query_string = request.query_string
logger.info(f"Извлечен query string: {query_string}")
@@ -165,11 +173,12 @@ async def handle_new_subscription(request):
base64_encoded = base64.b64encode("\n".join(combined_subscriptions).encode("utf-8")).decode("utf-8")
+ encoded_project_name = urllib.parse.quote(f"{PROJECT_NAME}\n{NEWS_MESSAGE}")
headers = {
"Content-Type": "text/plain; charset=utf-8",
"Content-Disposition": "inline",
"profile-update-interval": "7",
- "profile-title": email,
+ "profile-title": encoded_project_name,
}
logger.info(f"Возвращаем объединенные подписки для email: {email}")
diff --git a/handlers/profile.py b/handlers/profile.py
index 80775e0e..3a43d47f 100644
--- a/handlers/profile.py
+++ b/handlers/profile.py
@@ -68,18 +68,15 @@ async def view_tariffs_handler(callback_query: types.CallbackQuery):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
- image_path = os.path.join("img", "tariffs.jpg")
+ image_path = os.path.join("img", "tariffs.jpg")
- tariffs_message = (
- "🚀 Доступные тарифы VPN:\n\n"
- + "\n".join(
- [
- f"{months} {'месяц' if months == '1' else 'месяца' if int(months) in [2, 3, 4] else 'месяцев'}: "
- f"{RENEWAL_PLANS[months]['price']} "
- f"{'💳' if months == '1' else '🌟' if months == '3' else '🔥' if months == '6' else '🚀'} рублей"
- for months in sorted(RENEWAL_PLANS.keys(), key=int)
- ]
- )
+ tariffs_message = "🚀 Доступные тарифы VPN:\n\n" + "\n".join(
+ [
+ f"{months} {'месяц' if months == '1' else 'месяца' if int(months) in [2, 3, 4] else 'месяцев'}: "
+ f"{RENEWAL_PLANS[months]['price']} "
+ f"{'💳' if months == '1' else '🌟' if months == '3' else '🔥' if months == '6' else '🚀'} рублей"
+ for months in sorted(RENEWAL_PLANS.keys(), key=int)
+ ]
)
if os.path.isfile(image_path):
@@ -96,7 +93,6 @@ async def view_tariffs_handler(callback_query: types.CallbackQuery):
)
-
@router.callback_query(F.data == "invite")
async def invite_handler(callback_query: types.CallbackQuery):
chat_id = callback_query.message.chat.id