Merge branch 'feature/admin-optimize' into upstream-168

This commit is contained in:
Oleg Kraev
2025-03-25 17:34:46 +03:00
committed by GitHub
43 changed files with 422 additions and 389 deletions
+8 -1
View File
@@ -72,7 +72,14 @@ async def add_client(xui: py3xui.AsyncApi, config: ClientConfig) -> dict[str, An
async def extend_client_key(
xui: py3xui.AsyncApi, inbound_id: int, email: str, new_expiry_time: int, client_id: str, total_gb: int, sub_id: str, tg_id: int
xui: py3xui.AsyncApi,
inbound_id: int,
email: str,
new_expiry_time: int,
client_id: str,
total_gb: int,
sub_id: str,
tg_id: int,
) -> bool | None:
"""
Обновляет срок действия ключа клиента.
+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,7 +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,8 +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,39 @@
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,
from keyboard import (
build_clusters_editor_kb,
build_delete_server_kb,
build_manage_cluster_kb,
build_manage_server_kb,
AdminClusterCallback,
)
from logger import logger
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 +54,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 +96,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 +125,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 +133,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 +164,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 +200,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 +238,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
@@ -258,9 +259,9 @@ async def handle_clusters_manage(
)
@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 +279,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"🖥️ Проверка доступности серверов для кластера {cluster_name} завершена:\n\n"
for server in cluster_servers:
xui = AsyncApi(server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD, logger=logger)
@@ -289,92 +290,17 @@ async def handle_servers_availability(
online_users = len(await xui.client.online())
total_online_users += online_users
result_text += f"🌍 {server['server_name']}: {online_users} активных пользователей.\n"
except Exception as e:
result_text += f"{server['server_name']}: Не удалось получить информацию. Ошибка: {e}\n"
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 +323,12 @@ 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_clusters_sync(callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any):
cluster_name = callback_data.data
try:
@@ -418,7 +342,7 @@ 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
@@ -437,59 +361,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()
+63
View File
@@ -0,0 +1,63 @@
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
def build_clusters_editor_kb(servers: dict) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
cluster_names = list(servers.keys())
for i in range(0, len(cluster_names), 2):
row_buttons = []
for cluster_name in cluster_names[i:i+2]:
row_buttons.append(
InlineKeyboardButton(
text=f"⚙️ {cluster_name}",
callback_data=AdminClusterCallback(action="manage", data=cluster_name).pack(),
)
)
builder.row(*row_buttons)
builder.button(text=" Добавить кластер", callback_data=AdminClusterCallback(action="add").pack())
builder.row(build_admin_back_btn())
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=AdminServerCallback(action="manage", data=server["server_name"]).pack(),
)
builder.button(
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.button(
text="💾 Создать бэкап кластера",
callback_data=AdminClusterCallback(action="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()
+3
View File
@@ -0,0 +1,3 @@
__all__ = ("router",)
from coupons_handler import router
@@ -8,8 +8,8 @@ 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 ..panel.keyboard import AdminPanelCallback, build_admin_back_kb
from logger import logger
@@ -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: AdminPanelCallback, 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:
@@ -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,7 +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 handlers.admin.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
@@ -2,7 +2,7 @@ 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
from ..panel.keyboard import build_admin_back_btn
class AdminSenderCallback(CallbackData, prefix="admin_sender"):
@@ -7,8 +7,8 @@ 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 ..panel.keyboard import AdminPanelCallback, build_admin_back_kb
from keyboard import AdminSenderCallback, build_clusters_kb, build_sender_kb
from logger import logger
+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
from ..clusters.keyboard import AdminClusterCallback
class AdminServerCallback(CallbackData, prefix="admin_server"):
action: str
data: str
def build_manage_server_kb(server_name: str, cluster_name: str) -> InlineKeyboardMarkup:
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:
@@ -8,8 +8,8 @@ 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 ..panel.keyboard import AdminPanelCallback, build_admin_back_kb
from keyboard import build_stats_kb
from logger import logger
from utils.csv_export import export_payments_csv, export_users_csv
+3
View File
@@ -0,0 +1,3 @@
__all__ = ("router",)
from users_handler import router
@@ -5,7 +5,7 @@ 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 ..panel.keyboard import build_admin_back_btn
class AdminUserEditorCallback(CallbackData, prefix="admin_users"):
@@ -27,7 +27,7 @@ from database import (
from filters.admin import IsAdminFilter
from handlers.keys.key_utils import delete_key_from_cluster, get_user_traffic, renew_key_in_cluster, update_subscription
from handlers.utils import sanitize_key_name
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb, build_admin_back_btn
from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb, build_admin_back_btn
from keyboards.admin.users_kb import (
AdminUserEditorCallback,
AdminUserKeyEditorCallback,
@@ -733,10 +733,11 @@ async def confirm_restore_trials(callback_query: types.CallbackQuery):
await callback_query.message.edit_text(
text="⚠ Вы уверены, что хотите восстановить пробники для пользователей? \n\n"
"Только для тех, у кого нет активной подписки!",
reply_markup=builder.as_markup()
"Только для тех, у кого нет активной подписки!",
reply_markup=builder.as_markup(),
)
@router.callback_query(AdminPanelCallback.filter(F.action == "confirm_restore_trials"), IsAdminFilter())
async def restore_trials(callback_query: types.CallbackQuery, session: Any):
"""
@@ -759,15 +760,13 @@ async def restore_trials(callback_query: types.CallbackQuery, session: Any):
await callback_query.message.edit_text(
text="✅ Пробники успешно восстановлены для пользователей, у которых нет активных подписок.",
reply_markup=builder.as_markup()
reply_markup=builder.as_markup(),
)
@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_export_referrals"), IsAdminFilter())
async def handle_users_export_referrals(
callback_query: types.CallbackQuery,
callback_data: AdminUserEditorCallback,
session: Any
callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any
):
"""
Обработчик: получает tg_id реферера из callback_data,
@@ -783,6 +782,5 @@ async def handle_users_export_referrals(
return
await callback_query.message.answer_document(
document=csv_file,
caption=f"Список рефералов для пользователя {referrer_tg_id}."
)
document=csv_file, caption=f"Список рефералов для пользователя {referrer_tg_id}."
)
+1 -1
View File
@@ -4,4 +4,4 @@ BACK = "⬅️ Назад"
CUSTOM_SUM = "💰 Ввести свою сумму"
CUSTOM_SUM_ANSWER = "Пожалуйста, введите сумму пополнения."
PROFILE = "👤 Личный кабинет"
DEFAULT_PAYMENT_MESSAGE = "Вы выбрали пополнение на {amount} рублей. Перейдите по ссылке для оплаты:"
DEFAULT_PAYMENT_MESSAGE = "Вы выбрали пополнение на {amount} рублей. Перейдите по ссылке для оплаты:"
+16 -35
View File
@@ -26,7 +26,7 @@ from config import (
TOTAL_GB,
USE_COUNTRY_SELECTION,
USE_NEW_PAYMENT_FLOW,
TOGGLE_CLIENT
TOGGLE_CLIENT,
)
from bot import bot
@@ -177,7 +177,7 @@ async def process_callback_view_key(callback_query: CallbackQuery, session: Any)
reply_markup=keyboard,
media_path=image_path,
)
else:
key = record["key"]
expiry_time = record["expiry_time"]
@@ -188,17 +188,13 @@ async def process_callback_view_key(callback_query: CallbackQuery, session: Any)
time_left = expiry_date - current_date
if time_left.total_seconds() <= 0:
days_left_message = (
"<b>🕒 Статус подписки:</b>\n🔴 Истекла\nОсталось часов: 0\nОсталось минут: 0"
)
days_left_message = "<b>🕒 Статус подписки:</b>\n🔴 Истекла\nОсталось часов: 0\nОсталось минут: 0"
else:
total_seconds = int(time_left.total_seconds())
days = total_seconds // 86400
hours = (total_seconds % 86400) // 3600
minutes = (total_seconds % 3600) // 60
days_left_message = (
f"Осталось: <b>{days}</b> дней, <b>{hours}</b> часов, <b>{minutes}</b> минут"
)
days_left_message = f"Осталось: <b>{days}</b> дней, <b>{hours}</b> часов, <b>{minutes}</b> минут"
formatted_expiry_date = expiry_date.strftime("%d %B %Y года")
response_message = key_message(
@@ -248,18 +244,12 @@ async def process_callback_view_key(callback_query: CallbackQuery, session: Any)
)
else:
builder.row(
InlineKeyboardButton(
text="⏳ Продлить подписку",
callback_data=f"renew_key|{key_name}"
)
InlineKeyboardButton(text="⏳ Продлить подписку", callback_data=f"renew_key|{key_name}")
)
if USE_COUNTRY_SELECTION:
builder.row(
InlineKeyboardButton(
text="🌍 Сменить локацию",
callback_data=f"change_location|{key_name}"
)
InlineKeyboardButton(text="🌍 Сменить локацию", callback_data=f"change_location|{key_name}")
)
if TOGGLE_CLIENT:
@@ -300,6 +290,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(
@@ -355,7 +346,7 @@ async def process_callback_unfreeze_subscription_confirm(callback_query: Callbac
""",
new_expiry_time,
record["tg_id"],
client_id
client_id,
)
await renew_key_in_cluster(
@@ -367,9 +358,7 @@ async def process_callback_unfreeze_subscription_confirm(callback_query: Callbac
)
text_ok = SUBSCRIPTION_UNFROZEN_MSG
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="⬅️ Назад", callback_data=f"view_key|{key_name}")
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"view_key|{key_name}"))
await edit_or_send_message(
target_message=callback_query.message,
text=text_ok,
@@ -377,13 +366,10 @@ async def process_callback_unfreeze_subscription_confirm(callback_query: Callbac
)
else:
text_error = (
"Произошла ошибка при включении подписки.\n"
f"Детали: {result.get('error') or result.get('results')}"
f"Произошла ошибка при включении подписки.\nДетали: {result.get('error') or result.get('results')}"
)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="⬅️ Назад", callback_data=f"view_key|{key_name}")
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"view_key|{key_name}"))
await edit_or_send_message(
target_message=callback_query.message,
text=text_error,
@@ -447,7 +433,7 @@ async def process_callback_freeze_subscription_confirm(callback_query: CallbackQ
now_ms = int(time.time() * 1000)
time_left = record["expiry_time"] - now_ms
if time_left < 0:
time_left = 0
time_left = 0
update_result = await session.execute(
"""
@@ -459,14 +445,12 @@ async def process_callback_freeze_subscription_confirm(callback_query: CallbackQ
""",
time_left,
record["tg_id"],
client_id
client_id,
)
text_ok = SUBSCRIPTION_FROZEN_MSG
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="⬅️ Назад", callback_data=f"view_key|{key_name}")
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"view_key|{key_name}"))
await edit_or_send_message(
target_message=callback_query.message,
text=text_ok,
@@ -474,13 +458,10 @@ async def process_callback_freeze_subscription_confirm(callback_query: CallbackQ
)
else:
text_error = (
"Произошла ошибка при заморозке подписки.\n"
f"Детали: {result.get('error') or result.get('results')}"
f"Произошла ошибка при заморозке подписки.\nДетали: {result.get('error') or result.get('results')}"
)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="⬅️ Назад", callback_data=f"view_key|{key_name}")
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"view_key|{key_name}"))
await edit_or_send_message(
target_message=callback_query.message,
text=text_error,
@@ -15,7 +15,7 @@ from config import (
RENEWAL_PRICES,
TOTAL_GB,
TRIAL_TIME_DISABLE,
NOTIFY_INACTIVE_TRAFFIC
NOTIFY_INACTIVE_TRAFFIC,
)
from database import (
add_notification,
-8
View File
@@ -40,14 +40,6 @@ from handlers.buttons.profile import (
MY_SUBS,
PAYMENT,
)
from handlers.texts import (
get_referral_link,
invite_message_send,
profile_message_send,
BALANCE_MANAGEMENT_TEXT,
BALANCE_HISTORY_HEADER,
INVITE_TEXT_NON_INLINE
)
from keyboards.admin.panel_kb import AdminPanelCallback
from logger import logger
View File
-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()
+1 -1
View File
@@ -1,5 +1,5 @@
from collections.abc import Awaitable, Callable
from typing import Any,TypedDict
from typing import Any, TypedDict
from aiogram import BaseMiddleware
from aiogram.types import CallbackQuery, InlineQuery, Message, TelegramObject, User
+1 -1
View File
@@ -28,4 +28,4 @@ class ThrottlingMiddleware(BaseMiddleware):
else:
self.cache[user_id] = current_count + 1
return await handler(event, data)
return await handler(event, data)
+6 -7
View File
@@ -60,10 +60,7 @@ async def notify_admin(server_name: str, status: str, down_duration: timedelta =
)
else:
downtime = str(down_duration).split(".")[0]
message = (
f"✅ <b>Сервер '{server_name}' снова в сети!</b>\n\n"
f"⏳ Время простоя: {downtime}."
)
message = f"✅ <b>Сервер '{server_name}' снова в сети!</b>\n\n⏳ Время простоя: {downtime}."
for admin_id in ADMIN_ID:
logger.info(f"📨 Отправляем уведомление '{status}' администратору {admin_id} о сервере {server_name}")
@@ -119,11 +116,13 @@ async def check_servers():
if last_ping_time is None:
last_ping_times[server_name] = current_time
last_down_times[server_name] = current_time
last_down_times[server_name] = current_time
if last_ping_time and (current_time - last_ping_time > timedelta(seconds=PING_TIME * 3)):
if server_name not in notified_servers:
logger.warning(f"🚨 Уведомление: сервер {server_name} не отвечает более {PING_TIME * 3} секунд!")
logger.warning(
f"🚨 Уведомление: сервер {server_name} не отвечает более {PING_TIME * 3} секунд!"
)
await notify_admin(server_name, "down")
notified_servers.add(server_name)
last_down_times[server_name] = current_time
@@ -145,4 +144,4 @@ async def check_servers():
def extract_host(api_url: str) -> str:
"""Извлекает хост из `api_url`."""
match = re.match(r"(https?://)?([^:/]+)", api_url)
return match.group(2) if match else api_url
return match.group(2) if match else api_url
+3 -3
View File
@@ -115,14 +115,14 @@ async def export_referrals_csv(referrer_tg_id: int, session: Any) -> BufferedInp
WHERE r.referrer_tg_id = $1
ORDER BY r.referred_tg_id
""",
referrer_tg_id
referrer_tg_id,
)
if not rows:
return None
output = StringIO()
writer = csv.writer(output, delimiter=';')
writer = csv.writer(output, delimiter=";")
writer.writerow(["Приглашённый (tg_id)", "Имя"])
for row in rows:
@@ -136,4 +136,4 @@ async def export_referrals_csv(referrer_tg_id: int, session: Any) -> BufferedInp
csv_data = output.getvalue().encode("utf-8")
filename = f"referrals_{referrer_tg_id}.csv"
return BufferedInputFile(file=csv_data, filename=filename)
return BufferedInputFile(file=csv_data, filename=filename)