diff --git a/handlers/admin/clusters/cluster_sync.py b/handlers/admin/clusters/cluster_sync.py
index 879a0d0c..65fcf066 100644
--- a/handlers/admin/clusters/cluster_sync.py
+++ b/handlers/admin/clusters/cluster_sync.py
@@ -314,11 +314,6 @@ async def handle_sync_server(
sub = await remna.get_subscription_by_username(key["email"])
if sub:
new_remnawave_link = sub.get("subscriptionUrl")
- if HAPP_CRYPTOLINK:
- happ = sub.get("happ") or {}
- new_remnawave_link = (
- happ.get("cryptoLink") or happ.get("link") or new_remnawave_link
- )
if new_remnawave_link:
server_result = await session.execute(
@@ -336,7 +331,7 @@ async def handle_sync_server(
email=key["email"],
client_id=key["client_id"],
tg_id=key["tg_id"],
- remna_link_override=new_remnawave_link,
+ remna_link_override=None,
plan=key["tariff_id"],
)
@@ -544,11 +539,6 @@ async def handle_sync_cluster(
sub = await remna.get_subscription_by_username(key["email"])
if sub:
new_remnawave_link = sub.get("subscriptionUrl")
- if HAPP_CRYPTOLINK:
- happ = sub.get("happ") or {}
- new_remnawave_link = (
- happ.get("cryptoLink") or happ.get("link") or new_remnawave_link
- )
if new_remnawave_link:
servers = await get_servers(session)
@@ -561,7 +551,7 @@ async def handle_sync_cluster(
email=key["email"],
client_id=key["client_id"],
tg_id=key["tg_id"],
- remna_link_override=new_remnawave_link,
+ remna_link_override=None,
plan=key["tariff_id"],
)
diff --git a/handlers/admin/sender/sender_handler.py b/handlers/admin/sender/sender_handler.py
index 5685954a..f5a345f6 100644
--- a/handlers/admin/sender/sender_handler.py
+++ b/handlers/admin/sender/sender_handler.py
@@ -170,13 +170,24 @@ async def get_recipients(session: AsyncSession, send_to: str, cluster_name: str
return tg_ids, len(tg_ids)
+def strip_html_tags(text: str) -> str:
+ text = re.sub(r'([^<]*)', r"\1", text)
+ text = re.sub(r'<[^>]+>', '', text)
+ text = text.replace('<', '<').replace('>', '>').replace('&', '&')
+ return text.strip()
+
+
def parse_message_buttons(text: str) -> tuple[str, InlineKeyboardMarkup | None]:
- if "BUTTONS:" not in text:
+ buttons_match = re.search(r'(<[^>]+>)?\s*BUTTONS\s*:\s*([^>]+>)?', text, re.IGNORECASE)
+ if not buttons_match:
return text, None
- parts = text.split("BUTTONS:", 1)
- clean_text = parts[0].strip()
- buttons_text = parts[1].strip()
+ clean_text = text[:buttons_match.start()].strip()
+
+ buttons_section = text[buttons_match.start():].strip()
+ buttons_text = strip_html_tags(buttons_section)
+
+ buttons_text = re.sub(r'^.*?BUTTONS\s*:\s*', '', buttons_text, flags=re.IGNORECASE).strip()
if not buttons_text:
return clean_text, None
@@ -186,9 +197,7 @@ def parse_message_buttons(text: str) -> tuple[str, InlineKeyboardMarkup | None]:
for line in button_lines:
try:
- cleaned_line = re.sub(r'([^<]*)', r"\1", line)
-
- button_data = json.loads(cleaned_line)
+ button_data = json.loads(line)
if not isinstance(button_data, dict) or "text" not in button_data:
logger.warning(f"Неверный формат кнопки: {line}")
@@ -307,6 +316,21 @@ async def handle_message_input(message: Message, state: FSMContext, session: Asy
cluster_name = data.get("cluster_name")
_, user_count = await get_recipients(session, send_to, cluster_name)
+ if keyboard:
+ try:
+ keyboard_dict = keyboard.model_dump()
+ InlineKeyboardMarkup.model_validate(keyboard_dict)
+ except Exception as e:
+ await message.answer(
+ f"❌ Ошибка в клавиатуре!\n\n"
+ f"Не удалось сохранить клавиатуру из указанных кнопок.\n"
+ f"Ошибка: {str(e)}\n\n"
+ f"Пожалуйста, проверьте формат кнопок и попробуйте снова.",
+ reply_markup=build_admin_back_kb("sender"),
+ )
+ await state.clear()
+ return
+
await state.update_data(text=clean_text, photo=photo, keyboard=keyboard.model_dump() if keyboard else None)
await state.set_state(AdminSender.preview)
@@ -343,9 +367,33 @@ async def handle_send_confirm(callback_query: CallbackQuery, state: FSMContext,
keyboard = InlineKeyboardMarkup.model_validate(keyboard_data)
except Exception as e:
logger.error(f"Ошибка восстановления клавиатуры: {e}")
+ await callback_query.message.edit_text(
+ f"❌ Ошибка восстановления клавиатуры!\n\n"
+ f"Не удалось восстановить клавиатуру из сохраненных данных.\n"
+ f"Ошибка: {str(e)}\n\n"
+ f"Пожалуйста, создайте рассылку заново.",
+ reply_markup=build_admin_back_kb("sender"),
+ )
+ await state.clear()
+ return
tg_ids, total_users = await get_recipients(session, send_to, cluster_name)
+ if keyboard:
+ try:
+ keyboard.model_dump()
+ except Exception as e:
+ logger.error(f"Ошибка валидации клавиатуры перед рассылкой: {e}")
+ await callback_query.message.edit_text(
+ f"❌ Ошибка валидации клавиатуры!\n\n"
+ f"Клавиатура не прошла финальную проверку перед рассылкой.\n"
+ f"Ошибка: {str(e)}\n\n"
+ f"Пожалуйста, создайте рассылку заново.",
+ reply_markup=build_admin_back_kb("sender"),
+ )
+ await state.clear()
+ return
+
await callback_query.message.edit_text(f"📤 Рассылка начата!\n👥 Количество получателей: {total_users}")
messages = []
diff --git a/handlers/keys/key_mode/key_country_mode.py b/handlers/keys/key_mode/key_country_mode.py
index dbbe2ca2..78cd6dca 100644
--- a/handlers/keys/key_mode/key_country_mode.py
+++ b/handlers/keys/key_mode/key_country_mode.py
@@ -486,11 +486,7 @@ async def finalize_key_creation(
)
if not remnawave_link:
- if bool(MODES_CONFIG.get("HAPP_CRYPTOLINK_ENABLED", HAPP_CRYPTOLINK)):
- happ = sub.get("happ") or {}
- remnawave_link = happ.get("cryptoLink") or happ.get("link")
- if not remnawave_link:
- remnawave_link = sub.get("subscriptionUrl")
+ remnawave_link = sub.get("subscriptionUrl")
if old_key_name:
await session.execute(
diff --git a/handlers/keys/operations/aggregated_links.py b/handlers/keys/operations/aggregated_links.py
index 55fa9438..90bbc56b 100644
--- a/handlers/keys/operations/aggregated_links.py
+++ b/handlers/keys/operations/aggregated_links.py
@@ -172,6 +172,9 @@ async def make_aggregated_link(
):
logger.info("[agg_link] choose override Remnawave (non-vless)")
return remna_link_override
+ if sub_url and sub_url.startswith("happ://"):
+ logger.info("[agg_link] choose Remnawave cryptoLink (non-vless)")
+ return sub_url
kd = await get_key_details(session, email)
stored = kd.get("remnawave_link") if kd else None
if stored:
diff --git a/handlers/keys/operations/creation.py b/handlers/keys/operations/creation.py
index 5572fc89..a75b699a 100644
--- a/handlers/keys/operations/creation.py
+++ b/handlers/keys/operations/creation.py
@@ -115,6 +115,7 @@ async def create_key_on_cluster(
remnawave_created = False
remnawave_key = None
remnawave_client_id = None
+ remnawave_link_value = None
if remnawave_servers:
remna = RemnawaveAPI(remnawave_servers[0]["api_url"])
@@ -146,38 +147,15 @@ async def create_key_on_cluster(
if result:
remnawave_created = True
remnawave_client_id = result.get("uuid")
- link_vless = None
+ remnawave_link_value = result.get("subscriptionUrl")
+
+ remnawave_key = None
if need_vless_key:
try:
- link_vless = await get_vless_link_for_remnawave_by_username(remna, email, email)
+ remnawave_key = await get_vless_link_for_remnawave_by_username(remna, email, email)
except Exception as e:
logger.error(f"{PANEL_REMNA} Ошибка сборки VLESS: {e}")
-
- base_use_crypto_link = bool(
- MODES_CONFIG.get("HAPP_CRYPTOLINK_ENABLED", HAPP_CRYPTOLINK)
- )
- use_crypto_link = base_use_crypto_link
- try:
- hook_results = await run_hooks(
- "happ_cryptolink_override",
- cluster_id=cluster_id,
- plan=plan,
- session=session,
- email=email,
- tg_id=tg_id,
- happ_cryptolink=base_use_crypto_link,
- )
- if hook_results:
- for hook_result in hook_results:
- if hook_result is True or hook_result is False:
- use_crypto_link = hook_result
- break
- except Exception as e:
- logger.warning(f"[HAPP_CRYPTOLINK_OVERRIDE] Ошибка при применении хуков: {e}")
-
- remnawave_key = link_vless or (
- result["happ"]["cryptoLink"] if use_crypto_link else result.get("subscriptionUrl")
- )
+
logger.info(f"{PANEL_REMNA} Пользователь создан: {result}")
else:
logger.warning(f"{PANEL_REMNA} Нет inbound_id у серверов")
@@ -246,7 +224,7 @@ async def create_key_on_cluster(
expiry_time=expiry_timestamp,
key=public_link,
server_id=server_id_to_store,
- remnawave_link=remnawave_key,
+ remnawave_link=remnawave_link_value if remnawave_created else None,
tariff_id=plan,
)
await session.execute(update(User).where(User.tg_id == tg_id, User.trial.in_([0, -1])).values(trial=1))
diff --git a/handlers/keys/operations/renewal.py b/handlers/keys/operations/renewal.py
index 0f20dfe2..63988b08 100644
--- a/handlers/keys/operations/renewal.py
+++ b/handlers/keys/operations/renewal.py
@@ -287,6 +287,24 @@ async def renew_key_in_cluster(
await update_key_expiry(session, client_id, new_expiry_time)
for prefix in ["key_24h", "key_10h", "key_expired", "renew"]:
await delete_notification(session, tg_id, f"{email}_{prefix}")
+
+ try:
+ key_link = await make_aggregated_link(
+ session=session,
+ cluster_all=cluster_scope,
+ cluster_id=cluster_id,
+ email=email,
+ client_id=client_id,
+ tg_id=tg_id,
+ subgroup_code=target_subgroup,
+ remna_link_override=None,
+ plan=plan,
+ )
+ if key_link:
+ await update_key_link(session, email, key_link)
+ except Exception as le:
+ logger.warning(f"[Link] ошибка генерации/сохранения после продления: {le}")
+
return True
return False
diff --git a/handlers/keys/operations/update.py b/handlers/keys/operations/update.py
index 0659c9d9..67d16b7d 100644
--- a/handlers/keys/operations/update.py
+++ b/handlers/keys/operations/update.py
@@ -6,7 +6,7 @@ from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession
from config import PUBLIC_LINK, REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD, SUPERNODE
-from database import filter_cluster_by_subgroup, get_servers, store_key
+from database import filter_cluster_by_subgroup, get_servers, get_tariff_by_id, store_key
from database.models import Key, Tariff
from handlers.utils import get_least_loaded_cluster
from logger import (
@@ -62,6 +62,7 @@ async def update_key_on_cluster(
remnawave_client_id = None
remnawave_key = None
+ remnawave_link_value = None
if remnawave_servers:
inbound_ids = [s["inbound_id"] for s in remnawave_servers if s.get("inbound_id")]
@@ -104,7 +105,9 @@ async def update_key_on_cluster(
result = await remna.create_user(user_data)
if result:
remnawave_client_id = result.get("uuid")
- remnawave_key = result.get("subscriptionUrl")
+ remnawave_link_value = result.get("subscriptionUrl")
+ remnawave_key = None
+
logger.info(f"{PANEL_REMNA} Клиент заново создан, uuid={remnawave_client_id}")
else:
logger.error(f"{PANEL_REMNA} Ошибка создания клиента")
@@ -162,7 +165,7 @@ async def update_key_on_cluster(
await asyncio.gather(*tasks, return_exceptions=True)
logger.info(f"[Update] Ключ {remnawave_client_id} обновлён на серверах подгруппы в {cluster_id}")
- return remnawave_client_id, remnawave_key
+ return remnawave_client_id, remnawave_link_value
except Exception as e:
logger.error(f"[Update Error] Ошибка при обновлении ключа {client_id} на {cluster_id}: {e}")
@@ -246,7 +249,7 @@ async def update_subscription(
traffic_limit = int(tariff.traffic_limit) if tariff.traffic_limit is not None else None
device_limit = int(tariff.device_limit) if tariff.device_limit is not None else 0
- new_client_id, remnawave_key = await update_key_on_cluster(
+ new_client_id, remnawave_link_value = await update_key_on_cluster(
tg_id=tg_id,
client_id=client_id,
email=email,
@@ -267,7 +270,7 @@ async def update_subscription(
client_id=new_client_id,
tg_id=tg_id,
subgroup_code=subgroup_code,
- remna_link_override=remnawave_key,
+ remna_link_override=None,
plan=tariff_id,
)
@@ -280,7 +283,7 @@ async def update_subscription(
email=email,
expiry_time=expiry_time,
key=final_key_link,
- remnawave_link=remnawave_key,
+ remnawave_link=remnawave_link_value or remnawave_link,
server_id=new_cluster_id,
tariff_id=tariff_id,
alias=alias,