12 Commits

Author SHA1 Message Date
Vladless 5577780565 ping servers and notifications 2024-12-06 05:19:44 +03:00
Vladless 5db2b7c861 ping servers and notifications 2024-12-06 04:48:58 +03:00
Vladless 370ceb8382 back button in custom pay 2024-12-06 02:25:40 +03:00
Vladless 3226438e69 back button in custom pay 2024-12-06 02:25:18 +03:00
Vladless c432edf8fc preview pc_instruction disable 2024-12-06 01:42:14 +03:00
Vladless 49aa863eb4 Server availability 2024-12-06 01:14:25 +03:00
Vladless c58039cb9e logs folder and autoremove 2024-12-06 00:46:54 +03:00
Vladless ee242c98c7 sales in texts 2024-12-05 23:56:06 +03:00
Vladless f3dfb03749 backup for 3x-ui 2024-12-05 23:31:48 +03:00
Vladless 02ea333af1 fix refferal 2024-12-05 22:59:34 +03:00
Vladless 142a5ffaab fix refferal 2024-12-05 22:45:10 +03:00
Vladless db24a9abda backup time 2024-12-05 21:09:37 +03:00
17 changed files with 289 additions and 50 deletions
+5
View File
@@ -92,3 +92,8 @@ def _cleanup_old_backups():
logger.info("Старые бэкапы удалены.") logger.info("Старые бэкапы удалены.")
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
logger.error(f"Ошибка при удалении старых бэкапов: {e}") logger.error(f"Ошибка при удалении старых бэкапов: {e}")
async def create_backup_and_send_to_admins(xui):
await xui.login()
await xui.database.export()
+9 -10
View File
@@ -685,16 +685,20 @@ async def handle_referral_on_balance_update(tg_id: int, amount: float):
conn = await asyncpg.connect(DATABASE_URL) conn = await asyncpg.connect(DATABASE_URL)
logger.info(f"Начало обработки реферальной системы для пользователя {tg_id}") logger.info(f"Начало обработки реферальной системы для пользователя {tg_id}")
# Максимальное количество уровней рефералов
MAX_REFERRAL_LEVELS = len(REFERRAL_BONUS_PERCENTAGES.keys()) MAX_REFERRAL_LEVELS = len(REFERRAL_BONUS_PERCENTAGES.keys())
# Текущий уровень для отслеживания visited_tg_ids = set()
current_tg_id = tg_id current_tg_id = tg_id
referral_chain = [] referral_chain = []
# Собираем цепочку рефералов
for level in range(1, MAX_REFERRAL_LEVELS + 1): for level in range(1, MAX_REFERRAL_LEVELS + 1):
# Получаем информацию о рефере текущего уровня if current_tg_id in visited_tg_ids:
logger.warning(f"Обнаружен цикл в реферальной цепочке для пользователя {current_tg_id}. Прекращение.")
break
visited_tg_ids.add(current_tg_id)
referral = await conn.fetchrow( referral = await conn.fetchrow(
""" """
SELECT referrer_tg_id SELECT referrer_tg_id
@@ -704,30 +708,25 @@ async def handle_referral_on_balance_update(tg_id: int, amount: float):
current_tg_id, current_tg_id,
) )
# Если реферер не найден, прекращаем поиск
if not referral: if not referral:
break break
referrer_tg_id = referral['referrer_tg_id'] referrer_tg_id = referral['referrer_tg_id']
referral_chain.append({'tg_id': referrer_tg_id, 'level': level}) referral_chain.append({'tg_id': referrer_tg_id, 'level': level})
# Переходим к следующему уровню
current_tg_id = referrer_tg_id current_tg_id = referrer_tg_id
# Начисляем бонусы по цепочке рефералов
for referral in referral_chain: for referral in referral_chain:
referrer_tg_id = referral['tg_id'] referrer_tg_id = referral['tg_id']
level = referral['level'] level = referral['level']
# Расчет бонуса для текущего уровня
bonus_percent = REFERRAL_BONUS_PERCENTAGES.get(level, 0) bonus_percent = REFERRAL_BONUS_PERCENTAGES.get(level, 0)
bonus = amount * bonus_percent bonus = amount * bonus_percent
bonus = max(bonus, 0) # Гарантируем неотрицательный бонус bonus = max(bonus, 0)
if bonus > 0: if bonus > 0:
logger.info(f"Начисление бонуса {bonus} рублей рефереру {referrer_tg_id} на уровне {level}") logger.info(f"Начисление бонуса {bonus} рублей рефереру {referrer_tg_id} на уровне {level}")
# Обновляем баланс реферера
await update_balance(referrer_tg_id, bonus) await update_balance(referrer_tg_id, bonus)
except Exception as e: except Exception as e:
+75 -1
View File
@@ -4,8 +4,10 @@ from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder from aiogram.utils.keyboard import InlineKeyboardBuilder
import asyncpg import asyncpg
from py3xui import AsyncApi
from config import DATABASE_URL from backup import create_backup_and_send_to_admins
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL
from database import check_unique_server_name, get_servers_from_db from database import check_unique_server_name, get_servers_from_db
from filters.admin import IsAdminFilter from filters.admin import IsAdminFilter
@@ -267,6 +269,13 @@ async def handle_manage_cluster(callback_query: types.CallbackQuery, state: FSMC
) )
builder.row(InlineKeyboardButton(text=" Добавить сервер", callback_data=f"add_server|{cluster_name}")) builder.row(InlineKeyboardButton(text=" Добавить сервер", callback_data=f"add_server|{cluster_name}"))
builder.row(
InlineKeyboardButton(text="🌐 Доступность серверов", callback_data=f"server_availability|{cluster_name}")
)
builder.row(InlineKeyboardButton(text="💾 Создать бэкап кластера", callback_data=f"backup_cluster|{cluster_name}"))
builder.row(InlineKeyboardButton(text="🔙 Назад в управление кластерами", callback_data="servers_editor")) builder.row(InlineKeyboardButton(text="🔙 Назад в управление кластерами", callback_data="servers_editor"))
await callback_query.message.answer( await callback_query.message.answer(
@@ -274,6 +283,44 @@ async def handle_manage_cluster(callback_query: types.CallbackQuery, state: FSMC
) )
@router.callback_query(F.data.startswith("server_availability|"), IsAdminFilter())
async def handle_check_server_availability(callback_query: types.CallbackQuery):
cluster_name = callback_query.data.split("|")[1]
servers = await get_servers_from_db()
cluster_servers = servers.get(cluster_name, [])
if not cluster_servers:
await callback_query.answer(f"Кластер '{cluster_name}' не содержит серверов.")
return
in_progress_message = await callback_query.message.answer(
f"🖥️ Проверка доступности серверов для кластера {cluster_name}.\n\n"
"Это может занять до 1 минуты, пожалуйста, подождите..."
)
availability_message = f"🖥️ Проверка доступности серверов для кластера {cluster_name} завершена:\n\n"
for server in cluster_servers:
xui = AsyncApi(server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD)
try:
await xui.login()
online_users = len(await xui.client.online())
availability_message += f"🌍 {server['server_name']}: {online_users} активных пользователей.\n"
except Exception as e:
availability_message += f"{server['server_name']}: Не удалось получить информацию. Ошибка: {e}\n"
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}"))
await in_progress_message.edit_text(availability_message, reply_markup=builder.as_markup())
await callback_query.answer()
@router.callback_query(F.data.startswith("manage_server|"), IsAdminFilter()) @router.callback_query(F.data.startswith("manage_server|"), IsAdminFilter())
async def handle_manage_server(callback_query: types.CallbackQuery, state: FSMContext): async def handle_manage_server(callback_query: types.CallbackQuery, state: FSMContext):
server_name = callback_query.data.split("|")[1] server_name = callback_query.data.split("|")[1]
@@ -361,3 +408,30 @@ async def handle_add_server(callback_query: types.CallbackQuery, state: FSMConte
) )
await state.set_state(UserEditorState.waiting_for_server_name) await state.set_state(UserEditorState.waiting_for_server_name)
@router.callback_query(F.data.startswith("backup_cluster|"), IsAdminFilter())
async def handle_backup_cluster(callback_query: types.CallbackQuery):
cluster_name = callback_query.data.split("|")[1]
servers = await get_servers_from_db()
cluster_servers = servers.get(cluster_name, [])
for server in cluster_servers:
xui = AsyncApi(
server["api_url"],
username=ADMIN_USERNAME,
password=ADMIN_PASSWORD,
)
await create_backup_and_send_to_admins(xui)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="servers_editor"))
await callback_query.message.answer(
f"<b>Бэкап для кластера {cluster_name} был успешно создан и отправлен администраторам!</b>\n\n"
f"🔔 <i>Бэкапы отправлены в боты панелей.</i>",
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
await callback_query.answer()
+6 -1
View File
@@ -62,4 +62,9 @@ async def process_connect_pc(callback_query: types.CallbackQuery, session: Any):
builder.row(InlineKeyboardButton(text="🆘 Поддержка", url=f"{SUPPORT_CHAT_URL}")) builder.row(InlineKeyboardButton(text="🆘 Поддержка", url=f"{SUPPORT_CHAT_URL}"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(instruction_message, reply_markup=builder.as_markup()) await callback_query.message.answer(
instruction_message,
reply_markup=builder.as_markup(),
parse_mode="HTML",
disable_web_page_preview=True,
)
+11 -13
View File
@@ -22,7 +22,7 @@ from config import (
) )
from database import get_balance, get_trial, store_key, update_balance from database import get_balance, get_trial, store_key, update_balance
from handlers.keys.key_utils import create_key_on_cluster from handlers.keys.key_utils import create_key_on_cluster
from handlers.texts import KEY, NULL_BALANCE, key_message_success from handlers.texts import DISCOUNTS, KEY, NULL_BALANCE, key_message_success
from handlers.utils import generate_random_email, get_least_loaded_cluster from handlers.utils import generate_random_email, get_least_loaded_cluster
from logger import logger from logger import logger
@@ -90,22 +90,21 @@ async def handle_key_creation(
if trial_status == 0: if trial_status == 0:
expiry_time = current_time + timedelta(days=TRIAL_TIME) expiry_time = current_time + timedelta(days=TRIAL_TIME)
logger.info(f"Assigned 1-day trial to user {tg_id}.") logger.info(f"Assigned 1-day trial to user {tg_id}.")
await session.execute( await session.execute("UPDATE connections SET trial = 1 WHERE tg_id = $1", tg_id)
"UPDATE connections SET trial = 1 WHERE tg_id = $1", tg_id
)
await create_key(tg_id, expiry_time, state, session, message_or_query) await create_key(tg_id, expiry_time, state, session, message_or_query)
else: else:
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
for plan_id, price in RENEWAL_PRICES.items(): for index, (plan_id, price) in enumerate(RENEWAL_PRICES.items()):
discount_text = "" discount_text = ""
if plan_id == "3":
discount_text = " (5% скидка)" if plan_id in DISCOUNTS:
elif plan_id == "6": discount_percentage = DISCOUNTS[plan_id]
discount_text = " (10% скидка)" discount_text = f" ({discount_percentage}% скидка)"
elif plan_id == "12":
discount_text = " (20% 🔥)" if index == len(RENEWAL_PRICES) - 1:
discount_text = f" ({discount_percentage}% 🔥)"
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
@@ -122,7 +121,6 @@ async def handle_key_creation(
await state.set_state(Form.waiting_for_server_selection) await state.set_state(Form.waiting_for_server_selection)
@router.callback_query(F.data.startswith("select_plan_")) @router.callback_query(F.data.startswith("select_plan_"))
async def select_tariff_plan(callback_query: CallbackQuery, state: FSMContext, session: Any): async def select_tariff_plan(callback_query: CallbackQuery, state: FSMContext, session: Any):
tg_id = callback_query.message.chat.id tg_id = callback_query.message.chat.id
+8 -4
View File
@@ -17,6 +17,7 @@ from handlers.keys.key_utils import (
update_key_on_cluster, update_key_on_cluster,
) )
from handlers.texts import ( from handlers.texts import (
DISCOUNTS,
INSUFFICIENT_FUNDS_MSG, INSUFFICIENT_FUNDS_MSG,
KEY_NOT_FOUND_MSG, KEY_NOT_FOUND_MSG,
NO_KEYS, NO_KEYS,
@@ -290,31 +291,34 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery, sessio
expiry_time = record["expiry_time"] expiry_time = record["expiry_time"]
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text=f'📅 1 месяц ({RENEWAL_PLANS["1"]["price"]} руб.)', text=f'📅 1 месяц ({RENEWAL_PLANS["1"]["price"]} руб.)',
callback_data=f"renew_plan|1|{client_id}", callback_data=f"renew_plan|1|{client_id}",
) )
) )
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text=f'📅 3 месяца ({RENEWAL_PLANS["3"]["price"]} руб.) 5% скидка', text=f'📅 3 месяца ({RENEWAL_PLANS["3"]["price"]} руб.) {DISCOUNTS["3"]}% скидка',
callback_data=f"renew_plan|3|{client_id}", callback_data=f"renew_plan|3|{client_id}",
) )
) )
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text=f'📅 6 месяцев ({RENEWAL_PLANS["6"]["price"]} руб.) 10% скидка', text=f'📅 6 месяцев ({RENEWAL_PLANS["6"]["price"]} руб.) {DISCOUNTS["6"]}% скидка',
callback_data=f"renew_plan|6|{client_id}", callback_data=f"renew_plan|6|{client_id}",
) )
) )
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text=f'📅 12 месяцев ({RENEWAL_PLANS["12"]["price"]} руб.) (20% 🔥)', text=f'📅 12 месяцев ({RENEWAL_PLANS["12"]["price"]} руб.) ({DISCOUNTS["12"]}% 🔥)',
callback_data=f"renew_plan|12|{client_id}", callback_data=f"renew_plan|12|{client_id}",
) )
) )
back_button = InlineKeyboardButton(text="🔙 Назад", callback_data="view_keys") back_button = InlineKeyboardButton(text="🔙 Назад", callback_data="view_keys")
builder.row(back_button) builder.row(back_button)
+9 -1
View File
@@ -141,7 +141,15 @@ async def process_crypto_payment(payload):
@router.callback_query(F.data == "enter_custom_amount_crypto") @router.callback_query(F.data == "enter_custom_amount_crypto")
async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext): async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext):
await callback_query.message.answer(text="Введите сумму пополнения:")
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="pay_cryptobot"))
await callback_query.message.answer(
"Пожалуйста, введите сумму пополнения.",
reply_markup=builder.as_markup(),
)
await state.set_state(ReplenishBalanceState.entering_custom_amount_crypto) await state.set_state(ReplenishBalanceState.entering_custom_amount_crypto)
+8 -1
View File
@@ -204,7 +204,14 @@ async def process_custom_amount_selection(callback_query: types.CallbackQuery, s
tg_id = callback_query.message.chat.id tg_id = callback_query.message.chat.id
logger.info(f"User {tg_id} chose to enter a custom amount.") logger.info(f"User {tg_id} chose to enter a custom amount.")
await callback_query.message.answer(text="Пожалуйста, введите сумму пополнения в рублях (например, 150):") builder = InlineKeyboardBuilder()
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)
+9 -1
View File
@@ -129,7 +129,15 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F
@router.callback_query(F.data == "enter_custom_amount_stars") @router.callback_query(F.data == "enter_custom_amount_stars")
async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext): async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext):
await callback_query.message.answer(text="Введите сумму пополнения:")
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="pay_stars"))
await callback_query.message.answer(
"Пожалуйста, введите сумму пополнения.",
reply_markup=builder.as_markup(),
)
await state.set_state(ReplenishBalanceState.entering_custom_amount_stars) await state.set_state(ReplenishBalanceState.entering_custom_amount_stars)
+9 -1
View File
@@ -168,7 +168,15 @@ async def yookassa_webhook(request):
@router.callback_query(F.data == "enter_custom_amount_yookassa") @router.callback_query(F.data == "enter_custom_amount_yookassa")
async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext): async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext):
await callback_query.message.answer(text="Введите сумму пополнения:")
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="pay_yookassa"))
await callback_query.message.answer(
"Пожалуйста, введите сумму пополнения.",
reply_markup=builder.as_markup(),
)
await state.set_state(ReplenishBalanceState.entering_custom_amount_yookassa) await state.set_state(ReplenishBalanceState.entering_custom_amount_yookassa)
+1 -1
View File
@@ -22,7 +22,7 @@ async def process_callback_view_profile(callback_query: types.CallbackQuery, sta
if balance is None: if balance is None:
balance = 0 balance = 0
profile_message = profile_message_send(username, chat_id, balance, key_count) profile_message = profile_message_send(username, chat_id, int(balance), key_count)
if key_count == 0: if key_count == 0:
profile_message += "\n<pre>🔧 <i>Нажмите кнопку ➕ Устройство, чтобы настроить VPN-подключение</i></pre>" profile_message += "\n<pre>🔧 <i>Нажмите кнопку ➕ Устройство, чтобы настроить VPN-подключение</i></pre>"
+4 -4
View File
@@ -39,12 +39,12 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
if trial_status == 0: if trial_status == 0:
builder.row(InlineKeyboardButton(text="🔗 Подключить VPN", callback_data="connect_vpn")) builder.row(InlineKeyboardButton(text="🔗 Подключить VPN", callback_data="connect_vpn"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
builder.row( builder.row(
InlineKeyboardButton(text="📞 Техническая поддержка", url=SUPPORT_CHAT_URL), InlineKeyboardButton(text="📞 Поддержка", url=SUPPORT_CHAT_URL),
) InlineKeyboardButton(text="📢 Канал", url=CHANNEL_URL),
builder.row(
InlineKeyboardButton(text="📢 Официальный канал", url=CHANNEL_URL),
) )
if admin: if admin:
builder.row(InlineKeyboardButton(text="🔧 Администратор", callback_data="admin")) builder.row(InlineKeyboardButton(text="🔧 Администратор", callback_data="admin"))
builder.row(InlineKeyboardButton(text="🌐 О нашем VPN", callback_data="about_vpn")) builder.row(InlineKeyboardButton(text="🌐 О нашем VPN", callback_data="about_vpn"))
+11 -6
View File
@@ -1,10 +1,16 @@
from datetime import timedelta
import logging import logging
import os
import sys import sys
from loguru import logger from loguru import logger
logger.remove() log_folder = "logs"
if not os.path.exists(log_folder):
os.makedirs(log_folder)
logger.remove()
level_mapping = { level_mapping = {
50: "CRITICAL", 50: "CRITICAL",
@@ -27,7 +33,6 @@ logging.basicConfig(handlers=[InterceptHandler()], level=0)
logging.getLogger("httpcore").setLevel(logging.WARNING) logging.getLogger("httpcore").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING) logging.getLogger("httpx").setLevel(logging.WARNING)
# Настройка вывода в консоль
logger.add( logger.add(
sys.stderr, sys.stderr,
level="INFO", level="INFO",
@@ -35,13 +40,13 @@ logger.add(
colorize=True, colorize=True,
) )
# Настройка записи в файл log_file_path = os.path.join(log_folder, "logging.log")
logger.add( logger.add(
"logging.log", log_file_path,
level="DEBUG", level="DEBUG",
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {module}:{function}:{line} | {message}", format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {module}:{function}:{line} | {message}",
rotation="60 minute", rotation=timedelta(minutes=60),
retention=24, retention=timedelta(days=3),
) )
logger = logger logger = logger
+4 -2
View File
@@ -7,6 +7,7 @@ from aiohttp import web
from backup import backup_database from backup import backup_database
from bot import bot, dp, router from bot import bot, dp, router
from config import ( from config import (
BACKUP_TIME,
CRYPTO_BOT_ENABLE, CRYPTO_BOT_ENABLE,
DEV_MODE, DEV_MODE,
FREEKASSA_ENABLE, FREEKASSA_ENABLE,
@@ -27,7 +28,7 @@ from handlers.payments.freekassa_pay import freekassa_webhook
from handlers.payments.robokassa_pay import robokassa_webhook from handlers.payments.robokassa_pay import robokassa_webhook
from handlers.payments.yookassa_pay import yookassa_webhook from handlers.payments.yookassa_pay import yookassa_webhook
from logger import logger from logger import logger
from servers import sync_servers_with_db from servers import check_servers, sync_servers_with_db
async def periodic_notifications(): async def periodic_notifications():
@@ -39,7 +40,7 @@ async def periodic_notifications():
async def periodic_database_backup(): async def periodic_database_backup():
while True: while True:
await backup_database() await backup_database()
await asyncio.sleep(21600) await asyncio.sleep(BACKUP_TIME)
async def on_startup(app): async def on_startup(app):
@@ -47,6 +48,7 @@ async def on_startup(app):
await init_db() await init_db()
asyncio.create_task(periodic_notifications()) asyncio.create_task(periodic_notifications())
asyncio.create_task(periodic_database_backup()) asyncio.create_task(periodic_database_backup())
asyncio.create_task(check_servers())
async def on_shutdown(app): async def on_shutdown(app):
+1 -1
View File
@@ -13,7 +13,7 @@ class DeleteMessageMiddleware(BaseMiddleware):
) -> Any: ) -> Any:
if isinstance(event, (Message, CallbackQuery)): if isinstance(event, (Message, CallbackQuery)):
if isinstance(event, Message): if isinstance(event, Message):
if not event.text == "/start": if not event.text.startswith("/start"):
try: try:
await event.bot.delete_message(event.chat.id, event.message_id - 1) await event.bot.delete_message(event.chat.id, event.message_id - 1)
except Exception: except Exception:
+2 -1
View File
@@ -33,4 +33,5 @@ robokassa
flake8 flake8
black black
isort isort
pylint pylint
ping3
+117 -2
View File
@@ -1,6 +1,15 @@
import asyncpg import asyncio
from datetime import datetime, timedelta
import re
from config import DATABASE_URL from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
import asyncpg
from ping3 import ping
from bot import bot
from config import ADMIN_ID, DATABASE_URL
from database import get_servers_from_db
from logger import logger from logger import logger
try: try:
@@ -55,3 +64,109 @@ async def sync_servers_with_db():
finally: finally:
if 'conn' in locals(): if 'conn' in locals():
await conn.close() await conn.close()
last_ping_times = {}
last_notification_times = {}
async def ping_server(server_ip: str) -> bool:
"""
Функция пинга сервера.
Возвращает True, если сервер доступен, иначе False.
"""
try:
logger.debug(f"Пингуем сервер {server_ip}...")
response = ping(server_ip, timeout=3)
if response is False:
logger.warning(f"Сервер {server_ip} не отвечает.")
return False
return True
except Exception as e:
logger.error(f"Ошибка при пинге сервера {server_ip}: {e}")
return False
async def notify_admin(server_name: str):
"""
Отправляет уведомление всем администраторам о недоступности сервера.
Уведомления отправляются не чаще чем раз в 3 минуты.
"""
try:
current_time = datetime.now()
last_notification_time = last_notification_times.get(server_name)
if last_notification_time and current_time - last_notification_time < timedelta(minutes=3):
logger.info(f"Не отправляем уведомление для сервера {server_name}, так как прошло менее 3 минут.")
return
logger.info(f"Отправка уведомлений администратору о недоступности сервера {server_name}...")
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="Управление сервером", callback_data=f"manage_server|{server_name}"))
for admin_id in ADMIN_ID:
await bot.send_message(
admin_id,
(
f"❌ <b>Сервер '{server_name}'</b> не отвечает более 3 минут.\n\n"
"Проверьте соединение к серверу, подключение к панели или удалите его из таблицы серверов в боте, "
"чтобы не выдать подписку к неработающему серверу."
),
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
logger.info(f"Уведомление отправлено администратору с ID {admin_id} о сервере {server_name}.")
last_notification_times[server_name] = current_time
except Exception as e:
logger.error(f"Ошибка при отправке уведомления администраторам: {e}")
async def check_servers():
"""
Периодическая проверка серверов с учетом извлечения хоста из `api_url`.
"""
while True:
servers = await get_servers_from_db()
current_time = datetime.now()
logger.info(f"Начинаю проверку серверов: {current_time}")
for cluster_name, cluster_servers in servers.items():
logger.debug(f"Проверка кластеров: {cluster_name}")
for server in cluster_servers:
original_api_url = server["api_url"]
server_name = server["server_name"]
server_host = extract_host(original_api_url)
logger.debug(f"Проверка доступности сервера '{server_name}' с хостом {server_host}")
is_online = await ping_server(server_host)
if is_online:
last_ping_times[server_name] = current_time
else:
last_ping_time = last_ping_times.get(server_name)
if last_ping_time and current_time - last_ping_time > timedelta(minutes=3):
logger.warning(f"Сервер {server_name} не отвечает более 3 минут. Отправляю уведомление.")
await notify_admin(server_name)
elif not last_ping_time:
last_ping_times[server_name] = current_time
logger.info(f"Сервер {server_name} не отвечал ранее, но теперь зарегистрирован.")
logger.info("Завершена проверка всех серверов.")
await asyncio.sleep(30)
def extract_host(api_url: str) -> str:
"""
Извлекает только хост из `api_url` (без путей, портов и параметров).
"""
match = re.match(r"(https?://)?([^:/]+)", api_url)
if match:
host = match.group(2)
logger.debug(f"Извлечён хост: {host} из URL: {api_url}")
return host
logger.error(f"Не удалось извлечь хост из URL: {api_url}")
return api_url