Add revoke subs Remnawave
This commit is contained in:
@@ -260,6 +260,10 @@ def build_key_edit_kb(key_details: dict, email: str, is_configurable: bool = Fal
|
||||
text="🔄 Перевыпустить",
|
||||
callback_data=AdminUserEditorCallback(action="users_update_key", data=email, tg_id=key_details["tg_id"]).pack(),
|
||||
)
|
||||
builder.button(
|
||||
text="🔁 Пересоздать",
|
||||
callback_data=AdminUserEditorCallback(action="users_recreate_key", data=email, tg_id=key_details["tg_id"]).pack(),
|
||||
)
|
||||
builder.button(
|
||||
text="📦 Тариф",
|
||||
callback_data=AdminUserEditorCallback(action="users_renew", data=email, tg_id=key_details["tg_id"]).pack(),
|
||||
|
||||
@@ -640,6 +640,195 @@ async def admin_reissue_country(callback_query: CallbackQuery, session: AsyncSes
|
||||
await callback_query.message.answer(f"❗ Ошибка: {e}")
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
AdminUserEditorCallback.filter(F.action == "users_recreate_key"),
|
||||
IsAdminFilter(),
|
||||
)
|
||||
async def handle_recreate_key_start(
|
||||
callback_query: CallbackQuery,
|
||||
callback_data: AdminUserEditorCallback,
|
||||
session: AsyncSession,
|
||||
):
|
||||
tg_id = callback_data.tg_id
|
||||
email = callback_data.data
|
||||
|
||||
result = await session.execute(select(Key).where(Key.email == email))
|
||||
key_obj: Key | None = result.scalar_one_or_none()
|
||||
|
||||
if not key_obj:
|
||||
await callback_query.message.edit_text(
|
||||
text="🚫 Ключ не найден.",
|
||||
reply_markup=build_editor_kb(tg_id),
|
||||
)
|
||||
return
|
||||
|
||||
tariff_name = "—"
|
||||
if key_obj.tariff_id:
|
||||
tariff = await get_tariff_by_id(session, key_obj.tariff_id)
|
||||
if tariff:
|
||||
tariff_name = tariff.get("name", "—")
|
||||
|
||||
text = (
|
||||
"<b>🔁 Пересоздание ссылки подписки</b>\n\n"
|
||||
f"📦 <b>Тариф:</b> {tariff_name}\n\n"
|
||||
"⚠️ <b>Будет сгенерирована новая ссылка подписки.</b>\n"
|
||||
"Старая ссылка перестанет работать.\n\n"
|
||||
"✅ <i>Все данные подписки сохранятся.</i>"
|
||||
)
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text="✅ Пересоздать",
|
||||
callback_data=f"confirm_recreate|{tg_id}|{email}",
|
||||
)
|
||||
)
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text="🔙 Назад",
|
||||
callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email).pack(),
|
||||
)
|
||||
)
|
||||
|
||||
await callback_query.message.edit_text(text=text, reply_markup=builder.as_markup())
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("confirm_recreate|"), IsAdminFilter())
|
||||
async def handle_recreate_key_confirm(
|
||||
callback_query: CallbackQuery,
|
||||
session: AsyncSession,
|
||||
):
|
||||
_, tg_id, old_email = callback_query.data.split("|")
|
||||
tg_id = int(tg_id)
|
||||
|
||||
try:
|
||||
result = await session.execute(select(Key).where(Key.email == old_email))
|
||||
key_obj: Key | None = result.scalar_one_or_none()
|
||||
|
||||
if not key_obj:
|
||||
await callback_query.message.edit_text(
|
||||
text="🚫 Ключ не найден.",
|
||||
reply_markup=build_editor_kb(tg_id),
|
||||
)
|
||||
return
|
||||
|
||||
await callback_query.message.edit_text("⏳ Пересоздание ссылки подписки...")
|
||||
|
||||
client_id = key_obj.client_id
|
||||
cluster_id = key_obj.server_id
|
||||
old_link = key_obj.remnawave_link or key_obj.key
|
||||
|
||||
servers = await get_servers(session)
|
||||
cluster = servers.get(cluster_id)
|
||||
|
||||
if not cluster:
|
||||
for _, server_list in servers.items():
|
||||
for server_info in server_list:
|
||||
if server_info.get("server_name", "").lower() == cluster_id.lower():
|
||||
cluster = [server_info]
|
||||
break
|
||||
if cluster:
|
||||
break
|
||||
|
||||
if not cluster:
|
||||
await callback_query.message.edit_text(
|
||||
text=f"❗ Кластер {cluster_id} не найден.",
|
||||
reply_markup=build_editor_kb(tg_id),
|
||||
)
|
||||
return
|
||||
|
||||
remnawave_servers = [s for s in cluster if s.get("panel_type", "3x-ui").lower() == "remnawave"]
|
||||
|
||||
if not remnawave_servers:
|
||||
await callback_query.message.edit_text(
|
||||
text="❗ Revoke доступен только для Remnawave. Для 3x-ui используйте перевыпуск.",
|
||||
reply_markup=build_editor_kb(tg_id),
|
||||
)
|
||||
return
|
||||
|
||||
api_url = remnawave_servers[0].get("api_url")
|
||||
|
||||
from panels.remnawave_ext import revoke_user_subscription
|
||||
|
||||
user_data = await revoke_user_subscription(api_url, client_id)
|
||||
|
||||
if not user_data:
|
||||
await callback_query.message.edit_text(
|
||||
text="❗ Не удалось выполнить revoke. Проверьте логи.",
|
||||
reply_markup=build_editor_kb(tg_id),
|
||||
)
|
||||
return
|
||||
|
||||
new_link = user_data.get("subscriptionUrl")
|
||||
|
||||
if not new_link:
|
||||
await callback_query.message.edit_text(
|
||||
text="❗ Revoke выполнен, но новая ссылка не получена.",
|
||||
reply_markup=build_editor_kb(tg_id),
|
||||
)
|
||||
return
|
||||
|
||||
await session.execute(
|
||||
update(Key)
|
||||
.where(Key.email == old_email)
|
||||
.values(
|
||||
key=new_link,
|
||||
remnawave_link=new_link,
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
try:
|
||||
user_text = (
|
||||
"🔄 <b>Ваша подписка была перевыпущена</b>\n\n"
|
||||
f"🔗 <b>Новая ссылка подписки:</b>\n<code>{new_link}</code>\n\n"
|
||||
"<i>Старая ссылка больше не работает.</i>"
|
||||
)
|
||||
user_kb = InlineKeyboardBuilder()
|
||||
user_kb.row(InlineKeyboardButton(text="📱 Мои подписки", callback_data="view_keys"))
|
||||
user_kb.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
|
||||
|
||||
await callback_query.bot.send_message(
|
||||
chat_id=tg_id,
|
||||
text=user_text,
|
||||
reply_markup=user_kb.as_markup(),
|
||||
)
|
||||
notification_sent = True
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось отправить уведомление клиенту {tg_id}: {e}")
|
||||
notification_sent = False
|
||||
|
||||
text = (
|
||||
"✅ <b>Ссылка подписки пересоздана</b>\n\n"
|
||||
f"🔗 <b>Старая ссылка:</b>\n<code>{old_link}</code>\n\n"
|
||||
f"🔗 <b>Новая ссылка:</b>\n<code>{new_link}</code>\n\n"
|
||||
)
|
||||
if notification_sent:
|
||||
text += "📨 <i>Клиент уведомлён о новой ссылке.</i>"
|
||||
else:
|
||||
text += "⚠️ <i>Не удалось уведомить клиента.</i>"
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text="🔙 Назад",
|
||||
callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=old_email).pack(),
|
||||
)
|
||||
)
|
||||
|
||||
await callback_query.message.edit_text(
|
||||
text=text,
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при revoke ключа {old_email}: {e}")
|
||||
await callback_query.message.edit_text(
|
||||
text=f"❗ Ошибка при пересоздании: {e}",
|
||||
reply_markup=build_editor_kb(tg_id),
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
AdminUserEditorCallback.filter(F.action == "users_delete_key"),
|
||||
IsAdminFilter(),
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import aiohttp
|
||||
|
||||
from config import (
|
||||
REMNAWAVE_ACCESS_TOKEN,
|
||||
REMNAWAVE_LOGIN,
|
||||
REMNAWAVE_PASSWORD,
|
||||
REMNAWAVE_TOKEN_LOGIN_ENABLED,
|
||||
)
|
||||
from logger import logger
|
||||
|
||||
PANEL_REMNA = "[Remnawave]"
|
||||
|
||||
|
||||
async def revoke_user_subscription(
|
||||
api_url: str,
|
||||
user_uuid: str,
|
||||
short_uuid: str | None = None,
|
||||
) -> dict | None:
|
||||
base_url = api_url.rstrip("/")
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
if REMNAWAVE_TOKEN_LOGIN_ENABLED and REMNAWAVE_ACCESS_TOKEN:
|
||||
token = REMNAWAVE_ACCESS_TOKEN
|
||||
else:
|
||||
login_url = f"{base_url}/auth/login"
|
||||
login_data = {"username": REMNAWAVE_LOGIN, "password": REMNAWAVE_PASSWORD}
|
||||
|
||||
try:
|
||||
async with session.post(login_url, json=login_data) as resp:
|
||||
if resp.status != 200 and resp.status != 201:
|
||||
logger.error(f"{PANEL_REMNA} Авторизация не удалась: {resp.status}")
|
||||
return None
|
||||
|
||||
auth_response = await resp.json()
|
||||
token = auth_response.get("response", {}).get("accessToken")
|
||||
if not token:
|
||||
logger.error(f"{PANEL_REMNA} Не получен accessToken")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"{PANEL_REMNA} Ошибка авторизации: {e}")
|
||||
return None
|
||||
|
||||
revoke_url = f"{base_url}/users/{user_uuid}/actions/revoke"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
revoke_data = {}
|
||||
if short_uuid:
|
||||
revoke_data["shortUuid"] = short_uuid
|
||||
|
||||
try:
|
||||
async with session.post(revoke_url, json=revoke_data, headers=headers) as resp:
|
||||
if resp.status != 200 and resp.status != 201:
|
||||
error_text = await resp.text()
|
||||
logger.error(f"{PANEL_REMNA} Revoke не удался: {resp.status} - {error_text}")
|
||||
return None
|
||||
|
||||
result = await resp.json()
|
||||
user_data = result.get("response", {})
|
||||
|
||||
logger.info(f"{PANEL_REMNA} Подписка {user_uuid} успешно отозвана")
|
||||
return user_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"{PANEL_REMNA} Ошибка revoke: {e}")
|
||||
return None
|
||||
Reference in New Issue
Block a user