minor improvements
This commit is contained in:
@@ -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-beta(16-fixed)"
|
||||
version = "4.0.0-beta(21)"
|
||||
|
||||
register_middleware(dp)
|
||||
|
||||
|
||||
+33
-25
@@ -723,12 +723,13 @@ async def handle_referral_on_balance_update(tg_id: int, amount: float):
|
||||
referrer_tg_id = referral["tg_id"]
|
||||
level = referral["level"]
|
||||
|
||||
bonus_percent = REFERRAL_BONUS_PERCENTAGES.get(level, 0)
|
||||
if bonus_percent <= 0:
|
||||
bonus = REFERRAL_BONUS_PERCENTAGES.get(level, 0)
|
||||
if bonus <= 0:
|
||||
logger.warning(f"Процент бонуса для уровня {level} равен 0. Пропуск.")
|
||||
continue
|
||||
|
||||
bonus = round(amount * bonus_percent, 2)
|
||||
if isinstance(bonus, (int, float)):
|
||||
bonus = round(bonus, 2)
|
||||
|
||||
if bonus > 0:
|
||||
logger.info(f"Начисление бонуса {bonus} рублей рефереру {referrer_tg_id} на уровне {level}.")
|
||||
@@ -761,7 +762,9 @@ async def get_referral_stats(referrer_tg_id: int):
|
||||
|
||||
total_referrals = await conn.fetchval(
|
||||
"""
|
||||
SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1
|
||||
SELECT COUNT(*)
|
||||
FROM referrals
|
||||
WHERE referrer_tg_id = $1
|
||||
""",
|
||||
referrer_tg_id,
|
||||
)
|
||||
@@ -769,7 +772,9 @@ async def get_referral_stats(referrer_tg_id: int):
|
||||
|
||||
active_referrals = await conn.fetchval(
|
||||
"""
|
||||
SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1 AND reward_issued = TRUE
|
||||
SELECT COUNT(*)
|
||||
FROM referrals
|
||||
WHERE referrer_tg_id = $1 AND reward_issued = TRUE
|
||||
""",
|
||||
referrer_tg_id,
|
||||
)
|
||||
@@ -842,16 +847,17 @@ async def get_referral_stats(referrer_tg_id: int):
|
||||
total_referral_bonus_query = (
|
||||
bonus_cte
|
||||
+ f"""
|
||||
SELECT
|
||||
COALESCE(SUM(ep.amount * (
|
||||
CASE
|
||||
{" ".join([f"WHEN rl.level = {level} THEN {REFERRAL_BONUS_PERCENTAGES[level]}" for level in REFERRAL_BONUS_PERCENTAGES])}
|
||||
ELSE 0
|
||||
END)), 0) AS total_bonus
|
||||
FROM referral_levels rl
|
||||
JOIN earliest_payments ep ON rl.referred_tg_id = ep.tg_id
|
||||
WHERE rl.level <= {MAX_REFERRAL_LEVELS}
|
||||
"""
|
||||
SELECT
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
{" ".join([f"WHEN rl.level = {level} THEN {REFERRAL_BONUS_PERCENTAGES[level]} * ep.amount" if isinstance(REFERRAL_BONUS_PERCENTAGES[level], float) else f"WHEN rl.level = {level} THEN {REFERRAL_BONUS_PERCENTAGES[level]}" for level in REFERRAL_BONUS_PERCENTAGES])}
|
||||
ELSE 0
|
||||
END
|
||||
), 0) AS total_bonus
|
||||
FROM referral_levels rl
|
||||
JOIN earliest_payments ep ON rl.referred_tg_id = ep.tg_id
|
||||
WHERE rl.level <= {MAX_REFERRAL_LEVELS}
|
||||
"""
|
||||
)
|
||||
else:
|
||||
bonus_cte = f"""
|
||||
@@ -875,19 +881,21 @@ async def get_referral_stats(referrer_tg_id: int):
|
||||
WHERE rl.level < {MAX_REFERRAL_LEVELS}
|
||||
)
|
||||
"""
|
||||
|
||||
total_referral_bonus_query = (
|
||||
bonus_cte
|
||||
+ f"""
|
||||
SELECT
|
||||
COALESCE(SUM(p.amount * (
|
||||
CASE
|
||||
{" ".join([f"WHEN rl.level = {level} THEN {REFERRAL_BONUS_PERCENTAGES[level]}" for level in REFERRAL_BONUS_PERCENTAGES])}
|
||||
ELSE 0
|
||||
END)), 0) AS total_bonus
|
||||
FROM referral_levels rl
|
||||
JOIN payments p ON rl.referred_tg_id = p.tg_id
|
||||
WHERE p.status = 'success' AND rl.level <= {MAX_REFERRAL_LEVELS}
|
||||
"""
|
||||
SELECT
|
||||
COALESCE(SUM(
|
||||
CASE
|
||||
{" ".join([f"WHEN rl.level = {level} THEN {REFERRAL_BONUS_PERCENTAGES[level]} * p.amount" if isinstance(REFERRAL_BONUS_PERCENTAGES[level], float) else f"WHEN rl.level = {level} THEN {REFERRAL_BONUS_PERCENTAGES[level]}" for level in REFERRAL_BONUS_PERCENTAGES])}
|
||||
ELSE 0
|
||||
END
|
||||
), 0) AS total_bonus
|
||||
FROM referral_levels rl
|
||||
JOIN payments p ON rl.referred_tg_id = p.tg_id
|
||||
WHERE p.status = 'success' AND rl.level <= {MAX_REFERRAL_LEVELS}
|
||||
"""
|
||||
)
|
||||
|
||||
total_referral_bonus = await conn.fetchval(total_referral_bonus_query, referrer_tg_id)
|
||||
|
||||
@@ -103,15 +103,8 @@ async def handle_coupon_data_input(message: Message, state: FSMContext, session:
|
||||
)
|
||||
async def handle_coupons_list(callback_query: CallbackQuery, session: Any):
|
||||
try:
|
||||
if ":" in callback_query.data:
|
||||
parts = callback_query.data.split(":")
|
||||
if len(parts) > 1 and parts[1].isdigit():
|
||||
page = int(parts[1])
|
||||
else:
|
||||
page = 1
|
||||
else:
|
||||
page = 1
|
||||
|
||||
data = AdminPanelCallback.unpack(callback_query.data)
|
||||
page = data.page if data.page is not None else 1
|
||||
per_page = 10
|
||||
result = await get_all_coupons(session, page, per_page)
|
||||
coupons = result["coupons"]
|
||||
@@ -123,9 +116,8 @@ async def handle_coupons_list(callback_query: CallbackQuery, session: Any):
|
||||
)
|
||||
return
|
||||
|
||||
kb = build_coupons_list_kb(coupons)
|
||||
kb = build_coupons_list_kb(coupons, result["current_page"], result["pages"])
|
||||
coupon_list = "📜 Список всех купонов:\n\n"
|
||||
|
||||
for coupon in coupons:
|
||||
coupon_list += (
|
||||
f"🏷️ <b>Код:</b> {coupon['code']}\n"
|
||||
@@ -134,31 +126,47 @@ async def handle_coupons_list(callback_query: CallbackQuery, session: Any):
|
||||
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)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении списка купонов: {e}")
|
||||
await callback_query.message.answer("Произошла ошибка при получении списка купонов.")
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
AdminCouponDeleteCallback.filter(),
|
||||
IsAdminFilter(),
|
||||
)
|
||||
@router.callback_query(AdminCouponDeleteCallback.filter(), IsAdminFilter())
|
||||
async def handle_coupon_delete(callback_query: CallbackQuery, callback_data: AdminCouponDeleteCallback, session: Any):
|
||||
coupon_code = callback_data.coupon_code
|
||||
|
||||
try:
|
||||
result = await delete_coupon(coupon_code, session)
|
||||
|
||||
if result:
|
||||
await handle_coupons_list(callback_query, session)
|
||||
await callback_query.edit_text(f"Купон {coupon_code} удалён!")
|
||||
else:
|
||||
await callback_query.message.edit_text(
|
||||
text=f"❌ Купон с кодом <b>{coupon_code}</b> не найден.",
|
||||
)
|
||||
await handle_coupons_list(callback_query, session)
|
||||
|
||||
await callback_query.edit_text(f"❌ Купон с кодом {coupon_code} не найден.", show_alert=True)
|
||||
await update_coupons_list(callback_query.message, session)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении купона: {e}")
|
||||
await callback_query.edit_text("Произошла ошибка при удалении купона.", show_alert=True)
|
||||
|
||||
|
||||
async def update_coupons_list(message, session: Any, page: int = 1):
|
||||
per_page = 10
|
||||
result = await get_all_coupons(session, page, per_page)
|
||||
coupons = result["coupons"]
|
||||
|
||||
if not coupons:
|
||||
await message.edit_text(
|
||||
text="❌ На данный момент нет доступных купонов!",
|
||||
reply_markup=build_admin_back_kb("coupons"),
|
||||
)
|
||||
return
|
||||
|
||||
kb = build_coupons_list_kb(coupons, result["current_page"], result["pages"])
|
||||
coupon_list = "📜 Список всех купонов:\n\n"
|
||||
for coupon in coupons:
|
||||
coupon_list += (
|
||||
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"
|
||||
f"🔗 <b>Ссылка:</b> <code>https://t.me/{USERNAME_BOT}?start=coupons_{coupon['code']}</code>\n"
|
||||
)
|
||||
await message.edit_text(text=coupon_list, reply_markup=kb)
|
||||
|
||||
@@ -15,7 +15,16 @@ async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMC
|
||||
text = f"🤖 Панель администратора\n📌 Версия бота: {version}"
|
||||
|
||||
await state.clear()
|
||||
await callback_query.message.edit_text(text=text, reply_markup=build_panel_kb())
|
||||
|
||||
if callback_query.message.text:
|
||||
await callback_query.message.edit_text(text=text, reply_markup=build_panel_kb())
|
||||
else:
|
||||
try:
|
||||
await callback_query.message.delete()
|
||||
except Exception as e:
|
||||
print(f"Ошибка при удалении сообщения: {e}")
|
||||
|
||||
await callback_query.message.answer(text=text, reply_markup=build_panel_kb())
|
||||
|
||||
|
||||
@router.callback_query(F.data == "admin", IsAdminFilter())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
GIFT = "🎁 Подарить подписку"
|
||||
MY_GIFTS = "🎁 Мои подарки"
|
||||
PROFILE = "👤 Личный кабинет"
|
||||
BACK = "⬅️ Назад"
|
||||
BACK = "В меню подарков"
|
||||
GIFTS_ABOUT = "<b>Дарите подарки и следите, чтобы они дошли до адресата! 🎄</b>"
|
||||
SHARE_GIFT = "🎁 Поделиться подарком"
|
||||
|
||||
@@ -395,9 +395,7 @@ async def finalize_key_creation(
|
||||
callback_query: CallbackQuery,
|
||||
old_key_name: str = None,
|
||||
):
|
||||
"""Финализирует создание ключа с выбранной страной.
|
||||
Если old_key_name передан, после создания нового ключа старый будет удалён.
|
||||
"""
|
||||
"""Финализирует создание ключа с выбором стран."""
|
||||
|
||||
if not await check_connection_exists(tg_id):
|
||||
await add_connection(tg_id, balance=0.0, trial=0, session=session)
|
||||
@@ -405,12 +403,15 @@ async def finalize_key_creation(
|
||||
|
||||
expiry_time = expiry_time.astimezone(moscow_tz)
|
||||
|
||||
while True:
|
||||
key_name = generate_random_email()
|
||||
existing_key = await get_key_details(key_name, session)
|
||||
if not existing_key:
|
||||
break
|
||||
logger.warning(f"Key name '{key_name}' already exists for user {tg_id}. Generating a new one.")
|
||||
if old_key_name:
|
||||
key_name = old_key_name
|
||||
else:
|
||||
while True:
|
||||
key_name = generate_random_email()
|
||||
existing_key = await get_key_details(key_name, session)
|
||||
if not existing_key:
|
||||
break
|
||||
logger.warning(f"Key name '{key_name}' already exists for user {tg_id}. Generating a new one.")
|
||||
|
||||
client_id = str(uuid.uuid4())
|
||||
email = key_name.lower()
|
||||
@@ -426,6 +427,33 @@ async def finalize_key_creation(
|
||||
if not server_info:
|
||||
raise ValueError(f"Сервер {selected_country} не найден.")
|
||||
|
||||
if old_key_name:
|
||||
old_key_details = await get_key_details(old_key_name, session)
|
||||
old_client_id = old_key_details.get("client_id") if old_key_details else None
|
||||
old_email = old_key_details.get("email") if old_key_details else None
|
||||
old_server_id = old_key_details.get("server_id") if old_key_details else None
|
||||
|
||||
if old_client_id and old_email and old_server_id:
|
||||
old_server_info = await session.fetchrow(
|
||||
"SELECT api_url, inbound_id, server_name FROM servers WHERE server_name = $1",
|
||||
old_server_id,
|
||||
)
|
||||
|
||||
if old_server_info:
|
||||
xui = AsyncApi(
|
||||
old_server_info["api_url"],
|
||||
username=ADMIN_USERNAME,
|
||||
password=ADMIN_PASSWORD,
|
||||
)
|
||||
deletion_success = await delete_client(
|
||||
xui,
|
||||
old_server_info["inbound_id"],
|
||||
old_email,
|
||||
old_client_id,
|
||||
)
|
||||
if not deletion_success:
|
||||
raise ValueError(f"Не удалось удалить клиента с сервера {old_server_id}.")
|
||||
|
||||
semaphore = asyncio.Semaphore(2)
|
||||
await create_client_on_server(
|
||||
server_info=server_info,
|
||||
@@ -437,15 +465,37 @@ 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,
|
||||
email,
|
||||
expiry_timestamp,
|
||||
public_link,
|
||||
selected_country,
|
||||
session,
|
||||
)
|
||||
|
||||
if old_key_name:
|
||||
await session.execute(
|
||||
"""
|
||||
UPDATE keys
|
||||
SET client_id = $1, email = $2, expiry_time = $3, key = $4, server_id = $5
|
||||
WHERE tg_id = $6 AND email = $7
|
||||
""",
|
||||
client_id,
|
||||
email,
|
||||
expiry_timestamp,
|
||||
public_link,
|
||||
selected_country,
|
||||
tg_id,
|
||||
old_key_name,
|
||||
)
|
||||
else:
|
||||
created_at = int(datetime.now(moscow_tz).timestamp() * 1000)
|
||||
await session.execute(
|
||||
"""
|
||||
INSERT INTO keys (tg_id, client_id, email, created_at, expiry_time, key, server_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
""",
|
||||
tg_id,
|
||||
client_id,
|
||||
email,
|
||||
created_at,
|
||||
expiry_timestamp,
|
||||
public_link,
|
||||
selected_country,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error while creating the key for user {tg_id}: {e}")
|
||||
@@ -481,44 +531,3 @@ async def finalize_key_creation(
|
||||
|
||||
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}")
|
||||
|
||||
@@ -11,21 +11,30 @@ import asyncpg
|
||||
from aiohttp import web
|
||||
|
||||
from config import (
|
||||
DATABASE_URL, PROJECT_NAME, SUB_MESSAGE, SUPERNODE,
|
||||
TRANSITION_DATE_STR, USE_COUNTRY_SELECTION, SUPPORT_CHAT_URL, USERNAME_BOT, TOTAL_GB
|
||||
DATABASE_URL,
|
||||
PROJECT_NAME,
|
||||
SUB_MESSAGE,
|
||||
SUPERNODE,
|
||||
SUPPORT_CHAT_URL,
|
||||
TOTAL_GB,
|
||||
TRANSITION_DATE_STR,
|
||||
USE_COUNTRY_SELECTION,
|
||||
USERNAME_BOT,
|
||||
)
|
||||
from database import get_key_details, get_servers
|
||||
from logger import logger
|
||||
from handlers.utils import convert_to_bytes
|
||||
from logger import logger
|
||||
|
||||
db_pool = None
|
||||
|
||||
|
||||
async def init_db_pool():
|
||||
"""Инициализация пула соединений, если он ещё не создан."""
|
||||
global db_pool
|
||||
if not db_pool:
|
||||
db_pool = await asyncpg.create_pool(dsn=DATABASE_URL, min_size=5, max_size=20)
|
||||
|
||||
|
||||
async def fetch_url_content(url, tg_id):
|
||||
"""Получает содержимое подписки по URL и декодирует его."""
|
||||
try:
|
||||
@@ -47,6 +56,7 @@ async def fetch_url_content(url, tg_id):
|
||||
logger.error(f"Ошибка при получении {url} для tg_id: {tg_id}: {e}")
|
||||
return []
|
||||
|
||||
|
||||
async def combine_unique_lines(urls, tg_id, query_string):
|
||||
"""Объединяет строки подписки, удаляя дубликаты."""
|
||||
if SUPERNODE:
|
||||
@@ -68,11 +78,13 @@ async def combine_unique_lines(urls, tg_id, query_string):
|
||||
logger.info(f"Объединено {len(all_lines)} строк после фильтрации и удаления дубликатов для tg_id: {tg_id}")
|
||||
return list(all_lines)
|
||||
|
||||
|
||||
transition_date = datetime.strptime(TRANSITION_DATE_STR, "%Y-%m-%d %H:%M:%S")
|
||||
transition_timestamp_ms = int(transition_date.timestamp() * 1000)
|
||||
transition_timestamp_ms_adjusted = transition_timestamp_ms - (3 * 60 * 60 * 1000)
|
||||
logger.info(f"Время перехода (с поправкой на часовой пояс): {transition_timestamp_ms_adjusted}")
|
||||
|
||||
|
||||
async def get_subscription_urls(server_id: str, email: str, conn) -> list:
|
||||
"""
|
||||
Универсальная функция, которая в зависимости от флага USE_COUNTRY_SELECTION
|
||||
@@ -81,9 +93,7 @@ async def get_subscription_urls(server_id: str, email: str, conn) -> list:
|
||||
"""
|
||||
if USE_COUNTRY_SELECTION:
|
||||
logger.info(f"Режим выбора страны активен. Ищем сервер {server_id} в БД.")
|
||||
server_data = await conn.fetchrow(
|
||||
"SELECT subscription_url FROM servers WHERE server_name = $1", server_id
|
||||
)
|
||||
server_data = await conn.fetchrow("SELECT subscription_url FROM servers WHERE server_name = $1", server_id)
|
||||
if not server_data:
|
||||
logger.warning(f"Не найден сервер {server_id} в БД!")
|
||||
return []
|
||||
@@ -102,6 +112,7 @@ async def get_subscription_urls(server_id: str, email: str, conn) -> list:
|
||||
logger.info(f"Найдено {len(urls)} URL-адресов в кластере {server_id}")
|
||||
return urls
|
||||
|
||||
|
||||
def calculate_traffic(cleaned_subscriptions, expiry_time_ms):
|
||||
expire_timestamp = int(expiry_time_ms / 1000) if expiry_time_ms else 0
|
||||
if TOTAL_GB != 0:
|
||||
@@ -117,8 +128,8 @@ def calculate_traffic(cleaned_subscriptions, expiry_time_ms):
|
||||
country = parts[0].strip()
|
||||
remaining_str = parts[1].strip() if len(parts) == 2 else ""
|
||||
if remaining_str:
|
||||
remaining_str = remaining_str.replace(',', '.')
|
||||
m_total = re.search(r'([\d\.]+)\s*([GMKTB]B)', remaining_str, re.IGNORECASE)
|
||||
remaining_str = remaining_str.replace(",", ".")
|
||||
m_total = re.search(r"([\d\.]+)\s*([GMKTB]B)", remaining_str, re.IGNORECASE)
|
||||
if m_total:
|
||||
value = float(m_total.group(1))
|
||||
unit = m_total.group(2).upper()
|
||||
@@ -136,6 +147,7 @@ def calculate_traffic(cleaned_subscriptions, expiry_time_ms):
|
||||
|
||||
return f"upload=0; download={consumed_traffic_bytes}; total={total_traffic_bytes}; expire={expire_timestamp}"
|
||||
|
||||
|
||||
async def handle_subscription(request, old_subscription=False):
|
||||
"""Обрабатывает запрос на подписку (старую или новую)."""
|
||||
email = request.match_info.get("email")
|
||||
@@ -145,7 +157,9 @@ async def handle_subscription(request, old_subscription=False):
|
||||
logger.warning("Получен запрос с отсутствующими параметрами")
|
||||
return web.Response(text="❌ Неверные параметры запроса.", status=400)
|
||||
|
||||
logger.info(f"Обработка запроса для {'старого' if old_subscription else 'нового'} клиента: email={email}, tg_id={tg_id}")
|
||||
logger.info(
|
||||
f"Обработка запроса для {'старого' if old_subscription else 'нового'} клиента: email={email}, tg_id={tg_id}"
|
||||
)
|
||||
await init_db_pool()
|
||||
|
||||
async with db_pool.acquire() as conn:
|
||||
@@ -200,7 +214,7 @@ async def handle_subscription(request, old_subscription=False):
|
||||
traffic = ""
|
||||
for part in parts[1:]:
|
||||
part_decoded = urllib.parse.unquote(part).strip()
|
||||
if re.search(r'\d+(?:[.,]\d+)?\s*(?:GB|MB|KB|TB)', part_decoded, re.IGNORECASE):
|
||||
if re.search(r"\d+(?:[.,]\d+)?\s*(?:GB|MB|KB|TB)", part_decoded, re.IGNORECASE):
|
||||
traffic = part_decoded
|
||||
break
|
||||
meta_clean = f"{country} - {traffic}" if traffic else country
|
||||
@@ -227,14 +241,14 @@ async def handle_subscription(request, old_subscription=False):
|
||||
"support-url": SUPPORT_CHAT_URL,
|
||||
"announce": "base64:" + base64.b64encode(announce_str.encode("utf-8")).decode("utf-8"),
|
||||
"profile-web-page-url": f"https://t.me/{USERNAME_BOT}",
|
||||
"subscription-userinfo": subscription_userinfo
|
||||
"subscription-userinfo": subscription_userinfo,
|
||||
}
|
||||
elif "Hiddify" in user_agent:
|
||||
encoded_project_name = f"{PROJECT_NAME}\n📄 Подписка: {email}"
|
||||
headers = {
|
||||
"profile-update-interval": "3",
|
||||
"profile-title": "base64:" + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"),
|
||||
"subscription-userinfo": subscription_userinfo
|
||||
"subscription-userinfo": subscription_userinfo,
|
||||
}
|
||||
else:
|
||||
encoded_project_name = f"{PROJECT_NAME}\n{subscription_info}"
|
||||
@@ -242,16 +256,18 @@ async def handle_subscription(request, old_subscription=False):
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
"Content-Disposition": "inline",
|
||||
"profile-update-interval": "3",
|
||||
"profile-title": "base64:" + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8")
|
||||
"profile-title": "base64:" + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"),
|
||||
}
|
||||
|
||||
logger.info(f"Возвращаем объединенные подписки для email: {email}")
|
||||
return web.Response(text=base64_encoded, headers=headers)
|
||||
|
||||
|
||||
async def handle_old_subscription(request):
|
||||
"""Обработка запроса для старых клиентов."""
|
||||
return await handle_subscription(request, old_subscription=True)
|
||||
|
||||
|
||||
async def handle_new_subscription(request):
|
||||
"""Обработка запроса для новых клиентов."""
|
||||
return await handle_subscription(request, old_subscription=False)
|
||||
|
||||
@@ -280,12 +280,28 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time:
|
||||
|
||||
if last_notification_time is None:
|
||||
keyboard = build_notification_kb(email)
|
||||
|
||||
if NOTIFY_DELETE_DELAY > 0:
|
||||
hours = NOTIFY_DELETE_DELAY // 60
|
||||
minutes = NOTIFY_DELETE_DELAY % 60
|
||||
|
||||
if hours > 0:
|
||||
if minutes > 0:
|
||||
delay_message = f"⚠️ Ваша подписка {email} истекла.\n\nЕсли вы не продлите её, она будет удалена через {hours} час{'а' if hours == 1 else 'ов'} и {minutes} минут."
|
||||
else:
|
||||
delay_message = f"⚠️ Ваша подписка {email} истекла.\n\nЕсли вы не продлите её, она будет удалена через {hours} час{'а' if hours == 1 else 'ов'}."
|
||||
else:
|
||||
delay_message = f"⚠️ Ваша подписка {email} истекла.\n\nЕсли вы не продлите её, она будет удалена через {NOTIFY_DELETE_DELAY} минут."
|
||||
|
||||
else:
|
||||
delay_message = f"⚠ Ваша подписка {email} истекла!\n\nПродлите доступ, чтобы возобновить услуги."
|
||||
|
||||
try:
|
||||
await send_notification(
|
||||
bot,
|
||||
tg_id,
|
||||
"notify_expired.jpg",
|
||||
f"⚠ Ваша подписка {email} истекла!\n\nПродлите доступ, чтобы возобновить услуги.",
|
||||
delay_message,
|
||||
keyboard,
|
||||
)
|
||||
await add_notification(tg_id, notification_id, session=conn)
|
||||
|
||||
@@ -171,7 +171,7 @@ async def notify_users_no_traffic(bot: Bot, conn: asyncpg.Connection, current_ti
|
||||
keyboard = builder.as_markup()
|
||||
|
||||
message = (
|
||||
"⚠ <b>Ваша VPN-подписка активна, но трафик не используется.</b>\n\n"
|
||||
f"⚠ <b>Ваша VPN-подписка {email} активна, но трафик не используется.</b>\n\n"
|
||||
"<blockquote>Если у вас возникли сложности с подключением, "
|
||||
"нажмите кнопку ниже, чтобы связаться с поддержкой.</blockquote>\n\n"
|
||||
"🛠 Мы поможем вам разобраться! 💡"
|
||||
|
||||
+2219
-1853
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+11
-2
@@ -17,6 +17,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from config import (
|
||||
DATABASE_URL,
|
||||
INLINE_MODE,
|
||||
INSTRUCTIONS_BUTTON,
|
||||
NEWS_MESSAGE,
|
||||
REFERRAL_OFFERS,
|
||||
@@ -37,6 +38,7 @@ from handlers.buttons.profile import (
|
||||
PAYMENT,
|
||||
)
|
||||
from handlers.texts import get_referral_link, invite_message_send, profile_message_send
|
||||
from keyboards.admin.panel_kb import AdminPanelCallback
|
||||
from logger import logger
|
||||
|
||||
from .utils import edit_or_send_message
|
||||
@@ -91,7 +93,10 @@ async def process_callback_view_profile(
|
||||
if INSTRUCTIONS_BUTTON:
|
||||
builder.row(InlineKeyboardButton(text=INSTRUCTIONS, callback_data="instructions"))
|
||||
if admin:
|
||||
builder.row(InlineKeyboardButton(text="🔧 Администратор", callback_data="admin"))
|
||||
builder.row(
|
||||
InlineKeyboardButton(text="🔧 Администратор", callback_data=AdminPanelCallback(action="admin").pack())
|
||||
)
|
||||
|
||||
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="start"))
|
||||
|
||||
await edit_or_send_message(
|
||||
@@ -196,7 +201,11 @@ async def invite_handler(callback_query: CallbackQuery):
|
||||
image_path = os.path.join("img", "pic_invite.jpg")
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text="👥 Пригласить друга", switch_inline_query="invite")
|
||||
if INLINE_MODE:
|
||||
builder.button(text="👥 Пригласить друга", switch_inline_query="invite")
|
||||
else:
|
||||
invite_text = f"\nПриглашаю тебя пользоваться действительно быстрым VPN вместе:\n\n{referral_link}"
|
||||
builder.button(text="👥 Пригласить друга", switch_inline_query=invite_text)
|
||||
builder.button(text="👤 Личный кабинет", callback_data="profile")
|
||||
builder.adjust(1)
|
||||
|
||||
|
||||
+4
-1
@@ -32,6 +32,7 @@ from database import (
|
||||
from handlers.captcha import generate_captcha
|
||||
from handlers.keys.key_management import create_key
|
||||
from handlers.texts import WELCOME_TEXT, get_about_vpn
|
||||
from keyboards.admin.panel_kb import AdminPanelCallback
|
||||
from logger import logger
|
||||
|
||||
from .utils import edit_or_send_message
|
||||
@@ -338,7 +339,9 @@ async def show_start_menu(message: Message, admin: bool, session: Any):
|
||||
builder.row(InlineKeyboardButton(text="📞 Поддержка", url=SUPPORT_CHAT_URL))
|
||||
|
||||
if admin:
|
||||
builder.row(InlineKeyboardButton(text="🔧 Администратор", callback_data="admin"))
|
||||
builder.row(
|
||||
InlineKeyboardButton(text="🔧 Администратор", callback_data=AdminPanelCallback(action="admin").pack())
|
||||
)
|
||||
|
||||
builder.row(InlineKeyboardButton(text="🌐 О VPN", callback_data="about_vpn"))
|
||||
|
||||
|
||||
@@ -207,6 +207,7 @@ async def edit_or_send_message(
|
||||
disable_web_page_preview=disable_web_page_preview,
|
||||
)
|
||||
|
||||
|
||||
def convert_to_bytes(value: float, unit: str) -> int:
|
||||
"""
|
||||
Конвертирует значение с указанной единицей измерения в байты.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from aiogram.filters.callback_data import CallbackData
|
||||
from aiogram.types import InlineKeyboardMarkup
|
||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_btn
|
||||
@@ -17,7 +17,7 @@ def build_coupons_kb() -> InlineKeyboardMarkup:
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def build_coupons_list_kb(coupons: list) -> InlineKeyboardMarkup:
|
||||
def build_coupons_list_kb(coupons: list, current_page: int, total_pages: int) -> InlineKeyboardMarkup:
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
for coupon in coupons:
|
||||
@@ -27,6 +27,24 @@ def build_coupons_list_kb(coupons: list) -> InlineKeyboardMarkup:
|
||||
callback_data=AdminCouponDeleteCallback(coupon_code=coupon_code).pack(),
|
||||
)
|
||||
|
||||
pagination_buttons = []
|
||||
if current_page > 1:
|
||||
pagination_buttons.append(
|
||||
InlineKeyboardButton(
|
||||
text="⬅️ Назад",
|
||||
callback_data=AdminPanelCallback(action="coupons_list", page=current_page - 1).pack(),
|
||||
)
|
||||
)
|
||||
if current_page < total_pages:
|
||||
pagination_buttons.append(
|
||||
InlineKeyboardButton(
|
||||
text="Вперед ➡️",
|
||||
callback_data=AdminPanelCallback(action="coupons_list", page=current_page + 1).pack(),
|
||||
)
|
||||
)
|
||||
if pagination_buttons:
|
||||
builder.row(*pagination_buttons)
|
||||
|
||||
builder.row(build_admin_back_btn("coupons"))
|
||||
builder.adjust(2)
|
||||
return builder.as_markup()
|
||||
|
||||
@@ -5,6 +5,12 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
class AdminPanelCallback(CallbackData, prefix="admin_panel"):
|
||||
action: str
|
||||
page: int
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if "page" not in kwargs or kwargs["page"] is None:
|
||||
kwargs["page"] = 1
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
def build_panel_kb() -> InlineKeyboardMarkup:
|
||||
|
||||
+4
-4
@@ -18,15 +18,15 @@ PING_SEMAPHORE = asyncio.Semaphore(3)
|
||||
|
||||
|
||||
async def ping_server(server_ip: str) -> bool:
|
||||
"""Пингует сервер через ICMP или TCP 443, если ICMP недоступен."""
|
||||
"""Пингует сервер через ICMP или TCP 443, если ICMP недоступен или возникает ошибка."""
|
||||
async with PING_SEMAPHORE:
|
||||
try:
|
||||
response = ping(server_ip, timeout=3)
|
||||
return response is not None and response is not False
|
||||
except PermissionError:
|
||||
if response is not None and response is not False:
|
||||
return True
|
||||
return await check_tcp_connection(server_ip, 443)
|
||||
except Exception:
|
||||
return False
|
||||
return await check_tcp_connection(server_ip, 443)
|
||||
|
||||
|
||||
async def check_tcp_connection(host: str, port: int) -> bool:
|
||||
|
||||
Reference in New Issue
Block a user