Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5577780565 | |||
| 5db2b7c861 | |||
| 370ceb8382 | |||
| 3226438e69 | |||
| c432edf8fc | |||
| 49aa863eb4 | |||
| c58039cb9e | |||
| ee242c98c7 | |||
| f3dfb03749 | |||
| 02ea333af1 | |||
| 142a5ffaab | |||
| db24a9abda |
@@ -92,3 +92,8 @@ def _cleanup_old_backups():
|
||||
logger.info("Старые бэкапы удалены.")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Ошибка при удалении старых бэкапов: {e}")
|
||||
|
||||
|
||||
async def create_backup_and_send_to_admins(xui):
|
||||
await xui.login()
|
||||
await xui.database.export()
|
||||
|
||||
+9
-10
@@ -685,16 +685,20 @@ async def handle_referral_on_balance_update(tg_id: int, amount: float):
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
logger.info(f"Начало обработки реферальной системы для пользователя {tg_id}")
|
||||
|
||||
# Максимальное количество уровней рефералов
|
||||
MAX_REFERRAL_LEVELS = len(REFERRAL_BONUS_PERCENTAGES.keys())
|
||||
|
||||
# Текущий уровень для отслеживания
|
||||
visited_tg_ids = set()
|
||||
|
||||
current_tg_id = tg_id
|
||||
referral_chain = []
|
||||
|
||||
# Собираем цепочку рефералов
|
||||
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(
|
||||
"""
|
||||
SELECT referrer_tg_id
|
||||
@@ -704,30 +708,25 @@ async def handle_referral_on_balance_update(tg_id: int, amount: float):
|
||||
current_tg_id,
|
||||
)
|
||||
|
||||
# Если реферер не найден, прекращаем поиск
|
||||
if not referral:
|
||||
break
|
||||
|
||||
referrer_tg_id = referral['referrer_tg_id']
|
||||
referral_chain.append({'tg_id': referrer_tg_id, 'level': level})
|
||||
|
||||
# Переходим к следующему уровню
|
||||
current_tg_id = referrer_tg_id
|
||||
|
||||
# Начисляем бонусы по цепочке рефералов
|
||||
for referral in referral_chain:
|
||||
referrer_tg_id = referral['tg_id']
|
||||
level = referral['level']
|
||||
|
||||
# Расчет бонуса для текущего уровня
|
||||
bonus_percent = REFERRAL_BONUS_PERCENTAGES.get(level, 0)
|
||||
bonus = amount * bonus_percent
|
||||
bonus = max(bonus, 0) # Гарантируем неотрицательный бонус
|
||||
bonus = max(bonus, 0)
|
||||
|
||||
if bonus > 0:
|
||||
logger.info(f"Начисление бонуса {bonus} рублей рефереру {referrer_tg_id} на уровне {level}")
|
||||
|
||||
# Обновляем баланс реферера
|
||||
await update_balance(referrer_tg_id, bonus)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -4,8 +4,10 @@ from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.types import InlineKeyboardButton
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
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 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"server_availability|{cluster_name}")
|
||||
)
|
||||
|
||||
builder.row(InlineKeyboardButton(text="💾 Создать бэкап кластера", callback_data=f"backup_cluster|{cluster_name}"))
|
||||
|
||||
builder.row(InlineKeyboardButton(text="🔙 Назад в управление кластерами", callback_data="servers_editor"))
|
||||
|
||||
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())
|
||||
async def handle_manage_server(callback_query: types.CallbackQuery, state: FSMContext):
|
||||
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)
|
||||
|
||||
|
||||
@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()
|
||||
|
||||
@@ -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="👤 Личный кабинет", 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,
|
||||
)
|
||||
|
||||
@@ -22,7 +22,7 @@ from config import (
|
||||
)
|
||||
from database import get_balance, get_trial, store_key, update_balance
|
||||
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 logger import logger
|
||||
|
||||
@@ -90,22 +90,21 @@ async def handle_key_creation(
|
||||
if trial_status == 0:
|
||||
expiry_time = current_time + timedelta(days=TRIAL_TIME)
|
||||
logger.info(f"Assigned 1-day trial to user {tg_id}.")
|
||||
|
||||
await session.execute(
|
||||
"UPDATE connections SET trial = 1 WHERE tg_id = $1", 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 plan_id, price in RENEWAL_PRICES.items():
|
||||
for index, (plan_id, price) in enumerate(RENEWAL_PRICES.items()):
|
||||
discount_text = ""
|
||||
if plan_id == "3":
|
||||
discount_text = " (5% скидка)"
|
||||
elif plan_id == "6":
|
||||
discount_text = " (10% скидка)"
|
||||
elif plan_id == "12":
|
||||
discount_text = " (20% 🔥)"
|
||||
|
||||
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(
|
||||
@@ -122,7 +121,6 @@ async def handle_key_creation(
|
||||
await state.set_state(Form.waiting_for_server_selection)
|
||||
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("select_plan_"))
|
||||
async def select_tariff_plan(callback_query: CallbackQuery, state: FSMContext, session: Any):
|
||||
tg_id = callback_query.message.chat.id
|
||||
|
||||
@@ -17,6 +17,7 @@ from handlers.keys.key_utils import (
|
||||
update_key_on_cluster,
|
||||
)
|
||||
from handlers.texts import (
|
||||
DISCOUNTS,
|
||||
INSUFFICIENT_FUNDS_MSG,
|
||||
KEY_NOT_FOUND_MSG,
|
||||
NO_KEYS,
|
||||
@@ -290,31 +291,34 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery, sessio
|
||||
expiry_time = record["expiry_time"]
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text=f'📅 1 месяц ({RENEWAL_PLANS["1"]["price"]} руб.)',
|
||||
callback_data=f"renew_plan|1|{client_id}",
|
||||
)
|
||||
)
|
||||
|
||||
builder.row(
|
||||
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}",
|
||||
)
|
||||
)
|
||||
|
||||
builder.row(
|
||||
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}",
|
||||
)
|
||||
)
|
||||
|
||||
builder.row(
|
||||
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}",
|
||||
)
|
||||
)
|
||||
|
||||
back_button = InlineKeyboardButton(text="🔙 Назад", callback_data="view_keys")
|
||||
builder.row(back_button)
|
||||
|
||||
|
||||
@@ -141,7 +141,15 @@ async def process_crypto_payment(payload):
|
||||
|
||||
@router.callback_query(F.data == "enter_custom_amount_crypto")
|
||||
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)
|
||||
|
||||
|
||||
|
||||
@@ -204,7 +204,14 @@ async def process_custom_amount_selection(callback_query: types.CallbackQuery, s
|
||||
tg_id = callback_query.message.chat.id
|
||||
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)
|
||||
|
||||
|
||||
|
||||
@@ -129,7 +129,15 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F
|
||||
|
||||
@router.callback_query(F.data == "enter_custom_amount_stars")
|
||||
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)
|
||||
|
||||
|
||||
|
||||
@@ -168,7 +168,15 @@ async def yookassa_webhook(request):
|
||||
|
||||
@router.callback_query(F.data == "enter_custom_amount_yookassa")
|
||||
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)
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ async def process_callback_view_profile(callback_query: types.CallbackQuery, sta
|
||||
if balance is None:
|
||||
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:
|
||||
profile_message += "\n<pre>🔧 <i>Нажмите кнопку ➕ Устройство, чтобы настроить VPN-подключение</i></pre>"
|
||||
|
||||
+4
-4
@@ -39,12 +39,12 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
|
||||
if trial_status == 0:
|
||||
builder.row(InlineKeyboardButton(text="🔗 Подключить VPN", callback_data="connect_vpn"))
|
||||
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
|
||||
|
||||
builder.row(
|
||||
InlineKeyboardButton(text="📞 Техническая поддержка", url=SUPPORT_CHAT_URL),
|
||||
)
|
||||
builder.row(
|
||||
InlineKeyboardButton(text="📢 Официальный канал", url=CHANNEL_URL),
|
||||
InlineKeyboardButton(text="📞 Поддержка", url=SUPPORT_CHAT_URL),
|
||||
InlineKeyboardButton(text="📢 Канал", url=CHANNEL_URL),
|
||||
)
|
||||
|
||||
if admin:
|
||||
builder.row(InlineKeyboardButton(text="🔧 Администратор", callback_data="admin"))
|
||||
builder.row(InlineKeyboardButton(text="🌐 О нашем VPN", callback_data="about_vpn"))
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from loguru import logger
|
||||
|
||||
logger.remove()
|
||||
log_folder = "logs"
|
||||
|
||||
if not os.path.exists(log_folder):
|
||||
os.makedirs(log_folder)
|
||||
|
||||
logger.remove()
|
||||
|
||||
level_mapping = {
|
||||
50: "CRITICAL",
|
||||
@@ -27,7 +33,6 @@ logging.basicConfig(handlers=[InterceptHandler()], level=0)
|
||||
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
|
||||
# Настройка вывода в консоль
|
||||
logger.add(
|
||||
sys.stderr,
|
||||
level="INFO",
|
||||
@@ -35,13 +40,13 @@ logger.add(
|
||||
colorize=True,
|
||||
)
|
||||
|
||||
# Настройка записи в файл
|
||||
log_file_path = os.path.join(log_folder, "logging.log")
|
||||
logger.add(
|
||||
"logging.log",
|
||||
log_file_path,
|
||||
level="DEBUG",
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {module}:{function}:{line} | {message}",
|
||||
rotation="60 minute",
|
||||
retention=24,
|
||||
rotation=timedelta(minutes=60),
|
||||
retention=timedelta(days=3),
|
||||
)
|
||||
|
||||
logger = logger
|
||||
|
||||
@@ -7,6 +7,7 @@ from aiohttp import web
|
||||
from backup import backup_database
|
||||
from bot import bot, dp, router
|
||||
from config import (
|
||||
BACKUP_TIME,
|
||||
CRYPTO_BOT_ENABLE,
|
||||
DEV_MODE,
|
||||
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.yookassa_pay import yookassa_webhook
|
||||
from logger import logger
|
||||
from servers import sync_servers_with_db
|
||||
from servers import check_servers, sync_servers_with_db
|
||||
|
||||
|
||||
async def periodic_notifications():
|
||||
@@ -39,7 +40,7 @@ async def periodic_notifications():
|
||||
async def periodic_database_backup():
|
||||
while True:
|
||||
await backup_database()
|
||||
await asyncio.sleep(21600)
|
||||
await asyncio.sleep(BACKUP_TIME)
|
||||
|
||||
|
||||
async def on_startup(app):
|
||||
@@ -47,6 +48,7 @@ async def on_startup(app):
|
||||
await init_db()
|
||||
asyncio.create_task(periodic_notifications())
|
||||
asyncio.create_task(periodic_database_backup())
|
||||
asyncio.create_task(check_servers())
|
||||
|
||||
|
||||
async def on_shutdown(app):
|
||||
|
||||
@@ -13,7 +13,7 @@ class DeleteMessageMiddleware(BaseMiddleware):
|
||||
) -> Any:
|
||||
if isinstance(event, (Message, CallbackQuery)):
|
||||
if isinstance(event, Message):
|
||||
if not event.text == "/start":
|
||||
if not event.text.startswith("/start"):
|
||||
try:
|
||||
await event.bot.delete_message(event.chat.id, event.message_id - 1)
|
||||
except Exception:
|
||||
|
||||
+2
-1
@@ -33,4 +33,5 @@ robokassa
|
||||
flake8
|
||||
black
|
||||
isort
|
||||
pylint
|
||||
pylint
|
||||
ping3
|
||||
+117
-2
@@ -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
|
||||
|
||||
try:
|
||||
@@ -55,3 +64,109 @@ async def sync_servers_with_db():
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user