fix time and text/ cosmetic fixes/ description of the payment/ vat_code
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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,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()
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -19,6 +19,7 @@ INSTRUCTIONS = "📘 Инструкции"
|
||||
TOP_FIVE = "🏆 Топ-5"
|
||||
TRIAL_SUB = "🎁 Пробная подписка"
|
||||
MY_SUB = "🔐 Моя подписка"
|
||||
RENEW_SUB = "🔄 Обновить подписку"
|
||||
|
||||
# Меню Оплат и баланса
|
||||
|
||||
|
||||
+8
-4
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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.
Binary file not shown.
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user