mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-05-20 00:36:04 +00:00
v2.5.0: add key disable switches and pro UI polish
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
GoTelegram v2.5.0 Bot - MTProxy Management for Linux
|
||||
goTelegram Pro 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.
|
||||
@@ -103,6 +103,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
GOTELEGRAM_VERSION = "2.5.0"
|
||||
GOTELEGRAM_CONFIG = "/opt/gotelegram/config.json"
|
||||
DISABLED_USERS_FILE = "/opt/gotelegram/disabled_users.json"
|
||||
TELEMT_CONFIG = "/etc/telemt/config.toml"
|
||||
TELEMT_SERVICE = "telemt"
|
||||
WEBSITE_ROOT = "/var/www/gotelegram-site"
|
||||
@@ -113,6 +114,7 @@ INSTALL_SH = "/opt/gotelegram/install.sh"
|
||||
PROMO_LINK_1 = "https://vk.cc/ct29NQ"
|
||||
PROMO_LINK_2 = "https://vk.cc/cUxAhj"
|
||||
TIP_LINK = "https://pay.cloudtips.ru/p/7410814f"
|
||||
YOUTUBE_LINK = os.getenv("GOTELEGRAM_YOUTUBE_LINK", "").strip()
|
||||
PROMO_STAMP_FILE = "/opt/gotelegram/.promo_bot_last_shown"
|
||||
|
||||
BOT_TOKEN = os.getenv("BOT_TOKEN")
|
||||
@@ -1412,6 +1414,48 @@ def load_telemt_users() -> Dict[str, str]:
|
||||
}
|
||||
|
||||
|
||||
def load_disabled_users() -> Dict[str, str]:
|
||||
raw = load_json(DISABLED_USERS_FILE) or {}
|
||||
if not isinstance(raw, dict):
|
||||
return {}
|
||||
users = raw.get("users") if isinstance(raw.get("users"), dict) else raw
|
||||
if not isinstance(users, dict):
|
||||
return {}
|
||||
clean: Dict[str, str] = {}
|
||||
for name, secret in users.items():
|
||||
if name in {"version", "updated_at"}:
|
||||
continue
|
||||
name_s = str(name).strip()
|
||||
secret_s = str(secret or "").strip()
|
||||
if _USER_NAME_RE.match(name_s) and secret_s:
|
||||
clean[name_s] = secret_s
|
||||
return clean
|
||||
|
||||
|
||||
def save_disabled_users(users: Dict[str, str]) -> bool:
|
||||
payload = {
|
||||
"version": 1,
|
||||
"updated_at": datetime.utcnow().isoformat() + "Z",
|
||||
"users": {name: users[name] for name in sorted(users)},
|
||||
}
|
||||
ok = save_json(DISABLED_USERS_FILE, payload)
|
||||
if ok:
|
||||
try:
|
||||
os.chmod(DISABLED_USERS_FILE, 0o600)
|
||||
except OSError:
|
||||
pass
|
||||
return ok
|
||||
|
||||
|
||||
def load_user_records() -> Dict[str, Dict[str, Any]]:
|
||||
records: Dict[str, Dict[str, Any]] = {}
|
||||
for name, secret in load_disabled_users().items():
|
||||
records[name] = {"secret": secret, "enabled": False}
|
||||
for name, secret in load_telemt_users().items():
|
||||
records[name] = {"secret": secret, "enabled": True}
|
||||
return records
|
||||
|
||||
|
||||
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 {}
|
||||
@@ -1589,10 +1633,12 @@ async def cb_menu_share(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def _users_keyboard(users: Dict[str, str], user_id: Optional[int]) -> InlineKeyboardMarkup:
|
||||
def _users_keyboard(users: Dict[str, Dict[str, Any]], user_id: Optional[int]) -> InlineKeyboardMarkup:
|
||||
rows = []
|
||||
for name in sorted(users):
|
||||
rows.append([InlineKeyboardButton(f"👤 {name}", callback_data=f"user_view_{name}")])
|
||||
for name in sorted(users, key=lambda item: (item != "main", item)):
|
||||
enabled = bool(users[name].get("enabled"))
|
||||
icon = "🟢" if enabled else "⏸"
|
||||
rows.append([InlineKeyboardButton(f"{icon} {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"),
|
||||
@@ -1605,10 +1651,13 @@ async def cb_menu_users(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
user_id = _uid(update)
|
||||
users = load_telemt_users()
|
||||
users = load_user_records()
|
||||
|
||||
if users:
|
||||
user_lines = "\n".join(f"• <code>{html.escape(name)}</code>" for name in sorted(users))
|
||||
user_lines = "\n".join(
|
||||
f"{'🟢' if users[name].get('enabled') else '⏸'} <code>{html.escape(name)}</code>"
|
||||
for name in sorted(users, key=lambda item: (item != "main", item))
|
||||
)
|
||||
else:
|
||||
user_lines = "<i>Ключей пока нет</i>"
|
||||
|
||||
@@ -1635,9 +1684,9 @@ async def cb_menu_users(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
|
||||
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:
|
||||
async def _user_detail_text(name: str, secret: str, enabled: bool = True) -> str:
|
||||
link = await get_proxy_link_for_secret(secret)
|
||||
api = await telemt_api_get(f"/v1/users/{quote(name, safe='')}")
|
||||
api = await telemt_api_get(f"/v1/users/{quote(name, safe='')}") if enabled else None
|
||||
details = ""
|
||||
if api:
|
||||
data = api.get("data", api)
|
||||
@@ -1656,12 +1705,16 @@ async def _user_detail_text(name: str, secret: str) -> str:
|
||||
else:
|
||||
compact = json.dumps(data, ensure_ascii=False)[:600]
|
||||
details = f"\n<pre>{html.escape(compact)}</pre>"
|
||||
elif enabled:
|
||||
details = "\n<i>Runtime API недоступен. Новые установки goTelegram Pro включают его автоматически.</i>"
|
||||
else:
|
||||
details = "\n<i>Runtime API недоступен. Новые установки GoTelegram включают его автоматически.</i>"
|
||||
details = "\n<i>Ключ отключён и сейчас не принимается telemt.</i>"
|
||||
|
||||
link_line = html.escape(link) if link else "link unavailable"
|
||||
status_line = "🟢 enabled" if enabled else "⏸ disabled"
|
||||
return (
|
||||
f"<b>👤 {html.escape(name)}</b>\n\n"
|
||||
f"Status: <b>{status_line}</b>\n"
|
||||
f"Secret: <code>{html.escape(secret)}</code>\n\n"
|
||||
f"<b>Ссылка:</b>\n<code>{link_line}</code>\n"
|
||||
f"{details}"
|
||||
@@ -1673,23 +1726,31 @@ async def cb_user_view(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
|
||||
await query.answer()
|
||||
user_id = _uid(update)
|
||||
name = query.data.removeprefix("user_view_")
|
||||
users = load_telemt_users()
|
||||
secret = users.get(name)
|
||||
if not secret:
|
||||
users = load_user_records()
|
||||
record = users.get(name)
|
||||
if not record:
|
||||
await safe_edit_message(
|
||||
query,
|
||||
"❌ Пользователь не найден.",
|
||||
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_users")]]),
|
||||
)
|
||||
return
|
||||
enabled = bool(record.get("enabled"))
|
||||
secret = str(record.get("secret", ""))
|
||||
|
||||
buttons = [
|
||||
[InlineKeyboardButton("⏸ Отключить" if enabled else "▶️ Включить", callback_data=f"user_toggle_{name}")],
|
||||
[InlineKeyboardButton("🗑 Удалить", callback_data=f"user_del_{name}")],
|
||||
[InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_users")],
|
||||
]
|
||||
if name == "main":
|
||||
buttons = [
|
||||
[InlineKeyboardButton("🔒 Main key", callback_data=f"user_view_{name}")],
|
||||
[InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_users")],
|
||||
]
|
||||
await safe_edit_message(
|
||||
query,
|
||||
await _user_detail_text(name, secret),
|
||||
await _user_detail_text(name, secret, enabled),
|
||||
reply_markup=InlineKeyboardMarkup(buttons),
|
||||
parse_mode="HTML",
|
||||
disable_web_page_preview=True,
|
||||
@@ -1714,6 +1775,45 @@ async def cb_user_add(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Non
|
||||
)
|
||||
|
||||
|
||||
async def cb_user_toggle(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
user_id = _uid(update)
|
||||
name = query.data.removeprefix("user_toggle_")
|
||||
if name == "main":
|
||||
await query.answer("main нельзя отключить", show_alert=True)
|
||||
return
|
||||
active = load_telemt_users()
|
||||
disabled = load_disabled_users()
|
||||
records = load_user_records()
|
||||
record = records.get(name)
|
||||
if not record:
|
||||
await query.answer("Ключ не найден", show_alert=True)
|
||||
return
|
||||
enabled = not bool(record.get("enabled"))
|
||||
secret = str(record.get("secret", ""))
|
||||
if enabled:
|
||||
disabled.pop(name, None)
|
||||
active[name] = secret
|
||||
else:
|
||||
active.pop(name, None)
|
||||
disabled[name] = secret
|
||||
if enabled:
|
||||
saved = save_telemt_users(active) and save_disabled_users(disabled)
|
||||
else:
|
||||
saved = save_disabled_users(disabled) and save_telemt_users(active)
|
||||
if not saved:
|
||||
await safe_edit_message(query, "❌ Не удалось сохранить состояние ключа")
|
||||
return
|
||||
await refresh_telemt_after_user_change()
|
||||
await safe_edit_message(
|
||||
query,
|
||||
f"{'✅ Ключ включён' if enabled else '⏸ Ключ отключён'}: <code>{html.escape(name)}</code>",
|
||||
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(_t(user_id, "btn_back"), callback_data=f"user_view_{name}")]]),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
|
||||
async def cb_user_delete(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
@@ -1735,12 +1835,15 @@ async def cb_user_delete_confirm(update: Update, context: ContextTypes.DEFAULT_T
|
||||
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:
|
||||
active = load_telemt_users()
|
||||
disabled = load_disabled_users()
|
||||
records = load_user_records()
|
||||
if name == "main" or name not in records:
|
||||
await query.answer("Нельзя удалить этот ключ", show_alert=True)
|
||||
return
|
||||
users.pop(name, None)
|
||||
if not save_telemt_users(users):
|
||||
active.pop(name, None)
|
||||
disabled.pop(name, None)
|
||||
if not save_telemt_users(active) or not save_disabled_users(disabled):
|
||||
await safe_edit_message(query, "❌ Не удалось сохранить config.toml")
|
||||
return
|
||||
await refresh_telemt_after_user_change()
|
||||
@@ -1757,10 +1860,11 @@ async def create_user_from_text(update: Update, context: ContextTypes.DEFAULT_TY
|
||||
if not _USER_NAME_RE.match(name):
|
||||
await update.message.reply_text("❌ Некорректное имя. Используйте латиницу, цифры, _ . - и до 48 символов.")
|
||||
return
|
||||
users = load_telemt_users()
|
||||
if name in users:
|
||||
records = load_user_records()
|
||||
if name in records:
|
||||
await update.message.reply_text("❌ Такой пользователь уже есть.")
|
||||
return
|
||||
users = load_telemt_users()
|
||||
secret = hashlib.sha256(f"{name}:{time.time()}:{os.urandom(16).hex()}".encode()).hexdigest()[:32]
|
||||
users[name] = secret
|
||||
if not save_telemt_users(users):
|
||||
@@ -1773,7 +1877,7 @@ async def create_user_from_text(update: Update, context: ContextTypes.DEFAULT_TY
|
||||
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),
|
||||
reply_markup=_users_keyboard(load_user_records(), user_id),
|
||||
parse_mode="HTML",
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
@@ -2332,8 +2436,8 @@ async def cmd_deladmin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
|
||||
|
||||
|
||||
def get_promo_text() -> str:
|
||||
"""Return promo text with 2 hosters + donate."""
|
||||
return (
|
||||
"""Return promo text with 2 hosters, optional YouTube link and donate."""
|
||||
text = (
|
||||
"<b>💰 Хостинг #1 — скидка до 60%</b>\n"
|
||||
f"<a href='{PROMO_LINK_1}'>{PROMO_LINK_1}</a>\n\n"
|
||||
"<b>Промокоды:</b>\n"
|
||||
@@ -2349,6 +2453,13 @@ def get_promo_text() -> str:
|
||||
"<b>☕ Донат / Чаевые</b>\n"
|
||||
f"<a href='{TIP_LINK}'>{TIP_LINK}</a>"
|
||||
)
|
||||
if YOUTUBE_LINK:
|
||||
text += (
|
||||
"\n\n━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
|
||||
"<b>▶ YouTube-канал</b>\n"
|
||||
f"<a href='{html.escape(YOUTUBE_LINK)}'>{html.escape(YOUTUBE_LINK)}</a>"
|
||||
)
|
||||
return text
|
||||
|
||||
|
||||
def should_show_promo_bot() -> bool:
|
||||
@@ -2393,7 +2504,7 @@ async def cb_menu_credits(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
|
||||
text = (
|
||||
f"<b>ℹ️ Credits & Acknowledgements</b>\n\n"
|
||||
f"<b>GoTelegram v{GOTELEGRAM_VERSION}</b>\n\n"
|
||||
f"<b>goTelegram Pro v{GOTELEGRAM_VERSION}</b>\n\n"
|
||||
f"Built with love for the Telegram community\n\n"
|
||||
f"<b>Special thanks to:</b>\n\n"
|
||||
f"🙏 <b>telemt</b> - MTProxy engine\n"
|
||||
@@ -2405,7 +2516,7 @@ async def cb_menu_credits(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
f"🚀 <b>Start Bootstrap</b> - Bootstrap templates\n"
|
||||
f" Professional design framework\n\n"
|
||||
f"💬 <b>Community</b> - Your feedback & support\n\n"
|
||||
f"<i>GoTelegram is open-source and community-driven</i>"
|
||||
f"<i>goTelegram Pro is open-source and community-driven</i>"
|
||||
)
|
||||
|
||||
keyboard = InlineKeyboardMarkup(
|
||||
@@ -2425,7 +2536,7 @@ async def cb_menu_remove(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
await query.answer()
|
||||
|
||||
text = (
|
||||
"<b>⚠️ Remove GoTelegram</b>\n\n"
|
||||
"<b>⚠️ Remove goTelegram Pro</b>\n\n"
|
||||
"This will completely remove the installation.\n"
|
||||
"Are you sure?"
|
||||
)
|
||||
@@ -2443,7 +2554,7 @@ async def cb_remove_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE)
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
|
||||
await safe_edit_message(query,"⏳ Removing GoTelegram...")
|
||||
await safe_edit_message(query,"⏳ Removing goTelegram Pro...")
|
||||
|
||||
# Stop service
|
||||
await sh("systemctl", "stop", TELEMT_SERVICE)
|
||||
@@ -2452,7 +2563,7 @@ async def cb_remove_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE)
|
||||
for path in ["/opt/gotelegram", WEBSITE_ROOT]:
|
||||
await sh("rm", "-rf", path)
|
||||
|
||||
text = "✅ GoTelegram removed successfully"
|
||||
text = "✅ goTelegram Pro removed successfully"
|
||||
keyboard = InlineKeyboardMarkup(
|
||||
[[InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")]]
|
||||
)
|
||||
@@ -2617,6 +2728,8 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
await cb_user_add(update, context)
|
||||
elif data.startswith("user_view_"):
|
||||
await cb_user_view(update, context)
|
||||
elif data.startswith("user_toggle_"):
|
||||
await cb_user_toggle(update, context)
|
||||
elif data.startswith("user_del_yes_"):
|
||||
await cb_user_delete_confirm(update, context)
|
||||
elif data.startswith("user_del_"):
|
||||
@@ -2663,7 +2776,7 @@ async def handle_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||
if not ok:
|
||||
await update.message.reply_text(_t(user_id, info), parse_mode="HTML")
|
||||
return
|
||||
# Success — record in GoTelegram config. Use "template_id" (canonical
|
||||
# Success — record in goTelegram Pro config. Use "template_id" (canonical
|
||||
# field name written by install.sh/save_gotelegram_config).
|
||||
config = load_json(GOTELEGRAM_CONFIG) or {}
|
||||
config["template_id"] = tpl_id
|
||||
@@ -2734,7 +2847,7 @@ def main() -> None:
|
||||
application.add_error_handler(error_handler)
|
||||
|
||||
# Run the bot
|
||||
logger.info(f"GoTelegram v{GOTELEGRAM_VERSION} bot starting...")
|
||||
logger.info(f"goTelegram Pro v{GOTELEGRAM_VERSION} bot starting...")
|
||||
application.run_polling(allowed_updates=Update.ALL_TYPES)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user