diff --git a/database/payments.py b/database/payments.py index d8326c99..ec20c673 100644 --- a/database/payments.py +++ b/database/payments.py @@ -3,21 +3,26 @@ from datetime import datetime from sqlalchemy import insert, select from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession +from pytz import timezone from database.models import Payment from logger import logger +MOSCOW_TZ = timezone("Europe/Moscow") + + async def add_payment( session: AsyncSession, tg_id: int, amount: float, payment_system: str ): try: + now_moscow = datetime.now(MOSCOW_TZ).replace(tzinfo=None) stmt = insert(Payment).values( tg_id=tg_id, amount=amount, payment_system=payment_system, status="success", - created_at=datetime.utcnow(), + created_at=now_moscow, ) await session.execute(stmt) await session.commit() diff --git a/handlers/admin/clusters/clusters_handler.py b/handlers/admin/clusters/clusters_handler.py index ea3f6d86..2c60e25c 100644 --- a/handlers/admin/clusters/clusters_handler.py +++ b/handlers/admin/clusters/clusters_handler.py @@ -29,7 +29,6 @@ from handlers.keys.key_utils import ( ) from logger import logger from panels.remnawave import RemnawaveAPI -from panels.remnawave_time import get_all_nodes_with_online from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb from .keyboard import ( @@ -392,41 +391,20 @@ async def handle_cluster_availability( result_text += f"🌍 {prefix} {server_name} - {online_inbound_users} онлайн\n" elif panel_type == "remnawave": - # remna = RemnawaveAPI(server["api_url"]) - # if not await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD): - # raise Exception("Не удалось авторизоваться") - server_inbound_id = server.get("inbound_id") if not server_inbound_id: raise Exception("Не указан inbound_id сервера") -# all_nodes = await remna.get_all_nodes() -# if not all_nodes: -# raise Exception("Не удалось получить список нод") - -# matching_node = None -# for node in all_nodes: -# excluded_inbounds = node.get("excludedInbounds", []) -# if server_inbound_id not in excluded_inbounds: -# matching_node = node -# break - -# if not matching_node: -# raise Exception("Нода, обслуживающая этот inbound_id, не найдена") - -# online_remna_users = matching_node.get("usersOnline", 0) -# total_online_users += online_remna_users -# result_text += ( -# f"🌍 {prefix} {server_name} - {online_remna_users} онлайн\n" -# ) - - nodes_data = await get_all_nodes_with_online( - server["api_url"], REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD, server_inbound_id + remna = RemnawaveAPI(server["api_url"]) + nodes_data = await remna.get_all_nodes_with_online( + username=REMNAWAVE_LOGIN, + password=REMNAWAVE_PASSWORD, + inbound_id=server_inbound_id ) - + if nodes_data.get("error"): raise Exception(nodes_data["error"]) - + online_remna_users = nodes_data["total_online"] total_online_users += online_remna_users @@ -442,7 +420,7 @@ async def handle_cluster_availability( flag = ''.join(chr(ord(c) + 127397) for c in country_code.upper()) else: flag = country_code - + result_text += f" ↳ {flag} ({node_name}): {online_users} онлайн\n" else: result_text += ( diff --git a/handlers/admin/management/management_handler.py b/handlers/admin/management/management_handler.py index 66ebb1f5..0446dd80 100644 --- a/handlers/admin/management/management_handler.py +++ b/handlers/admin/management/management_handler.py @@ -12,7 +12,6 @@ import os, subprocess, sys import json from aiogram import Bot from panels.remnawave import RemnawaveAPI -from panels.remnawave_time import get_all_users_time, login_remnawave from tempfile import NamedTemporaryFile import traceback from datetime import datetime @@ -440,32 +439,14 @@ async def show_remnawave_clients(callback: CallbackQuery, session: AsyncSession) return server = servers[0] -# api = RemnawaveAPI(base_url=server.api_url) -# -# if not await api.login(username=REMNAWAVE_LOGIN, password=REMNAWAVE_PASSWORD): -# await callback.message.edit_text( -# "❌ Не удалось авторизоваться на Remnawave панели.", -# reply_markup=build_back_to_db_menu(), -# ) -# return -# -# users = await api.get_all_users() -# if not users: -# await callback.message.edit_text( -# "📭 На панели нет клиентов.", -# reply_markup=build_back_to_db_menu(), -# ) -# return - token = await login_remnawave(server.api_url, REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD) - if not token: - await callback.message.edit_text( - "❌ Не удалось авторизоваться на Remnawave панели.", - reply_markup=build_back_to_db_menu(), - ) - return + api = RemnawaveAPI(base_url=server.api_url) + + users = await api.get_all_users_time( + username=REMNAWAVE_LOGIN, + password=REMNAWAVE_PASSWORD, + ) - users = await get_all_users_time(server.api_url, REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD) if not users: await callback.message.edit_text( "📭 На панели нет клиентов.", diff --git a/handlers/admin/panel/keyboard.py b/handlers/admin/panel/keyboard.py index 1c23cae2..a3380112 100644 --- a/handlers/admin/panel/keyboard.py +++ b/handlers/admin/panel/keyboard.py @@ -17,7 +17,7 @@ class AdminPanelCallback(CallbackData, prefix="admin_panel"): super().__init__(**data) -def build_panel_kb() -> InlineKeyboardMarkup: +def build_panel_kb(admin_role: str) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button( text="👤 Поиск пользователя", @@ -49,28 +49,27 @@ def build_panel_kb() -> InlineKeyboardMarkup: ), ) - builder.button( - text="🤖 Управление ботом", - callback_data=AdminPanelCallback(action="management").pack(), - ) - builder.row( - InlineKeyboardButton( - text="📊 Статистика", - callback_data=AdminPanelCallback(action="stats").pack(), - ), - InlineKeyboardButton( - text="📈 Аналитика", - callback_data=AdminPanelCallback(action="ads").pack(), - ), - ) + if admin_role == "superadmin": + builder.button( + text="🤖 Управление ботом", + callback_data=AdminPanelCallback(action="management").pack(), + ) + builder.row( + InlineKeyboardButton( + text="📊 Статистика", + callback_data=AdminPanelCallback(action="stats").pack(), + ), + InlineKeyboardButton( + text="📈 Аналитика", + callback_data=AdminPanelCallback(action="ads").pack(), + ), + ) builder.button( text=MAIN_MENU, callback_data="profile", ) - - builder.adjust(1, 1, 1, 2, 2, 1, 2, 1) - + builder.adjust(1, 1, 1, 2, 2, 1, 2 if admin_role == "superadmin" else 0, 1) return builder.as_markup() diff --git a/handlers/admin/panel/panel_handler.py b/handlers/admin/panel/panel_handler.py index aa8a4e34..635c1c1c 100644 --- a/handlers/admin/panel/panel_handler.py +++ b/handlers/admin/panel/panel_handler.py @@ -6,6 +6,9 @@ from aiogram.types import CallbackQuery, Message from bot import version from filters.admin import IsAdminFilter +from database.models import Admin +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession from logger import logger from .keyboard import AdminPanelCallback, build_panel_kb @@ -14,16 +17,23 @@ router = Router() @router.callback_query(AdminPanelCallback.filter(F.action == "admin"), IsAdminFilter()) -async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMContext): - text = f"🤖 Панель администратора\n\n📌 Версия бота: {version}" +async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession): + text = f"🤖 Панель администратора\n\nВерсия бота:\n
{version}
" await state.clear() + result = await session.execute( + select(Admin.role).where(Admin.tg_id == callback_query.from_user.id) + ) + role = result.scalar_one_or_none() or "admin" + + markup = build_panel_kb(admin_role=role) + if callback_query.message.text: try: await callback_query.message.edit_text( text=text, - reply_markup=build_panel_kb(), + reply_markup=markup, disable_web_page_preview=True, ) except TelegramBadRequest as e: @@ -41,19 +51,30 @@ async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMC await callback_query.message.answer( text=text, - reply_markup=build_panel_kb(), + reply_markup=markup, disable_web_page_preview=True, ) @router.callback_query(F.data == "admin", IsAdminFilter()) -async def handle_admin_callback_query_simple(callback_query: CallbackQuery, state: FSMContext): - await handle_admin_callback_query(callback_query, state) +async def handle_admin_callback_query_simple(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession): + await handle_admin_callback_query(callback_query, state, session) @router.message(Command("admin"), IsAdminFilter()) -async def handle_admin_message(message: Message, state: FSMContext): - text = f"🤖 Панель администратора\n\n📌 Версия бота: {version}" +async def handle_admin_message(message: Message, state: FSMContext, session: AsyncSession): + text = f"🤖 Панель администратора\n\nВерсия бота:\n
{version}
" + await state.clear() - await message.answer(text=text, reply_markup=build_panel_kb()) + + result = await session.execute( + select(Admin.role).where(Admin.tg_id == message.from_user.id) + ) + role = result.scalar_one_or_none() or "admin" + + await message.answer( + text=text, + reply_markup=build_panel_kb(admin_role=role), + disable_web_page_preview=True, + ) \ No newline at end of file diff --git a/handlers/admin/servers/servers_handler.py b/handlers/admin/servers/servers_handler.py index 382154ed..2652f9f1 100644 --- a/handlers/admin/servers/servers_handler.py +++ b/handlers/admin/servers/servers_handler.py @@ -75,22 +75,25 @@ async def handle_server_manage( subscription_count = result.scalar() or 0 text = ( - f"🔧 Информация о сервере {server_name}:\n\n" - f"🗂 Кластер: {cluster_name}\n" - f"📡 API URL: {api_url}\n" + f"🔧 Информация о сервере {server_name}:\n" + f"
" + f"🗂 Кластер: {cluster_name}\n" + f"📡 API URL: {api_url}\n" ) if subscription_url: - text += f"🌐 Subscription URL: {subscription_url}\n" - + text += f"🌐 Subscription URL: {subscription_url}\n" + text += ( - f"🔑 Inbound ID: {inbound_id}\n" - f"⚙️ Тип панели: {panel_type}\n" - f"📈 Лимит ключей: {limit_display}" + f"🔑 Inbound ID: {inbound_id}\n" + f"⚙️ Тип панели: {panel_type}\n" + f"📈 Лимит ключей: {limit_display}\n" ) if subscription_count > 0: - text += f"\n🔑 Подписок на сервере: {subscription_count}" + text += f"🔑 Подписок на сервере: {subscription_count}\n" + + text += "
" await callback_query.message.edit_text( text=text, diff --git a/handlers/admin/users/users_handler.py b/handlers/admin/users/users_handler.py index f21941c2..99853d94 100644 --- a/handlers/admin/users/users_handler.py +++ b/handlers/admin/users/users_handler.py @@ -1241,8 +1241,8 @@ async def process_user_search( username, balance, created_at, updated_at = user_data balance = int(balance or 0) - created_at_str = created_at.astimezone(MOSCOW_TZ).strftime("%H:%M:%S %d.%m.%Y") - updated_at_str = updated_at.astimezone(MOSCOW_TZ).strftime("%H:%M:%S %d.%m.%Y") + created_at_str = created_at.replace(tzinfo=pytz.UTC).astimezone(MOSCOW_TZ).strftime("%H:%M:%S %d.%m.%Y") + updated_at_str = updated_at.replace(tzinfo=pytz.UTC).astimezone(MOSCOW_TZ).strftime("%H:%M:%S %d.%m.%Y") stmt_ref_count = ( select(func.count()) @@ -1266,15 +1266,19 @@ async def process_user_search( ) result_ban = await session.execute(stmt_ban) is_banned = result_ban.scalar_one_or_none() is not None + user_obj = await session.get(User, tg_id) + full_name = user_obj.first_name if user_obj else None text = ( - f"📊 Информация о пользователе" - f"\n\n🆔 ID: {tg_id}" - f"\n📄 Логин: @{username}" - f"\n📅 Дата регистрации: {created_at_str}" - f"\n🏃 Дата активности: {updated_at_str}" - f"\n💰 Баланс: {balance}" - f"\n👥 Количество рефералов: {referral_count}" + f"📊 Информация о пользователе\n" + f"
" + f"🆔 ID: {tg_id}\n" + f"📄 Логин: @{username}{f' ({full_name})' if full_name else ''}\n" + f"📅 Дата регистрации: {created_at_str}\n" + f"🏃 Дата активности: {updated_at_str}\n" + f"💰 Баланс: {balance}\n" + f"👥 Количество рефералов: {referral_count}" + f"
" ) kb = build_user_edit_kb(tg_id, key_records, is_banned=is_banned) diff --git a/handlers/buttons.py b/handlers/buttons.py index 744d3c57..91e5116d 100644 --- a/handlers/buttons.py +++ b/handlers/buttons.py @@ -19,6 +19,7 @@ INSTRUCTIONS = "📘 Инструкции" TOP_FIVE = "🏆 Топ-5" TRIAL_SUB = "🎁 Пробная подписка" MY_SUB = "🔐 Моя подписка" +RENEW_SUB = "🔄 Обновить подписку" # Меню Оплат и баланса diff --git a/handlers/coupons.py b/handlers/coupons.py index 325d47ce..c260fa4f 100644 --- a/handlers/coupons.py +++ b/handlers/coupons.py @@ -32,6 +32,8 @@ from handlers.texts import ( COUPON_ALREADY_USED_MSG, COUPON_INPUT_PROMPT, COUPON_NOT_FOUND_MSG, + COUPONS_DAYS_MESSAGE, + COUPON_DAYS_ACTIVATED_MSG ) from handlers.utils import edit_or_send_message, format_days from logger import logger @@ -154,9 +156,7 @@ async def activate_coupon( builder = InlineKeyboardBuilder() moscow_tz = pytz.timezone("Europe/Moscow") - response_message = ( - "🔑 Выберите подписку для продления:\n\n
" - ) + response_message = COUPONS_DAYS_MESSAGE for key in active_keys: key_display = html.escape((key.alias or key.email).strip()) @@ -256,7 +256,11 @@ async def handle_key_extension( new_expiry / 1000, tz=pytz.timezone("Europe/Moscow") ).strftime("%d.%m.%y, %H:%M") await callback_query.message.answer( - f"✅ Купон активирован, подписка {alias} продлена на {format_days(coupon.days)}⏳ до {expiry_date}📆." + COUPON_DAYS_ACTIVATED_MSG.format( + alias=alias, + days=format_days(coupon.days), + expiry=expiry_date + ) ) await process_callback_view_profile( callback_query.message, state, admin, session diff --git a/handlers/keys/key_view.py b/handlers/keys/key_view.py index 50a2e20f..f69f2142 100644 --- a/handlers/keys/key_view.py +++ b/handlers/keys/key_view.py @@ -37,18 +37,16 @@ from handlers.buttons import ( MAIN_MENU, PC_BUTTON, QR, - RENEW, - RENEW_FULL, TV_BUTTON, UNFREEZE, + RENEW_SUB ) -from handlers.texts import FROZEN_SUBSCRIPTION_MSG, NO_SUBSCRIPTIONS_MSG, key_message +from handlers.texts import FROZEN_SUBSCRIPTION_MSG, NO_SUBSCRIPTIONS_MSG, key_message, KEYS_HEADER, KEYS_FOOTER, RENAME_KEY_PROMPT, DAYS_LEFT_MESSAGE, SELECT_SUBS from handlers.utils import ( edit_or_send_message, format_days, format_hours, format_minutes, - format_months, get_russian_month, is_full_remnawave_cluster, ) @@ -97,7 +95,7 @@ def build_keys_response(records): moscow_tz = pytz.timezone("Europe/Moscow") if records: - response_message = "🔑 Список ваших подписок:\n\n
" + response_message = KEYS_HEADER for record in records: alias = record.alias email = record.email @@ -124,9 +122,7 @@ def build_keys_response(records): response_message += f"• {key_display} ({formatted_date_full})\n" - response_message += ( - "
\n\nНажмите на ✏️, чтобы переименовать подписку." - ) + response_message += KEYS_FOOTER else: response_message = NO_SUBSCRIPTIONS_MSG @@ -149,7 +145,7 @@ async def handle_rename_key(callback: CallbackQuery, state: FSMContext): await edit_or_send_message( target_message=callback.message, - text="✏️ Введите новое имя подписки (до 10 символов):", + text=RENAME_KEY_PROMPT, reply_markup=builder.as_markup(), ) @@ -250,7 +246,7 @@ async def render_key_info( time_left = expiry_date - datetime.utcnow() if time_left.total_seconds() <= 0: - days_left_message = "🕒 Статус подписки:\n🔴 Истекла" + days_left_message = DAYS_LEFT_MESSAGE else: total_seconds = int(time_left.total_seconds()) days = total_seconds // 86400 @@ -313,7 +309,7 @@ async def render_key_info( if ENABLE_UPDATE_SUBSCRIPTION_BUTTON: builder.row( InlineKeyboardButton( - text="🔄 Обновить подписку", + text=RENEW_SUB, callback_data=f"update_subscription|{key_name}", ) ) @@ -487,7 +483,7 @@ async def process_renew_menu(callback_query_or_message: CallbackQuery | Message, server_info = f" ({server_id})" if server_id in all_server_names else "" btn_text = f"🔑 {key_display} (⏳{days_text}) {server_info}" builder.row(InlineKeyboardButton(text=btn_text, callback_data=f"renew_key|{email}")) - text = "Выберите подписку для продления или купите новую" + text = SELECT_SUBS builder.row(InlineKeyboardButton(text=ADD_SUB, callback_data="create_key")) builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) image_path = os.path.join("img", "pic_view.jpg") diff --git a/handlers/keys/subscriptions.py b/handlers/keys/subscriptions.py index 579f2934..6a84c51a 100644 --- a/handlers/keys/subscriptions.py +++ b/handlers/keys/subscriptions.py @@ -21,6 +21,7 @@ from config import ( from database import get_key_details, get_servers from database.models import Server from handlers.utils import convert_to_bytes +from handlers.texts import SUBSCRIPTION_INFO_TEXT, HAPP_ANNOUNCE, V2RAYTUN_ANNOUNCE, HIDDIFY_PROFILE_TITLE from logger import logger @@ -195,7 +196,7 @@ def prepare_headers( ) -> dict[str, str]: if "Happ" in user_agent: encoded_project_name = f"{project_name}" - announce_str = f"↖️Бот | {subscription_info} | Поддержка↗️" + announce_str = HAPP_ANNOUNCE.format(subscription_info=subscription_info) return { "Content-Type": "text/plain; charset=utf-8", "Content-Disposition": "inline", @@ -211,7 +212,7 @@ def prepare_headers( elif "Hiddify" in user_agent: parts = subscription_info.split(" - ")[0].split(": ") key_info = parts[1] if len(parts) > 1 else parts[0] - encoded_project_name = f"{project_name}\n📄 Подписка: {key_info}" + encoded_project_name = HIDDIFY_PROFILE_TITLE.format(project_name=project_name, key_info=key_info) return { "profile-update-interval": "3", "profile-title": "base64:" @@ -220,7 +221,7 @@ def prepare_headers( } elif "v2raytun" in user_agent: encoded_project_name = f"{project_name}\n{subscription_info}" - announce_str = "🔑 Выберите сервер ⬇️ | 💬 Поддержка ➡️" + announce_str = V2RAYTUN_ANNOUNCE return { "Content-Type": "text/plain; charset=utf-8", "Content-Disposition": "inline", @@ -291,7 +292,7 @@ async def handle_subscription(request: web.Request) -> web.Response: base64_encoded = base64.b64encode( "\n".join(cleaned_subscriptions).encode("utf-8") ).decode("utf-8") - subscription_info = f"📄 Подписка: {email} — {time_left}" + subscription_info = SUBSCRIPTION_INFO_TEXT.format(email=email, time_left=time_left) user_agent = request.headers.get("User-Agent", "") subscription_userinfo = calculate_traffic( diff --git a/handlers/payments/robokassa_pay.py b/handlers/payments/robokassa_pay.py index 4c0bd694..d337a3e2 100644 --- a/handlers/payments/robokassa_pay.py +++ b/handlers/payments/robokassa_pay.py @@ -63,7 +63,7 @@ def generate_payment_link(amount, inv_id, description, tg_id): payment_link = robokassa._payment.link.generate_by_script( out_sum=amount, inv_id=inv_id, - description="пополнение баланса", + description=f"Пополнение баланса (tg_id: {tg_id})", id=f"{tg_id}", ) logger.info(f"Generated payment link: {payment_link}") diff --git a/handlers/payments/wata.py b/handlers/payments/wata.py index 248e2245..d873d260 100644 --- a/handlers/payments/wata.py +++ b/handlers/payments/wata.py @@ -5,8 +5,6 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder from sqlalchemy.ext.asyncio import AsyncSession -from datetime import datetime, timedelta -from sqlalchemy import select, and_ from config import ( WATA_RU_ENABLE, WATA_RU_TOKEN, @@ -15,16 +13,7 @@ from config import ( REDIRECT_LINK, FAIL_REDIRECT_LINK, ) -from database import ( - add_payment, - add_user, - async_session_maker, - check_user_exists, - get_key_count, - get_temporary_data, - update_balance, - Payment -) + from handlers.buttons import BACK, PAY_2, WATA_RU, WATA_SBP, WATA_INT from handlers.texts import ( WATA_RU_DESCRIPTION, WATA_SBP_DESCRIPTION, WATA_INT_DESCRIPTION, @@ -33,20 +22,24 @@ from handlers.texts import ( from handlers.utils import edit_or_send_message from logger import logger + router = Router() + class ReplenishBalanceWataState(StatesGroup): choosing_cassa = State() choosing_amount = State() waiting_for_payment_confirmation = State() entering_custom_amount = State() + WATA_CASSA_CONFIG = [ {"enable": WATA_RU_ENABLE, "token": WATA_RU_TOKEN, "name": "ru", "button": WATA_RU, "desc": WATA_RU_DESCRIPTION}, {"enable": WATA_SBP_ENABLE, "token": WATA_SBP_TOKEN, "name": "sbp", "button": WATA_SBP, "desc": WATA_SBP_DESCRIPTION}, {"enable": WATA_INT_ENABLE, "token": WATA_INT_TOKEN, "name": "int", "button": WATA_INT, "desc": WATA_INT_DESCRIPTION}, ] + @router.callback_query(F.data == "pay_wata") async def process_callback_pay_wata(callback_query: types.CallbackQuery, state: FSMContext, session: AsyncSession, cassa_name: str = None): tg_id = callback_query.message.chat.id @@ -104,6 +97,7 @@ async def process_callback_pay_wata(callback_query: types.CallbackQuery, state: await state.update_data(message_id=new_message.message_id, chat_id=new_message.chat.id) await state.set_state(ReplenishBalanceWataState.choosing_cassa) + @router.callback_query(F.data.startswith("wata_cassa|")) async def process_cassa_selection(callback_query: types.CallbackQuery, state: FSMContext): cassa_name = callback_query.data.split("|")[1] @@ -146,6 +140,7 @@ async def process_cassa_selection(callback_query: types.CallbackQuery, state: FS await state.update_data(message_id=new_message.message_id, chat_id=new_message.chat.id) await state.set_state(ReplenishBalanceWataState.choosing_amount) + @router.callback_query(F.data.startswith("wata_custom_amount|")) async def process_custom_amount_button(callback_query: types.CallbackQuery, state: FSMContext): cassa_name = callback_query.data.split("|")[1] @@ -178,7 +173,6 @@ async def handle_custom_amount_input(message: types.Message, state: FSMContext): amount = int(message.text.strip()) if amount <= 0: raise ValueError - # Проверка для SBP: минимум 50 рублей if cassa_name == "sbp" and amount < 50: await edit_or_send_message( target_message=message, @@ -211,11 +205,12 @@ async def handle_custom_amount_input(message: types.Message, state: FSMContext): ) await state.set_state(ReplenishBalanceWataState.waiting_for_payment_confirmation) + @router.callback_query(F.data.startswith("wata_amount|")) async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext): parts = callback_query.data.split("|") cassa_name = parts[1] - amount_str = parts[-1] # всегда последняя часть — сумма + amount_str = parts[-1] cassa = next((c for c in WATA_CASSA_CONFIG if c["name"] == cassa_name), None) if not cassa or not cassa["enable"]: await edit_or_send_message( @@ -253,6 +248,7 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F ) await state.set_state(ReplenishBalanceWataState.waiting_for_payment_confirmation) + async def generate_wata_payment_link(amount, tg_id, cassa): url = "https://api.wata.pro/api/h2h/links" headers = { diff --git a/handlers/payments/yookassa_pay.cpython-312-x86_64-linux-gnu.so b/handlers/payments/yookassa_pay.cpython-312-x86_64-linux-gnu.so index a9074f00..c26b61fc 100644 Binary files a/handlers/payments/yookassa_pay.cpython-312-x86_64-linux-gnu.so and b/handlers/payments/yookassa_pay.cpython-312-x86_64-linux-gnu.so differ diff --git a/panels/remnawave.cpython-312-x86_64-linux-gnu.so b/panels/remnawave.cpython-312-x86_64-linux-gnu.so index 9363ca86..aa974232 100644 Binary files a/panels/remnawave.cpython-312-x86_64-linux-gnu.so and b/panels/remnawave.cpython-312-x86_64-linux-gnu.so differ diff --git a/panels/remnawave_time.py b/panels/remnawave_time.py deleted file mode 100644 index eeec3582..00000000 --- a/panels/remnawave_time.py +++ /dev/null @@ -1,161 +0,0 @@ -import aiohttp -from typing import List, Dict, Any -from logger import logger -from config import REMNAWAVE_TOKEN_LOGIN_ENABLED, REMNAWAVE_ACCESS_TOKEN, REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD - - -async def login_remnawave(api_url: str, username: str, password: str) -> str | None: - if REMNAWAVE_TOKEN_LOGIN_ENABLED and REMNAWAVE_ACCESS_TOKEN: - logger.info("[Remnawave API] Используется авторизация по токену") - return REMNAWAVE_ACCESS_TOKEN - - async with aiohttp.ClientSession() as session: - auth_data = { - "username": username, - "password": password - } - - try: - auth_response = await session.post(f"{api_url}/auth/login", json=auth_data) - - if auth_response.status != 200: - logger.error(f"[Remnawave API] Ошибка HTTP статуса: {auth_response.status}") - return None - - auth_result = await auth_response.json() - token = None - if auth_result.get("success") and auth_result.get("data", {}).get("token"): - token = auth_result.get("data", {}).get("token") - elif auth_result.get("response", {}).get("accessToken"): - token = auth_result.get("response", {}).get("accessToken") - elif auth_result.get("token"): - token = auth_result.get("token") - - if not token: - logger.error(f"[Remnawave API] Токен не найден в ответе") - - return token - - except Exception as e: - logger.error(f"[Remnawave API] Ошибка при авторизации: {e}") - return None - - -async def get_all_nodes_with_online(api_url: str, username: str, password: str, inbound_id: str) -> Dict[str, Any]: - token = await login_remnawave(api_url, username, password) - if not token: - logger.error("[Remnawave API] Не удалось получить токен авторизации") - return {"total_online": 0, "nodes": [], "error": "Не удалось авторизоваться"} - - headers = {"Authorization": f"Bearer {token}"} - - async with aiohttp.ClientSession() as session: - try: - nodes_response = await session.get(f"{api_url}/nodes", headers=headers) - - if nodes_response.status != 200: - logger.error(f"[Remnawave API] Ошибка получения нод: {nodes_response.status}") - return {"total_online": 0, "nodes": [], "error": f"HTTP {nodes_response.status}"} - - nodes_result = await nodes_response.json() - - all_nodes = [] - if nodes_result.get("success") and "data" in nodes_result: - all_nodes = nodes_result["data"] - elif nodes_result.get("response"): - all_nodes = nodes_result["response"] - elif isinstance(nodes_result, list): - all_nodes = nodes_result - - if not all_nodes: - logger.warning("[Remnawave API] Список нод пуст") - return {"total_online": 0, "nodes": [], "error": "Список нод пуст"} - - matching_nodes = [] - total_online = 0 - - - - for node in all_nodes: - excluded_inbounds = node.get("excludedInbounds", []) - if inbound_id not in excluded_inbounds: - node_online = node.get("usersOnline", 0) - node_name = node.get("name", "Unknown Node") - node_id = node.get("id", "Unknown ID") - - matching_nodes.append({ - "name": node_name, - "online_users": node_online, - "country_code": node.get("countryCode", "Unknown") - }) - - total_online += node_online - - logger.info(f"[Remnawave API] Найдено {len(matching_nodes)} нод для inbound {inbound_id}, общий онлайн: {total_online}") - - return { - "total_online": total_online, - "nodes": matching_nodes, - "inbound_id": inbound_id - } - - except Exception as e: - logger.error(f"[Remnawave API] Ошибка при получении нод: {e}") - return {"total_online": 0, "nodes": [], "error": str(e)} - - -async def get_all_users_time(api_url: str, username: str, password: str) -> List[Dict[str, Any]]: - all_users = [] - page_size = 250 - start = 0 - - token = await login_remnawave(api_url, username, password) - if not token: - logger.error("[Remnawave API] Не удалось получить токен авторизации") - return [] - - headers = {"Authorization": f"Bearer {token}"} - - async with aiohttp.ClientSession() as session: - while True: - params = { - "size": page_size, - "start": start - } - - users_endpoint = f"{api_url}/users" - - users_response = await session.get(users_endpoint, params=params, headers=headers) - - if users_response.status != 200: - break - - users_result = await users_response.json() - - if users_result.get("success") is False: - break - - users_data = None - if "response" in users_result: - users_data = users_result.get("response", {}) - elif "data" in users_result: - users_data = users_result.get("data", {}) - else: - users_data = users_result - - if not users_data: - break - - users = users_data.get("users", []) - total = users_data.get("total", 0) - - if not users: - break - - all_users.extend(users) - start += len(users) - - if len(users) < page_size or start >= total: - break - - return all_users \ No newline at end of file diff --git a/web/wata_payment.py b/web/wata_payment.py index 875ce775..14159f79 100644 --- a/web/wata_payment.py +++ b/web/wata_payment.py @@ -2,23 +2,24 @@ import base64 import aiohttp from aiohttp import web import json -import logging from database import async_session_maker, update_balance, add_payment from handlers.payments.utils import send_payment_success_notification -from config import WATA_RU_TOKEN, WATA_SBP_TOKEN, WATA_INT_TOKEN from logger import logger from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.backends import default_backend + PUBLIC_KEY_URL = "https://api.wata.pro/api/h2h/public-key" + async def get_wata_public_key(): async with aiohttp.ClientSession() as session: async with session.get(PUBLIC_KEY_URL) as resp: data = await resp.json() return data["value"].encode() + async def verify_signature(raw_json: bytes, signature: str, public_key_pem: bytes) -> bool: try: public_key = serialization.load_pem_public_key(public_key_pem, backend=default_backend()) @@ -34,6 +35,7 @@ async def verify_signature(raw_json: bytes, signature: str, public_key_pem: byte logger.error(f"Ошибка проверки подписи WATA: {e}") return False + async def wata_payment_webhook(request: web.Request): try: raw_json = await request.read()