change_location/CASHBACK/logging/bug_fixes

This commit is contained in:
Vladless
2025-02-02 02:11:57 +03:00
parent 73fbc22324
commit 49eb0e68a1
29 changed files with 947 additions and 726 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ 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)"
version = "4.0.0-Alpha(01-dev)"
register_middleware(dp)
+31 -7
View File
@@ -5,7 +5,7 @@ from typing import Any
import asyncpg
import pytz
from config import DATABASE_URL, REFERRAL_BONUS_PERCENTAGES
from config import CASHBACK, DATABASE_URL, REFERRAL_BONUS_PERCENTAGES
from logger import logger
@@ -492,6 +492,25 @@ async def get_keys_by_server(tg_id: int | None, server_id: str, session: Any):
raise
async def get_key_by_server(tg_id: int, client_id: str, session: Any):
query = """
SELECT
tg_id,
client_id,
email,
created_at,
expiry_time,
key,
server_id,
notified,
notified_24h
FROM keys
WHERE tg_id = $1 AND client_id = $2
"""
record = await session.fetchrow(query, tg_id, client_id)
return record
async def get_balance(tg_id: int) -> float:
"""
Получает баланс пользователя из базы данных.
@@ -521,15 +540,15 @@ async def get_balance(tg_id: int) -> float:
async def update_balance(tg_id: int, amount: float, session: Any = None):
"""
Обновляет баланс пользователя в базе данных.
Обновляет баланс пользователя в базе данных с учетом кэшбека.
Args:
tg_id (int): Telegram ID пользователя
amount (float): Сумма для обновления баланса
tg_id (int): Telegram ID пользователя.
amount (float): Сумма для обновления баланса.
session (Any, optional): Сессия базы данных. Если не передана, создается новая.
Raises:
Exception: В случае ошибки при подключении к базе данных или обновлении баланса
Exception: В случае ошибки при подключении к базе данных или обновлении баланса.
"""
conn = None
try:
@@ -537,16 +556,21 @@ async def update_balance(tg_id: int, amount: float, session: Any = None):
conn = await asyncpg.connect(DATABASE_URL)
session = conn
extra = amount * (CASHBACK / 100.0) if CASHBACK > 0 else 0
total_amount = amount + extra
await session.execute(
"""
UPDATE connections
SET balance = balance + $1
WHERE tg_id = $2
""",
amount,
total_amount,
tg_id,
)
logger.info(f"Баланс пользователя {tg_id} обновлен на сумму {amount}")
logger.info(
f"Баланс пользователя {tg_id} обновлен на сумму {total_amount} (исходная сумма {amount}, кэшбек {extra})"
)
await handle_referral_on_balance_update(tg_id, amount)
+3 -1
View File
@@ -5,6 +5,7 @@ from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, Message
from config import USERNAME_BOT
from database import create_coupon, delete_coupon, get_all_coupons
from filters.admin import IsAdminFilter
from keyboards.admin.coupons_kb import AdminCouponDeleteCallback, build_coupons_kb, build_coupons_list_kb
@@ -131,7 +132,8 @@ async def handle_coupons_list(callback_query: CallbackQuery, session: Any):
f"🏷️ <b>Код:</b> {coupon['code']}\n"
f"💰 <b>Сумма:</b> {coupon['amount']} рублей\n"
f"🔢 <b>Лимит использования:</b> {coupon['usage_limit']} раз\n"
f"✅ <b>Использовано:</b> {coupon['usage_count']} раз\n\n"
f"✅ <b>Использовано:</b> {coupon['usage_count']} раз\n"
f"🔗 <b>Ссылка:</b> <code>https://t.me/{USERNAME_BOT}?start=coupons_{coupon['code']}</code>\n"
)
await callback_query.message.edit_text(text=coupon_list, reply_markup=kb)
+1 -1
View File
@@ -1,4 +1,4 @@
from aiogram import F, Router, types
from aiogram import F, Router
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
+2 -2
View File
@@ -1,14 +1,14 @@
from datetime import datetime
from typing import Any
from aiogram import F, Router, types
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, Message
from filters.admin import IsAdminFilter
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
from keyboards.admin.sender_kb import AdminSenderCallback, build_sender_kb, build_clusters_kb
from keyboards.admin.sender_kb import AdminSenderCallback, build_clusters_kb, build_sender_kb
from logger import logger
router = Router()
+15 -9
View File
@@ -106,6 +106,13 @@ async def handle_server_name_input(message: Message, state: FSMContext, session:
server_name = message.text.strip()
if len(server_name) > 14:
await message.answer(
text="❌ Имя сервера не должно превышать 10 символов. Попробуйте снова.",
reply_markup=build_admin_back_kb("servers"),
)
return
user_data = await state.get_data()
cluster_name = user_data.get("cluster_name")
@@ -234,7 +241,7 @@ async def handle_inbound_id_input(message: Message, state: FSMContext):
@router.callback_query(AdminServerEditorCallback.filter(F.action == "clusters_manage"), IsAdminFilter())
async def handle_clusters_manage(
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
):
cluster_name = callback_data.data
@@ -249,7 +256,7 @@ async def handle_clusters_manage(
@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_availability"), IsAdminFilter())
async def handle_servers_availability(
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
):
cluster_name = callback_data.data
@@ -325,7 +332,7 @@ async def handle_servers_delete(callback_query: CallbackQuery, callback_data: Ad
@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_delete_confirm"), IsAdminFilter())
async def handle_servers_delete_confirm(
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
):
server_name = callback_data.data
@@ -338,7 +345,7 @@ async def handle_servers_delete_confirm(
@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_add"), IsAdminFilter())
async def handle_servers_add(
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, state: FSMContext
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, state: FSMContext
):
cluster_name = callback_data.data
@@ -360,7 +367,7 @@ async def handle_servers_add(
@router.callback_query(AdminServerEditorCallback.filter(F.action == "clusters_backup"), IsAdminFilter())
async def handle_clusters_backup(
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
):
cluster_name = callback_data.data
@@ -388,7 +395,7 @@ async def handle_clusters_backup(
@router.callback_query(AdminServerEditorCallback.filter(F.action == "clusters_sync"), IsAdminFilter())
async def handle_clusters_backup(
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
):
cluster_name = callback_data.data
@@ -426,11 +433,10 @@ async def handle_clusters_backup(
await callback_query.message.answer(
text=f"✅ Ключи успешно синхронизированы для кластера {cluster_name}",
reply_markup=build_admin_back_kb("servers")
reply_markup=build_admin_back_kb("servers"),
)
except Exception as e:
logger.error(f"Ошибка синхронизации ключей в кластере {cluster_name}: {e}")
await callback_query.message.answer(
text=f"❌ Произошла ошибка при синхронизации: {e}",
reply_markup=build_admin_back_kb("servers")
text=f"❌ Произошла ошибка при синхронизации: {e}", reply_markup=build_admin_back_kb("servers")
)
+25 -16
View File
@@ -9,8 +9,16 @@ from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, Message
from config import TOTAL_GB
from database import delete_key, delete_user_data, get_client_id_by_email, get_servers, update_key_expiry, update_trial, \
get_balance, update_balance
from database import (
delete_key,
delete_user_data,
get_balance,
get_client_id_by_email,
get_servers,
update_balance,
update_key_expiry,
update_trial,
)
from filters.admin import IsAdminFilter
from handlers.keys.key_utils import (
delete_key_from_cluster,
@@ -132,13 +140,12 @@ async def handle_key_name_input(message: Message, state: FSMContext, session: An
IsAdminFilter(),
)
async def handle_send_message(
callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext
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)
text="✉️ Введите текст сообщения, которое вы хотите отправить пользователю:", reply_markup=build_editor_kb(tg_id)
)
await state.update_data(tg_id=tg_id)
@@ -164,7 +171,7 @@ async def handle_message_text_input(message: Message, state: FSMContext):
IsAdminFilter(),
)
async def handle_trial_restore(
callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any
callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any
):
tg_id = callback_data.tg_id
@@ -215,7 +222,7 @@ async def handle_balance_change(callback_query: CallbackQuery, callback_data: Ad
@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
callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext, session: Any
):
tg_id = callback_data.tg_id
amount = callback_data.data
@@ -289,8 +296,10 @@ async def handle_balance_input(message: Message, state: FSMContext, session: Any
@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_key_edit"), IsAdminFilter())
async def handle_key_edit(
callback_query: CallbackQuery, callback_data: AdminUserEditorCallback | AdminUserKeyEditorCallback,
session: Any, update: bool = False
callback_query: CallbackQuery,
callback_data: AdminUserEditorCallback | AdminUserKeyEditorCallback,
session: Any,
update: bool = False,
):
email = callback_data.data
key_details = await get_key_details(email, session)
@@ -328,7 +337,7 @@ async def handle_change_expiry(callback_query: CallbackQuery, callback_data: Adm
@router.callback_query(AdminUserKeyEditorCallback.filter(F.action == "add"), IsAdminFilter())
async def handle_expiry_add(
callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, state: FSMContext, session: Any
callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, state: FSMContext, session: Any
):
tg_id = callback_data.tg_id
email = callback_data.data
@@ -359,7 +368,7 @@ async def handle_expiry_add(
@router.callback_query(AdminUserKeyEditorCallback.filter(F.action == "take"), IsAdminFilter())
async def handle_expiry_take(
callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, state: FSMContext
callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, state: FSMContext
):
tg_id = callback_data.tg_id
email = callback_data.data
@@ -375,7 +384,7 @@ async def handle_expiry_take(
@router.callback_query(AdminUserKeyEditorCallback.filter(F.action == "set"), IsAdminFilter())
async def handle_expiry_set(
callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, state: FSMContext, session: Any
callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, state: FSMContext, session: Any
):
tg_id = callback_data.tg_id
email = callback_data.data
@@ -480,7 +489,7 @@ async def handle_delete_key(callback_query: CallbackQuery, callback_data: AdminU
@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
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)
@@ -516,7 +525,7 @@ async def handle_delete_user(callback_query: CallbackQuery, callback_data: Admin
@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
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)
@@ -548,13 +557,13 @@ async def handle_delete_user_confirm(
@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
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
message: types.Message, state: FSMContext, session: Any, tg_id: int, edit: bool = False
) -> None:
await state.clear()
+1 -1
View File
@@ -1,6 +1,6 @@
from typing import Any
from aiogram import F, Router, types
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton, Message
+1 -1
View File
@@ -1,4 +1,4 @@
from aiogram import F, Router, types
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton, LabeledPrice, Message, PreCheckoutQuery
+170 -61
View File
@@ -6,12 +6,15 @@ from typing import Any
import pytz
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from py3xui import AsyncApi
from bot import bot
from client import delete_client
from config import (
ADMIN_PASSWORD,
ADMIN_USERNAME,
CONNECT_ANDROID,
CONNECT_IOS,
DOWNLOAD_ANDROID,
@@ -25,6 +28,7 @@ from config import (
)
from database import (
create_temporary_data,
delete_key,
get_balance,
get_key_details,
get_trial,
@@ -48,22 +52,21 @@ from logger import logger
router = Router()
moscow_tz = pytz.timezone("Europe/Moscow")
class Form(StatesGroup):
waiting_for_server_selection = State()
waiting_for_key_name = State()
viewing_profile = State()
waiting_for_message = State()
class Form(FSMContext):
waiting_for_server_selection = "waiting_for_server_selection"
waiting_for_key_name = "waiting_for_key_name"
viewing_profile = "viewing_profile"
waiting_for_message = "waiting_for_message"
@router.callback_query(F.data == "create_key")
async def confirm_create_new_key(callback_query: CallbackQuery, state: FSMContext, session: Any):
tg_id = callback_query.message.chat.id
logger.info(f"User {tg_id} confirmed creation of a new key.")
logger.info(f"Balance for user {tg_id} is sufficient. Proceeding with key creation.")
await handle_key_creation(tg_id, state, session, callback_query)
@@ -74,37 +77,30 @@ async def handle_key_creation(
message_or_query: Message | CallbackQuery,
):
"""Создание ключа с учётом выбора тарифного плана."""
current_time = datetime.utcnow()
current_time = datetime.now(moscow_tz)
trial_status = await get_trial(tg_id, session)
if trial_status == 0:
expiry_time = current_time + timedelta(days=TRIAL_TIME)
logger.info(f"Assigned 1-day trial to user {tg_id}.")
logger.info(f"Assigned {TRIAL_TIME}-дневный пробный период пользователю {tg_id}.")
await session.execute("UPDATE connections SET trial = 1 WHERE tg_id = $1", tg_id)
await create_key(tg_id, expiry_time, state, session, message_or_query)
else:
builder = InlineKeyboardBuilder()
for index, (plan_id, price) in enumerate(RENEWAL_PRICES.items()):
discount_text = ""
if plan_id in DISCOUNTS:
discount_percentage = DISCOUNTS[plan_id]
discount_text = f" ({discount_percentage}% скидка)"
if index == len(RENEWAL_PRICES) - 1:
discount_text = f" ({discount_percentage}% 🔥)"
builder.row(
InlineKeyboardButton(
text=f"📅 {plan_id} мес. - {price}{discount_text}",
callback_data=f"select_plan_{plan_id}",
)
)
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await message_or_query.message.answer(
"💳 Выберите тарифный план для создания нового ключа:",
reply_markup=builder.as_markup(),
@@ -118,17 +114,13 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any):
tg_id = callback_query.message.chat.id
plan_id = callback_query.data.split("_")[-1]
plan_price = RENEWAL_PRICES.get(plan_id)
if plan_price is None:
await callback_query.message.answer("🚫 Неверный тарифный план.")
return
duration_days = int(plan_id) * 30
balance = await get_balance(tg_id)
if balance < plan_price:
required_amount = plan_price - balance
await create_temporary_data(
session,
tg_id,
@@ -140,7 +132,6 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any):
"required_amount": required_amount,
},
)
if USE_NEW_PAYMENT_FLOW == "YOOKASSA":
await process_custom_amount_input(callback_query, session)
elif USE_NEW_PAYMENT_FLOW == "ROBOKASSA":
@@ -149,14 +140,12 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="pay"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(
f"💳 Недостаточно средств. Для продолжения необходимо пополнить баланс на {required_amount}₽.",
reply_markup=builder.as_markup(),
)
return
expiry_time = datetime.utcnow() + timedelta(days=duration_days)
expiry_time = datetime.now(moscow_tz) + timedelta(days=duration_days)
await create_key(tg_id, expiry_time, None, session, callback_query)
await update_balance(tg_id, -plan_price, session)
@@ -167,18 +156,15 @@ async def create_key(
state: FSMContext | None,
session: Any,
message_or_query: Message | CallbackQuery | None = None,
old_key_name: str = None,
):
"""Создаёт ключ с заданным сроком действия."""
moscow_tz = pytz.timezone("Europe/Moscow")
expiry_time = expiry_time.astimezone(moscow_tz)
if USE_COUNTRY_SELECTION:
if USE_COUNTRY_SELECTION and message_or_query is not None:
logger.info("[Country Selection] USE_COUNTRY_SELECTION включен.")
logger.info("[Country Selection] Получение наименее загруженного кластера.")
least_loaded_cluster = await get_least_loaded_cluster()
logger.info(f"[Country Selection] Наименее загруженный кластер: {least_loaded_cluster}")
logger.info(f"[Country Selection] Получение списка серверов для кластера {least_loaded_cluster}.")
servers = await session.fetch(
"SELECT server_name FROM servers WHERE cluster_name = $1",
@@ -188,11 +174,14 @@ async def create_key(
logger.info(f"[Country Selection] Список серверов: {countries}")
builder = InlineKeyboardBuilder()
ts = int(expiry_time.timestamp())
for country in countries:
callback_data = f"select_country|{country}|{expiry_time.isoformat()}"
if old_key_name:
callback_data = f"select_country|{country}|{ts}|{old_key_name}"
else:
callback_data = f"select_country|{country}|{ts}"
builder.row(InlineKeyboardButton(text=country, callback_data=callback_data))
logger.info(f"[Country Selection] Добавлена кнопка для страны: {country} с callback_data: {callback_data}")
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="profile"))
logger.info("[Country Selection] Добавлена кнопка '⬅️ Назад'.")
@@ -227,7 +216,6 @@ async def create_key(
while True:
key_name = generate_random_email()
logger.info(f"[Key Generation] Сгенерировано имя ключа: {key_name} для пользователя {tg_id}")
existing_key = await get_key_details(key_name, session)
if not existing_key:
break
@@ -240,7 +228,6 @@ async def create_key(
try:
least_loaded_cluster = await get_least_loaded_cluster()
tasks = [
asyncio.create_task(
create_key_on_cluster(
@@ -252,10 +239,8 @@ async def create_key(
)
)
]
await asyncio.gather(*tasks)
logger.info(f"[Key Creation] Ключ создан на кластере {least_loaded_cluster} для пользователя {tg_id}")
await store_key(
tg_id,
client_id,
@@ -266,10 +251,8 @@ async def create_key(
session,
)
logger.info(f"[Database] Ключ сохранён в базе данных для пользователя {tg_id}")
except Exception as e:
logger.error(f"[Error] Ошибка при создании ключа для пользователя {tg_id}: {e}")
error_message = "❌ Произошла ошибка при создании подписки. Пожалуйста, попробуйте снова."
if isinstance(message_or_query, Message):
await message_or_query.answer(error_message)
@@ -297,40 +280,126 @@ async def create_key(
remaining_time = expiry_time - datetime.now(moscow_tz)
days = remaining_time.days
key_message = key_message_success(public_link, f"⏳ Осталось дней: {days} 📅")
key_message_text = key_message_success(public_link, f"⏳ Осталось дней: {days} 📅")
if isinstance(message_or_query, Message):
await message_or_query.answer(key_message, reply_markup=builder.as_markup())
await message_or_query.answer(key_message_text, reply_markup=builder.as_markup())
elif isinstance(message_or_query, CallbackQuery):
await message_or_query.message.answer(key_message, reply_markup=builder.as_markup())
await message_or_query.message.answer(key_message_text, reply_markup=builder.as_markup())
else:
await bot.send_message(chat_id=tg_id, text=key_message, reply_markup=builder.as_markup())
await bot.send_message(chat_id=tg_id, text=key_message_text, reply_markup=builder.as_markup())
if state:
await state.clear()
logger.info(f"[FSM] Состояние пользователя {tg_id} очищено")
if old_key_name:
try:
old_record = await get_key_details(old_key_name, session)
if old_record is not None:
old_client_id = old_record["client_id"]
old_email = old_record["email"]
server_name = old_record.get("server_id")
if server_name:
server_info = await session.fetchrow(
"SELECT api_url, inbound_id, server_name FROM servers WHERE server_name = $1",
server_name,
)
if server_info:
xui = AsyncApi(
server_info["api_url"],
username=ADMIN_USERNAME,
password=ADMIN_PASSWORD,
)
deletion_success = await delete_client(
xui,
server_info["inbound_id"],
old_email,
old_client_id,
)
if deletion_success:
logger.info(f"Клиент с ID {old_client_id} успешно удалён с сервера.")
else:
logger.warning(f"Не удалось удалить клиента с ID {old_client_id} с сервера.")
else:
logger.warning(f"Информация о сервере {server_name} не найдена в БД.")
else:
logger.warning("Имя сервера для старого ключа не указано.")
await delete_key(old_client_id, session)
logger.info(f"Старый ключ {old_key_name} (client_id: {old_client_id}) удалён для пользователя {tg_id}.")
else:
logger.warning(f"Запись для старого ключа {old_key_name} не найдена.")
except Exception as e:
logger.error(f"Ошибка при удалении старого ключа {old_key_name} для пользователя {tg_id}: {e}")
@router.callback_query(F.data.startswith("change_location|"))
async def change_location_callback(callback_query: CallbackQuery, session: Any):
try:
data = callback_query.data.split("|")
if len(data) < 2:
await callback_query.answer("❌ Некорректные данные", show_alert=True)
return
old_key_name = data[1]
record = await get_key_details(old_key_name, session)
if not record:
await callback_query.answer("❌ Ключ не найден", show_alert=True)
return
expiry_timestamp = record["expiry_time"]
ts = int(expiry_timestamp / 1000)
expiry_time = datetime.fromtimestamp(ts, tz=moscow_tz)
servers = await session.fetch("SELECT server_name FROM servers")
countries = [row["server_name"] for row in servers]
logger.info(f"Доступные страны для смены локации: {countries}")
builder = InlineKeyboardBuilder()
for country in countries:
callback_data = f"select_country|{country}|{ts}|{old_key_name}"
builder.row(InlineKeyboardButton(text=country, callback_data=callback_data))
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"view_key|{old_key_name}"))
await callback_query.message.answer(
"🌍 Пожалуйста, выберите новую локацию для вашей подписки:", reply_markup=builder.as_markup()
)
except Exception as e:
logger.error(f"Ошибка при смене локации для пользователя {callback_query.from_user.id}: {e}")
await callback_query.answer("❌ Ошибка смены локации. Попробуйте снова.", show_alert=True)
@router.callback_query(F.data.startswith("select_country|"))
async def handle_country_selection(callback_query: CallbackQuery, session: Any):
"""Обработчик выбора страны."""
"""
Обрабатывает выбор страны.
Формат callback data:
select_country|{selected_country}|{ts} [|{old_key_name} (опционально)]
Если передан old_key_name – значит, происходит смена локации.
"""
data = callback_query.data.split("|")
if len(data) < 3:
await callback_query.message.answer("❌ Некорректные данные. Попробуйте снова.")
return
selected_country = data[1]
expiry_time_str = data[2]
tg_id = callback_query.from_user.id
logger.info(f"Пользователь {tg_id} выбрал страну: {selected_country}")
logger.info(f"Получено время истечения: {expiry_time_str}")
try:
expiry_time = datetime.fromisoformat(expiry_time_str)
ts = int(data[2])
except ValueError:
logger.error(f"Ошибка преобразования времени истечения: {expiry_time_str}")
await callback_query.message.answer("❌ Некорректное время истечения. Попробуйте снова.")
return
await finalize_key_creation(tg_id, expiry_time, selected_country, None, session, callback_query)
expiry_time = datetime.fromtimestamp(ts, tz=moscow_tz)
old_key_name = data[3] if len(data) > 3 else None
tg_id = callback_query.from_user.id
logger.info(f"Пользователь {tg_id} выбрал страну: {selected_country}")
logger.info(f"Получено время истечения (timestamp): {ts}")
await finalize_key_creation(tg_id, expiry_time, selected_country, None, session, callback_query, old_key_name)
async def finalize_key_creation(
@@ -340,15 +409,16 @@ async def finalize_key_creation(
state: FSMContext | None,
session: Any,
callback_query: CallbackQuery,
old_key_name: str = None,
):
"""Финализирует создание ключа с выбранной страной."""
moscow_tz = pytz.timezone("Europe/Moscow")
"""Финализирует создание ключа с выбранной страной.
Если old_key_name передан, после создания нового ключа старый будет удалён.
"""
expiry_time = expiry_time.astimezone(moscow_tz)
while True:
key_name = generate_random_email()
logger.info(f"Generated random key name for user {tg_id}: {key_name}")
existing_key = await get_key_details(key_name, session)
if not existing_key:
break
@@ -369,7 +439,6 @@ async def finalize_key_creation(
raise ValueError(f"Сервер {selected_country} не найден.")
semaphore = asyncio.Semaphore(2)
await create_client_on_server(
server_info=server_info,
tg_id=tg_id,
@@ -380,7 +449,6 @@ async def finalize_key_creation(
)
logger.info(f"Key created on server {selected_country} for user {tg_id}.")
await store_key(
tg_id,
client_id,
@@ -414,9 +482,50 @@ async def finalize_key_creation(
remaining_time = expiry_time - datetime.now(moscow_tz)
days = remaining_time.days
key_message = key_message_success(public_link, f"⏳ Осталось дней: {days} 📅")
key_message_text = key_message_success(public_link, f"⏳ Осталось дней: {days} 📅")
await callback_query.message.answer(key_message, reply_markup=builder.as_markup())
await callback_query.message.answer(key_message_text, reply_markup=builder.as_markup())
if state:
await state.clear()
if old_key_name:
try:
old_record = await get_key_details(old_key_name, session)
if old_record is not None:
old_client_id = old_record["client_id"]
old_email = old_record["email"]
server_name = old_record.get("server_id")
if server_name:
server_info = await session.fetchrow(
"SELECT api_url, inbound_id, server_name FROM servers WHERE server_name = $1",
server_name,
)
if server_info:
xui = AsyncApi(
server_info["api_url"],
username=ADMIN_USERNAME,
password=ADMIN_PASSWORD,
)
deletion_success = await delete_client(
xui,
server_info["inbound_id"],
old_email,
old_client_id,
)
if deletion_success:
logger.info(f"Клиент с ID {old_client_id} успешно удалён с сервера.")
else:
logger.warning(f"Не удалось удалить клиента с ID {old_client_id} с сервера.")
else:
logger.warning(f"Информация о сервере {server_name} не найдена в БД.")
else:
logger.warning("Имя сервера для старого ключа не указано.")
await delete_key(old_client_id, session)
logger.info(f"Старый ключ {old_key_name} (client_id: {old_client_id}) удалён для пользователя {tg_id}.")
else:
logger.warning(f"Запись для старого ключа {old_key_name} не найдена.")
except Exception as e:
logger.error(f"Ошибка при удалении старого ключа {old_key_name} для пользователя {tg_id}: {e}")
+43 -15
View File
@@ -31,9 +31,9 @@ from database import (
create_temporary_data,
delete_key,
get_balance,
get_key_by_server,
get_key_details,
get_keys,
get_keys_by_server,
get_servers,
update_balance,
update_key_expiry,
@@ -162,6 +162,7 @@ async def process_callback_view_key(callback_query: CallbackQuery, session: Any)
key = record["key"]
expiry_time = record["expiry_time"]
server_name = record["server_id"]
country = server_name
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
current_date = datetime.utcnow()
time_left = expiry_date - current_date
@@ -180,7 +181,10 @@ async def process_callback_view_key(callback_query: CallbackQuery, session: Any)
)
formatted_expiry_date = expiry_date.strftime("%d %B %Y года")
response_message = key_message(key, formatted_expiry_date, days_left_message, server_name)
response_message = key_message(
key, formatted_expiry_date, days_left_message, server_name, country if USE_COUNTRY_SELECTION else None
)
builder = InlineKeyboardBuilder()
@@ -214,6 +218,12 @@ async def process_callback_view_key(callback_query: CallbackQuery, session: Any)
)
else:
builder.row(InlineKeyboardButton(text="⏳ Продлить", callback_data=f"renew_key|{key_name}"))
if USE_COUNTRY_SELECTION:
builder.row(
InlineKeyboardButton(text="🌍 Сменить локацию", callback_data=f"change_location|{key_name}")
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
@@ -386,7 +396,7 @@ async def process_callback_renew_plan(callback_query: CallbackQuery, session: An
total_gb = TOTAL_GB * gb_multiplier.get(plan, 1) if TOTAL_GB > 0 else 0
try:
record = await get_keys_by_server(tg_id, client_id, session)
record = await get_key_by_server(tg_id, client_id, session)
if record:
email = record["email"]
@@ -452,53 +462,71 @@ async def process_callback_renew_plan(callback_query: CallbackQuery, session: An
async def complete_key_renewal(tg_id, client_id, email, new_expiry_time, total_gb, cost, callback_query, plan):
logger.info(
f"[RENEW] Starting complete_key_renewal with parameters: "
f"tg_id={tg_id}, client_id={client_id}, email={email}, "
f"new_expiry_time={new_expiry_time}, total_gb={total_gb}, cost={cost}, "
f"callback_query={'present' if callback_query else 'None'}, plan={plan}"
)
response_message = SUCCESS_RENEWAL_MSG.format(months=plan)
logger.info(f"[RENEW] Constructed response message: {response_message}")
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
if callback_query:
logger.info("[RENEW] Sending response via callback_query.message.answer()")
await callback_query.message.answer(response_message, reply_markup=builder.as_markup())
else:
logger.info("[RENEW] Sending response via bot.send_message()")
await bot.send_message(tg_id, response_message, reply_markup=builder.as_markup())
logger.info("[RENEW] Connecting to database...")
conn = await asyncpg.connect(DATABASE_URL)
key_info = await get_key_details(email, conn)
logger.info("[RENEW] Connected to database.")
logger.info(f"[RENEW] Retrieving key details for email: {email}")
key_info = await get_key_details(email, conn)
if not key_info:
logger.error(f"[RENEW] Ключ с client_id {client_id} для пользователя {tg_id} не найден.")
await conn.close()
return
logger.info(f"[RENEW] Retrieved key_info: {key_info}")
server_id = key_info["server_id"]
logger.info(f"[RENEW] Using server_id: {server_id}")
if USE_COUNTRY_SELECTION:
logger.info(f"[RENEW] USE_COUNTRY_SELECTION is enabled. Checking cluster info for server_id: {server_id}")
cluster_info = await check_server_name_by_cluster(server_id, conn)
if not cluster_info:
logger.error(f"[RENEW] Сервер {server_id} не найден в таблице servers.")
await conn.close()
return
cluster_id = cluster_info["cluster_name"]
logger.info(f"[RENEW] Retrieved cluster info: {cluster_info}. Using cluster_id: {cluster_id}")
else:
cluster_id = server_id
logger.info(f"[RENEW] USE_COUNTRY_SELECTION is disabled. Using server_id as cluster_id: {cluster_id}")
logger.info(f"[RENEW] Запуск продления ключа для пользователя {tg_id} на {plan} мес. в кластере {cluster_id}.")
async def renew_key_on_cluster():
await renew_key_in_cluster(
cluster_id,
email,
client_id,
new_expiry_time,
total_gb,
logger.info(
f"[RENEW] Starting renew_key_on_cluster with parameters: "
f"cluster_id={cluster_id}, email={email}, client_id={client_id}, "
f"new_expiry_time={new_expiry_time}, total_gb={total_gb}"
)
await renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, total_gb)
logger.info("[RENEW] renew_key_in_cluster completed. Now updating key expiry in DB.")
await update_key_expiry(client_id, new_expiry_time, conn)
logger.info("[RENEW] Key expiry updated. Now updating balance.")
await update_balance(tg_id, -cost, conn)
logger.info(f"[RENEW] Ключ {client_id} успешно продлён на {plan} мес. для пользователя {tg_id}.")
await conn.close()
logger.info("[RENEW] Initiating key renewal process on cluster.")
await renew_key_on_cluster()
logger.info("[RENEW] Key renewal process completed. Closing database connection.")
await conn.close()
-87
View File
@@ -1,87 +0,0 @@
import asyncio
import uuid
from datetime import datetime, timedelta
from typing import Any
import pytz
from py3xui import AsyncApi
from client import ClientConfig, add_client
from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, PUBLIC_LINK, SUPERNODE, TOTAL_GB, TRIAL_TIME
from database import get_servers, get_trial, store_key, update_trial
from handlers.texts import INSTRUCTIONS
from handlers.utils import generate_random_email, get_least_loaded_cluster
from logger import logger
async def create_trial_key(tg_id: int, session: Any):
try:
trial_status = await get_trial(tg_id, session)
if trial_status == 1:
return {"error": "Вы уже использовали пробную версию."}
except Exception as e:
logger.error(f"Ошибка при проверке триала: {e}")
client_id = str(uuid.uuid4())
base_email = generate_random_email()
public_link = f"{PUBLIC_LINK}{base_email}/{tg_id}"
instructions = INSTRUCTIONS
result = {"key": public_link, "instructions": instructions, "email": base_email}
moscow_tz = pytz.timezone("Europe/Moscow")
current_time = datetime.now(moscow_tz)
expiry_time = current_time + timedelta(days=TRIAL_TIME)
expiry_timestamp = int(expiry_time.timestamp() * 1000)
clusters = await get_servers(session)
least_loaded_cluster = await get_least_loaded_cluster()
if least_loaded_cluster not in clusters:
raise ValueError(f"Кластер {least_loaded_cluster} не найден в базе данных.")
servers_in_cluster = clusters[least_loaded_cluster]
tasks = []
for server_info in servers_in_cluster:
server_name = server_info.get("server_name", "unknown")
if SUPERNODE:
email = f"{base_email}_{server_name.lower()}"
else:
email = base_email
tasks.append(
add_client(
AsyncApi(
server_info["api_url"],
username=ADMIN_USERNAME,
password=ADMIN_PASSWORD,
),
ClientConfig(
client_id=client_id,
email=email,
tg_id=tg_id,
limit_ip=LIMIT_IP,
total_gb=TOTAL_GB,
expiry_time=expiry_timestamp,
enable=True,
flow="xtls-rprx-vision",
inbound_id=int(server_info["inbound_id"]),
sub_id=base_email,
),
)
)
await asyncio.gather(*tasks)
await store_key(
tg_id,
client_id,
base_email,
expiry_timestamp,
public_link,
server_id=least_loaded_cluster,
session=session,
)
await update_trial(tg_id, 1, session)
return result
+2 -1
View File
@@ -4,7 +4,8 @@
{
"distutils": {
"extra_compile_args": [
"-O2"
"-O2",
"-static-libgcc"
],
"name": "handlers.payments.cryprobot_pay",
"sources": [
+2 -1
View File
@@ -4,7 +4,8 @@
{
"distutils": {
"extra_compile_args": [
"-O2"
"-O2",
"-static-libgcc"
],
"name": "handlers.payments.gift",
"sources": [
+29 -20
View File
@@ -52,7 +52,9 @@ if ROBOKASSA_ENABLE:
def generate_payment_link(amount, inv_id, description, tg_id):
"""Генерация ссылки на оплату."""
logger.debug(f"Generating payment link for amount: {amount}, inv_id: {inv_id}, description: {description}")
logger.debug(
f"Generating payment link for amount: {amount}, inv_id: {inv_id}, description: {description}"
)
payment_link = robokassa._payment.link.generate_by_script(
out_sum=amount,
inv_id=inv_id,
@@ -64,7 +66,9 @@ def generate_payment_link(amount, inv_id, description, tg_id):
@router.callback_query(F.data == "pay_robokassa")
async def process_callback_pay_robokassa(callback_query: types.CallbackQuery, state: FSMContext, session: Any):
async def process_callback_pay_robokassa(
callback_query: types.CallbackQuery, state: FSMContext, session: Any
):
tg_id = callback_query.message.chat.id
logger.info(f"User {tg_id} initiated Robokassa payment.")
@@ -74,18 +78,18 @@ async def process_callback_pay_robokassa(callback_query: types.CallbackQuery, st
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f"robokassa_amount|{PAYMENT_OPTIONS[i]['callback_data']}",
callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i]["callback_data"]}',
),
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i + 1]["text"],
callback_data=f"robokassa_amount|{PAYMENT_OPTIONS[i + 1]['callback_data']}",
callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i + 1]["callback_data"]}',
),
)
else:
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f"robokassa_amount|{PAYMENT_OPTIONS[i]['callback_data']}",
callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i]["callback_data"]}',
)
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="pay"))
@@ -107,7 +111,9 @@ async def process_callback_pay_robokassa(callback_query: types.CallbackQuery, st
@router.callback_query(F.data.startswith("robokassa_amount|"))
async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext):
async def process_amount_selection(
callback_query: types.CallbackQuery, state: FSMContext
):
logger.info(f"Получены данные callback_data: {callback_query.data}")
data = callback_query.data.split("|")
@@ -161,7 +167,9 @@ async def robokassa_webhook(request):
shp_id = params.get("shp_id")
signature_value = params.get("SignatureValue")
logger.info(f"OutSum: {amount}, InvId: {inv_id}, shp_id: {shp_id}, SignatureValue: {signature_value}")
logger.info(
f"OutSum: {amount}, InvId: {inv_id}, shp_id: {shp_id}, SignatureValue: {signature_value}"
)
if not check_payment_signature(params):
logger.error("Неверная подпись или данные запроса.")
@@ -200,7 +208,9 @@ def check_payment_signature(params):
logger.info(f"Signature string before hashing: {signature_string}")
expected_signature = hashlib.md5(signature_string.encode("utf-8")).hexdigest().upper()
expected_signature = (
hashlib.md5(signature_string.encode("utf-8")).hexdigest().upper()
)
logger.info(f"Expected signature: {expected_signature}")
logger.info(f"Received signature: {signature_value}")
@@ -209,25 +219,27 @@ def check_payment_signature(params):
@router.callback_query(F.data == "enter_custom_amount_robokassa")
async def process_custom_amount_selection(callback_query: types.CallbackQuery, state: FSMContext):
async def process_custom_amount_selection(
callback_query: types.CallbackQuery, state: FSMContext
):
tg_id = callback_query.message.chat.id
logger.info(f"User {tg_id} chose to enter a custom amount.")
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="pay_robokassa"))
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="pay_robokassa"))
await callback_query.message.answer(
"Пожалуйста, введите сумму пополнения.",
reply_markup=builder.as_markup(),
)
await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa)
await state.set_state(
ReplenishBalanceState.waiting_for_payment_confirmation_robokassa
)
@router.message(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa)
async def handle_custom_amount_input(
message: types.Message | types.CallbackQuery, state: FSMContext = None, session: Any = None
):
async def handle_custom_amount_input(message: types.Message | types.CallbackQuery, state: FSMContext = None, session: Any = None):
if isinstance(message, types.CallbackQuery):
tg_id = message.message.chat.id
else:
@@ -237,6 +249,7 @@ async def handle_custom_amount_input(
inv_id = 0
try:
conn = await asyncpg.connect(DATABASE_URL)
user_data = await get_temporary_data(conn, tg_id)
await conn.close()
@@ -264,13 +277,9 @@ async def handle_custom_amount_input(
)
if state_type == "waiting_for_payment":
message_text = (
f"Вы выбрали пополнение на {amount} рублей для создания нового ключа. Перейдите по ссылке для оплаты:"
)
message_text = f"Вы выбрали пополнение на {amount} рублей для создания нового ключа. Перейдите по ссылке для оплаты:"
elif state_type == "waiting_for_renewal_payment":
message_text = (
f"Вы выбрали пополнение на {amount} рублей для продления ключа. Перейдите по ссылке для оплаты:"
)
message_text = f"Вы выбрали пополнение на {amount} рублей для продления ключа. Перейдите по ссылке для оплаты:"
else:
await message.answer("Некорректное состояние данных. Попробуйте снова.")
return
+2 -1
View File
@@ -4,7 +4,8 @@
{
"distutils": {
"extra_compile_args": [
"-O2"
"-O2",
"-static-libgcc"
],
"name": "handlers.payments.stars_pay",
"sources": [
+511 -418
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -4,7 +4,8 @@
{
"distutils": {
"extra_compile_args": [
"-O2"
"-O2",
"-static-libgcc"
],
"name": "handlers.payments.yookassa_pay",
"sources": [
+2 -1
View File
@@ -4,7 +4,8 @@
{
"distutils": {
"extra_compile_args": [
"-O2"
"-O2",
"-static-libgcc"
],
"name": "handlers.payments.yoomoney_pay",
"sources": [
+6 -5
View File
@@ -3,12 +3,12 @@ from typing import Any
import aiofiles
import asyncpg
from aiogram import F, Router, types
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import BufferedInputFile, CallbackQuery, InlineKeyboardButton, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import DATABASE_URL, NEWS_MESSAGE, RENEWAL_PLANS
from config import DATABASE_URL, INSTRUCTIONS_BUTTON, NEWS_MESSAGE, RENEWAL_PLANS
from database import get_balance, get_key_count, get_last_payments, get_referral_stats, get_trial
from handlers.buttons.profile import (
ADD_SUB,
@@ -76,9 +76,10 @@ async def process_callback_view_profile(
InlineKeyboardButton(text=INVITE, callback_data="invite"),
InlineKeyboardButton(text=GIFTS, callback_data="gifts"),
)
builder.row(
InlineKeyboardButton(text=INSTRUCTIONS, callback_data="instructions"),
)
if INSTRUCTIONS_BUTTON:
builder.row(
InlineKeyboardButton(text=INSTRUCTIONS, callback_data="instructions"),
)
if admin:
builder.row(InlineKeyboardButton(text="🔧 Администратор", callback_data="admin"))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="start"))
+96 -69
View File
@@ -13,15 +13,14 @@ from aiogram.types import (
)
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot
from config import (
CAPTCHA_ENABLE,
CHANNEL_EXISTS,
CHANNEL_ID,
CHANNEL_REQUIRED,
CHANNEL_URL,
CONNECT_ANDROID,
CONNECT_IOS,
DONATIONS_ENABLE,
DOWNLOAD_ANDROID,
DOWNLOAD_IOS,
SUPPORT_CHAT_URL,
)
from database import (
@@ -31,20 +30,11 @@ from database import (
get_coupon_details,
get_referral_by_referred_id,
get_trial,
update_trial,
)
from handlers.buttons.add_subscribe import (
DOWNLOAD_ANDROID_BUTTON,
DOWNLOAD_IOS_BUTTON,
IMPORT_ANDROID,
IMPORT_IOS,
PC_BUTTON,
TV_BUTTON,
update_balance,
)
from handlers.captcha import generate_captcha
from handlers.keys.key_management import create_key
from handlers.keys.trial_key import create_trial_key
from handlers.texts import INSTRUCTIONS_TRIAL, WELCOME_TEXT, get_about_vpn
from handlers.texts import WELCOME_TEXT, get_about_vpn
from logger import logger
router = Router()
@@ -59,17 +49,42 @@ async def handle_start_callback_query(
@router.message(Command("start"))
async def start_command(message: Message, state: FSMContext, session: Any, admin: bool, captcha: bool = True):
"""Обрабатывает команду /start, включает логику рефералов и подарков."""
"""Обрабатывает команду /start, включая логику проверки подписки, рефералов и подарков."""
logger.info(f"Вызвана функция start_command для пользователя {message.chat.id}")
await state.clear()
# Проверка капчи, если включена
if CAPTCHA_ENABLE and captcha:
captcha = await generate_captcha(message, state)
await message.answer(text=captcha["text"], reply_markup=captcha["markup"])
captcha_data = await generate_captcha(message, state)
await message.answer(text=captcha_data["text"], reply_markup=captcha_data["markup"])
return
if CHANNEL_EXISTS and CHANNEL_REQUIRED:
try:
member = await bot.get_chat_member(CHANNEL_ID, message.chat.id)
if member.status not in ["member", "administrator", "creator"]:
await state.update_data(start_text=message.text)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="✅ Я подписался", callback_data="check_subscription"))
await message.answer(
f"Для использования бота, пожалуйста, подпишитесь на наш канал: {CHANNEL_URL}",
reply_markup=builder.as_markup(),
)
return
except Exception as e:
logger.error(f"Ошибка проверки подписки пользователя {message.chat.id}: {e}")
await state.update_data(start_text=message.text)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="✅ Я подписался", callback_data="check_subscription"))
await message.answer(
f"Пожалуйста, подпишитесь на наш канал: {CHANNEL_URL}", reply_markup=builder.as_markup()
)
return
await process_start_logic(message, state, session, admin)
async def process_start_logic(message: Message, state: FSMContext, session: Any, admin: bool):
if message.text:
try:
connection_exists = await check_connection_exists(message.chat.id)
@@ -79,13 +94,49 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
await add_connection(tg_id=message.chat.id, session=session)
logger.info(f"Пользователь {message.chat.id} успешно добавлен в базу данных.")
if "coupons_" in message.text:
logger.info(f"Обнаружена ссылка на купон: {message.text}")
coupon_code = message.text.split("coupons_")[1].strip()
logger.info(f"Пользователь {message.chat.id} ввёл купон: {coupon_code}")
coupon = await session.fetchrow(
"SELECT id, code, amount, usage_limit, usage_count, is_used FROM coupons WHERE code = $1",
coupon_code,
)
if coupon is None:
logger.warning(f"Купон {coupon_code} не найден.")
await message.answer("❌ Купон не найден!")
return await show_start_menu(message, admin, session)
if coupon["is_used"] or coupon["usage_count"] >= coupon["usage_limit"]:
logger.info(f"Купон {coupon_code} уже использован или исчерпан.")
await message.answer("❌ Этот купон уже использован!")
return await show_start_menu(message, admin, session)
await update_balance(message.chat.id, coupon["amount"])
logger.info(f"Начислено {coupon['amount']} единиц для пользователя {message.chat.id}")
new_usage_count = coupon["usage_count"] + 1
is_used = new_usage_count >= coupon["usage_limit"]
await session.execute(
"UPDATE coupons SET usage_count = $1, is_used = $2 WHERE code = $3",
new_usage_count,
is_used,
coupon_code,
)
logger.info(f"Купон {coupon_code} успешно использован, начислено {coupon['amount']} RUB.")
await message.answer(f"🎉 Ваш баланс пополнен на {coupon['amount']} RUB по купону!")
return await show_start_menu(message, admin, session)
if "gift_" in message.text:
logger.info(f"Обнаружена ссылка на подарок: {message.text}")
parts = message.text.split("gift_")[1].split("_")
gift_id = parts[0]
recipient_tg_id = message.chat.id
gift_info = await get_coupon_details(gift_id, session)
if gift_info is None:
@@ -100,7 +151,13 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
await message.answer("❌ Вы не можете получить подарок от самого себя.")
return await show_start_menu(message, admin, session)
await add_connection(tg_id=recipient_tg_id, session=session)
if not connection_exists:
await add_referral(recipient_tg_id, gift_info["sender_tg_id"], session)
logger.info(
f"Пользователь {recipient_tg_id} теперь является рефералом отправителя подарка {gift_info['sender_tg_id']}."
)
else:
logger.info(f"Пользователь {recipient_tg_id} уже зарегистрирован, реферал не добавляется.")
selected_months = gift_info["selected_months"]
expiry_time = gift_info["expiry_time"]
@@ -117,7 +174,8 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
)
await message.answer(
f"🎉 Ваш подарок на {selected_months} {'месяц' if selected_months == 1 else 'месяца' if selected_months in [2, 3, 4] else 'месяцев'} активирован!"
f"🎉 Ваш подарок на {selected_months} "
f"{'месяц' if selected_months == 1 else 'месяца' if selected_months in [2, 3, 4] else 'месяцев'} активирован!"
)
logger.info(f"Подарок на {selected_months} месяцев активирован для пользователя {recipient_tg_id}.")
return
@@ -137,7 +195,6 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
return await show_start_menu(message, admin, session)
existing_referral = await get_referral_by_referred_id(message.chat.id, session)
if existing_referral:
logger.info(f"Реферал с ID {message.chat.id} уже существует.")
return await show_start_menu(message, admin, session)
@@ -151,7 +208,7 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
return
else:
logger.info(f"Пользователь {message.chat.id} зашел без реферальной ссылки или подарка.")
logger.info(f"Пользователь {message.chat.id} зашел без реферальной ссылки, подарка или купона.")
await show_start_menu(message, admin, session)
@@ -162,6 +219,20 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
await show_start_menu(message, admin, session)
@router.callback_query(F.data == "check_subscription")
async def check_subscription_callback(callback_query: CallbackQuery, state: FSMContext, session: Any, admin: bool):
try:
member = await bot.get_chat_member(CHANNEL_ID, callback_query.from_user.id)
if member.status not in ["member", "administrator", "creator"]:
await callback_query.answer("Вы еще не подписаны на канал!", show_alert=True)
else:
await callback_query.answer("Подписка подтверждена!")
await process_start_logic(callback_query.message, state, session, admin)
except Exception as e:
logger.error(f"Ошибка проверки подписки (callback) для пользователя {callback_query.from_user.id}: {e}")
await callback_query.answer("Ошибка проверки подписки, повторите попытку", show_alert=True)
async def show_start_menu(message: Message, admin: bool, session: Any):
"""Функция для отображения стандартного меню"""
logger.info(f"Показываю главное меню для пользователя {message.chat.id}")
@@ -171,7 +242,7 @@ async def show_start_menu(message: Message, admin: bool, session: Any):
builder = InlineKeyboardBuilder()
if trial_status == 0:
builder.row(InlineKeyboardButton(text="🔗 Подключить VPN", callback_data="connect_vpn"))
builder.row(InlineKeyboardButton(text="🔗 Подключить VPN", callback_data="create_key"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
@@ -203,50 +274,6 @@ async def show_start_menu(message: Message, admin: bool, session: Any):
)
@router.callback_query(F.data == "connect_vpn")
async def handle_connect_vpn(callback_query: CallbackQuery, session: Any):
user_id = callback_query.message.chat.id
trial_key_info = await create_trial_key(user_id, session)
if "error" in trial_key_info:
await callback_query.message.answer(trial_key_info["error"])
else:
await update_trial(user_id, 1, session)
key_message = (
f"🔑 <b>Ваш персональный ключ доступа:</b>\n"
f"<code>{trial_key_info['key']}</code>\n\n"
f"📋 <b>Быстрая инструкция по подключению:</b>\n{INSTRUCTIONS_TRIAL}"
)
email = trial_key_info["email"]
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="💬 Поддержка", url=SUPPORT_CHAT_URL))
builder.row(
InlineKeyboardButton(text=DOWNLOAD_IOS_BUTTON, url=DOWNLOAD_IOS),
InlineKeyboardButton(text=DOWNLOAD_ANDROID_BUTTON, url=DOWNLOAD_ANDROID),
)
builder.row(
InlineKeyboardButton(
text=IMPORT_IOS,
url=f"{CONNECT_IOS}{trial_key_info['key']}",
),
InlineKeyboardButton(
text=IMPORT_ANDROID,
url=f"{CONNECT_ANDROID}{trial_key_info['key']}",
),
)
builder.row(
InlineKeyboardButton(text=PC_BUTTON, callback_data=f"connect_pc|{email}"),
InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{email}"),
)
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(key_message, reply_markup=builder.as_markup())
@router.callback_query(F.data == "about_vpn")
async def handle_about_vpn(callback_query: CallbackQuery):
builder = InlineKeyboardBuilder()
-2
View File
@@ -1,9 +1,7 @@
import json
import random
import re
import secrets
import string
from datetime import datetime, timedelta
import aiohttp
import asyncpg
+2 -5
View File
@@ -24,11 +24,8 @@ def build_sender_kb() -> InlineKeyboardMarkup:
def build_clusters_kb(clusters: list) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
for cluster in clusters:
name = cluster['cluster_name']
builder.button(
text=f"🌐 {name}",
callback_data=AdminSenderCallback(type="cluster", data=name).pack()
)
name = cluster["cluster_name"]
builder.button(text=f"🌐 {name}", callback_data=AdminSenderCallback(type="cluster", data=name).pack())
builder.row(build_admin_back_btn())
builder.adjust(1)