This commit is contained in:
hteppl
2025-03-26 02:05:24 +03:00
66 changed files with 605 additions and 477 deletions
+1 -3
View File
@@ -1,15 +1,13 @@
import os
import subprocess
from datetime import datetime, timedelta
from pathlib import Path
import aiofiles
from aiogram.types import BufferedInputFile
from config import ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER, PG_HOST, PG_PORT
from bot import bot
from config import ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER, PG_HOST, PG_PORT
from logger import logger
+1 -2
View File
@@ -8,13 +8,12 @@ from aiogram.filters import ExceptionTypeFilter
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import BufferedInputFile, ErrorEvent
from aiogram.utils.markdown import hbold
from config import ADMIN_ID, API_TOKEN
from config import ADMIN_ID, API_TOKEN
from filters.private import IsPrivateFilter
from logger import logger
from middlewares import register_middleware
bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
storage = MemoryStorage()
dp = Dispatcher(bot=bot, storage=storage)
-1
View File
@@ -5,7 +5,6 @@ import httpx
import py3xui
from config import LIMIT_IP, SUPERNODE
from logger import logger
-2
View File
@@ -1,5 +1,4 @@
import json
from datetime import datetime
from typing import Any
@@ -7,7 +6,6 @@ import asyncpg
import pytz
from config import CASHBACK, CHECK_REFERRAL_REWARD_ISSUED, DATABASE_URL, REFERRAL_BONUS_PERCENTAGES
from logger import logger
+1
View File
@@ -1,5 +1,6 @@
from aiogram.filters import BaseFilter
from aiogram.types import Message
from config import ADMIN_ID
-1
View File
@@ -14,7 +14,6 @@ from .payments import router as payments_router
from .profile import router as profile_router
from .start import router as start_router
router = Router(name="handlers_main_router")
router.include_routers(
+13 -10
View File
@@ -2,22 +2,25 @@ __all__ = ("router",)
from aiogram import Router
from .admin_backups import router as backups_router
from .admin_bans import router as bans_router
from .admin_coupons import router as coupons_router
from .admin_panel import router as panel_router
from .admin_restart import router as restart_router
from .admin_sender import router as sender_router
from .admin_servers import router as servers_router
from .admin_stats import router as stats_router
from .admin_users import router as users_router
from .backups import router as backups_router
from .bans import router as bans_router
from .clusters import router as clusters_router
from .coupons import router as coupons_router
from .management import router as management_router
from .panel import router as panel_router
from .restart import router as restart_router
from .sender import router as sender_router
from .servers import router as servers_router
from .stats import router as stats_router
from .users import router as users_router
router = Router(name="admins_main_router")
router.include_routers(
panel_router,
management_router,
servers_router,
clusters_router,
users_router,
stats_router,
backups_router,
+3
View File
@@ -0,0 +1,3 @@
__all__ = ("router",)
from .backups_handler import router
@@ -3,8 +3,7 @@ from aiogram.types import CallbackQuery
from backup import backup_database
from filters.admin import IsAdminFilter
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb
router = Router()
+3
View File
@@ -0,0 +1,3 @@
__all__ = ("router",)
from .bans_handler import router
@@ -5,9 +5,8 @@ from aiogram.types import BufferedInputFile, CallbackQuery
from database import delete_user_data
from filters.admin import IsAdminFilter
from keyboards.admin.bans_kb import build_bans_kb
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
from .keyboard import build_bans_kb
from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb
router = Router()
@@ -1,7 +1,7 @@
from aiogram.types import InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_btn
from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn
def build_bans_kb() -> InlineKeyboardMarkup:
+3
View File
@@ -0,0 +1,3 @@
__all__ = ("router",)
from .clusters_handler import router
@@ -1,45 +1,40 @@
import asyncio
from typing import Any
import asyncpg
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, Message
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL
from py3xui import AsyncApi
from backup import create_backup_and_send_to_admins
from database import check_unique_server_name, delete_server, get_servers
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL
from database import check_unique_server_name, get_servers
from filters.admin import IsAdminFilter
from handlers.keys.key_utils import create_key_on_cluster
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
from keyboards.admin.servers_kb import (
AdminServerEditorCallback,
build_clusters_editor_kb,
build_delete_server_kb,
build_manage_cluster_kb,
build_manage_server_kb,
)
from handlers.keys.key_utils import create_key_on_cluster, create_client_on_server
from logger import logger
from .keyboard import (
build_clusters_editor_kb,
build_manage_cluster_kb,
AdminClusterCallback,
build_sync_cluster_kb,
)
from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb
router = Router()
class AdminServersEditor(StatesGroup):
class AdminClusterStates(StatesGroup):
waiting_for_cluster_name = State()
waiting_for_api_url = State()
waiting_for_inbound_id = State()
waiting_for_server_name = State()
waiting_for_subscription_url = State()
waiting_for_new_domain = State()
@router.callback_query(
AdminPanelCallback.filter(F.action == "servers"),
AdminPanelCallback.filter(F.action == "clusters"),
IsAdminFilter(),
)
async def handle_servers(callback_query: CallbackQuery):
@@ -60,26 +55,34 @@ async def handle_servers(callback_query: CallbackQuery):
@router.callback_query(
AdminPanelCallback.filter(F.action == "clusters_add"),
AdminClusterCallback.filter(F.action == "add"),
IsAdminFilter(),
)
async def handle_clusters_add(callback_query: CallbackQuery, state: FSMContext):
text = (
"🔧 <b>Введите имя нового кластера:</b>\n\n"
"<b>Имя кластера должно быть уникальным!</b>\n"
"<b>Имя должно быть уникальным!</b>\n"
"<b>Имя не должно превышать 12 символов!</b>\n"
"<i>Пример:</i> <code>cluster1</code> или <code>us_east_1</code>"
)
await callback_query.message.edit_text(text=text, reply_markup=build_admin_back_kb("servers"))
await callback_query.message.edit_text(text=text, reply_markup=build_admin_back_kb("clusters"))
await state.set_state(AdminServersEditor.waiting_for_cluster_name)
await state.set_state(AdminClusterStates.waiting_for_cluster_name)
@router.message(AdminServersEditor.waiting_for_cluster_name, IsAdminFilter())
@router.message(AdminClusterStates.waiting_for_cluster_name, IsAdminFilter())
async def handle_cluster_name_input(message: Message, state: FSMContext):
if not message.text:
await message.answer(
text="❌ Имя кластера не может быть пустым. Попробуйте снова.", reply_markup=build_admin_back_kb("servers")
text="❌ Имя кластера не может быть пустым! Попробуйте снова.", reply_markup=build_admin_back_kb("clusters")
)
return
if len(message.text) > 12:
await message.answer(
text="❌ Имя кластера должно превышать 12 символов! Попробуйте снова.",
reply_markup=build_admin_back_kb("clusters"),
)
return
@@ -94,26 +97,26 @@ async def handle_cluster_name_input(message: Message, state: FSMContext):
await message.answer(
text=text,
reply_markup=build_admin_back_kb("servers"),
reply_markup=build_admin_back_kb("clusters"),
)
await state.set_state(AdminServersEditor.waiting_for_server_name)
await state.set_state(AdminClusterStates.waiting_for_server_name)
@router.message(AdminServersEditor.waiting_for_server_name, IsAdminFilter())
@router.message(AdminClusterStates.waiting_for_server_name, IsAdminFilter())
async def handle_server_name_input(message: Message, state: FSMContext, session: Any):
if not message.text:
await message.answer(
text="❌ Имя сервера не может быть пустым. Попробуйте снова.", reply_markup=build_admin_back_kb("servers")
text="❌ Имя сервера не может быть пустым. Попробуйте снова.", reply_markup=build_admin_back_kb("clusters")
)
return
server_name = message.text.strip()
if len(server_name) > 14:
if len(server_name) > 12:
await message.answer(
text="❌ Имя сервера не должно превышать 10 символов. Попробуйте снова.",
reply_markup=build_admin_back_kb("servers"),
text="❌ Имя сервера не должно превышать 12 символов. Попробуйте снова.",
reply_markup=build_admin_back_kb("clusters"),
)
return
@@ -123,7 +126,7 @@ async def handle_server_name_input(message: Message, state: FSMContext, session:
if not await check_unique_server_name(server_name, session, cluster_name):
await message.answer(
text="❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя.",
reply_markup=build_admin_back_kb("servers"),
reply_markup=build_admin_back_kb("clusters"),
)
return
@@ -131,25 +134,25 @@ async def handle_server_name_input(message: Message, state: FSMContext, session:
text = (
f"<b>Введите API URL для сервера {server_name} в кластере {cluster_name}:</b>\n\n"
"API URL должен быть в следующем формате:\n\n"
"<code>https://your_domain:port/panel_path</code>\n\n"
"URL должен быть без слэша на конце!\n"
"Ссылку можно найти в поисковой строке браузера, при входе в 3X-UI.\n\n"
"️ Формат API URL:\n"
"<code>https://your_domain:port/panel_path/</code>"
)
await message.answer(
text=text,
reply_markup=build_admin_back_kb("servers"),
reply_markup=build_admin_back_kb("clusters"),
)
await state.set_state(AdminServersEditor.waiting_for_api_url)
await state.set_state(AdminClusterStates.waiting_for_api_url)
@router.message(AdminServersEditor.waiting_for_api_url, IsAdminFilter())
async def handle_api_url_input(message: Message, state: FSMContext, session: Any):
@router.message(AdminClusterStates.waiting_for_api_url, IsAdminFilter())
async def handle_api_url_input(message: Message, state: FSMContext):
if not message.text or not message.text.strip().startswith("https://"):
await message.answer(
text="❌ API URL должен начинаться с <code>https://</code>. Попробуйте снова.",
reply_markup=build_admin_back_kb("servers"),
reply_markup=build_admin_back_kb("clusters"),
)
return
@@ -162,26 +165,25 @@ async def handle_api_url_input(message: Message, state: FSMContext, session: Any
text = (
f"<b>Введите subscription_url для сервера {server_name} в кластере {cluster_name}:</b>\n\n"
"Subscription URL должен быть в следующем формате:\n\n"
"<code>https://your_domain:port_sub/sub_path</code>\n\n"
"URL должен быть без слэша и имени клиента на конце!\n"
"Его можно увидеть в панели 3x-ui в информации о клиенте."
"Ссылку можно найти в панели 3X-UI, в информации о клиенте.\n\n"
"️ Формат Subscription URL:\n"
"<code>https://your_domain:port_sub/sub_path/</code>"
)
await message.answer(
text=text,
reply_markup=build_admin_back_kb("servers"),
reply_markup=build_admin_back_kb("clusters"),
)
await state.set_state(AdminServersEditor.waiting_for_subscription_url)
await state.set_state(AdminClusterStates.waiting_for_subscription_url)
@router.message(AdminServersEditor.waiting_for_subscription_url, IsAdminFilter())
@router.message(AdminClusterStates.waiting_for_subscription_url, IsAdminFilter())
async def handle_subscription_url_input(message: Message, state: FSMContext):
if not message.text or not message.text.strip().startswith("https://"):
await message.answer(
text="❌ subscription_url должен начинаться с <code>https://</code>. Попробуйте снова.",
reply_markup=build_admin_back_kb("servers"),
reply_markup=build_admin_back_kb("clusters"),
)
return
@@ -199,19 +201,19 @@ async def handle_subscription_url_input(message: Message, state: FSMContext):
await message.answer(
text=text,
reply_markup=build_admin_back_kb("servers"),
reply_markup=build_admin_back_kb("clusters"),
)
await state.set_state(AdminServersEditor.waiting_for_inbound_id)
await state.set_state(AdminClusterStates.waiting_for_inbound_id)
@router.message(AdminServersEditor.waiting_for_inbound_id, IsAdminFilter())
@router.message(AdminClusterStates.waiting_for_inbound_id, IsAdminFilter())
async def handle_inbound_id_input(message: Message, state: FSMContext):
inbound_id = message.text.strip()
if not inbound_id.isdigit():
await message.answer(
text="❌ inbound_id должен быть числовым значением. Попробуйте снова.",
reply_markup=build_admin_back_kb("servers"),
reply_markup=build_admin_back_kb("clusters"),
)
return
@@ -237,15 +239,15 @@ async def handle_inbound_id_input(message: Message, state: FSMContext):
await message.answer(
text=f"✅ Кластер {cluster_name} и сервер {server_name} успешно добавлены!",
reply_markup=build_admin_back_kb("servers"),
reply_markup=build_admin_back_kb("clusters"),
)
await state.clear()
@router.callback_query(AdminServerEditorCallback.filter(F.action == "clusters_manage"), IsAdminFilter())
@router.callback_query(AdminClusterCallback.filter(F.action == "manage"), IsAdminFilter())
async def handle_clusters_manage(
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any
):
cluster_name = callback_data.data
@@ -253,14 +255,14 @@ async def handle_clusters_manage(
cluster_servers = servers.get(cluster_name, [])
await callback_query.message.edit_text(
text=f"🔧 Управление серверами для кластера {cluster_name}",
text=f"<b>🔧 Управление кластером {cluster_name}</b>",
reply_markup=build_manage_cluster_kb(cluster_servers, cluster_name),
)
@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_availability"), IsAdminFilter())
async def handle_servers_availability(
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
@router.callback_query(AdminClusterCallback.filter(F.action == "availability"), IsAdminFilter())
async def handle_cluster_availability(
callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any
):
cluster_name = callback_data.data
@@ -278,8 +280,8 @@ async def handle_servers_availability(
await callback_query.message.edit_text(text=text)
result_text = f"🖥️ Проверка доступности серверов для кластера {cluster_name} завершена:\n\n"
total_online_users = 0
result_text = f"<b>🖥️ Проверка доступности серверов</b>\n\n⚙️ Кластер: <b>{cluster_name}</b>\n\n"
for server in cluster_servers:
xui = AsyncApi(server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD, logger=logger)
@@ -288,93 +290,18 @@ async def handle_servers_availability(
await xui.login()
online_users = len(await xui.client.online())
total_online_users += online_users
result_text += f"🌍 {server['server_name']}: {online_users} активных пользователей.\n"
result_text += f"🌍 <b>{server['server_name']}</b> - онлайн: {online_users}\n"
except Exception as e:
result_text += f"{server['server_name']}: Не удалось получить информацию. Ошибка: {e}\n"
result_text += f"<b>{server['server_name']}</b> - ошибка: {e}\n"
result_text += f"\n👥 Общее количество активных пользователей в кластере: {total_online_users}."
result_text += f"\n👥 Всего пользователей онлайн: {total_online_users}"
await callback_query.message.edit_text(text=result_text, reply_markup=build_admin_back_kb("servers"))
await callback_query.message.edit_text(text=result_text, reply_markup=build_admin_back_kb("clusters"))
@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_manage"), IsAdminFilter())
async def handle_servers_manage(callback_query: CallbackQuery, callback_data: AdminServerEditorCallback):
server_name = callback_data.data
servers = await get_servers()
cluster_name, server = next(
((c, s) for c, cs in servers.items() for s in cs if s["server_name"] == server_name), (None, None)
)
if server:
api_url = server["api_url"]
subscription_url = server["subscription_url"]
inbound_id = server["inbound_id"]
text = (
f"<b>🔧 Информация о сервере {server_name}:</b>\n\n"
f"<b>📡 API URL:</b> {api_url}\n"
f"<b>🌐 Subscription URL:</b> {subscription_url}\n"
f"<b>🔑 Inbound ID:</b> {inbound_id}"
)
await callback_query.message.edit_text(
text=text,
reply_markup=build_manage_server_kb(server_name, cluster_name),
)
else:
await callback_query.message.edit_text(text="❌ Сервер не найден.")
@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_delete"), IsAdminFilter())
async def handle_servers_delete(callback_query: CallbackQuery, callback_data: AdminServerEditorCallback):
server_name = callback_data.data
await callback_query.message.edit_text(
text=f"🗑️ Вы уверены, что хотите удалить сервер {server_name}?",
reply_markup=build_delete_server_kb(server_name),
)
@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_delete_confirm"), IsAdminFilter())
async def handle_servers_delete_confirm(
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
):
server_name = callback_data.data
await delete_server(server_name, session)
await callback_query.message.edit_text(
text=f"🗑️ Сервер {server_name} успешно удален.", reply_markup=build_admin_back_kb("servers")
)
@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_add"), IsAdminFilter())
async def handle_servers_add(
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, state: FSMContext
):
cluster_name = callback_data.data
await state.update_data(cluster_name=cluster_name)
text = (
f"<b>Введите имя сервера для кластера {cluster_name}:</b>\n\n"
"Рекомендуется указать локацию сервера в имени.\n\n"
"<i>Пример:</i> <code>server-asia</code>, <code>server-europe</code>"
)
await callback_query.message.edit_text(
text=text,
reply_markup=build_admin_back_kb("servers"),
)
await state.set_state(AdminServersEditor.waiting_for_server_name)
@router.callback_query(AdminServerEditorCallback.filter(F.action == "clusters_backup"), IsAdminFilter())
@router.callback_query(AdminClusterCallback.filter(F.action == "backup"), IsAdminFilter())
async def handle_clusters_backup(
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any
):
cluster_name = callback_data.data
@@ -397,14 +324,81 @@ async def handle_clusters_backup(
await callback_query.message.edit_text(
text=text,
reply_markup=build_admin_back_kb("servers"),
reply_markup=build_admin_back_kb("clusters"),
)
@router.callback_query(AdminServerEditorCallback.filter(F.action == "clusters_sync"), IsAdminFilter())
async def handle_clusters_sync(
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
):
@router.callback_query(AdminClusterCallback.filter(F.action == "sync"), IsAdminFilter())
async def handle_sync(callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any):
cluster_name = callback_data.data
servers = await get_servers(session)
cluster_servers = servers.get(cluster_name, [])
await callback_query.message.answer(
text=f"<b>🔄 Синхронизация кластера {cluster_name}</b>",
reply_markup=build_sync_cluster_kb(cluster_servers, cluster_name),
)
@router.callback_query(AdminClusterCallback.filter(F.action == "sync-server"), IsAdminFilter())
async def handle_sync_server(callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any):
server_name = callback_data.data
try:
query_keys = """
SELECT s.*, k.tg_id, k.client_id, k.email, k.expiry_time
FROM servers s
JOIN keys k ON s.cluster_name = k.server_id
WHERE s.server_name = $1;
"""
keys_to_sync = await session.fetch(query_keys, server_name)
if not keys_to_sync:
await callback_query.message.answer(
text=f"❌ Нет ключей для синхронизации в сервере {server_name}.",
reply_markup=build_admin_back_kb("clusters"),
)
return
text = f"<b>🔄 Синхронизация сервера {server_name}</b>\n\n🔑 Количество ключей: <b>{len(keys_to_sync)}</b>"
await callback_query.message.answer(
text=text,
)
semaphore = asyncio.Semaphore(2)
for key in keys_to_sync:
try:
await create_client_on_server(
{
"api_url": key["api_url"],
"inbound_id": key["inbound_id"],
"server_name": key["server_name"],
},
key["tg_id"],
key["client_id"],
key["email"],
key["expiry_time"],
semaphore,
)
await asyncio.sleep(0.6)
except Exception as e:
logger.error(f"Ошибка при добавлении ключа {key['client_id']} в сервер {server_name}: {e}")
await callback_query.message.answer(
text=f"✅ Ключи успешно синхронизированы для сервера {server_name}",
reply_markup=build_admin_back_kb("clusters"),
)
except Exception as e:
logger.error(f"Ошибка синхронизации ключей для сервера {server_name}: {e}")
await callback_query.message.answer(
text=f"❌ Произошла ошибка при синхронизации: {e}", reply_markup=build_admin_back_kb("clusters")
)
@router.callback_query(AdminClusterCallback.filter(F.action == "sync-cluster"), IsAdminFilter())
async def handle_sync_cluster(callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any):
cluster_name = callback_data.data
try:
@@ -418,10 +412,16 @@ async def handle_clusters_sync(
if not keys_to_sync:
await callback_query.message.answer(
text=f"❌ Нет ключей для синхронизации в кластере {cluster_name}.",
reply_markup=build_admin_back_kb("servers"),
reply_markup=build_admin_back_kb("clusters"),
)
return
text = f"<b>🔄 Синхронизация кластера {cluster_name}</b>\n\n🔑 Количество ключей: <b>{len(keys_to_sync)}</b>"
await callback_query.message.answer(
text=text,
)
for key in keys_to_sync:
try:
await create_key_on_cluster(
@@ -437,59 +437,10 @@ async def handle_clusters_sync(
await callback_query.message.answer(
text=f"✅ Ключи успешно синхронизированы для кластера {cluster_name}",
reply_markup=build_admin_back_kb("servers"),
reply_markup=build_admin_back_kb("clusters"),
)
except Exception as e:
logger.error(f"Ошибка синхронизации ключей в кластере {cluster_name}: {e}")
await callback_query.message.answer(
text=f"❌ Произошла ошибка при синхронизации: {e}", reply_markup=build_admin_back_kb("servers")
text=f"❌ Произошла ошибка при синхронизации: {e}", reply_markup=build_admin_back_kb("clusters")
)
@router.callback_query(AdminPanelCallback.filter(F.action == "change_domain"), IsAdminFilter())
async def request_new_domain(callback_query: CallbackQuery, state: FSMContext):
"""Запрашивает у администратора новый домен."""
await state.set_state(AdminServersEditor.waiting_for_new_domain)
await callback_query.message.edit_text(
text="🌐 Введите новый домен (без https://):\nПример: pocomachodomen.ru",
)
@router.message(AdminServersEditor.waiting_for_new_domain)
async def process_new_domain(message: Message, state: FSMContext, session: asyncpg.Connection):
"""Обновляет домен в таблице keys."""
new_domain = message.text.strip()
logger.info(f"[DomainChange] Новый домен, введённый администратором: '{new_domain}'")
if not new_domain or " " in new_domain or not new_domain.replace(".", "").isalnum():
logger.warning("[DomainChange] Некорректный домен")
await message.answer(
"🚫 Некорректный домен! Введите домен без http:// и без пробелов.",
reply_markup=build_admin_back_kb("admin"),
)
return
new_domain_url = f"https://{new_domain}"
logger.info(f"[DomainChange] Новый домен с протоколом: '{new_domain_url}'")
query = """
UPDATE keys
SET key = regexp_replace(key, '^https://[^/]+', $1::TEXT)
WHERE key NOT LIKE $1 || '%'
"""
try:
await session.execute(query, new_domain_url)
logger.info("[DomainChange] Запрос на обновление домена выполнен успешно.")
except Exception as e:
logger.error(f"[DomainChange] Ошибка при выполнении запроса: {e}")
await message.answer(f"❌ Ошибка при обновлении домена: {e}", reply_markup=build_admin_back_kb("admin"))
return
try:
sample = await session.fetchrow("SELECT key FROM keys LIMIT 1")
logger.info(f"[DomainChange] Пример обновленной записи: {sample}")
except Exception as e:
logger.error(f"[DomainChange] Ошибка при выборке обновленной записи: {e}")
await message.answer(f"✅ Домен успешно изменен на {new_domain}!", reply_markup=build_admin_back_kb("admin"))
await state.clear()
+96
View File
@@ -0,0 +1,96 @@
from aiogram.filters.callback_data import CallbackData
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from ..panel.keyboard import build_admin_back_btn
from ..servers.keyboard import AdminServerCallback
class AdminClusterCallback(CallbackData, prefix="admin_cluster"):
action: str
data: str = None
def build_clusters_editor_kb(servers: dict) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
cluster_names = list(servers.keys())
for i in range(0, len(cluster_names), 2):
builder.row(*[
InlineKeyboardButton(
text=f"⚙️ {name}",
callback_data=AdminClusterCallback(action="manage", data=name).pack(),
)
for name in cluster_names[i : i + 2]
])
builder.row(
InlineKeyboardButton(text=" Добавить кластер", callback_data=AdminClusterCallback(action="add").pack())
)
builder.row(build_admin_back_btn())
return builder.as_markup()
def build_manage_cluster_kb(cluster_servers: list, cluster_name: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
for server in cluster_servers:
builder.row(
InlineKeyboardButton(
text=f"🌍 {server['server_name']}",
callback_data=AdminServerCallback(action="manage", data=server["server_name"]).pack(),
)
)
builder.row(
InlineKeyboardButton(
text=" Добавить сервер",
callback_data=AdminServerCallback(action="add", data=cluster_name).pack(),
)
)
builder.row(
InlineKeyboardButton(
text="🌐 Доступность",
callback_data=AdminClusterCallback(action="availability", data=cluster_name).pack(),
),
InlineKeyboardButton(
text="🔄 Синхронизация",
callback_data=AdminClusterCallback(action="sync", data=cluster_name).pack(),
),
)
builder.row(
InlineKeyboardButton(
text="💾 Создать бэкап",
callback_data=AdminClusterCallback(action="backup", data=cluster_name).pack(),
)
)
builder.row(build_admin_back_btn("clusters"))
return builder.as_markup()
def build_sync_cluster_kb(cluster_servers: list, cluster_name: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
for server in cluster_servers:
builder.row(
InlineKeyboardButton(
text=f"🔄 Синхронизировать {server['server_name']}",
callback_data=AdminClusterCallback(action="sync-server", data=server["server_name"]).pack(),
)
)
builder.row(
InlineKeyboardButton(
text="📍 Синхронизировать кластер",
callback_data=AdminClusterCallback(action="sync-cluster", data=cluster_name).pack(),
)
)
builder.row(build_admin_back_btn("clusters"))
return builder.as_markup()
+3
View File
@@ -0,0 +1,3 @@
__all__ = ("router",)
from .coupons_handler import router
@@ -4,14 +4,13 @@ from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, Message
from config import USERNAME_BOT
from config import USERNAME_BOT
from database import create_coupon, delete_coupon, get_all_coupons
from filters.admin import IsAdminFilter
from keyboards.admin.coupons_kb import AdminCouponDeleteCallback, build_coupons_kb, build_coupons_list_kb
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
from .keyboard import AdminCouponDeleteCallback, build_coupons_kb, build_coupons_list_kb
from logger import logger
from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb
router = Router()
@@ -2,7 +2,7 @@ from aiogram.filters.callback_data import CallbackData
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_btn
from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn
class AdminCouponDeleteCallback(CallbackData, prefix="admin_coupon_delete"):
+3
View File
@@ -0,0 +1,3 @@
__all__ = ("router",)
from .management_handler import router
+16
View File
@@ -0,0 +1,16 @@
from aiogram.types import InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from ..panel.keyboard import build_admin_back_btn, AdminPanelCallback
def build_management_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(text="💾 Создать резервную копию", callback_data=AdminPanelCallback(action="backups").pack())
builder.button(text="🚫 Заблокировавшие бота", callback_data=AdminPanelCallback(action="bans").pack())
builder.button(text="🔄 Перезагрузить бота", callback_data=AdminPanelCallback(action="restart").pack())
builder.button(text="🌐 Сменить домен", callback_data=AdminPanelCallback(action="change_domain").pack())
builder.button(text="🔑 Восстановить пробники", callback_data=AdminPanelCallback(action="restore_trials").pack())
builder.row(build_admin_back_btn())
builder.adjust(1)
return builder.as_markup()
@@ -0,0 +1,73 @@
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import StatesGroup, State
from aiogram.types import CallbackQuery, Message
from asyncpg import Connection
from filters.admin import IsAdminFilter
from .keyboard import AdminPanelCallback, build_management_kb
from logger import logger
from ..panel.keyboard import build_admin_back_kb
router = Router()
class AdminManagementStates(StatesGroup):
waiting_for_new_domain = State()
@router.callback_query(AdminPanelCallback.filter(F.action == "management"), IsAdminFilter())
async def handle_management(callback_query: CallbackQuery):
await callback_query.message.edit_text(
text="🤖 Управление ботом",
reply_markup=build_management_kb(),
)
@router.callback_query(AdminPanelCallback.filter(F.action == "change_domain"), IsAdminFilter())
async def request_new_domain(callback_query: CallbackQuery, state: FSMContext):
"""Запрашивает у администратора новый домен."""
await state.set_state(AdminManagementStates.waiting_for_new_domain)
await callback_query.message.edit_text(
text="🌐 Введите новый домен (без https://):\nПример: pocomachodomen.ru",
)
@router.message(AdminManagementStates.waiting_for_new_domain)
async def process_new_domain(message: Message, state: FSMContext, session: Connection):
"""Обновляет домен в таблице keys."""
new_domain = message.text.strip()
logger.info(f"[DomainChange] Новый домен, введённый администратором: '{new_domain}'")
if not new_domain or " " in new_domain or not new_domain.replace(".", "").isalnum():
logger.warning("[DomainChange] Некорректный домен")
await message.answer(
"🚫 Некорректный домен! Введите домен без http:// и без пробелов.",
reply_markup=build_admin_back_kb("admin"),
)
return
new_domain_url = f"https://{new_domain}"
logger.info(f"[DomainChange] Новый домен с протоколом: '{new_domain_url}'")
query = """
UPDATE keys
SET key = regexp_replace(key, '^https://[^/]+', $1::TEXT)
WHERE key NOT LIKE $1 || '%'
"""
try:
await session.execute(query, new_domain_url)
logger.info("[DomainChange] Запрос на обновление домена выполнен успешно.")
except Exception as e:
logger.error(f"[DomainChange] Ошибка при выполнении запроса: {e}")
await message.answer(f"❌ Ошибка при обновлении домена: {e}", reply_markup=build_admin_back_kb("admin"))
return
try:
sample = await session.fetchrow("SELECT key FROM keys LIMIT 1")
logger.info(f"[DomainChange] Пример обновленной записи: {sample}")
except Exception as e:
logger.error(f"[DomainChange] Ошибка при выборке обновленной записи: {e}")
await message.answer(f"✅ Домен успешно изменен на {new_domain}!", reply_markup=build_admin_back_kb("admin"))
await state.clear()
+3
View File
@@ -0,0 +1,3 @@
__all__ = ("router",)
from .panel_handler import router
@@ -1,3 +1,5 @@
from typing import Any
from aiogram.filters.callback_data import CallbackData
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
@@ -7,10 +9,10 @@ class AdminPanelCallback(CallbackData, prefix="admin_panel"):
action: str
page: int
def __init__(self, *args, **kwargs) -> None:
if "page" not in kwargs or kwargs["page"] is None:
kwargs["page"] = 1
super().__init__(*args, **kwargs)
def __init__(self, /, **data: Any) -> None:
if "page" not in data or data["page"] is None:
data["page"] = 1
super().__init__(**data)
def build_panel_kb() -> InlineKeyboardMarkup:
@@ -18,7 +20,7 @@ def build_panel_kb() -> InlineKeyboardMarkup:
builder.button(text="👤 Поиск пользователя", callback_data=AdminPanelCallback(action="search_user").pack())
builder.button(text="🔑 Поиск по названию ключа", callback_data=AdminPanelCallback(action="search_key").pack())
builder.row(
InlineKeyboardButton(text="🖥️ Серверы", callback_data=AdminPanelCallback(action="servers").pack()),
InlineKeyboardButton(text="🖥️ Серверы", callback_data=AdminPanelCallback(action="clusters").pack()),
InlineKeyboardButton(text="🎟️ Купоны", callback_data=AdminPanelCallback(action="coupons").pack()),
)
builder.button(text="📢 Рассылка", callback_data=AdminPanelCallback(action="sender").pack())
@@ -31,18 +33,6 @@ def build_panel_kb() -> InlineKeyboardMarkup:
return builder.as_markup()
def build_management_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(text="💾 Создать резервную копию", callback_data=AdminPanelCallback(action="backups").pack())
builder.button(text="🚫 Заблокировавшие бота", callback_data=AdminPanelCallback(action="bans").pack())
builder.button(text="🔄 Перезагрузить бота", callback_data=AdminPanelCallback(action="restart").pack())
builder.button(text="🌐 Сменить домен", callback_data=AdminPanelCallback(action="change_domain").pack())
builder.button(text="🔑 Восстановить пробники", callback_data=AdminPanelCallback(action="restore_trials").pack())
builder.row(build_admin_back_btn())
builder.adjust(1)
return builder.as_markup()
def build_restart_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(text="✅ Да, перезагрузить", callback_data=AdminPanelCallback(action="restart_confirm").pack())
@@ -5,8 +5,7 @@ from aiogram.types import CallbackQuery, Message
from bot import version
from filters.admin import IsAdminFilter
from keyboards.admin.panel_kb import AdminPanelCallback, build_management_kb, build_panel_kb
from .keyboard import AdminPanelCallback, build_panel_kb
router = Router()
@@ -39,11 +38,3 @@ async def handle_admin_message(message: Message, state: FSMContext):
await state.clear()
await message.answer(text=text, reply_markup=build_panel_kb())
@router.callback_query(AdminPanelCallback.filter(F.action == "management"), IsAdminFilter())
async def handle_management(callback_query: CallbackQuery):
await callback_query.message.edit_text(
text="🤖 Управление ботом",
reply_markup=build_management_kb(),
)
+3
View File
@@ -0,0 +1,3 @@
__all__ = ("router",)
from .restart_handler import router
@@ -4,8 +4,7 @@ from aiogram import F, Router
from aiogram.types import CallbackQuery
from filters.admin import IsAdminFilter
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb, build_restart_kb
from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb, build_restart_kb
router = Router()
+3
View File
@@ -0,0 +1,3 @@
__all__ = ("router",)
from .sender_handler import router
+44
View File
@@ -0,0 +1,44 @@
from aiogram.filters.callback_data import CallbackData
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from ..panel.keyboard import build_admin_back_btn
class AdminSenderCallback(CallbackData, prefix="admin_sender"):
type: str
data: str | None = None
def build_sender_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="👥 Все пользователи", callback_data=AdminSenderCallback(type="all").pack()))
builder.row(
InlineKeyboardButton(text="С подпиской", callback_data=AdminSenderCallback(type="subscribed").pack()),
InlineKeyboardButton(text="❌ Без подписки", callback_data=AdminSenderCallback(type="unsubscribed").pack()),
)
builder.row(
InlineKeyboardButton(
text="📍 Не использовавшие триал", callback_data=AdminSenderCallback(type="untrial").pack()
)
)
builder.row(
InlineKeyboardButton(text="📢 Кластер", callback_data=AdminSenderCallback(type="cluster-select").pack())
)
builder.row(build_admin_back_btn())
return builder.as_markup()
def build_clusters_kb(clusters: list) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
for cluster in clusters:
name = cluster["cluster_name"]
builder.button(text=f"🌐 {name}", callback_data=AdminSenderCallback(type="cluster", data=name).pack())
builder.adjust(2)
builder.row(build_admin_back_btn())
return builder.as_markup()
@@ -7,10 +7,9 @@ from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, Message
from filters.admin import IsAdminFilter
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
from keyboards.admin.sender_kb import AdminSenderCallback, build_clusters_kb, build_sender_kb
from .keyboard import AdminSenderCallback, build_clusters_kb, build_sender_kb
from logger import logger
from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb
router = Router()
@@ -97,6 +96,8 @@ async def handle_message_input(message: Message, state: FSMContext, session: Any
""",
int(datetime.utcnow().timestamp() * 1000),
)
elif send_to == "untrial":
tg_ids = await session.fetch("SELECT DISTINCT tg_id FROM connections WHERE trial = 0")
elif send_to == "cluster":
cluster_name = state_data.get("cluster_name")
tg_ids = await session.fetch(
@@ -115,6 +116,10 @@ async def handle_message_input(message: Message, state: FSMContext, session: Any
total_users = len(tg_ids)
success_count = 0
text = f"📤 <b>Рассылка начата!</b>\n👥 Количество получателей: {total_users}"
await message.answer(text=text)
for record in tg_ids:
tg_id = record["tg_id"]
try:
@@ -131,11 +136,10 @@ async def handle_message_input(message: Message, state: FSMContext, session: Any
text = (
f"📤 <b>Рассылка завершена!</b>\n\n"
f"👥 <b>Всего пользователей:</b> {total_users}\n"
f"👥 <b>Количество получателей:</b> {total_users}\n"
f"✅ <b>Доставлено:</b> {success_count}\n"
f"❌ <b>Не доставлено:</b> {total_users - success_count}"
)
await message.answer(text=text, reply_markup=build_admin_back_kb("stats"), parse_mode="HTML")
await message.answer(text=text, reply_markup=build_admin_back_kb("sender"))
await state.clear()
+3
View File
@@ -0,0 +1,3 @@
__all__ = ("router",)
from .servers_handler import router
+26
View File
@@ -0,0 +1,26 @@
from aiogram.filters.callback_data import CallbackData
from aiogram.types import InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
class AdminServerCallback(CallbackData, prefix="admin_server"):
action: str
data: str
def build_manage_server_kb(server_name: str, cluster_name: str) -> InlineKeyboardMarkup:
from ..clusters.keyboard import AdminClusterCallback
builder = InlineKeyboardBuilder()
builder.button(text="🗑️ Удалить", callback_data=AdminServerCallback(action="delete", data=server_name).pack())
builder.button(text="🔙 Назад", callback_data=AdminClusterCallback(action="manage", data=cluster_name).pack())
builder.adjust(1)
return builder.as_markup()
def build_delete_server_kb(server_name: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(text="✅ Да", callback_data=AdminServerCallback(action="delete_confirm", data=server_name).pack())
builder.button(text="🔙 Назад", callback_data=AdminServerCallback(action="manage", data=server_name).pack())
builder.adjust(1)
return builder.as_markup()
+67
View File
@@ -0,0 +1,67 @@
from typing import Any
from aiogram import F, Router, types
from aiogram.types import CallbackQuery
from database import delete_server, get_servers
from filters.admin import IsAdminFilter
from .keyboard import (
AdminServerCallback,
build_delete_server_kb,
build_manage_server_kb,
)
from ..panel.keyboard import build_admin_back_kb
router = Router()
@router.callback_query(AdminServerCallback.filter(F.action == "manage"), IsAdminFilter())
async def handle_server_manage(callback_query: CallbackQuery, callback_data: AdminServerCallback):
server_name = callback_data.data
servers = await get_servers()
cluster_name, server = next(
((c, s) for c, cs in servers.items() for s in cs if s["server_name"] == server_name), (None, None)
)
if server:
api_url = server["api_url"]
subscription_url = server["subscription_url"]
inbound_id = server["inbound_id"]
text = (
f"<b>🔧 Информация о сервере {server_name}:</b>\n\n"
f"<b>📡 API URL:</b> {api_url}\n"
f"<b>🌐 Subscription URL:</b> {subscription_url}\n"
f"<b>🔑 Inbound ID:</b> {inbound_id}"
)
await callback_query.message.edit_text(
text=text,
reply_markup=build_manage_server_kb(server_name, cluster_name),
)
else:
await callback_query.message.edit_text(text="❌ Сервер не найден.")
@router.callback_query(AdminServerCallback.filter(F.action == "delete"), IsAdminFilter())
async def handle_server_delete(callback_query: CallbackQuery, callback_data: AdminServerCallback):
server_name = callback_data.data
await callback_query.message.edit_text(
text=f"🗑️ Вы уверены, что хотите удалить сервер {server_name}?",
reply_markup=build_delete_server_kb(server_name),
)
@router.callback_query(AdminServerCallback.filter(F.action == "delete_confirm"), IsAdminFilter())
async def handle_server_delete_confirm(
callback_query: types.CallbackQuery, callback_data: AdminServerCallback, session: Any
):
server_name = callback_data.data
await delete_server(server_name, session)
await callback_query.message.edit_text(
text=f"🗑️ Сервер {server_name} успешно удален.", reply_markup=build_admin_back_kb("clusters")
)
+3
View File
@@ -0,0 +1,3 @@
__all__ = ("router",)
from .stats_handler import router
@@ -1,7 +1,7 @@
from aiogram.types import InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_btn
from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn
def build_stats_kb() -> InlineKeyboardMarkup:
@@ -2,17 +2,15 @@ from datetime import datetime
from typing import Any
import pytz
from aiogram import F, Router
from aiogram.exceptions import TelegramBadRequest
from aiogram.types import CallbackQuery
from filters.admin import IsAdminFilter
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
from keyboards.admin.stats_kb import build_stats_kb
from .keyboard import build_stats_kb
from logger import logger
from utils.csv_export import export_payments_csv, export_users_csv
from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb
router = Router()
+3
View File
@@ -0,0 +1,3 @@
__all__ = ("router",)
from .users_handler import router
@@ -3,9 +3,9 @@ from datetime import datetime, timezone
from aiogram.filters.callback_data import CallbackData
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import RENEWAL_PRICES
from keyboards.admin.panel_kb import build_admin_back_btn
from config import RENEWAL_PRICES
from ..panel.keyboard import build_admin_back_btn
class AdminUserEditorCallback(CallbackData, prefix="admin_users"):
+1 -3
View File
@@ -1,20 +1,18 @@
import random
import secrets
from typing import Any
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import CAPTCHA_EMOJIS
from handlers.texts import CAPTCHA_PROMPT_MSG
from logger import logger
from .utils import edit_or_send_message
router = Router()
+1 -2
View File
@@ -3,10 +3,9 @@ from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton, LabeledPrice, Message, PreCheckoutQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import RUB_TO_XTR
from logger import logger
from .utils import edit_or_send_message
-1
View File
@@ -4,7 +4,6 @@ from aiogram import Router
from .instructions import router as instructions_router
router = Router(name="instructions_main_router")
router.include_routers(
+2 -4
View File
@@ -1,16 +1,15 @@
import os
from typing import Any
from aiogram import F, Router, types
from aiogram import F, Router
from aiogram.types import (
CallbackQuery,
InlineKeyboardButton,
Message,
)
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import CONNECT_MACOS, CONNECT_WINDOWS, SUPPORT_CHAT_URL
from config import CONNECT_MACOS, CONNECT_WINDOWS, SUPPORT_CHAT_URL
from database import get_key_details
from handlers.texts import (
CONNECT_TV_TEXT,
@@ -21,7 +20,6 @@ from handlers.texts import (
)
from handlers.utils import edit_or_send_message
router = Router()
-1
View File
@@ -5,7 +5,6 @@ from aiogram import Router
from .key_management import router as management_router
from .keys import router as keys_router
router = Router(name="keys_main_router")
router.include_routers(
+5 -8
View File
@@ -1,15 +1,18 @@
import asyncio
import uuid
from datetime import datetime, timedelta
from typing import Any
import pytz
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, InlineKeyboardButton, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from handlers.payments.yookassa_pay import process_custom_amount_input
from py3xui import AsyncApi
from bot import bot
from client import delete_client
from config import (
ADMIN_PASSWORD,
ADMIN_USERNAME,
@@ -27,10 +30,6 @@ from config import (
USE_COUNTRY_SELECTION,
USE_NEW_PAYMENT_FLOW,
)
from py3xui import AsyncApi
from bot import bot
from client import delete_client
from database import (
add_connection,
check_connection_exists,
@@ -52,7 +51,6 @@ from handlers.buttons.add_subscribe import (
)
from handlers.keys.key_utils import create_client_on_server, create_key_on_cluster
from handlers.payments.robokassa_pay import handle_custom_amount_input
from handlers.payments.yookassa_pay import process_custom_amount_input
from handlers.texts import (
CREATING_CONNECTION_MSG,
DISCOUNTS,
@@ -64,7 +62,6 @@ from handlers.texts import (
from handlers.utils import edit_or_send_message, generate_random_email, get_least_loaded_cluster
from logger import logger
router = Router()
moscow_tz = pytz.timezone("Europe/Moscow")
+2 -1
View File
@@ -1,13 +1,14 @@
import asyncio
from typing import Any
import asyncpg
from py3xui import AsyncApi
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, LIMIT_IP, PUBLIC_LINK, SUPERNODE, TOTAL_GB
from py3xui import AsyncApi
from client import ClientConfig, add_client, delete_client, extend_client_key, get_client_traffic, toggle_client
from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, PUBLIC_LINK, SUPERNODE, TOTAL_GB, DATABASE_URL
from database import get_servers, store_key
from handlers.utils import get_least_loaded_cluster
from logger import logger
+17 -18
View File
@@ -2,16 +2,17 @@ import asyncio
import locale
import os
import time
from datetime import datetime, timedelta
from typing import Any
import asyncpg
import pytz
from aiogram import F, Router, types
from aiogram.types import CallbackQuery, InlineKeyboardButton, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from handlers.payments.yookassa_pay import process_custom_amount_input
from bot import bot
from config import (
CONNECT_ANDROID,
CONNECT_IOS,
@@ -23,13 +24,11 @@ from config import (
ENABLE_UPDATE_SUBSCRIPTION_BUTTON,
PUBLIC_LINK,
RENEWAL_PLANS,
TOGGLE_CLIENT,
TOTAL_GB,
USE_COUNTRY_SELECTION,
USE_NEW_PAYMENT_FLOW,
TOGGLE_CLIENT,
)
from bot import bot
from database import (
check_server_name_by_cluster,
create_temporary_data,
@@ -53,32 +52,30 @@ from handlers.buttons.add_subscribe import (
from handlers.keys.key_utils import (
delete_key_from_cluster,
renew_key_in_cluster,
toggle_client_on_cluster,
update_subscription,
toggle_client_on_cluster,
)
from handlers.payments.robokassa_pay import handle_custom_amount_input
from handlers.payments.yookassa_pay import process_custom_amount_input
from handlers.texts import (
DELETE_KEY_CONFIRM_MSG,
DISCOUNTS,
FREEZE_SUBSCRIPTION_CONFIRM_MSG,
FROZEN_SUBSCRIPTION_MSG,
INSUFFICIENT_FUNDS_RENEWAL_MSG,
KEY_DELETED_MSG_SIMPLE,
KEY_NOT_FOUND_MSG,
NO_SUBSCRIPTIONS_MSG,
PLAN_SELECTION_MSG,
SUBSCRIPTION_DESCRIPTION,
SUBSCRIPTION_FROZEN_MSG,
SUBSCRIPTION_UNFROZEN_MSG,
SUCCESS_RENEWAL_MSG,
UNFREEZE_SUBSCRIPTION_CONFIRM_MSG,
key_message,
NO_SUBSCRIPTIONS_MSG,
FROZEN_SUBSCRIPTION_MSG,
UNFREEZE_SUBSCRIPTION_CONFIRM_MSG,
SUBSCRIPTION_UNFROZEN_MSG,
FREEZE_SUBSCRIPTION_CONFIRM_MSG,
SUBSCRIPTION_FROZEN_MSG,
DELETE_KEY_CONFIRM_MSG,
KEY_DELETED_MSG_SIMPLE,
INSUFFICIENT_FUNDS_RENEWAL_MSG,
)
from handlers.utils import edit_or_send_message, handle_error
from logger import logger
locale.setlocale(locale.LC_TIME, "ru_RU.UTF-8")
router = Router()
@@ -290,6 +287,7 @@ async def process_callback_view_key(callback_query: CallbackQuery, session: Any)
async def process_callback_unfreeze_subscription(callback_query: CallbackQuery, session: Any):
key_name = callback_query.data.split("|")[1]
confirm_text = UNFREEZE_SUBSCRIPTION_CONFIRM_MSG
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
@@ -384,6 +382,7 @@ async def process_callback_freeze_subscription(callback_query: CallbackQuery, se
"""
Показывает пользователю диалог подтверждения заморозки (отключения) подписки.
"""
tg_id = callback_query.message.chat.id
key_name = callback_query.data.split("|")[1]
confirm_text = FREEZE_SUBSCRIPTION_CONFIRM_MSG
@@ -433,7 +432,7 @@ async def process_callback_freeze_subscription_confirm(callback_query: CallbackQ
if time_left < 0:
time_left = 0
await session.execute(
update_result = await session.execute(
"""
UPDATE keys
SET expiry_time = $1,
+1 -4
View File
@@ -4,18 +4,16 @@ import random
import re
import time
import urllib.parse
from datetime import datetime
import aiohttp
import asyncpg
import pytz
from aiohttp import web
from config import (
DATABASE_URL,
PROJECT_NAME,
SUB_MESSAGE,
SUPERNODE,
SUPPORT_CHAT_URL,
TOTAL_GB,
@@ -23,7 +21,6 @@ from config import (
USERNAME_BOT,
USE_COUNTRY_SELECTION,
)
from database import get_key_details, get_servers
from handlers.utils import convert_to_bytes
from logger import logger
-1
View File
@@ -5,7 +5,6 @@ from aiogram import Router
from .general_notifications import router as general_notifications_router
from .special_notifications import router as special_notifications_router
router = Router(name="notifications_main_router")
router.include_routers(general_notifications_router, special_notifications_router)
@@ -1,25 +1,23 @@
import asyncio
from datetime import datetime, timedelta
import asyncpg
import pytz
from aiogram import Bot, Router
from config import (
DATABASE_URL,
NOTIFICATION_TIME,
NOTIFY_DELETE_DELAY,
NOTIFY_DELETE_KEY,
NOTIFY_INACTIVE_TRAFFIC,
NOTIFY_MAXPRICE,
NOTIFY_RENEW,
NOTIFY_RENEW_EXPIRED,
RENEWAL_PRICES,
TOTAL_GB,
TRIAL_TIME_DISABLE,
NOTIFY_INACTIVE_TRAFFIC,
)
from database import (
add_notification,
check_notification_time,
@@ -32,23 +30,21 @@ from database import (
)
from handlers.keys.key_utils import delete_key_from_cluster, renew_key_in_cluster
from handlers.texts import (
KEY_EXPIRY_10H,
KEY_EXPIRY_24H,
KEY_RENEWED,
KEY_RENEWED_TEMP_MSG,
KEY_DELETED_MSG,
KEY_EXPIRED_DELAY_HOURS_MINUTES_MSG,
KEY_EXPIRED_DELAY_HOURS_MSG,
KEY_EXPIRED_DELAY_MINUTES_MSG,
KEY_EXPIRED_NO_DELAY_MSG,
KEY_EXPIRY_10H,
KEY_EXPIRY_24H,
KEY_RENEWED,
KEY_RENEWED_TEMP_MSG,
)
from keyboards.notifications.notify_kb import build_notification_expired_kb, build_notification_kb
from logger import logger
from .notify_utils import send_notification
from .special_notifications import notify_inactive_trial_users, notify_users_no_traffic
router = Router()
moscow_tz = pytz.timezone("Europe/Moscow")
-1
View File
@@ -1,7 +1,6 @@
import os
import aiofiles
from aiogram import Bot
from aiogram.exceptions import TelegramForbiddenError
from aiogram.types import BufferedInputFile, InlineKeyboardMarkup
@@ -1,25 +1,22 @@
import asyncio
from datetime import datetime, timedelta
import asyncpg
import pytz
from aiogram import Bot, Router, types
from aiogram.exceptions import TelegramForbiddenError
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import NOTIFY_EXTRA_DAYS, NOTIFY_INACTIVE, NOTIFY_INACTIVE_TRAFFIC, SUPPORT_CHAT_URL, TRIAL_TIME
from config import NOTIFY_EXTRA_DAYS, NOTIFY_INACTIVE, NOTIFY_INACTIVE_TRAFFIC, SUPPORT_CHAT_URL, TRIAL_TIME
from database import (
add_notification,
check_notification_time,
create_blocked_user,
)
from handlers.keys.key_utils import get_user_traffic
from handlers.texts import TRIAL_INACTIVE_BONUS_MSG, TRIAL_INACTIVE_FIRST_MSG, ZERO_TRAFFIC_MSG
from handlers.texts import TRIAL_INACTIVE_FIRST_MSG, TRIAL_INACTIVE_BONUS_MSG, ZERO_TRAFFIC_MSG
from logger import logger
router = Router()
moscow_tz = pytz.timezone("Europe/Moscow")
+1 -1
View File
@@ -1,6 +1,7 @@
from aiogram import F, Router
from aiogram.types import CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import (
CRYPTO_BOT_ENABLE,
DONATIONS_ENABLE,
@@ -14,7 +15,6 @@ from handlers.texts import PAYMENT_METHODS_MSG
from .utils import edit_or_send_message
router = Router()
+1 -1
View File
@@ -1,6 +1,7 @@
__all__ = ("router",)
from aiogram import Router
from config import (
CRYPTO_BOT_ENABLE,
ROBOKASSA_ENABLE,
@@ -8,7 +9,6 @@ from config import (
YOOKASSA_ENABLE,
YOOMONEY_ENABLE,
)
from .cryprobot_pay import router as cryprobot_router
from .gift import router as gift_router
from .robokassa_pay import router as robokassa_router
+3 -3
View File
@@ -8,6 +8,9 @@ from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiohttp import web
from handlers.payments.utils import send_payment_success_notification
from robokassa import HashAlgorithm, Robokassa
from config import (
DATABASE_URL,
ROBOKASSA_ENABLE,
@@ -16,8 +19,6 @@ from config import (
ROBOKASSA_PASSWORD2,
ROBOKASSA_TEST_MODE,
)
from robokassa import HashAlgorithm, Robokassa
from database import (
add_connection,
add_payment,
@@ -26,7 +27,6 @@ from database import (
get_temporary_data,
update_balance,
)
from handlers.payments.utils import send_payment_success_notification
from handlers.texts import PAYMENT_OPTIONS
from handlers.utils import edit_or_send_message
from logger import logger
+3 -14
View File
@@ -1,10 +1,8 @@
import html
import os
from typing import Any
import asyncpg
from aiogram import F, Router
from aiogram.enums import ParseMode
from aiogram.fsm.context import FSMContext
@@ -17,6 +15,7 @@ from aiogram.types import (
Message,
)
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import (
DATABASE_URL,
INLINE_MODE,
@@ -27,7 +26,6 @@ from config import (
TRIAL_TIME,
USERNAME_BOT,
)
from database import get_balance, get_key_count, get_last_payments, get_referral_stats, get_trial
from handlers.buttons.profile import (
ADD_SUB,
@@ -40,20 +38,11 @@ from handlers.buttons.profile import (
MY_SUBS,
PAYMENT,
)
from handlers.texts import (
BALANCE_HISTORY_HEADER,
BALANCE_MANAGEMENT_TEXT,
INVITE_TEXT_NON_INLINE,
get_referral_link,
invite_message_send,
profile_message_send,
)
from keyboards.admin.panel_kb import AdminPanelCallback
from logger import logger
from .admin.panel.keyboard import AdminPanelCallback
from .texts import profile_message_send, invite_message_send, get_referral_link
from .utils import edit_or_send_message
router = Router()
+3 -5
View File
@@ -11,6 +11,8 @@ from aiogram.types import (
Message,
)
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot
from config import (
CAPTCHA_ENABLE,
CHANNEL_EXISTS,
@@ -20,8 +22,6 @@ from config import (
DONATIONS_ENABLE,
SUPPORT_CHAT_URL,
)
from bot import bot
from database import (
add_connection,
add_referral,
@@ -44,12 +44,10 @@ from handlers.texts import (
WELCOME_TEXT,
get_about_vpn,
)
from keyboards.admin.panel_kb import AdminPanelCallback
from logger import logger
from .admin.panel.keyboard import AdminPanelCallback
from .utils import edit_or_send_message
router = Router()
+1 -2
View File
@@ -7,11 +7,10 @@ import string
import aiofiles
import aiohttp
import asyncpg
from aiogram.types import BufferedInputFile, InlineKeyboardMarkup, InputMediaPhoto, Message
from config import DATABASE_URL
from bot import bot
from config import DATABASE_URL
from database import get_all_keys, get_servers
from logger import logger
View File
-32
View File
@@ -1,32 +0,0 @@
from aiogram.filters.callback_data import CallbackData
from aiogram.types import InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from keyboards.admin.panel_kb import build_admin_back_btn
class AdminSenderCallback(CallbackData, prefix="admin_sender"):
type: str
data: str | None = None
def build_sender_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(text="👥 Все пользователи", callback_data=AdminSenderCallback(type="all").pack())
builder.button(text="✅ Пользователи с подпиской", callback_data=AdminSenderCallback(type="subscribed").pack())
builder.button(text="❌ Пользователи без подписки", callback_data=AdminSenderCallback(type="unsubscribed").pack())
builder.button(text="📢 Пользователи кластера", callback_data=AdminSenderCallback(type="cluster-select").pack())
builder.row(build_admin_back_btn())
builder.adjust(1)
return builder.as_markup()
def build_clusters_kb(clusters: list) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
for cluster in clusters:
name = cluster["cluster_name"]
builder.button(text=f"🌐 {name}", callback_data=AdminSenderCallback(type="cluster", data=name).pack())
builder.row(build_admin_back_btn())
builder.adjust(1)
return builder.as_markup()
-81
View File
@@ -1,81 +0,0 @@
from aiogram.filters.callback_data import CallbackData
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_btn
class AdminServerEditorCallback(CallbackData, prefix="admin_servers"):
action: str
data: str
def build_clusters_editor_kb(servers: dict) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
for cluster_name in servers:
builder.button(
text=f"⚙️ {cluster_name}",
callback_data=AdminServerEditorCallback(action="clusters_manage", data=cluster_name).pack(),
)
builder.button(text=" Добавить кластер", callback_data=AdminPanelCallback(action="clusters_add").pack())
builder.row(build_admin_back_btn())
builder.adjust(1)
return builder.as_markup()
def build_manage_cluster_kb(cluster_servers, cluster_name) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
for server in cluster_servers:
builder.button(
text=f"🌍 {server['server_name']}",
callback_data=AdminServerEditorCallback(action="servers_manage", data=server["server_name"]).pack(),
)
builder.button(
text=" Добавить сервер",
callback_data=AdminServerEditorCallback(action="servers_add", data=cluster_name).pack(),
)
builder.row(
InlineKeyboardButton(
text="🌐 Доступность",
callback_data=AdminServerEditorCallback(action="servers_availability", data=cluster_name).pack(),
),
InlineKeyboardButton(
text="🔄 Синхронизация",
callback_data=AdminServerEditorCallback(action="clusters_sync", data=cluster_name).pack(),
),
)
builder.button(
text="💾 Создать бэкап кластера",
callback_data=AdminServerEditorCallback(action="clusters_backup", data=cluster_name).pack(),
)
builder.row(build_admin_back_btn("servers"))
builder.adjust(1, 1, 1, 1, 1, 2, 1)
return builder.as_markup()
def build_manage_server_kb(server_name: str, cluster_name: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text="🗑️ Удалить", callback_data=AdminServerEditorCallback(action="servers_delete", data=server_name).pack()
)
builder.button(
text="🔙 Назад", callback_data=AdminServerEditorCallback(action="clusters_manage", data=cluster_name).pack()
)
builder.adjust(1)
return builder.as_markup()
def build_delete_server_kb(server_name: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text="✅ Да", callback_data=AdminServerEditorCallback(action="servers_delete_confirm", data=server_name).pack()
)
builder.button(
text="🔙 Назад", callback_data=AdminServerEditorCallback(action="servers_manage", data=server_name).pack()
)
builder.adjust(1)
return builder.as_markup()
-2
View File
@@ -1,12 +1,10 @@
import logging
import os
import sys
from datetime import timedelta
from loguru import logger
log_folder = "logs"
if not os.path.exists(log_folder):
-1
View File
@@ -1,5 +1,4 @@
from collections.abc import Iterable
from typing import Any, Optional, Type, Union
from aiogram import Dispatcher
from aiogram.dispatcher.middlewares.base import BaseMiddleware
+1
View File
@@ -3,6 +3,7 @@ from typing import Any
from aiogram import BaseMiddleware
from aiogram.types import CallbackQuery, Message, TelegramObject
from config import ADMIN_ID
+1 -1
View File
@@ -2,9 +2,9 @@ from collections.abc import Awaitable, Callable
from typing import Any
import asyncpg
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
from config import DATABASE_URL
+3 -5
View File
@@ -1,19 +1,17 @@
import asyncio
import re
from datetime import datetime, timedelta
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import ADMIN_ID, PING_TIME
from ping3 import ping
from bot import bot
from config import ADMIN_ID, PING_TIME
from database import get_servers
from keyboards.admin.servers_kb import AdminServerEditorCallback
from handlers.admin.servers.keyboard import AdminServerCallback
from logger import logger
last_ping_times = {}
last_down_times = {}
notified_servers = set()
@@ -49,7 +47,7 @@ async def notify_admin(server_name: str, status: str, down_duration: timedelta =
builder.row(
InlineKeyboardButton(
text="Управление сервером",
callback_data=AdminServerEditorCallback(action="servers_manage", data=server_name).pack(),
callback_data=AdminServerCallback(action="manage", data=server_name).pack(),
)
)
-1
View File
@@ -1,5 +1,4 @@
import csv
from io import StringIO
from typing import Any