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("Старые бэкапы удалены.")
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
View File
@@ -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:
+75 -1
View File
@@ -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()
+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="👤 Личный кабинет", 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 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
+8 -4
View File
@@ -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)
+9 -1
View File
@@ -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)
+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
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)
+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")
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)
+9 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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"))
+11 -6
View File
@@ -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
+4 -2
View File
@@ -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):
+1 -1
View File
@@ -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
View File
@@ -33,4 +33,5 @@ robokassa
flake8
black
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
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