diff --git a/database.py b/database.py index 827a6a99..7c694e55 100644 --- a/database.py +++ b/database.py @@ -819,6 +819,7 @@ async def update_key_expiry(client_id: str, new_expiry_time: int, session: Any): Args: client_id (str): Уникальный идентификатор клиента new_expiry_time (int): Новое время истечения ключа + session (Any): Сессия подключения к базе данных Raises: Exception: В случае ошибки при подключении к базе данных или обновлении ключа diff --git a/handlers/admin/admin_sender.py b/handlers/admin/admin_sender.py index af0c7535..6e236045 100644 --- a/handlers/admin/admin_sender.py +++ b/handlers/admin/admin_sender.py @@ -8,7 +8,7 @@ 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_sender_kb +from keyboards.admin.sender_kb import AdminSenderCallback, build_sender_kb, build_clusters_kb from logger import logger router = Router() @@ -30,7 +30,7 @@ async def handle_sender(callback_query: CallbackQuery): @router.callback_query( - AdminSenderCallback.filter(), + AdminSenderCallback.filter(F.type != "cluster-select"), IsAdminFilter(), ) async def handle_sender_callback(callback_query: CallbackQuery, callback_data: AdminSenderCallback, state: FSMContext): @@ -38,10 +38,22 @@ async def handle_sender_callback(callback_query: CallbackQuery, callback_data: A text="✍️ Введите текст сообщения для рассылки:", reply_markup=build_admin_back_kb("sender"), ) - await state.update_data(type=callback_data.type) + await state.update_data(type=callback_data.type, cluster_name=callback_data.data) await state.set_state(AdminSender.waiting_for_message) +@router.callback_query( + AdminSenderCallback.filter(F.type == "cluster-select"), + IsAdminFilter(), +) +async def handle_sender_callback(callback_query: CallbackQuery, session: Any): + clusters = await session.fetch("SELECT DISTINCT cluster_name FROM servers") + await callback_query.message.answer( + "✍️ Выберите кластер для рассылки сообщений:", + reply_markup=build_clusters_kb(clusters), + ) + + @router.message( AdminSender.waiting_for_message, IsAdminFilter(), @@ -74,6 +86,18 @@ async def handle_message_input(message: Message, state: FSMContext, session: Any """, int(datetime.utcnow().timestamp() * 1000), ) + elif send_to == "cluster": + cluster_name = state_data.get("cluster_name") + tg_ids = await session.fetch( + """ + SELECT DISTINCT c.tg_id + FROM connections c + JOIN keys k ON c.tg_id = k.tg_id + JOIN servers s ON k.server_id = s.cluster_name + WHERE s.cluster_name = $1 + """, + cluster_name, + ) else: tg_ids = await session.fetch("SELECT DISTINCT tg_id FROM connections") diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py index 792bc81f..efffc44c 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/admin_servers.py @@ -1,3 +1,4 @@ +import asyncio from typing import Any import asyncpg @@ -11,6 +12,7 @@ from backup import create_backup_and_send_to_admins from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL from database import check_unique_server_name, delete_server, 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, @@ -19,6 +21,7 @@ from keyboards.admin.servers_kb import ( build_manage_cluster_kb, build_manage_server_kb, ) +from logger import logger router = Router() @@ -230,7 +233,9 @@ async def handle_inbound_id_input(message: Message, state: FSMContext): @router.callback_query(AdminServerEditorCallback.filter(F.action == "clusters_manage"), IsAdminFilter()) -async def handle_clusters_manage(callback_query: CallbackQuery, callback_data: AdminServerEditorCallback, session: Any): +async def handle_clusters_manage( + callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any +): cluster_name = callback_data.data servers = await get_servers(session) @@ -244,7 +249,7 @@ async def handle_clusters_manage(callback_query: CallbackQuery, callback_data: A @router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_availability"), IsAdminFilter()) async def handle_servers_availability( - callback_query: CallbackQuery, callback_data: AdminServerEditorCallback, session: Any + callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any ): cluster_name = callback_data.data @@ -320,7 +325,7 @@ async def handle_servers_delete(callback_query: CallbackQuery, callback_data: Ad @router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_delete_confirm"), IsAdminFilter()) async def handle_servers_delete_confirm( - callback_query: CallbackQuery, callback_data: AdminServerEditorCallback, session: Any + callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any ): server_name = callback_data.data @@ -333,7 +338,7 @@ async def handle_servers_delete_confirm( @router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_add"), IsAdminFilter()) async def handle_servers_add( - callback_query: CallbackQuery, callback_data: AdminServerEditorCallback, state: FSMContext + callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, state: FSMContext ): cluster_name = callback_data.data @@ -354,7 +359,9 @@ async def handle_servers_add( @router.callback_query(AdminServerEditorCallback.filter(F.action == "clusters_backup"), IsAdminFilter()) -async def handle_clusters_backup(callback_query: CallbackQuery, callback_data: AdminServerEditorCallback, session: Any): +async def handle_clusters_backup( + callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any +): cluster_name = callback_data.data servers = await get_servers(session) @@ -377,3 +384,53 @@ async def handle_clusters_backup(callback_query: CallbackQuery, callback_data: A text=text, reply_markup=build_admin_back_kb("servers"), ) + + +@router.callback_query(AdminServerEditorCallback.filter(F.action == "clusters_sync"), IsAdminFilter()) +async def handle_clusters_backup( + callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any +): + cluster_name = callback_data.data + + try: + query_keys = """ + SELECT tg_id, client_id, email, expiry_time + FROM keys + WHERE server_id = $1 + """ + keys_to_sync = await session.fetch(query_keys, cluster_name) + + if not keys_to_sync: + await callback_query.message.answer( + text=f"❌ Нет ключей для синхронизации в кластере {cluster_name}.", + reply_markup=build_admin_back_kb("servers"), + ) + return + + servers = await get_servers(session) + cluster_servers = servers.get(cluster_name, []) + + for key in keys_to_sync: + for _server in cluster_servers: + try: + await create_key_on_cluster( + cluster_name, + key["tg_id"], + key["client_id"], + key["email"], + key["expiry_time"], + ) + await asyncio.sleep(0.6) + except Exception as e: + logger.error(f"Ошибка при добавлении ключа {key['client_id']} в кластер {cluster_name}: {e}") + + await callback_query.message.answer( + text=f"✅ Ключи успешно синхронизированы для кластера {cluster_name}", + reply_markup=build_admin_back_kb("servers") + ) + 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") + ) diff --git a/handlers/admin/admin_users.py b/handlers/admin/admin_users.py index e73b3fb2..9b35ad2d 100644 --- a/handlers/admin/admin_users.py +++ b/handlers/admin/admin_users.py @@ -4,13 +4,13 @@ from typing import Any from aiogram import F, Router, types from aiogram.exceptions import TelegramBadRequest -from aiogram.filters.callback_data import CallbackData from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, Message from config import TOTAL_GB -from database import delete_key, delete_user_data, get_client_id_by_email, get_servers, update_key_expiry, update_trial +from database import delete_key, delete_user_data, get_client_id_by_email, get_servers, update_key_expiry, update_trial, \ + get_balance, update_balance from filters.admin import IsAdminFilter from handlers.keys.key_utils import ( delete_key_from_cluster, @@ -131,11 +131,14 @@ async def handle_key_name_input(message: Message, state: FSMContext, session: An AdminUserEditorCallback.filter(F.action == "users_send_message"), IsAdminFilter(), ) -async def handle_send_message(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext): +async def handle_send_message( + callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext +): tg_id = callback_data.tg_id await callback_query.message.edit_text( - text="✉️ Введите текст сообщения, которое вы хотите отправить пользователю:", reply_markup=build_editor_kb(tg_id) + text="✉️ Введите текст сообщения, которое вы хотите отправить пользователю:", + reply_markup=build_editor_kb(tg_id) ) await state.update_data(tg_id=tg_id) @@ -160,7 +163,9 @@ async def handle_message_text_input(message: Message, state: FSMContext): AdminUserEditorCallback.filter(F.action == "users_trial_restore"), IsAdminFilter(), ) -async def handle_trial_restore(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, session: Any): +async def handle_trial_restore( + callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any +): tg_id = callback_data.tg_id await update_trial(tg_id, 0, session) @@ -182,7 +187,7 @@ async def handle_balance_change(callback_query: CallbackQuery, callback_data: Ad tg_id, ) - balance = await get_user_balance(tg_id, session) + balance = await get_balance(tg_id) text = ( f"💵 Изменение баланса" @@ -210,13 +215,13 @@ async def handle_balance_change(callback_query: CallbackQuery, callback_data: Ad @router.callback_query(AdminUserEditorCallback.filter(F.action == "users_balance_add"), IsAdminFilter()) async def handle_balance_add( - callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext, session: Any + callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext, session: Any ): tg_id = callback_data.tg_id amount = callback_data.data if amount: - await add_user_balance(tg_id, int(amount), session) + await update_balance(tg_id, int(amount), session) await handle_balance_change(callback_query, callback_data, session) return @@ -271,10 +276,10 @@ async def handle_balance_input(message: Message, state: FSMContext, session: Any if op_type == "add": text = f"✅ К балансу пользователя добавлено {amount}Р" - await add_user_balance(tg_id, amount, session) + await update_balance(tg_id, amount, session) elif op_type == "take": text = f"✅ Из баланса пользователя было вычтено {amount}Р" - await add_user_balance(tg_id, -amount, session) + await update_balance(tg_id, -amount, session) else: text = f"✅ Баланс пользователя изменен на {amount}Р" await set_user_balance(tg_id, amount, session) @@ -284,7 +289,8 @@ async def handle_balance_input(message: Message, state: FSMContext, session: Any @router.callback_query(AdminUserEditorCallback.filter(F.action == "users_key_edit"), IsAdminFilter()) async def handle_key_edit( - callback_query: CallbackQuery, callback_data: CallbackData, session: Any, update: bool = False + callback_query: CallbackQuery, callback_data: AdminUserEditorCallback | AdminUserKeyEditorCallback, + session: Any, update: bool = False ): email = callback_data.data key_details = await get_key_details(email, session) @@ -322,7 +328,7 @@ async def handle_change_expiry(callback_query: CallbackQuery, callback_data: Adm @router.callback_query(AdminUserKeyEditorCallback.filter(F.action == "add"), IsAdminFilter()) async def handle_expiry_add( - callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, state: FSMContext, session: Any + callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, state: FSMContext, session: Any ): tg_id = callback_data.tg_id email = callback_data.data @@ -353,7 +359,7 @@ async def handle_expiry_add( @router.callback_query(AdminUserKeyEditorCallback.filter(F.action == "take"), IsAdminFilter()) async def handle_expiry_take( - callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, state: FSMContext + callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, state: FSMContext ): tg_id = callback_data.tg_id email = callback_data.data @@ -369,18 +375,27 @@ async def handle_expiry_take( @router.callback_query(AdminUserKeyEditorCallback.filter(F.action == "set"), IsAdminFilter()) async def handle_expiry_set( - callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, state: FSMContext + callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, state: FSMContext, session: Any ): tg_id = callback_data.tg_id email = callback_data.data + key_details = await get_key_details(email, session) + + if not key_details: + await callback_query.message.edit_text( + text="🚫 Информация о ключе не найдена.", + reply_markup=build_editor_kb(tg_id), + ) + return + await state.update_data(tg_id=tg_id, email=email, op_type="set") await state.set_state(UserEditorState.waiting_for_expiry_time) text = ( "✍️ Введите новое время действия ключа:" "\n\n📌 Формат: год-месяц-день час:минута" - "\n Пример: 2025-02-09 09:01" + f"\n\n📄 Текущая дата: {datetime.fromtimestamp(key_details['expiry_time'] / 1000).strftime('%Y-%m-%d %H:%M')}" ) await callback_query.message.edit_text(text=text, reply_markup=build_users_key_show_kb(tg_id, email)) @@ -437,7 +452,7 @@ async def handle_update_key(callback_query: CallbackQuery, callback_data: AdminU try: await update_subscription(tg_id, email, session) - await handle_key_edit(callback_query, callback_data, session) + await handle_key_edit(callback_query, callback_data, session, True) except TelegramBadRequest: pass except Exception as e: @@ -465,7 +480,7 @@ async def handle_delete_key(callback_query: CallbackQuery, callback_data: AdminU @router.callback_query(AdminUserEditorCallback.filter(F.action == "users_delete_key_confirm"), IsAdminFilter()) async def handle_delete_key_confirm( - callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, session: Any + callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any ): email = callback_data.data record = await session.fetchrow("SELECT client_id FROM keys WHERE email = $1", email) @@ -501,7 +516,7 @@ async def handle_delete_user(callback_query: CallbackQuery, callback_data: Admin @router.callback_query(AdminUserEditorCallback.filter(F.action == "users_delete_user_confirm"), IsAdminFilter()) async def handle_delete_user_confirm( - callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, session: Any + callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any ): tg_id = callback_data.tg_id key_records = await session.fetch("SELECT email, client_id FROM keys WHERE tg_id = $1", tg_id) @@ -533,13 +548,13 @@ async def handle_delete_user_confirm( @router.callback_query(AdminUserEditorCallback.filter(F.action == "users_editor"), IsAdminFilter()) async def handle_editor( - callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext, session: Any + callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext, session: Any ): await process_user_search(callback_query.message, state, session, callback_data.tg_id, callback_data.edit) async def process_user_search( - message: Message, state: FSMContext, session: Any, tg_id: int, edit: bool = False + message: types.Message, state: FSMContext, session: Any, tg_id: int, edit: bool = False ) -> None: await state.clear() @@ -636,28 +651,6 @@ async def change_expiry_time(expiry_time: int, email: str, session: Any) -> Exce await update_key_expiry(client_id, expiry_time, session) -async def get_user_balance(tg_id: int, session: Any) -> float: - try: - return await session.fetchval( - "SELECT balance FROM connections WHERE tg_id = $1", - tg_id, - ) - except Exception as e: - logger.error(f"Ошибка при получении баланса для пользователя {tg_id}: {e}") - return -1 - - -async def add_user_balance(tg_id: int, balance: int, session: Any) -> None: - try: - await session.execute( - "UPDATE connections SET balance = balance + $1 WHERE tg_id = $2", - balance, - tg_id, - ) - except Exception as e: - logger.error(f"Ошибка при добавлении баланса для пользователя {tg_id}: {e}") - - async def set_user_balance(tg_id: int, balance: int, session: Any) -> None: try: await session.execute( diff --git a/keyboards/admin/panel_kb.py b/keyboards/admin/panel_kb.py index 2c23381d..b4388d57 100644 --- a/keyboards/admin/panel_kb.py +++ b/keyboards/admin/panel_kb.py @@ -53,7 +53,7 @@ def build_admin_singleton_kb(text: str, action: str) -> InlineKeyboardMarkup: return builder.as_markup() -def build_admin_back_btn(action: str = "admin") -> InlineKeyboardMarkup: +def build_admin_back_btn(action: str = "admin") -> InlineKeyboardButton: return build_admin_btn("🔙 Назад", action) diff --git a/keyboards/admin/sender_kb.py b/keyboards/admin/sender_kb.py index d0acfc59..9d1055f0 100644 --- a/keyboards/admin/sender_kb.py +++ b/keyboards/admin/sender_kb.py @@ -7,6 +7,7 @@ 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: @@ -14,6 +15,21 @@ def build_sender_kb() -> InlineKeyboardMarkup: 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() diff --git a/keyboards/admin/servers_kb.py b/keyboards/admin/servers_kb.py index e857408b..c3498724 100644 --- a/keyboards/admin/servers_kb.py +++ b/keyboards/admin/servers_kb.py @@ -46,6 +46,10 @@ def build_manage_cluster_kb(cluster_servers, cluster_name) -> InlineKeyboardMark text="💾 Создать бэкап кластера", callback_data=AdminServerEditorCallback(action="clusters_backup", data=cluster_name).pack(), ) + builder.button( + text="🔄 Синхронизировать", + callback_data=AdminServerEditorCallback(action="clusters_sync", data=cluster_name).pack(), + ) builder.row(build_admin_back_btn("servers")) builder.adjust(1) return builder.as_markup() diff --git a/keyboards/admin/users_kb.py b/keyboards/admin/users_kb.py index 54d056a9..ae13f2ea 100644 --- a/keyboards/admin/users_kb.py +++ b/keyboards/admin/users_kb.py @@ -20,6 +20,7 @@ class AdminUserKeyEditorCallback(CallbackData, prefix="admin_users_key"): tg_id: int data: str month: int | None = None + edit: bool = False def build_user_edit_kb(tg_id: int, key_records: list) -> InlineKeyboardMarkup: