fix time and text/ cosmetic fixes/ description of the payment/ vat_code

This commit is contained in:
Vladless
2025-07-18 02:07:30 +03:00
parent 16cf8bb1d5
commit b44302deb3
17 changed files with 129 additions and 299 deletions
+6 -1
View File
@@ -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()
+8 -30
View File
@@ -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"🌍 <b>{prefix} {server_name}</b> - {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"🌍 <b>{prefix} {server_name}</b> - {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 += (
@@ -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(
"📭 На панели нет клиентов.",
+17 -18
View File
@@ -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()
+30 -9
View File
@@ -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<blockquote>{version}</blockquote>"
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<blockquote>{version}</blockquote>"
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,
)
+12 -9
View File
@@ -75,22 +75,25 @@ async def handle_server_manage(
subscription_count = result.scalar() or 0
text = (
f"<b>🔧 Информация о сервере {server_name}:</b>\n\n"
f"<b>🗂 Кластер:</b> {cluster_name}\n"
f"<b>📡 API URL:</b> {api_url}\n"
f"<b>🔧 Информация о сервере {server_name}:</b>\n"
f"<blockquote>"
f"🗂 Кластер: <b>{cluster_name}</b>\n"
f"📡 API URL: <b>{api_url}</b>\n"
)
if subscription_url:
text += f"<b>🌐 Subscription URL:</b> {subscription_url}\n"
text += f"🌐 Subscription URL: <b>{subscription_url}</b>\n"
text += (
f"<b>🔑 Inbound ID:</b> {inbound_id}\n"
f"<b>⚙️ Тип панели:</b> {panel_type}\n"
f"<b>📈 Лимит ключей:</b> {limit_display}"
f"🔑 Inbound ID: <b>{inbound_id}</b>\n"
f"⚙️ Тип панели: <b>{panel_type}</b>\n"
f"📈 Лимит ключей: <b>{limit_display}</b>\n"
)
if subscription_count > 0:
text += f"\n<b>🔑 Подписок на сервере:</b> {subscription_count}"
text += f"🔑 Подписок на сервере: <b>{subscription_count}</b>\n"
text += "</blockquote>"
await callback_query.message.edit_text(
text=text,
+13 -9
View File
@@ -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"<b>📊 Информация о пользователе</b>"
f"\n\n🆔 ID: <b>{tg_id}</b>"
f"\n📄 Логин: <b>@{username}</b>"
f"\n📅 Дата регистрации: <b>{created_at_str}</b>"
f"\n🏃 Дата активности: <b>{updated_at_str}</b>"
f"\n💰 Баланс: <b>{balance}</b>"
f"\n👥 Количество рефералов: <b>{referral_count}</b>"
f"<b>📊 Информация о пользователе</b>\n"
f"<blockquote>"
f"🆔 ID: <b>{tg_id}</b>\n"
f"📄 Логин: <b>@{username}</b>{f' ({full_name})' if full_name else ''}\n"
f"📅 Дата регистрации: <b>{created_at_str}</b>\n"
f"🏃 Дата активности: <b>{updated_at_str}</b>\n"
f"💰 Баланс: <b>{balance}</b>\n"
f"👥 Количество рефералов: <b>{referral_count}</b>"
f"</blockquote>"
)
kb = build_user_edit_kb(tg_id, key_records, is_banned=is_banned)
+1
View File
@@ -19,6 +19,7 @@ INSTRUCTIONS = "📘 Инструкции"
TOP_FIVE = "🏆 Топ-5"
TRIAL_SUB = "🎁 Пробная подписка"
MY_SUB = "🔐 Моя подписка"
RENEW_SUB = "🔄 Обновить подписку"
# Меню Оплат и баланса
+8 -4
View File
@@ -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 = (
"<b>🔑 Выберите подписку для продления:</b>\n\n<blockquote>"
)
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"✅ Купон активирован, подписка <b>{alias}</b> продлена на {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
+8 -12
View File
@@ -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 = "<b>🔑 Список ваших подписок:</b>\n\n<blockquote>"
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"• <b>{key_display}</b> ({formatted_date_full})\n"
response_message += (
"</blockquote>\n\n<i>Нажмите на ✏️, чтобы переименовать подписку.</i>"
)
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 = "<b>🕒 Статус подписки:</b>\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")
+5 -4
View File
@@ -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(
+1 -1
View File
@@ -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}")
+10 -14
View File
@@ -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 = {
Binary file not shown.
-161
View File
@@ -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
+4 -2
View File
@@ -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()