mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-05-19 22:06:17 +00:00
v2.5.0: maintenance and bot user management
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
GoTelegram v2.4 Bot - MTProxy Management for Linux
|
||||
GoTelegram v2.5.0 Bot - MTProxy Management for Linux
|
||||
Manages telemt engine via Telegram interface with full CLI feature parity
|
||||
Uses python-telegram-bot v21+
|
||||
Supports EN/RU UI with per-user language preferences.
|
||||
@@ -23,6 +23,7 @@ from datetime import datetime
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from typing import Tuple, Optional, List, Dict, Any
|
||||
from urllib.parse import quote
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from telegram import (
|
||||
@@ -100,7 +101,7 @@ logger = logging.getLogger(__name__)
|
||||
# CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
GOTELEGRAM_VERSION = "2.4.6"
|
||||
GOTELEGRAM_VERSION = "2.5.0"
|
||||
GOTELEGRAM_CONFIG = "/opt/gotelegram/config.json"
|
||||
TELEMT_CONFIG = "/etc/telemt/config.toml"
|
||||
TELEMT_SERVICE = "telemt"
|
||||
@@ -256,6 +257,7 @@ _DOMAIN_RE = re.compile(
|
||||
r"^(?=.{1,253}$)(?:(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.)+"
|
||||
r"(?!-)[A-Za-z0-9-]{2,63}(?<!-)$"
|
||||
)
|
||||
_USER_NAME_RE = re.compile(r"^[A-Za-z0-9_.-]{1,48}$")
|
||||
|
||||
|
||||
async def run_bot_action(action: str, timeout: int = 300, **params) -> Dict:
|
||||
@@ -342,6 +344,22 @@ def save_json(path: str, data: Dict) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def template_display_name(template_id: str) -> str:
|
||||
"""Resolve a template id to a human-friendly name from catalog/config."""
|
||||
if not template_id:
|
||||
return ""
|
||||
if template_id.startswith("custom_"):
|
||||
config = load_json(GOTELEGRAM_CONFIG) or {}
|
||||
source = config.get("template_source", "")
|
||||
return f"{template_id} ({source})" if source else template_id
|
||||
catalog = load_json(TEMPLATES_CATALOG) or {}
|
||||
for cat in catalog.get("categories", []):
|
||||
for tpl in cat.get("templates", []):
|
||||
if tpl.get("id") == template_id:
|
||||
return f"{tpl.get('name', template_id)} ({template_id})"
|
||||
return template_id
|
||||
|
||||
|
||||
async def safe_edit_message(
|
||||
query,
|
||||
text: str,
|
||||
@@ -498,14 +516,17 @@ def get_main_menu(user_id: Optional[int] = None) -> InlineKeyboardMarkup:
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(_t(user_id, "menu_stats"), callback_data="menu_stats"),
|
||||
InlineKeyboardButton(_t(user_id, "menu_users"), callback_data="menu_users"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(_t(user_id, "menu_remove"), callback_data="menu_remove"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(_t(user_id, "menu_admins"), callback_data="menu_admins"),
|
||||
InlineKeyboardButton(_t(user_id, "menu_credits"), callback_data="menu_credits"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(_t(user_id, "menu_credits"), callback_data="menu_credits"),
|
||||
InlineKeyboardButton(_t(user_id, "menu_language"), callback_data="menu_lang"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(_t(user_id, "menu_close"), callback_data="close_menu"),
|
||||
],
|
||||
]
|
||||
@@ -653,7 +674,7 @@ async def get_status_text(user_id: Optional[int] = None) -> str:
|
||||
# install.sh/save_gotelegram_config uses "template_id" (not "template")
|
||||
tpl = config.get("template_id") or config.get("template")
|
||||
if tpl:
|
||||
lines.append(f"<b>{_t(user_id, 'status_template')}:</b> {html.escape(str(tpl))}")
|
||||
lines.append(f"<b>{_t(user_id, 'status_template')}:</b> {html.escape(template_display_name(str(tpl)))}")
|
||||
if config.get("domain"):
|
||||
lines.append(f"<b>{_t(user_id, 'status_domain')}:</b> {html.escape(str(config['domain']))}")
|
||||
if config.get("port"):
|
||||
@@ -689,6 +710,15 @@ async def get_status_text(user_id: Optional[int] = None) -> str:
|
||||
|
||||
async def get_traffic_stats() -> str:
|
||||
"""Get formatted traffic statistics."""
|
||||
await sh(
|
||||
"bash",
|
||||
"-lc",
|
||||
"source /opt/gotelegram/lib/common.sh; "
|
||||
"source /opt/gotelegram/lib/stats.sh; "
|
||||
"stats_init >/dev/null 2>&1 || true; stats_collect >/dev/null 2>&1 || true",
|
||||
timeout=15,
|
||||
)
|
||||
|
||||
# Read current snapshot
|
||||
current_file = "/run/gotelegram/stats_current.json"
|
||||
history_file = "/opt/gotelegram/stats_history.csv"
|
||||
@@ -706,6 +736,8 @@ async def get_traffic_stats() -> str:
|
||||
reader = csv.reader(f)
|
||||
for row in reader:
|
||||
if len(row) >= 3:
|
||||
if not row[0].isdigit():
|
||||
continue
|
||||
history.append({
|
||||
"ts": int(row[0]),
|
||||
"proxy": int(row[1]),
|
||||
@@ -824,12 +856,12 @@ async def cb_menu_status(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def get_install_mode_menu() -> InlineKeyboardMarkup:
|
||||
def get_install_mode_menu(user_id: Optional[int] = None) -> InlineKeyboardMarkup:
|
||||
"""Install mode selection menu."""
|
||||
buttons = [
|
||||
[InlineKeyboardButton("⚡ Lite", callback_data="install_mode_lite")],
|
||||
[InlineKeyboardButton("🛡 Pro", callback_data="install_mode_pro")],
|
||||
[InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")],
|
||||
[InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_main")],
|
||||
]
|
||||
return InlineKeyboardMarkup(buttons)
|
||||
|
||||
@@ -858,7 +890,7 @@ async def cb_menu_install(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
keyboard = InlineKeyboardMarkup(buttons)
|
||||
else:
|
||||
text = "Select installation mode:"
|
||||
keyboard = get_install_mode_menu()
|
||||
keyboard = get_install_mode_menu(_uid(update))
|
||||
|
||||
await safe_edit_message(query,
|
||||
text, reply_markup=keyboard, parse_mode="HTML"
|
||||
@@ -1362,6 +1394,96 @@ async def cb_pro_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
# PROXY LINK & SHARE
|
||||
# ============================================================================
|
||||
|
||||
def load_telemt_users() -> Dict[str, str]:
|
||||
"""Return users from [access.users] in telemt config."""
|
||||
telemt_cfg = load_toml(TELEMT_CONFIG) or {}
|
||||
users = telemt_cfg.get("access", {}).get("users", {})
|
||||
if not isinstance(users, dict):
|
||||
return {}
|
||||
return {
|
||||
str(name): str(secret)
|
||||
for name, secret in users.items()
|
||||
if isinstance(name, str) and isinstance(secret, str)
|
||||
}
|
||||
|
||||
|
||||
def save_telemt_users(users: Dict[str, str]) -> bool:
|
||||
"""Persist [access.users] while keeping the rest of the TOML structure."""
|
||||
telemt_cfg = load_toml(TELEMT_CONFIG) or {}
|
||||
access = telemt_cfg.setdefault("access", {})
|
||||
access["users"] = dict(sorted(users.items()))
|
||||
try:
|
||||
os.makedirs(os.path.dirname(TELEMT_CONFIG), exist_ok=True)
|
||||
with open(TELEMT_CONFIG, "w") as f:
|
||||
toml.dump(telemt_cfg, f)
|
||||
os.chmod(TELEMT_CONFIG, 0o600)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save telemt users: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def refresh_telemt_after_user_change() -> bool:
|
||||
"""Restart telemt after config user changes."""
|
||||
code, _, _ = await sh("systemctl", "restart", TELEMT_SERVICE, timeout=20)
|
||||
return code == 0
|
||||
|
||||
|
||||
async def telemt_api_get(path: str) -> Optional[Dict[str, Any]]:
|
||||
"""Read telemt local API if it is enabled in config."""
|
||||
code, stdout, _ = await sh(
|
||||
"curl",
|
||||
"-sS",
|
||||
"--max-time",
|
||||
"3",
|
||||
f"http://127.0.0.1:9091{path}",
|
||||
timeout=5,
|
||||
)
|
||||
if code != 0 or not stdout.strip():
|
||||
return None
|
||||
try:
|
||||
data = json.loads(stdout)
|
||||
return data if isinstance(data, dict) else None
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
|
||||
def _extract_traffic_value(data: Any, keys: List[str]) -> int:
|
||||
if isinstance(data, dict):
|
||||
total = 0
|
||||
for key, value in data.items():
|
||||
if key in keys and isinstance(value, (int, float)):
|
||||
total += int(value)
|
||||
elif isinstance(value, (dict, list)):
|
||||
total += _extract_traffic_value(value, keys)
|
||||
return total
|
||||
if isinstance(data, list):
|
||||
return sum(_extract_traffic_value(item, keys) for item in data)
|
||||
return 0
|
||||
|
||||
|
||||
async def get_proxy_link_for_secret(secret: str) -> Optional[str]:
|
||||
"""Generate a fake-TLS proxy link for an arbitrary telemt user secret."""
|
||||
config = load_json(GOTELEGRAM_CONFIG) or {}
|
||||
if not secret:
|
||||
return None
|
||||
|
||||
mode = config.get("mode", "lite")
|
||||
domain = config.get("domain", "")
|
||||
port = config.get("port", 443)
|
||||
|
||||
if mode == "pro" and domain:
|
||||
domain_hex = str(domain).encode().hex()
|
||||
return f"tg://proxy?server={domain}&port={port}&secret=ee{secret}{domain_hex}"
|
||||
|
||||
code, stdout, _ = await sh("curl", "-s", "-4", "--max-time", "5", "https://api.ipify.org")
|
||||
server = stdout.strip() if code == 0 and stdout.strip() else "0.0.0.0"
|
||||
mask_host = config.get("mask_host", "")
|
||||
if mask_host:
|
||||
domain_hex = str(mask_host).encode().hex()
|
||||
return f"tg://proxy?server={server}&port={port}&secret=ee{secret}{domain_hex}"
|
||||
return f"tg://proxy?server={server}&port={port}&secret={secret}"
|
||||
|
||||
|
||||
async def get_proxy_link() -> Optional[str]:
|
||||
"""Generate proxy link from config. Pro-mode uses domain + fake-TLS secret."""
|
||||
@@ -1381,27 +1503,7 @@ async def get_proxy_link() -> Optional[str]:
|
||||
if not secret:
|
||||
return None
|
||||
|
||||
mode = config.get("mode", "lite")
|
||||
domain = config.get("domain", "")
|
||||
port = config.get("port", 443)
|
||||
|
||||
# Pro-режим: ссылка с доменом и fake-TLS секретом (ee + secret + hex domain)
|
||||
if mode == "pro" and domain:
|
||||
domain_hex = domain.encode().hex()
|
||||
faketls_secret = f"ee{secret}{domain_hex}"
|
||||
return f"tg://proxy?server={domain}&port={port}&secret={faketls_secret}"
|
||||
|
||||
# Lite-режим: IP + fake-TLS с mask_host
|
||||
code, stdout, _ = await sh("curl", "-s", "-4", "--max-time", "5", "https://api.ipify.org")
|
||||
server = stdout.strip() if code == 0 and stdout.strip() else "0.0.0.0"
|
||||
|
||||
mask_host = config.get("mask_host", "")
|
||||
if mask_host:
|
||||
domain_hex = mask_host.encode().hex()
|
||||
faketls_secret = f"ee{secret}{domain_hex}"
|
||||
return f"tg://proxy?server={server}&port={port}&secret={faketls_secret}"
|
||||
|
||||
return f"tg://proxy?server={server}&port={port}&secret={secret}"
|
||||
return await get_proxy_link_for_secret(secret)
|
||||
|
||||
|
||||
async def cb_menu_link(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
@@ -1477,6 +1579,201 @@ async def cb_menu_share(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TELEMT USERS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def _users_keyboard(users: Dict[str, str], user_id: Optional[int]) -> InlineKeyboardMarkup:
|
||||
rows = []
|
||||
for name in sorted(users):
|
||||
rows.append([InlineKeyboardButton(f"👤 {name}", callback_data=f"user_view_{name}")])
|
||||
rows.append([InlineKeyboardButton("➕ Добавить ключ", callback_data="user_add")])
|
||||
rows.append([
|
||||
InlineKeyboardButton(_t(user_id, "btn_refresh"), callback_data="menu_users"),
|
||||
InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_main"),
|
||||
])
|
||||
return InlineKeyboardMarkup(rows)
|
||||
|
||||
|
||||
async def cb_menu_users(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
user_id = _uid(update)
|
||||
users = load_telemt_users()
|
||||
|
||||
if users:
|
||||
user_lines = "\n".join(f"• <code>{html.escape(name)}</code>" for name in sorted(users))
|
||||
else:
|
||||
user_lines = "<i>Ключей пока нет</i>"
|
||||
|
||||
api_summary = await telemt_api_get("/v1/stats/summary")
|
||||
api_note = ""
|
||||
if api_summary and isinstance(api_summary.get("data"), dict):
|
||||
data = api_summary["data"]
|
||||
configured = data.get("configured_users")
|
||||
active = data.get("active_connections") or data.get("connections_active")
|
||||
bits = []
|
||||
if configured is not None:
|
||||
bits.append(f"users: <code>{configured}</code>")
|
||||
if active is not None:
|
||||
bits.append(f"active: <code>{active}</code>")
|
||||
if bits:
|
||||
api_note = "\n\nAPI: " + ", ".join(bits)
|
||||
|
||||
text = (
|
||||
"<b>🔑 Ключи пользователей</b>\n\n"
|
||||
f"{user_lines}"
|
||||
f"{api_note}\n\n"
|
||||
"<i>Нажмите на пользователя, чтобы увидеть ссылку, статистику и действия.</i>"
|
||||
)
|
||||
await safe_edit_message(query, text, reply_markup=_users_keyboard(users, user_id), parse_mode="HTML")
|
||||
|
||||
|
||||
async def _user_detail_text(name: str, secret: str) -> str:
|
||||
link = await get_proxy_link_for_secret(secret)
|
||||
api = await telemt_api_get(f"/v1/users/{quote(name, safe='')}")
|
||||
details = ""
|
||||
if api:
|
||||
data = api.get("data", api)
|
||||
up = _extract_traffic_value(data, ["upload_bytes", "uplink_bytes", "tx_bytes", "sent_bytes", "up"])
|
||||
down = _extract_traffic_value(data, ["download_bytes", "downlink_bytes", "rx_bytes", "received_bytes", "down"])
|
||||
active_ips = _extract_traffic_value(data, ["active_ips", "unique_ips"])
|
||||
parts = []
|
||||
if up:
|
||||
parts.append(f"↑ {up} B")
|
||||
if down:
|
||||
parts.append(f"↓ {down} B")
|
||||
if active_ips:
|
||||
parts.append(f"active IPs: {active_ips}")
|
||||
if parts:
|
||||
details = "\n" + "\n".join(parts)
|
||||
else:
|
||||
compact = json.dumps(data, ensure_ascii=False)[:600]
|
||||
details = f"\n<pre>{html.escape(compact)}</pre>"
|
||||
else:
|
||||
details = "\n<i>Runtime API недоступен. Новые установки GoTelegram включают его автоматически.</i>"
|
||||
|
||||
link_line = html.escape(link) if link else "link unavailable"
|
||||
return (
|
||||
f"<b>👤 {html.escape(name)}</b>\n\n"
|
||||
f"Secret: <code>{html.escape(secret)}</code>\n\n"
|
||||
f"<b>Ссылка:</b>\n<code>{link_line}</code>\n"
|
||||
f"{details}"
|
||||
)
|
||||
|
||||
|
||||
async def cb_user_view(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
user_id = _uid(update)
|
||||
name = query.data.removeprefix("user_view_")
|
||||
users = load_telemt_users()
|
||||
secret = users.get(name)
|
||||
if not secret:
|
||||
await safe_edit_message(
|
||||
query,
|
||||
"❌ Пользователь не найден.",
|
||||
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_users")]]),
|
||||
)
|
||||
return
|
||||
|
||||
buttons = [
|
||||
[InlineKeyboardButton("🗑 Удалить", callback_data=f"user_del_{name}")],
|
||||
[InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_users")],
|
||||
]
|
||||
await safe_edit_message(
|
||||
query,
|
||||
await _user_detail_text(name, secret),
|
||||
reply_markup=InlineKeyboardMarkup(buttons),
|
||||
parse_mode="HTML",
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
|
||||
|
||||
async def cb_user_add(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
user_id = _uid(update)
|
||||
context.user_data["awaiting_user_name"] = True
|
||||
text = (
|
||||
"<b>➕ Новый ключ</b>\n\n"
|
||||
"Отправьте имя пользователя: латиница, цифры, <code>_ . -</code>, до 48 символов.\n"
|
||||
"Пример: <code>ivan</code> или <code>family-1</code>."
|
||||
)
|
||||
await safe_edit_message(
|
||||
query,
|
||||
text,
|
||||
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(_t(user_id, "btn_cancel"), callback_data="menu_users")]]),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
|
||||
async def cb_user_delete(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
user_id = _uid(update)
|
||||
name = query.data.removeprefix("user_del_")
|
||||
if name == "main":
|
||||
await query.answer("main нельзя удалить", show_alert=True)
|
||||
return
|
||||
text = f"Удалить ключ <code>{html.escape(name)}</code>?"
|
||||
buttons = [
|
||||
[InlineKeyboardButton("✅ Удалить", callback_data=f"user_del_yes_{name}")],
|
||||
[InlineKeyboardButton(_t(user_id, "btn_cancel"), callback_data=f"user_view_{name}")],
|
||||
]
|
||||
await safe_edit_message(query, text, reply_markup=InlineKeyboardMarkup(buttons), parse_mode="HTML")
|
||||
|
||||
|
||||
async def cb_user_delete_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
user_id = _uid(update)
|
||||
name = query.data.removeprefix("user_del_yes_")
|
||||
users = load_telemt_users()
|
||||
if name == "main" or name not in users:
|
||||
await query.answer("Нельзя удалить этот ключ", show_alert=True)
|
||||
return
|
||||
users.pop(name, None)
|
||||
if not save_telemt_users(users):
|
||||
await safe_edit_message(query, "❌ Не удалось сохранить config.toml")
|
||||
return
|
||||
await refresh_telemt_after_user_change()
|
||||
await safe_edit_message(
|
||||
query,
|
||||
f"✅ Ключ <code>{html.escape(name)}</code> удалён.",
|
||||
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_users")]]),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
|
||||
async def create_user_from_text(update: Update, context: ContextTypes.DEFAULT_TYPE, name: str) -> None:
|
||||
user_id = update.effective_user.id
|
||||
if not _USER_NAME_RE.match(name):
|
||||
await update.message.reply_text("❌ Некорректное имя. Используйте латиницу, цифры, _ . - и до 48 символов.")
|
||||
return
|
||||
users = load_telemt_users()
|
||||
if name in users:
|
||||
await update.message.reply_text("❌ Такой пользователь уже есть.")
|
||||
return
|
||||
secret = hashlib.sha256(f"{name}:{time.time()}:{os.urandom(16).hex()}".encode()).hexdigest()[:32]
|
||||
users[name] = secret
|
||||
if not save_telemt_users(users):
|
||||
await update.message.reply_text("❌ Не удалось сохранить /etc/telemt/config.toml")
|
||||
return
|
||||
await refresh_telemt_after_user_change()
|
||||
link = await get_proxy_link_for_secret(secret)
|
||||
await update.message.reply_text(
|
||||
f"✅ <b>Ключ создан</b>\n\n"
|
||||
f"Пользователь: <code>{html.escape(name)}</code>\n"
|
||||
f"Secret: <code>{secret}</code>\n\n"
|
||||
f"<code>{html.escape(link or '')}</code>",
|
||||
reply_markup=_users_keyboard(load_telemt_users(), user_id),
|
||||
parse_mode="HTML",
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# RESTART & LOGS
|
||||
# ============================================================================
|
||||
@@ -1795,7 +2092,7 @@ async def cb_install_migrate(update: Update, context: ContextTypes.DEFAULT_TYPE)
|
||||
"✅ <b>v1 container stopped and removed</b>\n\n"
|
||||
"Now select installation mode for v2:"
|
||||
)
|
||||
keyboard = get_install_mode_menu()
|
||||
keyboard = get_install_mode_menu(_uid(update))
|
||||
await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML")
|
||||
|
||||
|
||||
@@ -2218,6 +2515,7 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
"menu_promo": cb_menu_promo,
|
||||
"menu_credits": cb_menu_credits,
|
||||
"menu_admins": cb_menu_admins,
|
||||
"menu_users": cb_menu_users,
|
||||
"menu_remove": cb_menu_remove,
|
||||
"install_mode_lite": cb_install_mode_lite,
|
||||
"install_mode_pro": cb_install_mode_pro,
|
||||
@@ -2251,6 +2549,14 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
# Pattern-based handlers
|
||||
if data.startswith("lite_dom_"):
|
||||
await cb_lite_domain(update, context)
|
||||
elif data == "user_add":
|
||||
await cb_user_add(update, context)
|
||||
elif data.startswith("user_view_"):
|
||||
await cb_user_view(update, context)
|
||||
elif data.startswith("user_del_yes_"):
|
||||
await cb_user_delete_confirm(update, context)
|
||||
elif data.startswith("user_del_"):
|
||||
await cb_user_delete(update, context)
|
||||
elif data.startswith("pro_cat_"):
|
||||
await cb_pro_category(update, context)
|
||||
elif data.startswith("pro_tpl_"):
|
||||
@@ -2277,6 +2583,10 @@ async def handle_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||
if not is_user_allowed(update.effective_user.id):
|
||||
return
|
||||
user_id = update.effective_user.id
|
||||
if context.user_data.pop("awaiting_user_name", False):
|
||||
await create_user_from_text(update, context, update.message.text.strip())
|
||||
return
|
||||
|
||||
# Only act when we're explicitly waiting for a custom-git URL
|
||||
if not _CUSTOM_GIT_WAITERS.pop(user_id, False):
|
||||
return
|
||||
@@ -2295,6 +2605,24 @@ async def handle_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||
config["template_id"] = tpl_id
|
||||
config["template_source"] = url
|
||||
save_json(GOTELEGRAM_CONFIG, config)
|
||||
if config.get("mode") == "pro" and os.path.isdir(info):
|
||||
try:
|
||||
os.makedirs(WEBSITE_ROOT, exist_ok=True)
|
||||
for entry in os.listdir(WEBSITE_ROOT):
|
||||
path = os.path.join(WEBSITE_ROOT, entry)
|
||||
if os.path.isdir(path) and not os.path.islink(path):
|
||||
shutil.rmtree(path)
|
||||
else:
|
||||
os.remove(path)
|
||||
for entry in os.listdir(info):
|
||||
src = os.path.join(info, entry)
|
||||
dst = os.path.join(WEBSITE_ROOT, entry)
|
||||
if os.path.isdir(src):
|
||||
shutil.copytree(src, dst)
|
||||
else:
|
||||
shutil.copy2(src, dst)
|
||||
except OSError as e:
|
||||
logger.error("custom template deploy failed: %s", e)
|
||||
await update.message.reply_text(
|
||||
_tf(user_id, "cg_ok_fmt", html.escape(tpl_id)),
|
||||
reply_markup=get_main_menu(user_id),
|
||||
|
||||
Reference in New Issue
Block a user