mirror of
https://github.com/PRVTPRO/Amnezia-Web-Panel.git
synced 2026-09-18 01:44:10 +02:00
69893ba340
Today a client leaves to the internet from the same server it connected to.
Exit nodes split the two: clients connect to an *entry* node (reachable,
possibly behind DPI) and egress from an *exit* node (clean IP, other country).
One exit serves many entries and keeps serving its own AWG clients.
client --AWG--> [entry] amnezia-awg2 --WG/AWG link--> [exit] amnezia-exit --> internet
awg0 10.8.1.0/24 transit0 10.9.0.0/24
exit0 10.9.0.N (Table=off) one [Peer] per entry, /32
Exit side: a new protocol `exit` = container `amnezia-exit`, an AmneziaWG
listener on 55520/udp over the transit subnet 10.9.0.0/24. Peers are panel
nodes, never end users, and live in their own peersTable. ExitManager
subclasses AWGManager and closes every client-facing method.
Entry side: a second interface `exit0` inside the existing AWG container with
`Table = off`, plus policy routing - `from <client subnet> lookup 200`,
`default dev exit0` in table 200, and `blackhole default metric 1000` as a
kill-switch installed *before* the client tunnel comes up, so a container
restart cannot leak. Clients are MASQUERADEd into the entry's transit /32, so
the identical 10.8.1.0/24 of every instance can never collide at the exit.
TCP MSS is clamped, and rules 190/191 keep client<->client and AmneziaDNS
traffic local. State is file-driven: exit0.conf present means linked, and the
same shell block runs at container start and on a live (un)link.
Link crypto is WireGuard; AWG obfuscation is a per-exit-node property for hops
that cross DPI, and S3/S4 are deliberately not written on the transit - they
pad every transport packet and would eat the MTU budget of a tunnel inside a
tunnel. Private keys are generated inside the containers; the panel carries
public keys and a per-peer PSK only.
exit_link_service.py orchestrates linking with per-(entry, protocol) locks,
SSH off the event loop, a 15-second handshake gate that rolls back, and
lifecycle hooks for every way a link can go stale: exit uninstalled, stopped,
deleted, host cleared, entry reinstalled, container gone, backup restored.
Servers get a stable `uid` (already upstream) so reorder and delete cannot
break a link.
IPv4 only for now: while linked, client IPv6 is refused with `unreachable`
rather than leaking out of the entry.
Also included: an egress check that curls a public IP service bound to the
tunnel gateway (the client's exact path) and compares it with the direct one;
optional DNS resolution at the exit node; a default exit node applied to new
AWG installs; and reconciliation after a protocol restore.
244 tests pass. CURRENT_VERSION is untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1832 lines
89 KiB
Python
1832 lines
89 KiB
Python
"""
|
||
Telegram bot for Amnezia Web Panel.
|
||
Uses raw Telegram Bot API via httpx — no library version conflicts.
|
||
Runs as a background asyncio task alongside the FastAPI app.
|
||
"""
|
||
import asyncio
|
||
import html
|
||
import logging
|
||
import os
|
||
import shlex
|
||
import sys
|
||
import time
|
||
import uuid
|
||
from typing import Optional, Callable
|
||
|
||
import httpx
|
||
|
||
from connection_service import (
|
||
SELF_SERVICE_PROTOCOLS,
|
||
protocol_base,
|
||
protocol_instance,
|
||
sanitize_allowed_protocols,
|
||
self_service_protocol_display_name,
|
||
server_self_service_protocols,
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# ----------------------------------------------------------------------- #
|
||
# Global state
|
||
# ----------------------------------------------------------------------- #
|
||
_bot_task: Optional[asyncio.Task] = None
|
||
_callback_refs = {}
|
||
_pending_inputs = {}
|
||
|
||
_EXTRA_PROTOCOL_DISPLAY_NAMES = {
|
||
"dns": "AmneziaDNS",
|
||
"socks5": "SOCKS5",
|
||
"exit": "Exit Node",
|
||
"adguard": "AdGuard Home",
|
||
"nginx": "NGINX",
|
||
}
|
||
SERVICE_PROTOCOLS = {"dns", "adguard", "socks5", "nginx", "exit"}
|
||
|
||
TG_TRANSLATIONS = {
|
||
"en": {
|
||
"hi": "Hi",
|
||
"account_not_linked": "Your Telegram account is not linked to any panel user.",
|
||
"contact_admin_to_link": "Please contact your administrator — they need to add your Telegram ID to your profile.",
|
||
"your_telegram_id": "Your Telegram ID",
|
||
"registered_admin": "You are registered as <b>{username}</b> with <b>Admin</b> role.",
|
||
"choose_action": "Choose an action:",
|
||
"registered_as": "You are registered as <b>{username}</b>.",
|
||
"no_connections_create": "You have no connections yet. Tap the button below to create one!",
|
||
"no_connections_contact_admin": "You have no connections yet. Please contact your administrator.",
|
||
"your_connections": "<b>Your connections</b> ({count}) — tap to get config:",
|
||
"updated": "Updated!",
|
||
"access_denied": "Access denied.",
|
||
"no_connections_create_short": "You have no connections. Tap the button below to create one!",
|
||
"no_connections": "You have no connections.",
|
||
"fetching_config": "Fetching config...",
|
||
"connection_not_found": "Connection not found.",
|
||
"server_not_found": "Server not found.",
|
||
"fetching_config_for": "Fetching config for <b>{name}</b>...",
|
||
"failed_retrieve_config": "Failed to retrieve configuration.",
|
||
"admin_menu": "Admin menu",
|
||
"action_expired": "Action expired. Use /start again.",
|
||
"btn_create_connection": "Create connection",
|
||
"btn_refresh_list": "Refresh list",
|
||
"btn_back": "Back",
|
||
"btn_do_not_assign": "Do not assign",
|
||
"btn_cancel": "Cancel",
|
||
"btn_servers": "Servers",
|
||
"btn_users": "Users",
|
||
"btn_my_connections": "My connections",
|
||
"btn_how_add_server": "How to add a server",
|
||
"btn_admin_menu": "Admin menu",
|
||
"btn_no_installed_protocols": "No installed protocols",
|
||
"btn_connections": "Connections",
|
||
"btn_stop": "Stop",
|
||
"btn_start": "Start",
|
||
"btn_protocols": "Protocols",
|
||
"btn_config": "Config",
|
||
"btn_disable": "Disable",
|
||
"btn_enable": "Enable",
|
||
"btn_delete": "Delete",
|
||
"btn_protocol": "Protocol",
|
||
"choose_server": "Choose a server:",
|
||
"choose_protocol": "Choose a protocol:",
|
||
"protocol_label": "Protocol",
|
||
"send_device_name": "Send the device name in the next message.",
|
||
"cancel_instruction": "Send <code>/cancel</code> to cancel.",
|
||
"example_label": "Example",
|
||
"delete_connection_confirm": "Delete connection <b>{name}</b>?",
|
||
"cannot_be_undone": "This cannot be undone.",
|
||
"connection_deleted": "Connection deleted.",
|
||
"connection_deleted_no_connections": "Connection deleted. You have no connections.",
|
||
"connection_created_no_config": "Connection created, but no config is available.",
|
||
"creating_connection_named": "Creating connection <b>{name}</b>...",
|
||
"self_service_creation_disabled": "Self-service connection creation is currently disabled.\nPlease contact your administrator.",
|
||
"self_service_no_servers": "No servers are available for self-service connection creation.\nPlease contact your administrator.",
|
||
"self_service_disabled_contact": "Self-service is disabled. Contact your administrator.",
|
||
"self_service_unavailable": "Self-service is not available.",
|
||
"server_not_self_service": "This server is not available for self-service.",
|
||
"no_protocols_available": "No protocols available on this server.",
|
||
"name_empty": "Name cannot be empty. Send a connection name or /cancel.",
|
||
"error": "Error",
|
||
"saving_not_available": "Saving is not available for this bot instance.",
|
||
"servers_title": "Servers",
|
||
"choose_a_server": "Choose a server:",
|
||
"users_title": "Users",
|
||
"choose_a_user": "Choose a user:",
|
||
"user_not_found": "User not found.",
|
||
"protocol_not_found": "Protocol not found.",
|
||
"updating_protocol": "Updating protocol container...",
|
||
"loading_connections": "Loading connections...",
|
||
"no_connections_short": "No connections.",
|
||
"creating_connection": "Creating connection...",
|
||
"updating_connection": "Updating connection...",
|
||
"removing_connection": "Removing connection...",
|
||
"connection_removed": "Connection removed.",
|
||
"action_cancelled": "Action cancelled.",
|
||
"add_server_label": "Add server",
|
||
"add_server_command": "Use command:\n<code>/addserver host username password [ssh_port] [name]</code>\n\nExample:\n<code>/addserver 203.0.113.10 root myPassword 22 Prod VPS</code>\n\n⚠️ Telegram messages are not a secrets manager. Prefer adding servers in the web panel if possible.",
|
||
"server_added": "Server added: <b>{name}</b>\nHost: <code>{host}</code>",
|
||
"server_config": "Config",
|
||
"send_connection_name": "Send the connection name in the next message.\nExample: <code>Ivan iPhone</code>\n\nSend <code>/cancel</code> to cancel.",
|
||
"connection_name_label": "Connection name",
|
||
"assign_to_user": "Assign this connection to a panel user?",
|
||
"connection_created": "Connection created: <b>{name}</b>",
|
||
"assigned_to": "Assigned to: <b>{username}</b>",
|
||
"assigned_not_linked": "Assigned: <b>not linked</b>",
|
||
"config_label": "Configuration",
|
||
"connection_link_label": "Connection link",
|
||
"config_part": "Configuration (part {part}/{total}):",
|
||
"vpn_link_label": "VPN Link",
|
||
"config_file_label": "Config file",
|
||
"protocol_status": "Status",
|
||
"protocol_port": "Port",
|
||
"client_status": "Status",
|
||
"enabled_label": "enabled",
|
||
"disabled_label": "disabled",
|
||
"traffic_label": "Traffic",
|
||
"rx_label": "RX",
|
||
"tx_label": "TX",
|
||
"connections_count": "Connections",
|
||
"create_new_connection": "Create connection",
|
||
"connection_created_success": "Connection created successfully.",
|
||
"role_label": "Role",
|
||
"enabled_status": "Enabled",
|
||
"email_label": "Email",
|
||
"tg_id_label": "Telegram ID",
|
||
"description_label": "Description",
|
||
"protocol_display": "Protocol",
|
||
},
|
||
"ru": {
|
||
"hi": "Привет",
|
||
"account_not_linked": "Ваш аккаунт Telegram не привязан ни к одному пользователю панели.",
|
||
"contact_admin_to_link": "Обратитесь к администратору — он должен добавить ваш Telegram ID в профиль.",
|
||
"your_telegram_id": "Ваш Telegram ID",
|
||
"registered_admin": "Вы зарегистрированы как <b>{username}</b> с ролью <b>Admin</b>.",
|
||
"choose_action": "Выберите действие:",
|
||
"registered_as": "Вы зарегистрированы как <b>{username}</b>.",
|
||
"no_connections_create": "У вас пока нет подключений. Нажмите кнопку ниже, чтобы создать первое.",
|
||
"no_connections_contact_admin": "У вас пока нет подключений. Обратитесь к администратору.",
|
||
"your_connections": "<b>Ваши подключения</b> ({count}) — нажмите, чтобы получить конфигурацию:",
|
||
"updated": "Обновлено!",
|
||
"access_denied": "Доступ запрещён.",
|
||
"no_connections_create_short": "У вас нет подключений. Нажмите кнопку ниже, чтобы создать первое.",
|
||
"no_connections": "У вас нет подключений.",
|
||
"fetching_config": "Получаю конфигурацию...",
|
||
"connection_not_found": "Подключение не найдено.",
|
||
"server_not_found": "Сервер не найден.",
|
||
"fetching_config_for": "Получаю конфигурацию для <b>{name}</b>...",
|
||
"failed_retrieve_config": "Не удалось получить конфигурацию.",
|
||
"admin_menu": "Меню администратора",
|
||
"action_expired": "Действие устарело. Используйте /start снова.",
|
||
"btn_create_connection": "Создать подключение",
|
||
"btn_refresh_list": "Обновить список",
|
||
"btn_back": "Назад",
|
||
"btn_do_not_assign": "Не назначать",
|
||
"btn_cancel": "Отмена",
|
||
"btn_servers": "Серверы",
|
||
"btn_users": "Пользователи",
|
||
"btn_my_connections": "Мои подключения",
|
||
"btn_how_add_server": "Как добавить сервер",
|
||
"btn_admin_menu": "Меню администратора",
|
||
"btn_no_installed_protocols": "Нет установленных протоколов",
|
||
"btn_connections": "Подключения",
|
||
"btn_stop": "Остановить",
|
||
"btn_start": "Запустить",
|
||
"btn_protocols": "Протоколы",
|
||
"btn_config": "Конфигурация",
|
||
"btn_disable": "Отключить",
|
||
"btn_enable": "Включить",
|
||
"btn_delete": "Удалить",
|
||
"btn_protocol": "Протокол",
|
||
"choose_server": "Выберите сервер:",
|
||
"choose_protocol": "Выберите протокол:",
|
||
"protocol_label": "Протокол",
|
||
"send_device_name": "Отправьте имя устройства следующим сообщением.",
|
||
"cancel_instruction": "Отправьте <code>/cancel</code> для отмены.",
|
||
"example_label": "Пример",
|
||
"delete_connection_confirm": "Удалить подключение <b>{name}</b>?",
|
||
"cannot_be_undone": "Это действие нельзя отменить.",
|
||
"connection_deleted": "Подключение удалено.",
|
||
"connection_deleted_no_connections": "Подключение удалено. У вас нет подключений.",
|
||
"connection_created_no_config": "Подключение создано, но конфигурация недоступна.",
|
||
"creating_connection_named": "Создаю подключение <b>{name}</b>...",
|
||
"self_service_creation_disabled": "Создание подключений через self-service сейчас отключено.\nОбратитесь к администратору.",
|
||
"self_service_no_servers": "Нет серверов, доступных для создания подключений через self-service.\nОбратитесь к администратору.",
|
||
"self_service_disabled_contact": "Self-service отключён. Обратитесь к администратору.",
|
||
"self_service_unavailable": "Self-service недоступен.",
|
||
"server_not_self_service": "Этот сервер недоступен для self-service.",
|
||
"no_protocols_available": "На этом сервере нет доступных протоколов.",
|
||
"name_empty": "Имя не может быть пустым. Отправьте имя подключения или /cancel.",
|
||
"error": "Ошибка",
|
||
"saving_not_available": "Сохранение недоступно для этой версии бота.",
|
||
"servers_title": "Серверы",
|
||
"choose_a_server": "Выберите сервер:",
|
||
"users_title": "Пользователи",
|
||
"choose_a_user": "Выберите пользователя:",
|
||
"user_not_found": "Пользователь не найден.",
|
||
"protocol_not_found": "Протокол не найден.",
|
||
"updating_protocol": "Обновляю контейнер протокола...",
|
||
"loading_connections": "Загружаю подключения...",
|
||
"no_connections_short": "Нет подключений.",
|
||
"creating_connection": "Создаю подключение...",
|
||
"updating_connection": "Обновляю подключение...",
|
||
"removing_connection": "Удаляю подключение...",
|
||
"connection_removed": "Подключение удалено.",
|
||
"action_cancelled": "Действие отменено.",
|
||
"add_server_label": "Добавить сервер",
|
||
"add_server_command": "Используйте команду:\n<code>/addserver хост имя_пользователя пароль [ssh_порт] [имя]</code>\n\nПример:\n<code>/addserver 203.0.113.10 root myPassword 22 Prod VPS</code>\n\n⚠️ Сообщения Telegram — не менеджер секретов. Лучше добавлять серверы через веб-панель.",
|
||
"server_added": "Сервер добавлен: <b>{name}</b>\nХост: <code>{host}</code>",
|
||
"server_config": "Конфигурация",
|
||
"send_connection_name": "Отправьте имя подключения следующим сообщением.\nПример: <code>Ivan iPhone</code>\n\nОтправьте <code>/cancel</code> для отмены.",
|
||
"connection_name_label": "Имя подключения",
|
||
"assign_to_user": "Назначить это подключение пользователю панели?",
|
||
"connection_created": "Подключение создано: <b>{name}</b>",
|
||
"assigned_to": "Назначено: <b>{username}</b>",
|
||
"assigned_not_linked": "Назначено: <b>не привязано</b>",
|
||
"config_label": "Конфигурация",
|
||
"connection_link_label": "Ссылка подключения",
|
||
"config_part": "Конфигурация (часть {part}/{total}):",
|
||
"vpn_link_label": "VPN ссылка",
|
||
"config_file_label": "Файл конфигурации",
|
||
"protocol_status": "Статус",
|
||
"protocol_port": "Порт",
|
||
"client_status": "Статус",
|
||
"enabled_label": "включён",
|
||
"disabled_label": "отключён",
|
||
"traffic_label": "Трафик",
|
||
"rx_label": "Входящий",
|
||
"tx_label": "Исходящий",
|
||
"connections_count": "Подключения",
|
||
"create_new_connection": "Создать подключение",
|
||
"connection_created_success": "Подключение успешно создано.",
|
||
"role_label": "Роль",
|
||
"enabled_status": "Включён",
|
||
"email_label": "Email",
|
||
"tg_id_label": "Telegram ID",
|
||
"description_label": "Описание",
|
||
"protocol_display": "Протокол",
|
||
},
|
||
}
|
||
|
||
|
||
# ----------------------------------------------------------------------- #
|
||
# Public lifecycle
|
||
# ----------------------------------------------------------------------- #
|
||
def is_running() -> bool:
|
||
return _bot_task is not None and not _bot_task.done()
|
||
|
||
|
||
def launch_bot(token: str, load_data_fn: Callable, generate_vpn_link_fn: Callable, save_data_fn: Optional[Callable] = None, self_service_svc=None):
|
||
global _bot_task
|
||
_bot_task = asyncio.create_task(
|
||
_run_bot(token, load_data_fn, generate_vpn_link_fn, save_data_fn, self_service_svc),
|
||
name="telegram_bot",
|
||
)
|
||
return _bot_task
|
||
|
||
|
||
async def stop_bot():
|
||
global _bot_task
|
||
if _bot_task and not _bot_task.done():
|
||
_bot_task.cancel()
|
||
try:
|
||
await _bot_task
|
||
except asyncio.CancelledError:
|
||
pass
|
||
_bot_task = None
|
||
logger.info("Telegram bot stopped.")
|
||
|
||
|
||
# ----------------------------------------------------------------------- #
|
||
# Low-level Telegram API helpers
|
||
# ----------------------------------------------------------------------- #
|
||
class TelegramAPI:
|
||
def __init__(self, token: str, client: httpx.AsyncClient):
|
||
self.base = f"https://api.telegram.org/bot{token}"
|
||
self.client = client
|
||
|
||
async def call(self, method: str, **params) -> dict:
|
||
r = await self.client.post(f"{self.base}/{method}", json=params, timeout=30)
|
||
return r.json()
|
||
|
||
async def get_updates(self, offset: int = 0, timeout: int = 25) -> list:
|
||
r = await self.client.post(
|
||
f"{self.base}/getUpdates",
|
||
json={"offset": offset, "timeout": timeout, "allowed_updates": ["message", "callback_query"]},
|
||
timeout=timeout + 10,
|
||
)
|
||
data = r.json()
|
||
if data.get("ok"):
|
||
return data["result"]
|
||
return []
|
||
|
||
async def send_message(self, chat_id, text: str, reply_markup=None, parse_mode="HTML") -> dict:
|
||
import json
|
||
params = {"chat_id": chat_id, "text": text, "parse_mode": parse_mode}
|
||
if reply_markup:
|
||
params["reply_markup"] = json.dumps(reply_markup)
|
||
return await self.call("sendMessage", **params)
|
||
|
||
async def edit_message(self, chat_id, message_id, text: str, reply_markup=None, parse_mode="HTML"):
|
||
import json
|
||
params = {"chat_id": chat_id, "message_id": message_id, "text": text, "parse_mode": parse_mode}
|
||
if reply_markup:
|
||
params["reply_markup"] = json.dumps(reply_markup)
|
||
await self.call("editMessageText", **params)
|
||
|
||
async def answer_callback(self, callback_query_id: str, text: str = ""):
|
||
await self.call("answerCallbackQuery", callback_query_id=callback_query_id, text=text)
|
||
|
||
async def send_document(self, chat_id, filename: str, content: bytes, caption: str = ""):
|
||
files = {"document": (filename, content, "text/plain")}
|
||
data = {"chat_id": str(chat_id), "caption": caption}
|
||
r = await self.client.post(f"{self.base}/sendDocument", data=data, files=files, timeout=30)
|
||
return r.json()
|
||
|
||
|
||
async def _set_default_commands(api: TelegramAPI):
|
||
await api.call(
|
||
"setMyCommands",
|
||
commands=[
|
||
{"command": "start", "description": "Open bot menu"},
|
||
{"command": "connections", "description": "Show my connections"},
|
||
{"command": "connect", "description": "Create a new connection"},
|
||
{"command": "disconnect", "description": "Delete a connection"},
|
||
],
|
||
)
|
||
|
||
|
||
# ----------------------------------------------------------------------- #
|
||
# Generic helpers
|
||
# ----------------------------------------------------------------------- #
|
||
def _e(value) -> str:
|
||
return html.escape(str(value if value is not None else ""))
|
||
|
||
|
||
|
||
|
||
def _tg_lang(from_user: Optional[dict]) -> str:
|
||
code = str((from_user or {}).get("language_code") or "").lower()
|
||
if code.startswith("ru"):
|
||
return "ru"
|
||
return "en"
|
||
|
||
|
||
def _tt(lang: str, key: str, **kwargs) -> str:
|
||
text = TG_TRANSLATIONS.get(lang, TG_TRANSLATIONS["en"]).get(key, TG_TRANSLATIONS["en"].get(key, key))
|
||
return text.format(**kwargs) if kwargs else text
|
||
|
||
|
||
def _format_bytes(value) -> str:
|
||
try:
|
||
value = float(value or 0)
|
||
except Exception:
|
||
value = 0
|
||
units = ["B", "KB", "MB", "GB", "TB"]
|
||
idx = 0
|
||
while value >= 1024 and idx < len(units) - 1:
|
||
value /= 1024
|
||
idx += 1
|
||
if idx == 0:
|
||
return f"{int(value)} {units[idx]}"
|
||
return f"{value:.2f} {units[idx]}"
|
||
|
||
|
||
def _protocol_display_name(protocol: str) -> str:
|
||
base = protocol_base(protocol)
|
||
if base in SELF_SERVICE_PROTOCOLS:
|
||
return self_service_protocol_display_name(protocol)
|
||
name = _EXTRA_PROTOCOL_DISPLAY_NAMES.get(base, base)
|
||
idx = protocol_instance(protocol)
|
||
return name if idx <= 1 else f"{name} #{idx}"
|
||
|
||
|
||
def _find_user(load_data_fn: Callable, tg_id: str, username: Optional[str] = None):
|
||
data = load_data_fn()
|
||
tg_id_clean = str(tg_id).lstrip("@")
|
||
for u in data.get("users", []):
|
||
stored = str(u.get("telegramId", "") or "").lstrip("@")
|
||
if stored and stored == tg_id_clean:
|
||
return u
|
||
return None
|
||
|
||
|
||
def _pending_key(chat_id, from_id) -> str:
|
||
return f"{chat_id}:{from_id}"
|
||
|
||
|
||
def _is_private_chat(chat: Optional[dict]) -> bool:
|
||
return (chat or {}).get("type", "private") == "private"
|
||
|
||
|
||
async def _reject_non_private(api: TelegramAPI, chat_id, lang: str = "en", callback_id: Optional[str] = None):
|
||
if callback_id:
|
||
await api.answer_callback(callback_id, "Use a private chat")
|
||
await api.send_message(chat_id, "For security, use this command in a private chat with the bot.")
|
||
|
||
|
||
def _is_admin(panel_user: dict) -> bool:
|
||
return str((panel_user or {}).get("role", "")).lower() == "admin"
|
||
|
||
|
||
def _ref(action: str, payload: dict) -> str:
|
||
"""Short callback_data indirection; Telegram callback_data is limited to 64 bytes."""
|
||
key = uuid.uuid4().hex[:12]
|
||
_callback_refs[key] = {"action": action, "payload": payload, "ts": time.time()}
|
||
# Opportunistic cleanup.
|
||
if len(_callback_refs) > 500:
|
||
cutoff = time.time() - 6 * 3600
|
||
for k in [k for k, v in _callback_refs.items() if v.get("ts", 0) < cutoff]:
|
||
_callback_refs.pop(k, None)
|
||
return f"r:{key}"
|
||
|
||
|
||
def _resolve_ref(data_str: str):
|
||
if not data_str.startswith("r:"):
|
||
return None
|
||
return _callback_refs.get(data_str[2:])
|
||
|
||
|
||
# ----------------------------------------------------------------------- #
|
||
# Self-service helpers
|
||
# ----------------------------------------------------------------------- #
|
||
def _self_service_settings(data: dict) -> dict:
|
||
return (data.get("settings") or {}).get("self_service") or {}
|
||
|
||
|
||
def _self_service_is_enabled(data: dict) -> bool:
|
||
"""Returns True if self-service is enabled globally in panel settings."""
|
||
return bool(_self_service_settings(data).get("enabled", False))
|
||
|
||
|
||
def _self_service_telegram_enabled(data: dict) -> bool:
|
||
"""Returns True if self-service is enabled and Telegram channel is active."""
|
||
ss = _self_service_settings(data)
|
||
return bool(ss.get("enabled", False) and ss.get("telegram_enabled", False))
|
||
|
||
|
||
def _get_eligible_servers(data: dict, allowed_protocols=None) -> list:
|
||
"""Returns list of (server_id, server, available_protos) tuples for self-service."""
|
||
if allowed_protocols is None:
|
||
allowed_protocols = _self_service_settings(data).get("allowed_protocols", [])
|
||
allowed_bases = sanitize_allowed_protocols(allowed_protocols)
|
||
servers = data.get("servers", [])
|
||
eligible = []
|
||
for sid, server in enumerate(servers):
|
||
if not server.get("self_service_enabled", False):
|
||
continue
|
||
available = server_self_service_protocols(server, allowed_bases)
|
||
if available:
|
||
eligible.append((sid, server, available))
|
||
return eligible
|
||
|
||
|
||
def _user_create_server_keyboard(eligible, lang: str = "en") -> dict:
|
||
rows = []
|
||
for sid, server, protos in eligible:
|
||
name = server.get("name") or server.get("host") or f"Server {sid + 1}"
|
||
proto_text = ", ".join(self_service_protocol_display_name(p) for p in protos)
|
||
rows.append([{"text": f"🖥 {name} ({proto_text})", "callback_data": _ref("user_create_server", {"sid": sid})}])
|
||
rows.append([{"text": f"❌ {_tt(lang, 'btn_cancel')}", "callback_data": "user_create_cancel"}])
|
||
return {"inline_keyboard": rows}
|
||
|
||
|
||
def _build_connections_keyboard(conns: list, data: dict, lang: str = "en") -> dict:
|
||
"""Build inline keyboard where each button = one connection.
|
||
When self-service is enabled, adds delete buttons for self-service connections
|
||
and a 'Create connection' button."""
|
||
rows = []
|
||
servers = data.get("servers", [])
|
||
ss_enabled = _self_service_telegram_enabled(data)
|
||
for c in conns:
|
||
sid = c.get("server_id", 0)
|
||
server_name = "Unknown"
|
||
if isinstance(sid, int) and sid < len(servers):
|
||
srv = servers[sid]
|
||
server_name = srv.get("name") or srv.get("host", "Unknown")[:20]
|
||
proto = c.get("protocol", "").upper()
|
||
name = c.get("name", "Connection")
|
||
label = f"🔐 {name} · {proto} · {server_name}"
|
||
row = [{"text": label, "callback_data": f"cfg:{c['id']}"}]
|
||
if ss_enabled and c.get("created_by") == "self_service":
|
||
row.append({"text": "🗑", "callback_data": _ref("user_delete", {"conn_id": c["id"], "name": name})})
|
||
rows.append(row)
|
||
if ss_enabled:
|
||
rows.append([{"text": f"➕ {_tt(lang, 'btn_create_connection')}", "callback_data": "user_create"}])
|
||
rows.append([{"text": f"🔄 {_tt(lang, 'btn_refresh_list')}", "callback_data": "refresh"}])
|
||
return {"inline_keyboard": rows}
|
||
|
||
|
||
def _connection_lookup(data: dict, server_id: int, proto: str) -> dict:
|
||
return {
|
||
c.get("client_id"): c
|
||
for c in data.get("user_connections", [])
|
||
if c.get("server_id") == server_id and c.get("protocol") == proto and c.get("client_id")
|
||
}
|
||
|
||
|
||
def _client_display_name(client: dict, conn: Optional[dict] = None) -> str:
|
||
if conn and conn.get("name"):
|
||
return conn.get("name")
|
||
user_data = client.get("userData") or {}
|
||
return (
|
||
client.get("name")
|
||
or client.get("username")
|
||
or user_data.get("clientName")
|
||
or user_data.get("name")
|
||
or str(client.get("clientId") or client.get("client_id") or client.get("id") or "Connection")[:12]
|
||
)
|
||
|
||
|
||
def _user_label(user: dict) -> str:
|
||
label = user.get("username") or user.get("id", "user")
|
||
role = user.get("role") or "user"
|
||
suffix = f" · {role}"
|
||
if user.get("telegramId"):
|
||
suffix += f" · tg:{user.get('telegramId')}"
|
||
if user.get("enabled") is False:
|
||
suffix += " · disabled"
|
||
return f"{label}{suffix}"
|
||
|
||
|
||
def _users_keyboard(data: dict, back_callback: str = "adm:menu", lang: str = "en") -> dict:
|
||
rows = []
|
||
for user in data.get("users", [])[:40]:
|
||
rows.append([{"text": f" {_user_label(user)}", "callback_data": _ref("user", {"uid": user.get("id")})}])
|
||
rows.append([{"text": f"⬅️ {_tt(lang, 'btn_back')}", "callback_data": back_callback}])
|
||
return {"inline_keyboard": rows}
|
||
|
||
|
||
def _assign_user_keyboard(data: dict, server_id: int, proto: str, name: str, lang: str = "en") -> dict:
|
||
rows = [[{"text": f"🚫 {_tt(lang, 'btn_do_not_assign')}", "callback_data": _ref("create_client", {"sid": server_id, "proto": proto, "name": name, "user_id": None})}]]
|
||
for user in data.get("users", [])[:40]:
|
||
rows.append([{"text": f" {_user_label(user)}", "callback_data": _ref("create_client", {"sid": server_id, "proto": proto, "name": name, "user_id": user.get("id")})}])
|
||
rows.append([{"text": f"❌ {_tt(lang, 'btn_cancel')}", "callback_data": _ref("clients", {"sid": server_id, "proto": proto})}])
|
||
return {"inline_keyboard": rows}
|
||
|
||
|
||
def _admin_main_keyboard(lang: str = "en") -> dict:
|
||
return {
|
||
"inline_keyboard": [
|
||
[{"text": f"🖥 {_tt(lang, 'btn_servers')}", "callback_data": "adm:servers"}],
|
||
[{"text": f"👤 {_tt(lang, 'btn_users')}", "callback_data": "adm:users"}],
|
||
[{"text": f"🔐 {_tt(lang, 'btn_my_connections')}", "callback_data": "adm:myconns"}],
|
||
[{"text": f"➕ {_tt(lang, 'btn_how_add_server')}", "callback_data": "adm:addserver_help"}],
|
||
]
|
||
}
|
||
|
||
|
||
def _server_keyboard(data: dict, lang: str = "en") -> dict:
|
||
rows = []
|
||
for sid, srv in enumerate(data.get("servers", [])):
|
||
name = srv.get("name") or srv.get("host") or f"Server {sid + 1}"
|
||
rows.append([{"text": f"🖥 {name}", "callback_data": f"srv:{sid}"}])
|
||
rows.append([{"text": f"⬅️ {_tt(lang, 'btn_admin_menu')}", "callback_data": "adm:menu"}])
|
||
return {"inline_keyboard": rows}
|
||
|
||
|
||
def _protocol_status_icon(info: dict) -> str:
|
||
if info.get("status_error"):
|
||
return "⚪"
|
||
running = info.get("container_running")
|
||
if running is True:
|
||
return "🟢"
|
||
if running is False:
|
||
return "🔴"
|
||
return "⚪"
|
||
|
||
|
||
def _protocol_status_text(info: dict, lang: str = "en") -> str:
|
||
if info.get("status_error"):
|
||
return "unknown ⚪"
|
||
running = info.get("container_running")
|
||
if running is True:
|
||
return "running 🟢"
|
||
if running is False:
|
||
return "stopped 🔴"
|
||
return "unknown ⚪"
|
||
|
||
|
||
def _protocols_keyboard(server_id: int, server: dict, lang: str = "en") -> dict:
|
||
rows = []
|
||
protocols = server.get("protocols", {}) or {}
|
||
for proto, info in protocols.items():
|
||
installed = "✅" if info.get("installed", True) else ""
|
||
rows.append([{"text": f"{installed}{_protocol_status_icon(info)} {_protocol_display_name(proto)}", "callback_data": _ref("proto", {"sid": server_id, "proto": proto})}])
|
||
if not rows:
|
||
rows.append([{"text": _tt(lang, "btn_no_installed_protocols"), "callback_data": f"noop"}])
|
||
rows.append([{"text": f"️ {_tt(lang, 'btn_servers')}", "callback_data": "adm:servers"}])
|
||
return {"inline_keyboard": rows}
|
||
|
||
|
||
def _protocol_keyboard(server_id: int, proto: str, proto_info: dict, lang: str = "en") -> dict:
|
||
base = protocol_base(proto)
|
||
rows = []
|
||
if base in SELF_SERVICE_PROTOCOLS:
|
||
rows.append([{"text": f" {_tt(lang, 'btn_connections')}", "callback_data": _ref("clients", {"sid": server_id, "proto": proto})}])
|
||
rows.append([{"text": f" {_tt(lang, 'btn_create_connection')}", "callback_data": _ref("add_client", {"sid": server_id, "proto": proto})}])
|
||
is_running = proto_info.get("container_running") is True
|
||
rows.append([{"text": f"Stop" if is_running else f"Start", "callback_data": _ref("toggle_proto", {"sid": server_id, "proto": proto, "start": not is_running})}])
|
||
rows.append([{"text": f" {_tt(lang, 'btn_protocols')}", "callback_data": f"srv:{server_id}"}])
|
||
return {"inline_keyboard": rows}
|
||
|
||
|
||
def _client_keyboard(server_id: int, proto: str, client: dict, lang: str = "en") -> dict:
|
||
client_id = client.get("clientId") or client.get("client_id") or client.get("id") or ""
|
||
enabled = client.get("enabled")
|
||
if enabled is None:
|
||
enabled = client.get("isEnabled")
|
||
enabled = bool(enabled) if enabled is not None else True
|
||
return {
|
||
"inline_keyboard": [
|
||
[{"text": f" {_tt(lang, 'btn_config')}", "callback_data": _ref("client_cfg", {"sid": server_id, "proto": proto, "client_id": client_id, "name": client.get("name") or client.get("username") or "Connection"})}],
|
||
[{"text": f"Disable" if enabled else f"Enable", "callback_data": _ref("toggle_client", {"sid": server_id, "proto": proto, "client_id": client_id, "enable": not enabled})}],
|
||
[{"text": f" {_tt(lang, 'btn_delete')}", "callback_data": _ref("remove_client", {"sid": server_id, "proto": proto, "client_id": client_id})}],
|
||
[{"text": f" {_tt(lang, 'btn_connections')}", "callback_data": _ref("clients", {"sid": server_id, "proto": proto})}],
|
||
]
|
||
}
|
||
|
||
|
||
def _get_ssh_and_manager(server: dict, proto: str):
|
||
sys.path.insert(0, os.path.dirname(__file__))
|
||
from managers.ssh_manager import SSHManager
|
||
from managers.awg_manager import AWGManager
|
||
from managers.xray_manager import XrayManager
|
||
from managers.telemt_manager import TelemtManager
|
||
from managers.wireguard_manager import WireGuardManager
|
||
from managers.dns_manager import DNSManager
|
||
from managers.socks5_manager import Socks5Manager
|
||
from managers.adguard_manager import AdguardManager
|
||
from managers.nginx_manager import NginxManager
|
||
from managers.exit_manager import ExitManager
|
||
|
||
ssh = SSHManager(
|
||
server["host"],
|
||
server.get("ssh_port", 22),
|
||
server["username"],
|
||
server.get("password", ""),
|
||
server.get("private_key", ""),
|
||
)
|
||
base = protocol_base(proto)
|
||
if base == "xray":
|
||
manager = XrayManager(ssh, proto)
|
||
elif base == "telemt":
|
||
manager = TelemtManager(ssh, proto)
|
||
elif base == "wireguard":
|
||
manager = WireGuardManager(ssh)
|
||
elif base == "dns":
|
||
manager = DNSManager(ssh)
|
||
elif base == "socks5":
|
||
manager = Socks5Manager(ssh, proto)
|
||
elif base == "adguard":
|
||
manager = AdguardManager(ssh)
|
||
elif base == "nginx":
|
||
manager = NginxManager(ssh, proto)
|
||
elif base == "exit":
|
||
manager = ExitManager(ssh, proto)
|
||
else:
|
||
manager = AWGManager(ssh)
|
||
return ssh, manager
|
||
|
||
|
||
def _manager_call(manager, method_name: str, proto: str, *args, **kwargs):
|
||
method = getattr(manager, method_name)
|
||
try:
|
||
return method(proto, *args, **kwargs)
|
||
except TypeError:
|
||
return method(*args, **kwargs)
|
||
|
||
|
||
def _refresh_server_protocol_statuses(server: dict) -> dict:
|
||
"""Refresh saved protocol metadata with live Docker status for Telegram admin views."""
|
||
protocols = server.get("protocols", {}) or {}
|
||
if not protocols:
|
||
return server
|
||
|
||
ssh = None
|
||
try:
|
||
ssh, _ = _get_ssh_and_manager(server, "awg")
|
||
ssh.connect()
|
||
for proto, info in protocols.items():
|
||
container = info.get("container_name")
|
||
if not container:
|
||
try:
|
||
_, manager = _get_ssh_and_manager(server, proto)
|
||
container = getattr(manager, "container_name", None) or getattr(manager, "CONTAINER_NAME", None)
|
||
except Exception:
|
||
container = None
|
||
if not container:
|
||
info["status_error"] = "Container name is unknown"
|
||
continue
|
||
out, err, code = ssh.run_sudo_command(
|
||
f"docker inspect -f '{{{{.State.Running}}}}' {shlex.quote(str(container))} 2>/dev/null"
|
||
)
|
||
if code == 0:
|
||
info["container_running"] = out.strip().lower() == "true"
|
||
info["container_exists"] = True
|
||
info.pop("status_error", None)
|
||
else:
|
||
info["container_running"] = False
|
||
info["container_exists"] = False
|
||
info["status_error"] = (err or out or "container not found").strip()
|
||
except Exception as e:
|
||
logger.warning("Telegram bot: failed to refresh protocol statuses for %s: %s", server.get("name") or server.get("host"), e)
|
||
for info in protocols.values():
|
||
info["status_error"] = str(e)
|
||
finally:
|
||
if ssh:
|
||
try:
|
||
ssh.disconnect()
|
||
except Exception:
|
||
pass
|
||
return server
|
||
|
||
|
||
async def _refresh_server_protocol_statuses_async(server: dict) -> dict:
|
||
return await asyncio.to_thread(_refresh_server_protocol_statuses, server)
|
||
|
||
|
||
# ----------------------------------------------------------------------- #
|
||
# /start and user connection handlers
|
||
# ----------------------------------------------------------------------- #
|
||
async def _handle_start(api: TelegramAPI, msg: dict, load_data_fn: Callable):
|
||
chat_id = msg["chat"]["id"]
|
||
tg_id = str(msg["from"]["id"])
|
||
tg_username = msg["from"].get("username")
|
||
first_name = msg["from"].get("first_name", "")
|
||
lang = _tg_lang(msg["from"])
|
||
|
||
panel_user = _find_user(load_data_fn, tg_id, tg_username)
|
||
|
||
if not panel_user:
|
||
await api.send_message(
|
||
chat_id,
|
||
f"👋 {_tt(lang, 'hi')}, <b>{_e(first_name)}</b>!\n\n"
|
||
f"{_tt(lang, 'account_not_linked')}\n"
|
||
f"{_tt(lang, 'contact_admin_to_link')}\n\n"
|
||
f"{_tt(lang, 'your_telegram_id')}: <code>{_e(tg_id)}</code>",
|
||
)
|
||
return
|
||
|
||
if _is_admin(panel_user):
|
||
await api.send_message(
|
||
chat_id,
|
||
f"👋 {_tt(lang, 'hi')}, <b>{_e(first_name)}</b>!\n\n"
|
||
f"{_tt(lang, 'registered_admin', username=_e(panel_user.get('username')))}\n"
|
||
f"{_tt(lang, 'choose_action')}",
|
||
reply_markup=_admin_main_keyboard(lang),
|
||
)
|
||
return
|
||
|
||
await _send_user_connections(api, chat_id, panel_user, load_data_fn, first_name=first_name, lang=lang)
|
||
|
||
|
||
async def _send_user_connections(api: TelegramAPI, chat_id: int, panel_user: dict, load_data_fn: Callable, first_name: str = "", lang: str = "en"):
|
||
data = load_data_fn()
|
||
conns = [c for c in data.get("user_connections", []) if c.get("user_id") == panel_user.get("id")]
|
||
|
||
if not conns:
|
||
greeting = f"👋 {_tt(lang, 'hi')}, <b>{_e(first_name)}</b>!\n\n" if first_name else ""
|
||
if _self_service_telegram_enabled(data):
|
||
kb = _build_connections_keyboard(conns, data, lang)
|
||
await api.send_message(
|
||
chat_id,
|
||
greeting + f"{_tt(lang, 'registered_as', username=_e(panel_user.get('username')))}.\n\n"
|
||
f"{_tt(lang, 'no_connections_create')}",
|
||
reply_markup=kb,
|
||
)
|
||
else:
|
||
await api.send_message(
|
||
chat_id,
|
||
greeting + f"{_tt(lang, 'registered_as', username=_e(panel_user.get('username')))}.\n\n"
|
||
f"{_tt(lang, 'no_connections_contact_admin')}",
|
||
)
|
||
return
|
||
|
||
kb = _build_connections_keyboard(conns, data, lang)
|
||
greeting = f"👋 {_tt(lang, 'hi')}, <b>{_e(first_name)}</b>!\n\n" if first_name else ""
|
||
await api.send_message(
|
||
chat_id,
|
||
greeting + f"{_tt(lang, 'registered_as', username=_e(panel_user.get('username')))}.\n\n"
|
||
f"{_tt(lang, 'your_connections', count=len(conns))}",
|
||
reply_markup=kb,
|
||
)
|
||
|
||
|
||
async def _handle_refresh(api: TelegramAPI, chat_id: int, message_id: int, callback_id: str, tg_id: str, tg_username: Optional[str], load_data_fn: Callable, lang: str = "en"):
|
||
await api.answer_callback(callback_id, _tt(lang, "updated"))
|
||
panel_user = _find_user(load_data_fn, tg_id, tg_username)
|
||
if not panel_user:
|
||
await api.edit_message(chat_id, message_id, f"❌ {_tt(lang, 'access_denied')}")
|
||
return
|
||
data = load_data_fn()
|
||
conns = [c for c in data.get("user_connections", []) if c.get("user_id") == panel_user.get("id")]
|
||
if not conns:
|
||
if _self_service_telegram_enabled(data):
|
||
kb = _build_connections_keyboard(conns, data, lang)
|
||
await api.edit_message(chat_id, message_id, _tt(lang, "no_connections_create_short"), reply_markup=kb)
|
||
else:
|
||
await api.edit_message(chat_id, message_id, _tt(lang, "no_connections"))
|
||
return
|
||
kb = _build_connections_keyboard(conns, data, lang)
|
||
await api.edit_message(chat_id, message_id, _tt(lang, "your_connections", count=len(conns)), reply_markup=kb)
|
||
|
||
|
||
async def _handle_get_config(api: TelegramAPI, chat_id: int, message_id: int, callback_id: str, conn_id: str, tg_id: str, tg_username: Optional[str], load_data_fn: Callable, generate_vpn_link_fn: Callable, lang: str = "en"):
|
||
await api.answer_callback(callback_id, _tt(lang, "fetching_config"))
|
||
|
||
panel_user = _find_user(load_data_fn, tg_id, tg_username)
|
||
if not panel_user:
|
||
await api.send_message(chat_id, f"❌ {_tt(lang, 'access_denied')}")
|
||
return
|
||
|
||
data = load_data_fn()
|
||
conn = next((c for c in data.get("user_connections", []) if c.get("id") == conn_id and (_is_admin(panel_user) or c.get("user_id") == panel_user.get("id"))), None)
|
||
if not conn:
|
||
await api.send_message(chat_id, f"❌ {_tt(lang, 'connection_not_found')}")
|
||
return
|
||
|
||
servers = data.get("servers", [])
|
||
sid = conn.get("server_id")
|
||
if not isinstance(sid, int) or sid >= len(servers):
|
||
await api.send_message(chat_id, f"❌ {_tt(lang, 'server_not_found')}")
|
||
return
|
||
|
||
await _send_config_by_client(api, chat_id, servers[sid], conn.get("protocol", "awg"), conn.get("client_id"), conn.get("name", "Connection"), generate_vpn_link_fn, lang)
|
||
|
||
|
||
async def _send_config_by_client(api: TelegramAPI, chat_id: int, server: dict, proto: str, client_id: str, conn_name: str, generate_vpn_link_fn: Callable, lang: str = "en"):
|
||
loading_result = await api.send_message(chat_id, _tt(lang, "fetching_config_for", name=_e(conn_name)))
|
||
loading_msg_id = loading_result.get("result", {}).get("message_id")
|
||
try:
|
||
proto_info = server.get("protocols", {}).get(proto, {})
|
||
port = proto_info.get("port", "55424")
|
||
|
||
def _get_cfg():
|
||
ssh, manager = _get_ssh_and_manager(server, proto)
|
||
try:
|
||
ssh.connect()
|
||
return _manager_call(manager, "get_client_config", proto, client_id, server["host"], port)
|
||
finally:
|
||
ssh.disconnect()
|
||
|
||
config = await asyncio.to_thread(_get_cfg)
|
||
if not config:
|
||
if loading_msg_id:
|
||
await api.edit_message(chat_id, loading_msg_id, f"❌ {_tt(lang, 'failed_retrieve_config')}")
|
||
return
|
||
|
||
if loading_msg_id:
|
||
await api.call("deleteMessage", chat_id=chat_id, message_id=loading_msg_id)
|
||
|
||
server_name = server.get("name") or server.get("host", "Unknown")
|
||
await api.send_message(chat_id, f"✅ <b>{_e(conn_name)}</b>\n🌐 {_tt(lang, 'servers_title')}: <b>{_e(server_name)}</b>\n🔌 {_tt(lang, 'protocol_label')}: <b>{_e(proto.upper())}</b>")
|
||
|
||
is_link_proto = protocol_base(proto) in ("xray", "telemt")
|
||
if is_link_proto:
|
||
await api.send_message(chat_id, f"🔗 <b>{_tt(lang, 'connection_link_label')}</b> (tap to copy):\n<code>{_e(config)}</code>")
|
||
else:
|
||
MAX_LEN = 4000
|
||
if len(config) <= MAX_LEN:
|
||
await api.send_message(chat_id, f"<b>📄 {_tt(lang, 'config_label')}:</b>\n<pre>{_e(config)}</pre>")
|
||
else:
|
||
chunks = [config[i:i + MAX_LEN] for i in range(0, len(config), MAX_LEN)]
|
||
for i, chunk in enumerate(chunks, 1):
|
||
await api.send_message(chat_id, f"<b>📄 {_tt(lang, 'config_part', part=i, total=len(chunks))}:</b>\n<pre>{_e(chunk)}</pre>")
|
||
|
||
vpn_link = generate_vpn_link_fn(config, server, proto) if config else ""
|
||
if vpn_link:
|
||
await api.send_message(chat_id, f"🔗 <b>{_tt(lang, 'vpn_link_label')}</b> (tap to copy):\n<code>{_e(vpn_link)}</code>")
|
||
filename = f"{str(conn_name).replace(' ', '_')}.conf"
|
||
await api.send_document(chat_id, filename=filename, content=config.encode("utf-8"), caption=f"📁 {_tt(lang, 'config_file_label')}: {conn_name}")
|
||
except Exception as e:
|
||
logger.exception("Bot: error getting config")
|
||
if loading_msg_id:
|
||
await api.edit_message(chat_id, loading_msg_id, f"❌ {_tt(lang, 'error')}: {_e(e)}")
|
||
else:
|
||
await api.send_message(chat_id, f"❌ {_tt(lang, 'error')}: {_e(e)}")
|
||
|
||
|
||
# ----------------------------------------------------------------------- #
|
||
# Admin handlers
|
||
# ----------------------------------------------------------------------- #
|
||
def _require_admin(load_data_fn: Callable, tg_id: str, tg_username: Optional[str] = None):
|
||
user = _find_user(load_data_fn, tg_id, tg_username)
|
||
if not user or not _is_admin(user):
|
||
return None
|
||
return user
|
||
|
||
|
||
async def _handle_add_server_command(api: TelegramAPI, msg: dict, load_data_fn: Callable, save_data_fn: Optional[Callable], lang: str = "en"):
|
||
chat_id = msg["chat"]["id"]
|
||
tg_id = str(msg["from"]["id"])
|
||
if not _require_admin(load_data_fn, tg_id):
|
||
await api.send_message(chat_id, f"❌ {_tt(lang, 'access_denied')}")
|
||
return
|
||
if not save_data_fn:
|
||
await api.send_message(chat_id, f"❌ {_tt(lang, 'saving_not_available')}")
|
||
return
|
||
|
||
text = msg.get("text", "")
|
||
parts = text.split(maxsplit=5)
|
||
if len(parts) < 4:
|
||
await api.send_message(
|
||
chat_id,
|
||
_tt(lang, "add_server_command"),
|
||
)
|
||
return
|
||
|
||
host = parts[1]
|
||
username = parts[2]
|
||
password = parts[3]
|
||
ssh_port = 22
|
||
name = host
|
||
if len(parts) >= 5:
|
||
try:
|
||
ssh_port = int(parts[4])
|
||
except Exception:
|
||
name = parts[4]
|
||
if len(parts) >= 6:
|
||
name = parts[5] or host
|
||
|
||
data = load_data_fn()
|
||
data.setdefault("servers", []).append({
|
||
"name": name,
|
||
"host": host,
|
||
"ssh_port": ssh_port,
|
||
"username": username,
|
||
"password": password,
|
||
"private_key": "",
|
||
"protocols": {},
|
||
"created_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||
})
|
||
save_data_fn(data)
|
||
await api.send_message(chat_id, _tt(lang, "server_added", name=_e(name), host=_e(host)))
|
||
|
||
|
||
async def _admin_servers(api: TelegramAPI, chat_id: int, message_id: Optional[int], load_data_fn: Callable, lang: str = "en"):
|
||
data = load_data_fn()
|
||
servers = data.get("servers", [])
|
||
text = f"🖥 <b>{_tt(lang, 'servers_title')}</b> ({len(servers)})\n\n{_tt(lang, 'choose_a_server')}"
|
||
if message_id:
|
||
await api.edit_message(chat_id, message_id, text, reply_markup=_server_keyboard(data, lang))
|
||
else:
|
||
await api.send_message(chat_id, text, reply_markup=_server_keyboard(data, lang))
|
||
|
||
|
||
async def _admin_users(api: TelegramAPI, chat_id: int, message_id: int, load_data_fn: Callable, lang: str = "en"):
|
||
data = load_data_fn()
|
||
users = data.get("users", [])
|
||
await api.edit_message(chat_id, message_id, f"👤 <b>{_tt(lang, 'users_title')}</b> ({len(users)})\n\n{_tt(lang, 'choose_a_user')}", reply_markup=_users_keyboard(data, lang))
|
||
|
||
|
||
async def _admin_user_detail(api: TelegramAPI, chat_id: int, message_id: int, user_id: str, load_data_fn: Callable, lang: str = "en"):
|
||
data = load_data_fn()
|
||
user = next((u for u in data.get("users", []) if u.get("id") == user_id), None)
|
||
if not user:
|
||
await api.edit_message(chat_id, message_id, "❌ User not found.", reply_markup={"inline_keyboard": [[{"text": "⬅️ Users", "callback_data": "adm:users"}]]})
|
||
return
|
||
conns = [c for c in data.get("user_connections", []) if c.get("user_id") == user_id]
|
||
lines = [
|
||
f"👤 <b>{_e(user.get('username'))}</b>",
|
||
f"{_tt(lang, 'role_label')}: <b>{_e(user.get('role', 'user'))}</b>",
|
||
f"{_tt(lang, 'enabled_status')}: <b>{'yes ✅' if user.get('enabled', True) else 'no 🚫'}</b>",
|
||
f"{_tt(lang, 'tg_id_label')}: <code>{_e(user.get('telegramId') or '-')}</code>",
|
||
f"{_tt(lang, 'email_label')}: <code>{_e(user.get('email') or '-')}</code>",
|
||
f"{_tt(lang, 'connections_count')}: <b>{len(conns)}</b>",
|
||
]
|
||
if user.get("description"):
|
||
lines.append(f"{_tt(lang, 'description_label')}: {_e(user.get('description'))}")
|
||
rows = []
|
||
servers = data.get("servers", [])
|
||
for c in conns[:20]:
|
||
sid = c.get("server_id")
|
||
server_name = "Unknown"
|
||
if isinstance(sid, int) and sid < len(servers):
|
||
server_name = servers[sid].get("name") or servers[sid].get("host") or "Unknown"
|
||
rows.append([{"text": f"🔐 {c.get('name', 'Connection')} · {c.get('protocol', '').upper()} · {server_name}", "callback_data": f"cfg:{c.get('id')}"}])
|
||
rows.append([{"text": f"⬅️ {_tt(lang, 'btn_users')}", "callback_data": "adm:users"}])
|
||
rows.append([{"text": f"⬅️ {_tt(lang, 'btn_admin_menu')}", "callback_data": "adm:menu"}])
|
||
await api.edit_message(chat_id, message_id, "\n".join(lines), reply_markup={"inline_keyboard": rows})
|
||
|
||
|
||
async def _admin_server_detail(api: TelegramAPI, chat_id: int, message_id: int, server_id: int, load_data_fn: Callable, lang: str = "en"):
|
||
data = load_data_fn()
|
||
servers = data.get("servers", [])
|
||
if server_id < 0 or server_id >= len(servers):
|
||
await api.edit_message(chat_id, message_id, f"❌ {_tt(lang, 'server_not_found')}")
|
||
return
|
||
server = await _refresh_server_protocol_statuses_async(servers[server_id])
|
||
protocols = server.get("protocols", {}) or {}
|
||
text = (
|
||
f"🖥 <b>{_e(server.get('name') or server.get('host'))}</b>\n"
|
||
f"Host: <code>{_e(server.get('host'))}</code>\n"
|
||
f"SSH: <code>{_e(server.get('username'))}@{_e(server.get('host'))}:{_e(server.get('ssh_port', 22))}</code>\n\n"
|
||
f"<b>{_tt(lang, 'btn_protocols')}</b> ({len(protocols)}):"
|
||
)
|
||
await api.edit_message(chat_id, message_id, text, reply_markup=_protocols_keyboard(server_id, server, lang))
|
||
|
||
|
||
async def _admin_protocol_detail(api: TelegramAPI, chat_id: int, message_id: int, server_id: int, proto: str, load_data_fn: Callable, lang: str = "en"):
|
||
data = load_data_fn()
|
||
servers = data.get("servers", [])
|
||
if server_id < 0 or server_id >= len(servers):
|
||
await api.edit_message(chat_id, message_id, f"❌ {_tt(lang, 'server_not_found')}")
|
||
return
|
||
server = await _refresh_server_protocol_statuses_async(servers[server_id])
|
||
info = (server.get("protocols", {}) or {}).get(proto)
|
||
if not info:
|
||
await api.edit_message(chat_id, message_id, f"❌ {_tt(lang, 'protocol_not_found')}")
|
||
return
|
||
lines = [
|
||
f" <b>{_e(_protocol_display_name(proto))}</b>",
|
||
f"{_tt(lang, 'servers_title')}: <b>{_e(server.get('name') or server.get('host'))}</b>",
|
||
f"{_tt(lang, 'protocol_status')}: <b>{_protocol_status_text(info, lang)}</b>",
|
||
]
|
||
exit_link = info.get("exit_link") or {}
|
||
if exit_link:
|
||
lines.append(f"exit: <code>{_e(exit_link.get('exit_name'))}</code>"
|
||
+ (f" ({_e(exit_link.get('stale'))})" if exit_link.get("stale") else ""))
|
||
for key in ("port", "container_name", "domain", "site_url", "web_port", "mode", "subnet", "peers_count"):
|
||
if info.get(key) not in (None, ""):
|
||
lines.append(f"{_e(key)}: <code>{_e(info.get(key))}</code>")
|
||
if info.get("status_error"):
|
||
lines.append(f"status_error: <code>{_e(info.get('status_error'))}</code>")
|
||
await api.edit_message(chat_id, message_id, "\n".join(lines), reply_markup=_protocol_keyboard(server_id, proto, info, lang))
|
||
|
||
|
||
async def _admin_toggle_protocol(api: TelegramAPI, chat_id: int, message_id: int, server_id: int, proto: str, start: bool, load_data_fn: Callable, lang: str = "en"):
|
||
await api.edit_message(chat_id, message_id, f"⏳ {_tt(lang, 'updating_protocol')}")
|
||
|
||
def _toggle():
|
||
data = load_data_fn()
|
||
server = data["servers"][server_id]
|
||
ssh, manager = _get_ssh_and_manager(server, proto)
|
||
try:
|
||
ssh.connect()
|
||
container = (server.get("protocols", {}).get(proto, {}) or {}).get("container_name")
|
||
if not container:
|
||
# fallback: most managers expose CONTAINER_NAME for base/first instances
|
||
container = getattr(manager, "CONTAINER_NAME", None)
|
||
if not container:
|
||
raise RuntimeError("Container name is unknown")
|
||
action = "start" if start else "stop"
|
||
out, err, code = ssh.run_sudo_command(f"docker {action} {container}")
|
||
if code != 0:
|
||
raise RuntimeError(err or out or f"docker {action} failed")
|
||
return data
|
||
finally:
|
||
ssh.disconnect()
|
||
|
||
try:
|
||
await asyncio.to_thread(_toggle)
|
||
await _admin_protocol_detail(api, chat_id, message_id, server_id, proto, load_data_fn, lang)
|
||
except Exception as e:
|
||
logger.exception("Bot admin: protocol toggle failed")
|
||
await api.edit_message(chat_id, message_id, f"❌ {_tt(lang, 'error')}: {_e(e)}", reply_markup={"inline_keyboard": [[{"text": f"⬅️ {_tt(lang, 'btn_protocol')}", "callback_data": _ref("proto", {"sid": server_id, "proto": proto})}]]})
|
||
|
||
|
||
async def _admin_clients(api: TelegramAPI, chat_id: int, message_id: int, server_id: int, proto: str, load_data_fn: Callable, lang: str = "en"):
|
||
await api.edit_message(chat_id, message_id, f"⏳ {_tt(lang, 'loading_connections')}")
|
||
|
||
def _load_clients():
|
||
data = load_data_fn()
|
||
server = data["servers"][server_id]
|
||
ssh, manager = _get_ssh_and_manager(server, proto)
|
||
try:
|
||
ssh.connect()
|
||
return data, _manager_call(manager, "get_clients", proto)
|
||
finally:
|
||
ssh.disconnect()
|
||
|
||
try:
|
||
data, clients = await asyncio.to_thread(_load_clients)
|
||
if not clients:
|
||
await api.edit_message(chat_id, message_id, f"👥 {_tt(lang, 'no_connections_short')}", reply_markup={"inline_keyboard": [[{"text": f"➕ {_tt(lang, 'btn_create_connection')}", "callback_data": _ref("add_client", {"sid": server_id, "proto": proto})}], [{"text": f"⬅️ {_tt(lang, 'btn_protocol')}", "callback_data": _ref("proto", {"sid": server_id, "proto": proto})}]]})
|
||
return
|
||
rows = []
|
||
conn_by_client = _connection_lookup(data, server_id, proto)
|
||
users_by_id = {u.get("id"): u for u in data.get("users", [])}
|
||
for c in clients[:40]:
|
||
client_id = c.get("clientId") or c.get("client_id") or c.get("id") or ""
|
||
conn = conn_by_client.get(client_id)
|
||
name = _client_display_name(c, conn)
|
||
traffic = ""
|
||
user_data = c.get("userData") or {}
|
||
if user_data:
|
||
total = (user_data.get("dataReceivedBytes") or 0) + (user_data.get("dataSentBytes") or 0)
|
||
traffic = f" · {_format_bytes(total)}"
|
||
assigned = ""
|
||
if conn and conn.get("user_id") in users_by_id:
|
||
assigned = f" · @{users_by_id[conn.get('user_id')].get('username')}"
|
||
c["name"] = name
|
||
c["assigned_user_id"] = conn.get("user_id") if conn else None
|
||
rows.append([{"text": f" {name}{assigned}{traffic}", "callback_data": _ref("client", {"sid": server_id, "proto": proto, "client_id": client_id, "name": name, "client": c})}])
|
||
rows.append([{"text": f"➕ {_tt(lang, 'btn_create_connection')}", "callback_data": _ref("add_client", {"sid": server_id, "proto": proto})}])
|
||
rows.append([{"text": f"⬅️ {_tt(lang, 'btn_protocol')}", "callback_data": _ref("proto", {"sid": server_id, "proto": proto})}])
|
||
await api.edit_message(chat_id, message_id, f"👥 <b>{_e(_protocol_display_name(proto))} {_tt(lang, 'connections_count')}</b> ({len(clients)})", reply_markup={"inline_keyboard": rows})
|
||
except Exception as e:
|
||
logger.exception("Bot admin: load clients failed")
|
||
await api.edit_message(chat_id, message_id, f"❌ {_tt(lang, 'error')}: {_e(e)}", reply_markup={"inline_keyboard": [[{"text": f"️ {_tt(lang, 'btn_protocol')}", "callback_data": _ref("proto", {"sid": server_id, "proto": proto})}]]})
|
||
|
||
|
||
async def _admin_client_detail(api: TelegramAPI, chat_id: int, message_id: int, server_id: int, proto: str, client: dict, lang: str = "en"):
|
||
client_id = client.get("clientId") or client.get("client_id") or client.get("id") or ""
|
||
name = _client_display_name(client)
|
||
user_data = client.get("userData") or {}
|
||
rx = user_data.get("dataReceivedBytes") or 0
|
||
tx = user_data.get("dataSentBytes") or 0
|
||
enabled = client.get("enabled")
|
||
if enabled is None:
|
||
enabled = client.get("isEnabled")
|
||
enabled_text = f"{_tt(lang, 'enabled_label')} ✅" if (enabled is None or enabled) else f"{_tt(lang, 'disabled_label')} 🚫"
|
||
text = (
|
||
f"👤 <b>{_e(name)}</b>\n"
|
||
f"{_tt(lang, 'protocol_label')}: <b>{_e(_protocol_display_name(proto))}</b>\n"
|
||
f"Client ID: <code>{_e(client_id)}</code>\n"
|
||
f"{_tt(lang, 'client_status')}: <b>{enabled_text}</b>"
|
||
)
|
||
if user_data:
|
||
text += f"\n{_tt(lang, 'traffic_label')}: <b>{_format_bytes(rx + tx)}</b>\n{_tt(lang, 'rx_label')}: {_format_bytes(rx)} · {_tt(lang, 'tx_label')}: {_format_bytes(tx)}"
|
||
await api.edit_message(chat_id, message_id, text, reply_markup=_client_keyboard(server_id, proto, client, lang))
|
||
|
||
|
||
async def _admin_add_client(api: TelegramAPI, chat_id: int, message_id: int, server_id: int, proto: str, panel_user: dict, load_data_fn: Callable, save_data_fn: Optional[Callable], generate_vpn_link_fn: Callable, lang: str = "en", tg_id: Optional[str] = None):
|
||
if not save_data_fn:
|
||
await api.edit_message(chat_id, message_id, f"❌ {_tt(lang, 'saving_not_available')}")
|
||
return
|
||
_pending_inputs[_pending_key(chat_id, tg_id or panel_user.get('telegramId') or panel_user.get('id'))] = {
|
||
"kind": "add_client_name",
|
||
"sid": server_id,
|
||
"proto": proto,
|
||
"admin_user_id": panel_user.get("id"),
|
||
"ts": time.time(),
|
||
}
|
||
await api.edit_message(
|
||
chat_id,
|
||
message_id,
|
||
f"➕ <b>{_tt(lang, 'create_new_connection')}</b>\n\n"
|
||
f"{_tt(lang, 'servers_title')}/{_tt(lang, 'protocol_label')}: <b>{_e(_protocol_display_name(proto))}</b>\n\n"
|
||
f"{_tt(lang, 'send_connection_name')}",
|
||
reply_markup={"inline_keyboard": [[{"text": f"❌ {_tt(lang, 'btn_cancel')}", "callback_data": _ref("clients", {"sid": server_id, "proto": proto})}]]},
|
||
)
|
||
|
||
|
||
async def _admin_choose_client_user(api: TelegramAPI, chat_id: int, name: str, server_id: int, proto: str, load_data_fn: Callable, lang: str = "en"):
|
||
data = load_data_fn()
|
||
await api.send_message(
|
||
chat_id,
|
||
f"✅ {_tt(lang, 'connection_name_label')}: <b>{_e(name)}</b>\n\n"
|
||
f"{_tt(lang, 'assign_to_user')}",
|
||
reply_markup=_assign_user_keyboard(data, server_id, proto, name, lang),
|
||
)
|
||
|
||
|
||
async def _admin_create_client(api: TelegramAPI, chat_id: int, message_id: int, server_id: int, proto: str, name: str, user_id: Optional[str], load_data_fn: Callable, save_data_fn: Optional[Callable], generate_vpn_link_fn: Callable, lang: str = "en"):
|
||
if not save_data_fn:
|
||
await api.edit_message(chat_id, message_id, f"❌ {_tt(lang, 'saving_not_available')}")
|
||
return
|
||
await api.edit_message(chat_id, message_id, f"⏳ {_tt(lang, 'creating_connection')}")
|
||
|
||
def _create():
|
||
data = load_data_fn()
|
||
server = data["servers"][server_id]
|
||
proto_info = (server.get("protocols", {}) or {}).get(proto, {})
|
||
port = proto_info.get("port", "55424")
|
||
ssh, manager = _get_ssh_and_manager(server, proto)
|
||
try:
|
||
ssh.connect()
|
||
if protocol_base(proto) == "telemt":
|
||
result = manager.add_client(proto, name, server["host"], port)
|
||
elif protocol_base(proto) == "wireguard":
|
||
result = manager.add_client(name, server["host"])
|
||
else:
|
||
result = manager.add_client(proto, name, server["host"], port)
|
||
finally:
|
||
ssh.disconnect()
|
||
client_id = result.get("client_id") or result.get("clientId")
|
||
assigned_user = None
|
||
if user_id:
|
||
assigned_user = next((u for u in data.get("users", []) if u.get("id") == user_id), None)
|
||
if user_id and client_id:
|
||
data.setdefault("user_connections", []).append({
|
||
"id": str(uuid.uuid4()),
|
||
"user_id": user_id,
|
||
"server_id": server_id,
|
||
"protocol": proto,
|
||
"client_id": client_id,
|
||
"name": name,
|
||
"created_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||
})
|
||
save_data_fn(data)
|
||
return server, result, client_id, assigned_user
|
||
|
||
try:
|
||
server, result, client_id, assigned_user = await asyncio.to_thread(_create)
|
||
assigned_text = f"\n{_tt(lang, 'assigned_to', username=_e(assigned_user.get('username')))}" if assigned_user else f"\n{_tt(lang, 'assigned_not_linked')}"
|
||
await api.edit_message(chat_id, message_id, _tt(lang, "connection_created", name=_e(name)) + assigned_text)
|
||
config = result.get("config")
|
||
if config:
|
||
await _send_config_text(api, chat_id, server, proto, name, config, generate_vpn_link_fn, lang)
|
||
elif client_id:
|
||
await _send_config_by_client(api, chat_id, server, proto, client_id, name, generate_vpn_link_fn, lang)
|
||
except Exception as e:
|
||
logger.exception("Bot admin: add client failed")
|
||
await api.edit_message(chat_id, message_id, f" {_tt(lang, 'error')}: {_e(e)}", reply_markup={"inline_keyboard": [[{"text": f"⬅️ {_tt(lang, 'btn_protocol')}", "callback_data": _ref("proto", {"sid": server_id, "proto": proto})}]]})
|
||
|
||
|
||
async def _send_config_text(api: TelegramAPI, chat_id: int, server: dict, proto: str, conn_name: str, config: str, generate_vpn_link_fn: Callable, lang: str = "en"):
|
||
await api.send_message(chat_id, f"✅ <b>{_e(conn_name)}</b>\n🌐 {_tt(lang, 'servers_title')}: <b>{_e(server.get('name') or server.get('host'))}</b>\n {_tt(lang, 'protocol_label')}: <b>{_e(proto.upper())}</b>")
|
||
if protocol_base(proto) in ("xray", "telemt"):
|
||
await api.send_message(chat_id, f"🔗 <b>{_tt(lang, 'connection_link_label')}</b>:\n<code>{_e(config)}</code>")
|
||
else:
|
||
await api.send_message(chat_id, f"<b>📄 Configuration:</b>\n<pre>{_e(config)}</pre>")
|
||
vpn_link = generate_vpn_link_fn(config, server, proto) if config else ""
|
||
if vpn_link:
|
||
await api.send_message(chat_id, f" <b>{_tt(lang, 'vpn_link_label')}</b>:\n<code>{_e(vpn_link)}</code>")
|
||
await api.send_document(chat_id, filename=f"{conn_name}.conf", content=config.encode("utf-8"), caption=f" {_tt(lang, 'config_file_label')}: {conn_name}")
|
||
|
||
|
||
async def _admin_toggle_client(api: TelegramAPI, chat_id: int, message_id: int, server_id: int, proto: str, client_id: str, enable: bool, load_data_fn: Callable, lang: str = "en"):
|
||
await api.edit_message(chat_id, message_id, f" {_tt(lang, 'updating_connection')}")
|
||
|
||
def _toggle():
|
||
data = load_data_fn()
|
||
server = data["servers"][server_id]
|
||
ssh, manager = _get_ssh_and_manager(server, proto)
|
||
try:
|
||
ssh.connect()
|
||
return _manager_call(manager, "toggle_client", proto, client_id, enable)
|
||
finally:
|
||
ssh.disconnect()
|
||
|
||
try:
|
||
await asyncio.to_thread(_toggle)
|
||
await api.edit_message(chat_id, message_id, f"✅ {_tt(lang, 'updated')}", reply_markup={"inline_keyboard": [[{"text": f"⬅️ {_tt(lang, 'btn_connections')}", "callback_data": _ref("clients", {"sid": server_id, "proto": proto})}]]})
|
||
except Exception as e:
|
||
logger.exception("Bot admin: toggle client failed")
|
||
await api.edit_message(chat_id, message_id, f"❌ {_tt(lang, 'error')}: {_e(e)}")
|
||
|
||
|
||
async def _admin_remove_client(api: TelegramAPI, chat_id: int, message_id: int, server_id: int, proto: str, client_id: str, load_data_fn: Callable, save_data_fn: Optional[Callable], lang: str = "en"):
|
||
if not save_data_fn:
|
||
await api.edit_message(chat_id, message_id, f"❌ {_tt(lang, 'saving_not_available')}")
|
||
return
|
||
await api.edit_message(chat_id, message_id, f" {_tt(lang, 'removing_connection')}")
|
||
|
||
def _remove():
|
||
data = load_data_fn()
|
||
server = data["servers"][server_id]
|
||
ssh, manager = _get_ssh_and_manager(server, proto)
|
||
try:
|
||
ssh.connect()
|
||
_manager_call(manager, "remove_client", proto, client_id)
|
||
finally:
|
||
ssh.disconnect()
|
||
data["user_connections"] = [
|
||
c for c in data.get("user_connections", [])
|
||
if not (c.get("server_id") == server_id and c.get("protocol") == proto and c.get("client_id") == client_id)
|
||
]
|
||
save_data_fn(data)
|
||
|
||
try:
|
||
await asyncio.to_thread(_remove)
|
||
await api.edit_message(chat_id, message_id, f"✅ {_tt(lang, 'connection_removed')}", reply_markup={"inline_keyboard": [[{"text": f"⬅️ {_tt(lang, 'btn_connections')}", "callback_data": _ref("clients", {"sid": server_id, "proto": proto})}]]})
|
||
except Exception as e:
|
||
logger.exception("Bot admin: remove client failed")
|
||
await api.edit_message(chat_id, message_id, f" {_tt(lang, 'error')}: {_e(e)}")
|
||
|
||
|
||
async def _handle_pending_input(api: TelegramAPI, msg: dict, load_data_fn: Callable, save_data_fn: Optional[Callable], generate_vpn_link_fn: Callable, self_service_svc=None) -> bool:
|
||
chat_id = msg["chat"]["id"]
|
||
from_id = msg["from"]["id"]
|
||
lang = _tg_lang(msg.get("from"))
|
||
key = _pending_key(chat_id, from_id)
|
||
state = _pending_inputs.get(key)
|
||
if not state:
|
||
return False
|
||
|
||
text = (msg.get("text") or "").strip()
|
||
if text.lower() in ("/cancel", "cancel"):
|
||
_pending_inputs.pop(key, None)
|
||
await api.send_message(chat_id, f"❌ {_tt(lang, 'action_cancelled')}", reply_markup=_admin_main_keyboard(lang))
|
||
return True
|
||
if text.startswith("/"):
|
||
_pending_inputs.pop(key, None)
|
||
return False
|
||
|
||
if state.get("kind") == "add_client_name":
|
||
panel_user = _require_admin(load_data_fn, str(msg["from"]["id"]), msg["from"].get("username"))
|
||
if not panel_user:
|
||
_pending_inputs.pop(key, None)
|
||
await api.send_message(chat_id, f"❌ {_tt(lang, 'access_denied')}")
|
||
return True
|
||
name = text[:80].strip()
|
||
if not name:
|
||
await api.send_message(chat_id, _tt(lang, "name_empty"))
|
||
return True
|
||
_pending_inputs.pop(key, None)
|
||
await _admin_choose_client_user(api, chat_id, name, int(state.get("sid", 0)), state.get("proto", "awg"), load_data_fn, lang)
|
||
return True
|
||
|
||
if state.get("kind") == "user_add_client_name":
|
||
panel_user = _find_user(load_data_fn, str(msg["from"]["id"]), msg["from"].get("username"))
|
||
if not panel_user:
|
||
_pending_inputs.pop(key, None)
|
||
await api.send_message(chat_id, f"❌ {_tt(lang, 'access_denied')}")
|
||
return True
|
||
name = text[:80].strip()
|
||
if not name:
|
||
await api.send_message(chat_id, _tt(lang, "name_empty"))
|
||
return True
|
||
_pending_inputs.pop(key, None)
|
||
if not self_service_svc:
|
||
await api.send_message(chat_id, f"❌ {_tt(lang, 'self_service_unavailable')}")
|
||
return True
|
||
data = load_data_fn()
|
||
if not _self_service_telegram_enabled(data):
|
||
await api.send_message(chat_id, _tt(lang, "self_service_creation_disabled"))
|
||
return True
|
||
sid = int(state.get("sid", 0))
|
||
proto = state.get("proto", "awg")
|
||
await _user_create_connection(api, chat_id, panel_user, sid, proto, name, load_data_fn, generate_vpn_link_fn, self_service_svc, lang)
|
||
return True
|
||
|
||
return False
|
||
|
||
|
||
# ----------------------------------------------------------------------- #
|
||
# Self-service user create wizard
|
||
# ----------------------------------------------------------------------- #
|
||
async def _user_create_start(api: TelegramAPI, chat_id: int, message_id: int, callback_id: str, tg_id: str, tg_username: Optional[str], load_data_fn: Callable, self_service_svc, lang: str = "en"):
|
||
panel_user = _find_user(load_data_fn, tg_id, tg_username)
|
||
if not panel_user:
|
||
await api.answer_callback(callback_id, text="Access denied")
|
||
await api.edit_message(chat_id, message_id, "❌ Access denied.")
|
||
return
|
||
|
||
data = load_data_fn()
|
||
if not _self_service_telegram_enabled(data):
|
||
await api.answer_callback(callback_id)
|
||
await api.edit_message(
|
||
chat_id,
|
||
message_id,
|
||
_tt(lang, "self_service_disabled_contact"),
|
||
)
|
||
return
|
||
|
||
eligible = _get_eligible_servers(data)
|
||
if not eligible:
|
||
await api.answer_callback(callback_id)
|
||
await api.edit_message(
|
||
chat_id,
|
||
message_id,
|
||
_tt(lang, "self_service_no_servers"),
|
||
)
|
||
return
|
||
|
||
await api.answer_callback(callback_id)
|
||
await api.edit_message(
|
||
chat_id,
|
||
message_id,
|
||
f"➕ <b>{_tt(lang, 'btn_create_connection')}</b>\n\n{_tt(lang, 'choose_server')}",
|
||
reply_markup=_user_create_server_keyboard(eligible, lang),
|
||
)
|
||
|
||
|
||
async def _user_create_start_message(api: TelegramAPI, chat_id: int, tg_id: str, tg_username: Optional[str], load_data_fn: Callable, lang: str = "en"):
|
||
panel_user = _find_user(load_data_fn, tg_id, tg_username)
|
||
if not panel_user:
|
||
await api.send_message(chat_id, f"❌ {_tt(lang, 'access_denied')}")
|
||
return
|
||
|
||
data = load_data_fn()
|
||
if not _self_service_telegram_enabled(data):
|
||
await api.send_message(chat_id, _tt(lang, "self_service_disabled_contact"))
|
||
return
|
||
|
||
eligible = _get_eligible_servers(data)
|
||
if not eligible:
|
||
await api.send_message(chat_id, _tt(lang, "self_service_no_servers"))
|
||
return
|
||
|
||
await api.send_message(
|
||
chat_id,
|
||
f"➕ <b>{_tt(lang, 'btn_create_connection')}</b>\n\n{_tt(lang, 'choose_server')}",
|
||
reply_markup=_user_create_server_keyboard(eligible, lang),
|
||
)
|
||
|
||
|
||
async def _user_create_server(api: TelegramAPI, chat_id: int, message_id: int, callback_id: str, tg_id: str, tg_username: Optional[str], server_id: int, load_data_fn: Callable, self_service_svc, lang: str = "en"):
|
||
panel_user = _find_user(load_data_fn, tg_id, tg_username)
|
||
if not panel_user:
|
||
await api.answer_callback(callback_id, text="Access denied")
|
||
await api.edit_message(chat_id, message_id, f"❌ {_tt(lang, 'access_denied')}")
|
||
return
|
||
|
||
await api.answer_callback(callback_id)
|
||
|
||
data = load_data_fn()
|
||
if not _self_service_telegram_enabled(data):
|
||
await api.edit_message(chat_id, message_id, _tt(lang, "self_service_disabled_contact"))
|
||
return
|
||
|
||
servers = data.get("servers", [])
|
||
if server_id < 0 or server_id >= len(servers):
|
||
await api.edit_message(chat_id, message_id, f" {_tt(lang, 'server_not_found')}")
|
||
return
|
||
|
||
server = servers[server_id]
|
||
if not server.get("self_service_enabled", False):
|
||
await api.edit_message(chat_id, message_id, _tt(lang, "server_not_self_service"))
|
||
return
|
||
|
||
allowed_bases = sanitize_allowed_protocols(_self_service_settings(data).get("allowed_protocols", []))
|
||
available_protos = server_self_service_protocols(server, allowed_bases)
|
||
if not available_protos:
|
||
await api.edit_message(chat_id, message_id, _tt(lang, "no_protocols_available"))
|
||
return
|
||
|
||
server_name = server.get("name") or server.get("host") or f"Server {server_id + 1}"
|
||
rows = []
|
||
for proto in available_protos:
|
||
rows.append([{"text": _protocol_display_name(proto), "callback_data": _ref("user_create_protocol", {"sid": server_id, "proto": proto})}])
|
||
rows.append([{"text": f" {_tt(lang, 'btn_back')}", "callback_data": "user_create"}])
|
||
|
||
await api.edit_message(
|
||
chat_id,
|
||
message_id,
|
||
f" {_tt(lang, 'servers_title')}: <b>{_e(server_name)}</b>\n\n{_tt(lang, 'choose_protocol')}",
|
||
reply_markup={"inline_keyboard": rows},
|
||
)
|
||
|
||
|
||
async def _user_create_protocol(api: TelegramAPI, chat_id: int, message_id: int, callback_id: str, tg_id: str, tg_username: Optional[str], server_id: int, proto: str, load_data_fn: Callable, lang: str = "en"):
|
||
panel_user = _find_user(load_data_fn, tg_id, tg_username)
|
||
if not panel_user:
|
||
await api.answer_callback(callback_id, text="Access denied")
|
||
await api.edit_message(chat_id, message_id, f"❌ {_tt(lang, 'access_denied')}")
|
||
return
|
||
|
||
await api.answer_callback(callback_id)
|
||
|
||
_pending_inputs[_pending_key(chat_id, tg_id)] = {
|
||
"kind": "user_add_client_name",
|
||
"sid": server_id,
|
||
"proto": proto,
|
||
"ts": time.time(),
|
||
}
|
||
|
||
await api.edit_message(
|
||
chat_id,
|
||
message_id,
|
||
f"➕ <b>{_tt(lang, 'btn_create_connection')}</b>\n\n"
|
||
f"{_tt(lang, 'protocol_label')}: <b>{_e(_protocol_display_name(proto))}</b>\n\n"
|
||
f"{_tt(lang, 'send_device_name')}\n"
|
||
f"{_tt(lang, 'example_label')}: <code>Ivan iPhone</code>\n\n"
|
||
f"{_tt(lang, 'cancel_instruction')}",
|
||
reply_markup={"inline_keyboard": [[{"text": f"❌ {_tt(lang, 'btn_cancel')}", "callback_data": "user_create_cancel"}]]},
|
||
)
|
||
|
||
|
||
async def _user_create_cancel(api: TelegramAPI, chat_id: int, message_id: int, callback_id: str, tg_id: str, tg_username: Optional[str], load_data_fn: Callable, lang: str = "en"):
|
||
_pending_inputs.pop(_pending_key(chat_id, tg_id), None)
|
||
panel_user = _find_user(load_data_fn, tg_id, tg_username)
|
||
if not panel_user:
|
||
await api.answer_callback(callback_id, text="Access denied")
|
||
await api.edit_message(chat_id, message_id, f" {_tt(lang, 'access_denied')}")
|
||
return
|
||
await api.answer_callback(callback_id)
|
||
await _send_user_connections(api, chat_id, panel_user, load_data_fn, lang=lang)
|
||
|
||
|
||
async def _user_delete(api: TelegramAPI, chat_id: int, message_id: int, callback_id: str, tg_id: str, tg_username: Optional[str], conn_id: str, name: str, load_data_fn: Callable, lang: str = "en"):
|
||
panel_user = _find_user(load_data_fn, tg_id, tg_username)
|
||
if not panel_user:
|
||
await api.answer_callback(callback_id, text="Access denied")
|
||
await api.edit_message(chat_id, message_id, f"❌ {_tt(lang, 'access_denied')}")
|
||
return
|
||
|
||
await api.answer_callback(callback_id)
|
||
|
||
data = load_data_fn()
|
||
if not _self_service_telegram_enabled(data):
|
||
await api.edit_message(chat_id, message_id, _tt(lang, "self_service_disabled_contact"))
|
||
return
|
||
|
||
conn = next((c for c in data.get("user_connections", []) if c.get("id") == conn_id and c.get("user_id") == panel_user.get("id")), None)
|
||
if not conn:
|
||
await api.edit_message(chat_id, message_id, f" {_tt(lang, 'connection_not_found')}")
|
||
return
|
||
|
||
conn_name = conn.get("name") or name or "Connection"
|
||
rows = [
|
||
[{"text": f"🗑 {_tt(lang, 'btn_delete')}", "callback_data": _ref("user_delete_confirm", {"conn_id": conn_id})}],
|
||
[{"text": f"❌ {_tt(lang, 'btn_cancel')}", "callback_data": "refresh"}],
|
||
]
|
||
await api.edit_message(
|
||
chat_id,
|
||
message_id,
|
||
_tt(lang, "delete_connection_confirm", name=_e(conn_name)) + f"\n\n{_tt(lang, 'cannot_be_undone')}",
|
||
reply_markup={"inline_keyboard": rows},
|
||
)
|
||
|
||
|
||
async def _user_delete_confirm(api: TelegramAPI, chat_id: int, message_id: int, callback_id: str, tg_id: str, tg_username: Optional[str], conn_id: str, load_data_fn: Callable, self_service_svc, lang: str = "en"):
|
||
panel_user = _find_user(load_data_fn, tg_id, tg_username)
|
||
if not panel_user:
|
||
await api.answer_callback(callback_id, text="Access denied")
|
||
await api.edit_message(chat_id, message_id, f"❌ {_tt(lang, 'access_denied')}")
|
||
return
|
||
|
||
if not self_service_svc:
|
||
await api.answer_callback(callback_id, text="Self-service not available")
|
||
await api.edit_message(chat_id, message_id, _tt(lang, "self_service_unavailable"))
|
||
return
|
||
|
||
await api.answer_callback(callback_id)
|
||
|
||
data = load_data_fn()
|
||
if not _self_service_telegram_enabled(data):
|
||
await api.edit_message(chat_id, message_id, _tt(lang, "self_service_disabled_contact"))
|
||
return
|
||
|
||
try:
|
||
await self_service_svc.delete_user_connection(panel_user["id"], conn_id, "telegram")
|
||
except Exception as e:
|
||
logger.exception("Bot: self-service delete failed")
|
||
await api.edit_message(chat_id, message_id, f"❌ {_tt(lang, 'error')}: {_tt(lang, 'self_service_unavailable')}")
|
||
return
|
||
|
||
data = load_data_fn()
|
||
conns = [c for c in data.get("user_connections", []) if c.get("user_id") == panel_user.get("id")]
|
||
if not conns:
|
||
if _self_service_telegram_enabled(data):
|
||
kb = _build_connections_keyboard(conns, data, lang)
|
||
await api.edit_message(chat_id, message_id, f"✅ {_tt(lang, 'connection_deleted')}\n\n{_tt(lang, 'no_connections_create_short')}", reply_markup=kb)
|
||
else:
|
||
await api.edit_message(chat_id, message_id, f"✅ {_tt(lang, 'connection_deleted_no_connections')}")
|
||
return
|
||
kb = _build_connections_keyboard(conns, data, lang)
|
||
await api.edit_message(chat_id, message_id, f"✅ {_tt(lang, 'connection_deleted')}\n\n{_tt(lang, 'your_connections', count=len(conns))}", reply_markup=kb)
|
||
|
||
|
||
async def _user_add_client_final(api: TelegramAPI, chat_id: int, message_id: int, callback_id: str, tg_id: str, tg_username: Optional[str], server_id: int, proto: str, name: str, load_data_fn: Callable, generate_vpn_link_fn: Callable, self_service_svc, lang: str = "en"):
|
||
panel_user = _find_user(load_data_fn, tg_id, tg_username)
|
||
if not panel_user:
|
||
await api.answer_callback(callback_id, text="Access denied")
|
||
await api.edit_message(chat_id, message_id, f"❌ {_tt(lang, 'access_denied')}")
|
||
return
|
||
|
||
if not self_service_svc:
|
||
await api.answer_callback(callback_id, text="Self-service not available")
|
||
await api.edit_message(chat_id, message_id, _tt(lang, "self_service_unavailable"))
|
||
return
|
||
|
||
data = load_data_fn()
|
||
if not _self_service_telegram_enabled(data):
|
||
await api.answer_callback(callback_id)
|
||
await api.edit_message(chat_id, message_id, _tt(lang, "self_service_disabled_contact"))
|
||
return
|
||
|
||
await api.answer_callback(callback_id)
|
||
|
||
clean_name = (name or "Connection").strip()[:80]
|
||
await _user_create_connection(api, chat_id, panel_user, server_id, proto, clean_name, load_data_fn, generate_vpn_link_fn, self_service_svc, lang)
|
||
|
||
|
||
async def _user_create_connection(api: TelegramAPI, chat_id: int, panel_user: dict, server_id: int, proto: str, name: str, load_data_fn: Callable, generate_vpn_link_fn: Callable, self_service_svc, lang: str = "en"):
|
||
try:
|
||
result = await self_service_svc.create_user_connection(panel_user["id"], server_id, proto, name, "telegram")
|
||
|
||
loading_msg = await api.send_message(chat_id, _tt(lang, "creating_connection_named", name=_e(name)))
|
||
loading_msg_id = loading_msg.get("result", {}).get("message_id")
|
||
|
||
data = load_data_fn()
|
||
servers = data.get("servers", [])
|
||
if server_id < 0 or server_id >= len(servers):
|
||
await api.edit_message(chat_id, loading_msg_id, f"❌ {_tt(lang, 'server_not_found')}")
|
||
return
|
||
server = servers[server_id]
|
||
|
||
config = result.get("config", "")
|
||
if config:
|
||
if loading_msg_id:
|
||
await api.call("deleteMessage", chat_id=chat_id, message_id=loading_msg_id)
|
||
await _send_config_text(api, chat_id, server, proto, name, config, generate_vpn_link_fn, lang)
|
||
else:
|
||
conn = result.get("connection", {})
|
||
client_id = conn.get("client_id")
|
||
if client_id:
|
||
if loading_msg_id:
|
||
await api.call("deleteMessage", chat_id=chat_id, message_id=loading_msg_id)
|
||
await _send_config_by_client(api, chat_id, server, proto, client_id, name, generate_vpn_link_fn, lang)
|
||
else:
|
||
if loading_msg_id:
|
||
await api.edit_message(chat_id, loading_msg_id, f"✅ {_tt(lang, 'connection_created_success')}")
|
||
except Exception as e:
|
||
logger.exception("Bot: self-service create failed")
|
||
await api.send_message(chat_id, f"❌ {_tt(lang, 'error')}: {_tt(lang, 'self_service_unavailable')}")
|
||
|
||
|
||
# ----------------------------------------------------------------------- #
|
||
# Main polling loop and dispatcher
|
||
# ----------------------------------------------------------------------- #
|
||
async def _run_bot(token: str, load_data_fn: Callable, generate_vpn_link_fn: Callable, save_data_fn: Optional[Callable] = None, self_service_svc=None):
|
||
offset = 0
|
||
logger.info("Telegram bot started (raw httpx polling).")
|
||
|
||
async with httpx.AsyncClient() as client:
|
||
api = TelegramAPI(token, client)
|
||
|
||
me = await api.call("getMe")
|
||
if not me.get("ok"):
|
||
logger.error(f"Telegram bot: invalid token or API error: {me}")
|
||
return
|
||
logger.info(f"Telegram bot logged in as @{me['result']['username']}")
|
||
await _set_default_commands(api)
|
||
|
||
while True:
|
||
try:
|
||
updates = await api.get_updates(offset=offset, timeout=25)
|
||
except asyncio.CancelledError:
|
||
logger.info("Telegram bot polling cancelled.")
|
||
return
|
||
except Exception as e:
|
||
logger.warning(f"Telegram bot polling error: {e}")
|
||
await asyncio.sleep(5)
|
||
continue
|
||
|
||
for update in updates:
|
||
offset = update["update_id"] + 1
|
||
try:
|
||
await _dispatch(api, update, load_data_fn, generate_vpn_link_fn, save_data_fn, self_service_svc)
|
||
except asyncio.CancelledError:
|
||
return
|
||
except Exception as e:
|
||
logger.exception(f"Telegram bot: error handling update {update['update_id']}: {e}")
|
||
|
||
|
||
async def _dispatch(api: TelegramAPI, update: dict, load_data_fn: Callable, generate_vpn_link_fn: Callable, save_data_fn: Optional[Callable] = None, self_service_svc=None):
|
||
if "message" in update:
|
||
msg = update["message"]
|
||
text = msg.get("text", "")
|
||
tg_username = msg["from"].get("username")
|
||
lang = _tg_lang(msg["from"])
|
||
chat_id = msg["chat"]["id"]
|
||
if text.startswith(("/connections", "/connect", "/disconnect")) and not _is_private_chat(msg.get("chat")):
|
||
await _reject_non_private(api, chat_id, lang)
|
||
return
|
||
if await _handle_pending_input(api, msg, load_data_fn, save_data_fn, generate_vpn_link_fn, self_service_svc):
|
||
return
|
||
if text.startswith("/start") or text.startswith("/admin"):
|
||
await _handle_start(api, msg, load_data_fn)
|
||
elif text.startswith("/connections"):
|
||
panel_user = _find_user(load_data_fn, str(msg["from"]["id"]), tg_username)
|
||
if not panel_user:
|
||
await api.send_message(chat_id, f" {_tt(lang, 'access_denied')}")
|
||
else:
|
||
await _send_user_connections(api, chat_id, panel_user, load_data_fn, lang=lang)
|
||
elif text.startswith("/connect"):
|
||
await _user_create_start_message(api, chat_id, str(msg["from"]["id"]), tg_username, load_data_fn, lang)
|
||
elif text.startswith("/disconnect"):
|
||
panel_user = _find_user(load_data_fn, str(msg["from"]["id"]), tg_username)
|
||
if not panel_user:
|
||
await api.send_message(chat_id, f" {_tt(lang, 'access_denied')}")
|
||
else:
|
||
await _send_user_connections(api, chat_id, panel_user, load_data_fn, lang=lang)
|
||
elif text.startswith("/servers"):
|
||
if _require_admin(load_data_fn, str(msg["from"]["id"]), tg_username):
|
||
await _admin_servers(api, msg["chat"]["id"], None, load_data_fn, lang)
|
||
else:
|
||
await api.send_message(msg["chat"]["id"], f" {_tt(lang, 'access_denied')}")
|
||
elif text.startswith("/addserver"):
|
||
await _handle_add_server_command(api, msg, load_data_fn, save_data_fn, lang)
|
||
|
||
elif "callback_query" in update:
|
||
cq = update["callback_query"]
|
||
callback_id = cq["id"]
|
||
data_str = cq.get("data", "")
|
||
chat_id = cq["message"]["chat"]["id"]
|
||
chat = cq["message"].get("chat", {})
|
||
message_id = cq["message"]["message_id"]
|
||
tg_id = str(cq["from"]["id"])
|
||
tg_username = cq["from"].get("username")
|
||
lang = _tg_lang(cq["from"])
|
||
|
||
if data_str == "noop":
|
||
await api.answer_callback(callback_id)
|
||
return
|
||
if data_str == "refresh":
|
||
await _handle_refresh(api, chat_id, message_id, callback_id, tg_id, tg_username, load_data_fn, lang)
|
||
return
|
||
if data_str.startswith("cfg:"):
|
||
if not _is_private_chat(chat):
|
||
await _reject_non_private(api, chat_id, lang, callback_id)
|
||
return
|
||
await _handle_get_config(api, chat_id, message_id, callback_id, data_str[4:], tg_id, tg_username, load_data_fn, generate_vpn_link_fn, lang)
|
||
return
|
||
|
||
# Self-service user callbacks (before admin gate).
|
||
if data_str == "user_create":
|
||
if not _is_private_chat(chat):
|
||
await _reject_non_private(api, chat_id, lang, callback_id)
|
||
return
|
||
await _user_create_start(api, chat_id, message_id, callback_id, tg_id, tg_username, load_data_fn, self_service_svc, lang)
|
||
return
|
||
if data_str == "user_create_cancel":
|
||
await _user_create_cancel(api, chat_id, message_id, callback_id, tg_id, tg_username, load_data_fn, lang)
|
||
return
|
||
|
||
ref = _resolve_ref(data_str)
|
||
if ref:
|
||
action = ref.get("action")
|
||
payload = ref.get("payload", {})
|
||
if action == "user_create_server":
|
||
if not _is_private_chat(chat):
|
||
await _reject_non_private(api, chat_id, lang, callback_id)
|
||
return
|
||
await _user_create_server(api, chat_id, message_id, callback_id, tg_id, tg_username, int(payload.get("sid", 0)), load_data_fn, self_service_svc, lang)
|
||
return
|
||
if action == "user_create_protocol":
|
||
if not _is_private_chat(chat):
|
||
await _reject_non_private(api, chat_id, lang, callback_id)
|
||
return
|
||
await _user_create_protocol(api, chat_id, message_id, callback_id, tg_id, tg_username, int(payload.get("sid", 0)), payload.get("proto", "awg"), load_data_fn, lang)
|
||
return
|
||
if action == "user_add_client":
|
||
if not _is_private_chat(chat):
|
||
await _reject_non_private(api, chat_id, lang, callback_id)
|
||
return
|
||
await _user_add_client_final(api, chat_id, message_id, callback_id, tg_id, tg_username, int(payload.get("sid", 0)), payload.get("proto", "awg"), payload.get("name", ""), load_data_fn, generate_vpn_link_fn, self_service_svc, lang)
|
||
return
|
||
if action == "user_delete":
|
||
if not _is_private_chat(chat):
|
||
await _reject_non_private(api, chat_id, lang, callback_id)
|
||
return
|
||
await _user_delete(api, chat_id, message_id, callback_id, tg_id, tg_username, payload.get("conn_id", ""), payload.get("name", ""), load_data_fn, lang)
|
||
return
|
||
if action == "user_delete_confirm":
|
||
if not _is_private_chat(chat):
|
||
await _reject_non_private(api, chat_id, lang, callback_id)
|
||
return
|
||
await _user_delete_confirm(api, chat_id, message_id, callback_id, tg_id, tg_username, payload.get("conn_id", ""), load_data_fn, self_service_svc, lang)
|
||
return
|
||
|
||
panel_user = _require_admin(load_data_fn, tg_id, tg_username)
|
||
if not panel_user:
|
||
await api.answer_callback(callback_id, "Access denied")
|
||
return
|
||
|
||
await api.answer_callback(callback_id)
|
||
|
||
if data_str == "adm:menu":
|
||
await api.edit_message(chat_id, message_id, f"<b>{_tt(lang, 'admin_menu')}</b>", reply_markup=_admin_main_keyboard(lang))
|
||
elif data_str == "adm:servers":
|
||
await _admin_servers(api, chat_id, message_id, load_data_fn, lang)
|
||
elif data_str == "adm:users":
|
||
await _admin_users(api, chat_id, message_id, load_data_fn, lang)
|
||
elif data_str == "adm:myconns":
|
||
await _send_user_connections(api, chat_id, panel_user, load_data_fn, lang=lang)
|
||
elif data_str == "adm:addserver_help":
|
||
await api.edit_message(
|
||
chat_id,
|
||
message_id,
|
||
f"➕ <b>{_tt(lang, 'add_server_label')}</b>\n\n"
|
||
f"{_tt(lang, 'add_server_command')}\n",
|
||
reply_markup={"inline_keyboard": [[{"text": f"⬅️ {_tt(lang, 'btn_admin_menu')}", "callback_data": "adm:menu"}]]},
|
||
)
|
||
elif data_str.startswith("srv:"):
|
||
await _admin_server_detail(api, chat_id, message_id, int(data_str.split(":", 1)[1]), load_data_fn, lang)
|
||
else:
|
||
ref = _resolve_ref(data_str)
|
||
if not ref:
|
||
await api.edit_message(chat_id, message_id, f" {_tt(lang, 'action_expired')}")
|
||
return
|
||
action = ref.get("action")
|
||
payload = ref.get("payload", {})
|
||
sid = int(payload.get("sid", 0) or 0)
|
||
proto = payload.get("proto", "awg")
|
||
if action == "user":
|
||
await _admin_user_detail(api, chat_id, message_id, payload.get("uid"), load_data_fn, lang)
|
||
elif action == "proto":
|
||
await _admin_protocol_detail(api, chat_id, message_id, sid, proto, load_data_fn, lang)
|
||
elif action == "toggle_proto":
|
||
await _admin_toggle_protocol(api, chat_id, message_id, sid, proto, bool(payload.get("start")), load_data_fn, lang)
|
||
elif action == "clients":
|
||
await _admin_clients(api, chat_id, message_id, sid, proto, load_data_fn, lang)
|
||
elif action == "client":
|
||
await _admin_client_detail(api, chat_id, message_id, sid, proto, payload.get("client", {}), lang)
|
||
elif action == "client_cfg":
|
||
if not _is_private_chat(chat):
|
||
await _reject_non_private(api, chat_id, lang, callback_id)
|
||
return
|
||
data = load_data_fn()
|
||
server = data["servers"][sid]
|
||
await _send_config_by_client(api, chat_id, server, proto, payload.get("client_id"), payload.get("name", "Connection"), generate_vpn_link_fn, lang)
|
||
elif action == "add_client":
|
||
await _admin_add_client(api, chat_id, message_id, sid, proto, panel_user, load_data_fn, save_data_fn, generate_vpn_link_fn, lang, tg_id)
|
||
elif action == "create_client":
|
||
await _admin_create_client(api, chat_id, message_id, sid, proto, payload.get("name", "Connection"), payload.get("user_id"), load_data_fn, save_data_fn, generate_vpn_link_fn, lang)
|
||
elif action == "toggle_client":
|
||
await _admin_toggle_client(api, chat_id, message_id, sid, proto, payload.get("client_id"), bool(payload.get("enable")), load_data_fn, lang)
|
||
elif action == "remove_client":
|
||
await _admin_remove_client(api, chat_id, message_id, sid, proto, payload.get("client_id"), load_data_fn, save_data_fn, lang)
|