remnawave in country_mode/save shortid/bug fixes

This commit is contained in:
Vladless
2025-04-16 22:50:19 +03:00
parent 1246511af3
commit e0212866b4
28 changed files with 310 additions and 329 deletions
+40 -54
View File
@@ -1,6 +1,7 @@
import asyncio
import time
from datetime import datetime, timedelta
from typing import Any
import asyncpg
@@ -10,14 +11,27 @@ from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, Message
from py3xui import AsyncApi
from datetime import datetime, timedelta
from backup import create_backup_and_send_to_admins
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, TOTAL_GB, USE_COUNTRY_SELECTION, REMNAWAVE_PASSWORD, REMNAWAVE_LOGIN
from config import (
ADMIN_PASSWORD,
ADMIN_USERNAME,
DATABASE_URL,
REMNAWAVE_LOGIN,
REMNAWAVE_PASSWORD,
TOTAL_GB,
USE_COUNTRY_SELECTION,
)
from database import check_unique_server_name, get_servers, update_key_expiry
from filters.admin import IsAdminFilter
from handlers.keys.key_utils import create_client_on_server, create_key_on_cluster, renew_key_in_cluster, delete_key_from_cluster
from handlers.keys.key_utils import (
create_client_on_server,
create_key_on_cluster,
delete_key_from_cluster,
renew_key_in_cluster,
)
from logger import logger
from panels.remnawave import RemnawaveAPI
from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb
from .keyboard import (
@@ -26,10 +40,9 @@ from .keyboard import (
build_cluster_management_kb,
build_clusters_editor_kb,
build_manage_cluster_kb,
build_panel_type_kb,
build_sync_cluster_kb,
build_panel_type_kb
)
from panels.remnawave import RemnawaveAPI
router = Router()
@@ -194,7 +207,7 @@ async def handle_subscription_url_input(message: Message, state: FSMContext):
await message.answer(
text=f"<b>Введите inbound_id для сервера {server_name} в кластере {cluster_name}:</b>\n\n"
f"Для Remnawave это UUID Инбаунда, для 3x-ui — просто ID (например, <code>1</code>).",
f"Для Remnawave это UUID Инбаунда, для 3x-ui — просто ID (например, <code>1</code>).",
reply_markup=build_admin_back_kb("clusters"),
)
await state.set_state(AdminClusterStates.waiting_for_inbound_id)
@@ -216,7 +229,9 @@ async def handle_inbound_id_input(message: Message, state: FSMContext):
@router.callback_query(AdminClusterCallback.filter(F.action.in_(["panel_3xui", "panel_remnawave"])), IsAdminFilter())
async def handle_panel_type_selection(callback_query: CallbackQuery, callback_data: AdminClusterCallback, state: FSMContext):
async def handle_panel_type_selection(
callback_query: CallbackQuery, callback_data: AdminClusterCallback, state: FSMContext
):
panel_type = "3x-ui" if callback_data.action == "panel_3xui" else "remnawave"
user_data = await state.get_data()
@@ -437,7 +452,7 @@ async def handle_sync_cluster(callback_query: types.CallbackQuery, callback_data
try:
query_keys = """
SELECT tg_id, client_id, email, expiry_time
SELECT tg_id, client_id, email, expiry_time, remnawave_link
FROM keys
WHERE server_id = $1
"""
@@ -458,18 +473,17 @@ async def handle_sync_cluster(callback_query: types.CallbackQuery, callback_data
await delete_key_from_cluster(cluster_name, key["email"], key["client_id"])
await session.execute(
"DELETE FROM keys WHERE tg_id = $1 AND client_id = $2",
key["tg_id"],
key["client_id"]
"DELETE FROM keys WHERE tg_id = $1 AND client_id = $2", key["tg_id"], key["client_id"]
)
result = await create_key_on_cluster(
await create_key_on_cluster(
cluster_name,
key["tg_id"],
key["client_id"],
key["email"],
key["expiry_time"],
session=session,
remnawave_link=key.get("remnawave_link"),
)
await asyncio.sleep(0.5)
@@ -477,7 +491,6 @@ async def handle_sync_cluster(callback_query: types.CallbackQuery, callback_data
except Exception as e:
logger.error(f"Ошибка при синхронизации ключа {key['client_id']} в {cluster_name}: {e}")
await callback_query.message.edit_text(
text=f"✅ Ключи успешно синхронизированы для кластера {cluster_name}",
reply_markup=build_admin_back_kb("clusters"),
@@ -622,8 +635,7 @@ async def handle_new_cluster_name_input(message: Message, state: FSMContext, ses
conn = await asyncpg.connect(DATABASE_URL)
try:
existing_cluster = await conn.fetchval(
"SELECT cluster_name FROM servers WHERE cluster_name = $1 LIMIT 1",
new_cluster_name
"SELECT cluster_name FROM servers WHERE cluster_name = $1 LIMIT 1", new_cluster_name
)
if existing_cluster:
await message.answer(
@@ -632,23 +644,16 @@ async def handle_new_cluster_name_input(message: Message, state: FSMContext, ses
)
return
keys_count = await conn.fetchval(
"SELECT COUNT(*) FROM keys WHERE server_id = $1",
old_cluster_name
)
keys_count = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE server_id = $1", old_cluster_name)
async with conn.transaction():
await conn.execute(
"UPDATE servers SET cluster_name = $1 WHERE cluster_name = $2",
new_cluster_name,
old_cluster_name
"UPDATE servers SET cluster_name = $1 WHERE cluster_name = $2", new_cluster_name, old_cluster_name
)
if keys_count > 0:
await conn.execute(
"UPDATE keys SET server_id = $1 WHERE server_id = $2",
new_cluster_name,
old_cluster_name
"UPDATE keys SET server_id = $1 WHERE server_id = $2", new_cluster_name, old_cluster_name
)
await message.answer(
@@ -729,7 +734,7 @@ async def handle_new_server_name_input(message: Message, state: FSMContext, sess
existing_server = await conn.fetchval(
"SELECT server_name FROM servers WHERE cluster_name = $1 AND server_name = $2 LIMIT 1",
cluster_name,
new_server_name
new_server_name,
)
if existing_server:
await message.answer(
@@ -738,24 +743,19 @@ async def handle_new_server_name_input(message: Message, state: FSMContext, sess
)
return
keys_count = await conn.fetchval(
"SELECT COUNT(*) FROM keys WHERE server_id = $1",
old_server_name
)
keys_count = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE server_id = $1", old_server_name)
async with conn.transaction():
await conn.execute(
"UPDATE servers SET server_name = $1 WHERE cluster_name = $2 AND server_name = $3",
new_server_name,
cluster_name,
old_server_name
old_server_name,
)
if keys_count > 0:
await conn.execute(
"UPDATE keys SET server_id = $1 WHERE server_id = $2",
new_server_name,
old_server_name
"UPDATE keys SET server_id = $1 WHERE server_id = $2", new_server_name, old_server_name
)
final_text = f"✅ Название сервера успешно изменено с '{old_server_name}' на '{new_server_name}' в кластере '{cluster_name}'!"
@@ -787,20 +787,14 @@ async def handle_server_transfer(callback_query: CallbackQuery, state: FSMContex
conn = await asyncpg.connect(DATABASE_URL)
try:
async with conn.transaction():
await conn.execute(
"UPDATE keys SET server_id = $1 WHERE server_id = $2",
new_server_name,
old_server_name
)
await conn.execute("UPDATE keys SET server_id = $1 WHERE server_id = $2", new_server_name, old_server_name)
await conn.execute(
"DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2",
cluster_name,
old_server_name
"DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2", cluster_name, old_server_name
)
base_text = f"✅ Ключи успешно перенесены на сервер '{new_server_name}', сервер '{old_server_name}' удален!"
sync_reminder = "\n\n⚠️ Не забудьте сделать \"Синхронизацию\"."
sync_reminder = '\n\n⚠️ Не забудьте сделать "Синхронизацию".'
final_text = base_text + (sync_reminder if USE_COUNTRY_SELECTION else "")
await callback_query.message.edit_text(
@@ -831,21 +825,13 @@ async def handle_cluster_transfer(callback_query: CallbackQuery, state: FSMConte
conn = await asyncpg.connect(DATABASE_URL)
try:
async with conn.transaction():
await conn.execute("UPDATE keys SET server_id = $1 WHERE server_id = $2", new_cluster_name, old_server_name)
await conn.execute(
"UPDATE keys SET server_id = $1 WHERE server_id = $2",
new_cluster_name,
old_server_name
)
await conn.execute(
"UPDATE keys SET server_id = $1 WHERE server_id = $2",
new_cluster_name,
old_cluster_name
"UPDATE keys SET server_id = $1 WHERE server_id = $2", new_cluster_name, old_cluster_name
)
await conn.execute(
"DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2",
cluster_name,
old_server_name
"DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2", cluster_name, old_server_name
)
await callback_query.message.edit_text(
+2 -10
View File
@@ -1,5 +1,3 @@
from typing import Optional
from aiogram.filters.callback_data import CallbackData
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
@@ -134,13 +132,7 @@ def build_sync_cluster_kb(cluster_servers: list, cluster_name: str) -> InlineKey
def build_panel_type_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text="🌐 3X-UI",
callback_data=AdminClusterCallback(action="panel_3xui").pack()
)
builder.button(
text="🌀 Remnawave",
callback_data=AdminClusterCallback(action="panel_remnawave").pack()
)
builder.button(text="🌐 3X-UI", callback_data=AdminClusterCallback(action="panel_3xui").pack())
builder.button(text="🌀 Remnawave", callback_data=AdminClusterCallback(action="panel_remnawave").pack())
builder.row(build_admin_back_btn("clusters"))
return builder.as_markup()
+16 -22
View File
@@ -1,8 +1,10 @@
from datetime import datetime
import html
import pytz
from datetime import datetime
from typing import Any
import pytz
from aiogram import F, Router
from aiogram.enums import ParseMode
from aiogram.fsm.context import FSMContext
@@ -217,18 +219,15 @@ async def handle_coupon_delete(callback_query: CallbackQuery, callback_data: Adm
coupon_code = callback_data.coupon_code
kb = InlineKeyboardBuilder()
kb.button(
text="✅ Да, удалить",
callback_data=AdminCouponDeleteCallback(coupon_code=coupon_code, confirm=True).pack()
text="✅ Да, удалить", callback_data=AdminCouponDeleteCallback(coupon_code=coupon_code, confirm=True).pack()
)
kb.button(
text="❌ Нет, отменить",
callback_data=AdminCouponDeleteCallback(coupon_code=coupon_code, confirm=False).pack()
text="❌ Нет, отменить", callback_data=AdminCouponDeleteCallback(coupon_code=coupon_code, confirm=False).pack()
)
kb.adjust(1)
await callback_query.message.edit_text(
f"Вы уверены, что хотите удалить купон <b>{coupon_code}</b>?",
reply_markup=kb.as_markup()
f"Вы уверены, что хотите удалить купон <b>{coupon_code}</b>?", reply_markup=kb.as_markup()
)
@@ -242,15 +241,13 @@ async def confirm_coupon_delete(callback_query: CallbackQuery, callback_data: Ad
result = await delete_coupon(coupon_code, session)
if not result:
await callback_query.message.edit_text(
f"❌ Купон с кодом {coupon_code} не найден.",
reply_markup=build_admin_back_kb("coupons")
f"❌ Купон с кодом {coupon_code} не найден.", reply_markup=build_admin_back_kb("coupons")
)
return
except Exception as e:
logger.error(f"Ошибка при удалении купона: {e}")
await callback_query.message.edit_text(
"Произошла ошибка при удалении купона.",
reply_markup=build_admin_back_kb("coupons")
"Произошла ошибка при удалении купона.", reply_markup=build_admin_back_kb("coupons")
)
return
@@ -295,7 +292,11 @@ async def inline_coupon_handler(inline_query: InlineQuery, session: Any):
return
title = f"Купон {coupon['code']}"
description = f"Получи {coupon['amount']} рублей!" if coupon["amount"] > 0 else f"Продли подписку на {format_days(coupon['days'])}!"
description = (
f"Получи {coupon['amount']} рублей!"
if coupon["amount"] > 0
else f"Продли подписку на {format_days(coupon['days'])}!"
)
message_text = (
f"🎫 <b>Купон:</b> {coupon['code']}\n"
f"{'💰 <b>Бонус:</b> ' + str(coupon['amount']) + ' рублей' if coupon['amount'] > 0 else '⏳ <b>Продление:</b> ' + format_days(coupon['days'])}\n"
@@ -309,15 +310,8 @@ async def inline_coupon_handler(inline_query: InlineQuery, session: Any):
id=coupon_code,
title=title,
description=description,
input_message_content=InputTextMessageContent(
message_text=message_text,
parse_mode=ParseMode.HTML
),
input_message_content=InputTextMessageContent(message_text=message_text, parse_mode=ParseMode.HTML),
reply_markup=builder.as_markup(),
)
await inline_query.answer(
results=[result],
cache_time=86400,
is_personal=True
)
await inline_query.answer(results=[result], cache_time=86400, is_personal=True)
+6 -2
View File
@@ -12,7 +12,7 @@ from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn
class AdminCouponDeleteCallback(CallbackData, prefix="admin_coupon_delete"):
coupon_code: str
confirm: Optional[bool] = None
confirm: bool | None = None
def build_coupons_kb() -> InlineKeyboardMarkup:
@@ -59,7 +59,11 @@ def build_coupons_list_kb(coupons: list, current_page: int, total_pages: int) ->
def format_coupons_list(coupons: list, username_bot: str) -> str:
coupon_list = "📜 Список всех купонов:\n\n"
for coupon in coupons:
value_text = f"💰 <b>Сумма:</b> {coupon['amount']} рублей" if coupon["amount"] > 0 else f"⏳ <b>{format_days(coupon['days'])}</b>"
value_text = (
f"💰 <b>Сумма:</b> {coupon['amount']} рублей"
if coupon["amount"] > 0
else f"⏳ <b>{format_days(coupon['days'])}</b>"
)
coupon_list += (
f"🏷️ <b>Код:</b> {coupon['code']}\n"
f"{value_text}\n"
+2 -3
View File
@@ -1,16 +1,15 @@
from aiogram import F, Router
from aiogram.exceptions import TelegramBadRequest
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
from aiogram.exceptions import TelegramBadRequest
from bot import version
from filters.admin import IsAdminFilter
from logger import logger
from .keyboard import AdminPanelCallback, build_panel_kb
from logger import logger
router = Router()
+3 -1
View File
@@ -15,7 +15,9 @@ def build_manage_server_kb(server_name: str, cluster_name: str) -> InlineKeyboar
builder = InlineKeyboardBuilder()
builder.button(text="🗑️ Удалить", callback_data=AdminServerCallback(action="delete", data=server_name).pack())
builder.button(text="✏️ Сменить название", callback_data=AdminServerCallback(action="rename", data=server_name).pack())
builder.button(
text="✏️ Сменить название", callback_data=AdminServerCallback(action="rename", data=server_name).pack()
)
builder.button(text=BACK, callback_data=AdminClusterCallback(action="manage", data=cluster_name).pack())
builder.adjust(1)
return builder.as_markup()
+13 -28
View File
@@ -4,10 +4,10 @@ from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from handlers.buttons import BACK
from database import delete_server, get_servers
from filters.admin import IsAdminFilter
from handlers.buttons import BACK
from ..panel.keyboard import build_admin_back_kb
from .keyboard import (
@@ -73,10 +73,7 @@ async def process_callback_delete_server(
)
return
keys_count = await session.fetchval(
"SELECT COUNT(*) FROM keys WHERE server_id = $1",
server_name
)
keys_count = await session.fetchval("SELECT COUNT(*) FROM keys WHERE server_id = $1", server_name)
if keys_count > 0:
await state.update_data(server_name=server_name, cluster_name=cluster_name)
@@ -87,7 +84,7 @@ async def process_callback_delete_server(
FROM servers
WHERE server_name != $1
""",
server_name
server_name,
)
if all_servers:
@@ -96,13 +93,12 @@ async def process_callback_delete_server(
builder.row(
InlineKeyboardButton(
text=f"{server['server_name']} ({server['key_count']})",
callback_data=f"transfer_to_server|{server['server_name']}|{server_name}"
callback_data=f"transfer_to_server|{server['server_name']}|{server_name}",
)
)
builder.row(
InlineKeyboardButton(
text=BACK,
callback_data=AdminServerCallback(action="manage", data=server_name).pack()
text=BACK, callback_data=AdminServerCallback(action="manage", data=server_name).pack()
)
)
@@ -114,22 +110,16 @@ async def process_callback_delete_server(
return
remaining_servers = await session.fetchval(
"SELECT COUNT(*) FROM servers WHERE cluster_name = $1 AND server_name != $2",
cluster_name,
server_name
"SELECT COUNT(*) FROM servers WHERE cluster_name = $1 AND server_name != $2", cluster_name, server_name
)
if remaining_servers == 0:
other_clusters = await session.fetch(
"SELECT DISTINCT cluster_name FROM servers WHERE cluster_name != $1",
cluster_name
"SELECT DISTINCT cluster_name FROM servers WHERE cluster_name != $1", cluster_name
)
if other_clusters:
cluster_keys_count = await session.fetchval(
"SELECT COUNT(*) FROM keys WHERE server_id = $1",
cluster_name
)
cluster_keys_count = await session.fetchval("SELECT COUNT(*) FROM keys WHERE server_id = $1", cluster_name)
if cluster_keys_count > 0:
await state.update_data(server_name=server_name, cluster_name=cluster_name)
@@ -141,7 +131,7 @@ async def process_callback_delete_server(
WHERE cluster_name != $1
GROUP BY cluster_name
""",
cluster_name
cluster_name,
)
builder = InlineKeyboardBuilder()
@@ -149,13 +139,12 @@ async def process_callback_delete_server(
builder.row(
InlineKeyboardButton(
text=f"{cluster['cluster_name']} ({cluster['key_count']})",
callback_data=f"transfer_to_cluster|{cluster['cluster_name']}|{cluster_name}|{server_name}"
callback_data=f"transfer_to_cluster|{cluster['cluster_name']}|{cluster_name}|{server_name}",
)
)
builder.row(
InlineKeyboardButton(
text=BACK,
callback_data=AdminServerCallback(action="manage", data=server_name).pack()
text=BACK, callback_data=AdminServerCallback(action="manage", data=server_name).pack()
)
)
@@ -167,9 +156,7 @@ async def process_callback_delete_server(
return
await session.execute(
"DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2",
cluster_name,
server_name
"DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2", cluster_name, server_name
)
await callback_query.message.edit_text(
text=f"✅ Сервер '{server_name}' удален. Кластер '{cluster_name}' также удален, так как в нем не осталось серверов.",
@@ -177,9 +164,7 @@ async def process_callback_delete_server(
)
else:
await session.execute(
"DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2",
cluster_name,
server_name
"DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2", cluster_name, server_name
)
await callback_query.message.edit_text(
text=f"✅ Сервер '{server_name}' удален.",
+11 -31
View File
@@ -826,7 +826,9 @@ async def handle_users_export_referrals(
@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_create_key"), IsAdminFilter())
async def handle_create_key_start(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext, session: Any):
async def handle_create_key_start(
callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext, session: Any
):
tg_id = callback_data.tg_id
await state.update_data(tg_id=tg_id)
@@ -838,8 +840,7 @@ async def handle_create_key_start(callback_query: CallbackQuery, callback_data:
if not countries:
await callback_query.message.edit_text(
"❌ Нет доступных стран для создания ключа.",
reply_markup=build_editor_kb(tg_id)
"❌ Нет доступных стран для создания ключа.", reply_markup=build_editor_kb(tg_id)
)
return
@@ -850,8 +851,7 @@ async def handle_create_key_start(callback_query: CallbackQuery, callback_data:
builder.row(build_admin_back_btn())
await callback_query.message.edit_text(
"🌍 <b>Выберите страну для создания ключа:</b>",
reply_markup=builder.as_markup()
"🌍 <b>Выберите страну для создания ключа:</b>", reply_markup=builder.as_markup()
)
return
@@ -862,8 +862,7 @@ async def handle_create_key_start(callback_query: CallbackQuery, callback_data:
if not cluster_names:
await callback_query.message.edit_text(
"❌ Нет доступных кластеров для создания ключа.",
reply_markup=build_editor_kb(tg_id)
"❌ Нет доступных кластеров для создания ключа.", reply_markup=build_editor_kb(tg_id)
)
return
@@ -873,8 +872,7 @@ async def handle_create_key_start(callback_query: CallbackQuery, callback_data:
builder.row(build_admin_back_btn())
await callback_query.message.edit_text(
"🌐 <b>Выберите кластер для создания ключа:</b>",
reply_markup=builder.as_markup()
"🌐 <b>Выберите кластер для создания ключа:</b>", reply_markup=builder.as_markup()
)
@@ -891,8 +889,7 @@ async def handle_create_key_country(callback_query: CallbackQuery, state: FSMCon
builder.row(build_admin_back_btn())
await callback_query.message.edit_text(
text=f"🕒 <b>Выберите срок действия ключа для страны {country}:</b>",
reply_markup=builder.as_markup()
text=f"🕒 <b>Выберите срок действия ключа для страны {country}:</b>", reply_markup=builder.as_markup()
)
@@ -928,15 +925,7 @@ async def handle_create_key_duration(callback_query: CallbackQuery, state: FSMCo
if USE_COUNTRY_SELECTION and "country" in data:
country = data["country"]
await create_key_on_cluster(
country,
tg_id,
client_id,
email,
expiry_ms,
plan=months,
session=session
)
await create_key_on_cluster(country, tg_id, client_id, email, expiry_ms, plan=months, session=session)
await state.clear()
await callback_query.message.edit_text(
@@ -946,15 +935,7 @@ async def handle_create_key_duration(callback_query: CallbackQuery, state: FSMCo
elif "cluster_name" in data:
cluster_name = data["cluster_name"]
await create_key_on_cluster(
cluster_name,
tg_id,
client_id,
email,
expiry_ms,
plan=months,
session=session
)
await create_key_on_cluster(cluster_name, tg_id, client_id, email, expiry_ms, plan=months, session=session)
await state.clear()
await callback_query.message.edit_text(
@@ -968,8 +949,7 @@ async def handle_create_key_duration(callback_query: CallbackQuery, state: FSMCo
except Exception as e:
logger.error(f"Ошибка при создании ключа: {e}")
await callback_query.message.edit_text(
"❌ Не удалось создать ключ. Попробуйте позже.",
reply_markup=build_editor_kb(data.get("tg_id", 0))
"❌ Не удалось создать ключ. Попробуйте позже.", reply_markup=build_editor_kb(data.get("tg_id", 0))
)