minor improvements

This commit is contained in:
Vladless
2025-02-21 22:36:03 +03:00
parent c9e791fa76
commit 769f5eaf9a
18 changed files with 2459 additions and 1990 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-beta(16-fixed)"
version = "4.0.0-beta(21)"
register_middleware(dp)
+33 -25
View File
@@ -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)
+33 -25
View File
@@ -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)
+10 -1
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
GIFT = "🎁 Подарить подписку"
MY_GIFTS = "🎁 Мои подарки"
PROFILE = "👤 Личный кабинет"
BACK = "⬅️ Назад"
BACK = "В меню подарков"
GIFTS_ABOUT = "<b>Дарите подарки и следите, чтобы они дошли до адресата! 🎄</b>"
SHARE_GIFT = "🎁 Поделиться подарком"
+68 -59
View File
@@ -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}")
+29 -13
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+11 -2
View File
@@ -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
View File
@@ -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"))
+1
View File
@@ -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:
"""
Конвертирует значение с указанной единицей измерения в байты.
+20 -2
View File
@@ -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()
+6
View File
@@ -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:
+1 -1
View File
File diff suppressed because one or more lines are too long
+4 -4
View File
@@ -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: