mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-05-19 11:26:03 +00:00
v2.5.0: add QR import and backup scheduling
This commit is contained in:
@@ -20,6 +20,7 @@ Production-quality Telegram bot for managing MTProxy (telemt engine) on Linux se
|
||||
|
||||
- **Template Browsing** - Browse categories → templates → preview → install
|
||||
- **Per-user MTProxy Keys** - Manage telemt `[access.users]` from inline bot menus
|
||||
- **Per-user QR Import** - Show QR codes for every Telegram proxy key
|
||||
- **Local Web Admin** - Shows SSH tunnel instructions for the 127.0.0.1:1984 dashboard
|
||||
- **V1 Migration** - Detects old mtg Docker container and offers migration
|
||||
- **Access Control** - ALLOWED_IDS from .env
|
||||
@@ -96,6 +97,7 @@ WantedBy=multi-user.target
|
||||
- `TELEMT_SERVICE` - `telemt` (systemd service name)
|
||||
- `WEBSITE_ROOT` - `/var/www/gotelegram-site`
|
||||
- `BACKUP_DIR` - `/opt/gotelegram/backups`
|
||||
- `BACKUP_SCHEDULE_FILE` - `/opt/gotelegram/backup_schedule.json`
|
||||
- `TEMPLATES_CATALOG` - `/opt/gotelegram/templates_catalog.json`
|
||||
|
||||
## Architecture
|
||||
@@ -114,6 +116,7 @@ Organized by feature:
|
||||
- Installation (quick/stealth modes)
|
||||
- Status monitoring
|
||||
- Backup/restore
|
||||
- Backup schedules: off, daily, weekly, monthly
|
||||
- SSL management
|
||||
- Updates
|
||||
- Removal
|
||||
|
||||
@@ -15,6 +15,7 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -111,6 +112,7 @@ TELEMT_CONFIG = "/etc/telemt/config.toml"
|
||||
TELEMT_SERVICE = "telemt"
|
||||
WEBSITE_ROOT = "/var/www/gotelegram-site"
|
||||
BACKUP_DIR = "/opt/gotelegram/backups"
|
||||
BACKUP_SCHEDULE_FILE = "/opt/gotelegram/backup_schedule.json"
|
||||
TEMPLATES_CATALOG = "/opt/gotelegram/templates_catalog.json"
|
||||
INSTALL_SH = "/opt/gotelegram/install.sh"
|
||||
|
||||
@@ -1846,12 +1848,14 @@ async def cb_user_view(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
|
||||
|
||||
buttons = [
|
||||
[InlineKeyboardButton("⏸ Отключить" if enabled else "▶️ Включить", callback_data=f"user_toggle_{name}")],
|
||||
[InlineKeyboardButton("📷 QR", callback_data=f"user_qr_{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("📷 QR", callback_data=f"user_qr_{name}")],
|
||||
[InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_users")],
|
||||
]
|
||||
await safe_edit_message(
|
||||
@@ -1863,6 +1867,48 @@ async def cb_user_view(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
|
||||
)
|
||||
|
||||
|
||||
async def cb_user_qr(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
user_id = _uid(update)
|
||||
name = query.data.removeprefix("user_qr_")
|
||||
users = load_user_records()
|
||||
record = users.get(name)
|
||||
if not record:
|
||||
await query.answer("Ключ не найден", show_alert=True)
|
||||
return
|
||||
link = await get_proxy_link_for_secret(str(record.get("secret", "")))
|
||||
if not link:
|
||||
await query.answer("Ссылка недоступна", show_alert=True)
|
||||
return
|
||||
|
||||
qr_file = f"/tmp/gotelegram_user_qr_{hashlib.sha256(name.encode()).hexdigest()[:10]}.png"
|
||||
code, _, _ = await sh("which", "qrencode")
|
||||
if code == 0:
|
||||
code, _, _ = await sh("qrencode", "-o", qr_file, link)
|
||||
if code == 0 and os.path.exists(qr_file):
|
||||
try:
|
||||
with open(qr_file, "rb") as f:
|
||||
await query.message.reply_photo(
|
||||
photo=f,
|
||||
caption=f"<b>📷 QR: {html.escape(name)}</b>\n\n<code>{html.escape(link)}</code>",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
os.remove(qr_file)
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
await safe_edit_message(
|
||||
query,
|
||||
f"<b>🔗 {html.escape(name)}</b>\n\n<code>{html.escape(link)}</code>",
|
||||
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(_t(user_id, "btn_back"), callback_data=f"user_view_{name}")]]),
|
||||
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()
|
||||
@@ -2046,34 +2092,146 @@ async def cb_menu_logs(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
|
||||
# BACKUP & RESTORE
|
||||
# ============================================================================
|
||||
|
||||
def list_backup_names(limit: int = 10) -> List[str]:
|
||||
try:
|
||||
if not os.path.exists(BACKUP_DIR):
|
||||
return []
|
||||
names = [
|
||||
f for f in os.listdir(BACKUP_DIR)
|
||||
if f.endswith((".tar.gz", ".tar.gz.enc")) and not f.endswith(".sha256")
|
||||
]
|
||||
return sorted(names, reverse=True)[:limit]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def safe_backup_path(name: str) -> Optional[str]:
|
||||
raw = os.path.basename(str(name or "").strip())
|
||||
if raw != name or not raw.endswith((".tar.gz", ".tar.gz.enc")) or raw.endswith(".sha256"):
|
||||
return None
|
||||
path = os.path.abspath(os.path.join(BACKUP_DIR, raw))
|
||||
base = os.path.abspath(BACKUP_DIR)
|
||||
if os.path.dirname(path) != base or not os.path.exists(path):
|
||||
return None
|
||||
return path
|
||||
|
||||
|
||||
def backup_schedule_state() -> Dict[str, Any]:
|
||||
raw = load_json(BACKUP_SCHEDULE_FILE) or {}
|
||||
if not isinstance(raw, dict):
|
||||
raw = {}
|
||||
frequency = str(raw.get("frequency") or "off")
|
||||
if frequency not in {"off", "daily", "weekly", "monthly"}:
|
||||
frequency = "off"
|
||||
return {
|
||||
"frequency": frequency,
|
||||
"calendar": raw.get("calendar") or "",
|
||||
"updated_at": raw.get("updated_at") or "",
|
||||
}
|
||||
|
||||
|
||||
async def run_full_backup() -> Tuple[bool, str]:
|
||||
script = (
|
||||
"source /opt/gotelegram/lib/common.sh; "
|
||||
"source /opt/gotelegram/lib/i18n.sh; "
|
||||
"source /opt/gotelegram/lib/telemt.sh; "
|
||||
"source /opt/gotelegram/lib/website.sh; "
|
||||
"source /opt/gotelegram/lib/backup.sh; "
|
||||
"load_language \"$(detect_language 2>/dev/null || echo en)\"; "
|
||||
"create_backup \"\"; "
|
||||
"cleanup_old_backups 30"
|
||||
)
|
||||
code, stdout, stderr = await sh("bash", "-lc", script, timeout=240)
|
||||
message = (stdout.strip().splitlines()[-1:] or stderr.strip().splitlines()[-1:] or [""])[0]
|
||||
return code == 0, message
|
||||
|
||||
|
||||
async def set_full_backup_schedule(frequency: str) -> Tuple[bool, str]:
|
||||
if frequency not in {"off", "daily", "weekly", "monthly"}:
|
||||
return False, "unsupported schedule"
|
||||
script = (
|
||||
"source /opt/gotelegram/lib/common.sh; "
|
||||
"source /opt/gotelegram/lib/i18n.sh; "
|
||||
"source /opt/gotelegram/lib/backup.sh; "
|
||||
"load_language \"$(detect_language 2>/dev/null || echo en)\"; "
|
||||
f"set_backup_schedule {shlex.quote(frequency)}"
|
||||
)
|
||||
code, stdout, stderr = await sh("bash", "-lc", script, timeout=120)
|
||||
message = (stdout.strip().splitlines()[-1:] or stderr.strip().splitlines()[-1:] or [""])[0]
|
||||
return code == 0, message
|
||||
|
||||
|
||||
async def launch_full_restore(backup_path: str) -> None:
|
||||
quoted_path = shlex.quote(backup_path)
|
||||
script = (
|
||||
"sleep 1; "
|
||||
"source /opt/gotelegram/lib/common.sh; "
|
||||
"source /opt/gotelegram/lib/i18n.sh; "
|
||||
"source /opt/gotelegram/lib/telemt.sh; "
|
||||
"source /opt/gotelegram/lib/website.sh; "
|
||||
"source /opt/gotelegram/lib/backup.sh; "
|
||||
"load_language \"$(detect_language 2>/dev/null || echo en)\"; "
|
||||
"create_backup \"\" >/dev/null 2>&1 || true; "
|
||||
f"restore_backup {quoted_path} \"\" yes; "
|
||||
"cleanup_old_backups 30"
|
||||
)
|
||||
await asyncio.create_subprocess_exec(
|
||||
"bash",
|
||||
"-lc",
|
||||
script,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
start_new_session=True,
|
||||
)
|
||||
|
||||
|
||||
async def cb_menu_backup(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Backup menu."""
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
user_id = _uid(update)
|
||||
backups = list_backup_names()
|
||||
schedule = backup_schedule_state()
|
||||
labels = {
|
||||
"off": "выключено" if get_user_lang(user_id) == "ru" else "off",
|
||||
"daily": "каждый день" if get_user_lang(user_id) == "ru" else "daily",
|
||||
"weekly": "каждую неделю" if get_user_lang(user_id) == "ru" else "weekly",
|
||||
"monthly": "каждый месяц" if get_user_lang(user_id) == "ru" else "monthly",
|
||||
}
|
||||
|
||||
# List existing backups
|
||||
backups = []
|
||||
try:
|
||||
if os.path.exists(BACKUP_DIR):
|
||||
backups = sorted(
|
||||
[f for f in os.listdir(BACKUP_DIR)
|
||||
if f.endswith((".tar.gz", ".tar.gz.enc")) and not f.endswith(".sha256")],
|
||||
reverse=True,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
buttons = [[InlineKeyboardButton("💾 Create Backup", callback_data="backup_create")]]
|
||||
buttons = [
|
||||
[InlineKeyboardButton("💾 Создать сейчас" if get_user_lang(user_id) == "ru" else "💾 Create now", callback_data="backup_create")],
|
||||
[
|
||||
InlineKeyboardButton("◯ Выкл" if get_user_lang(user_id) == "ru" else "◯ Off", callback_data="backup_schedule_off"),
|
||||
InlineKeyboardButton("☀ День" if get_user_lang(user_id) == "ru" else "☀ Daily", callback_data="backup_schedule_daily"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton("◷ Неделя" if get_user_lang(user_id) == "ru" else "◷ Weekly", callback_data="backup_schedule_weekly"),
|
||||
InlineKeyboardButton("◴ Месяц" if get_user_lang(user_id) == "ru" else "◴ Monthly", callback_data="backup_schedule_monthly"),
|
||||
],
|
||||
]
|
||||
|
||||
if backups:
|
||||
buttons.append(
|
||||
[InlineKeyboardButton("📋 List Backups", callback_data="backup_list")]
|
||||
buttons.append([InlineKeyboardButton("📋 Список" if get_user_lang(user_id) == "ru" else "📋 List", callback_data="backup_list")])
|
||||
buttons.append([InlineKeyboardButton("↩️ Восстановить" if get_user_lang(user_id) == "ru" else "↩️ Restore", callback_data="menu_restore")])
|
||||
|
||||
buttons.append([InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_main")])
|
||||
|
||||
if get_user_lang(user_id) == "ru":
|
||||
text = (
|
||||
"<b>💾 Бекапы</b>\n\n"
|
||||
f"Файлов: <code>{len(backups)}</code>\n"
|
||||
f"Расписание: <b>{labels.get(schedule['frequency'], schedule['frequency'])}</b>\n\n"
|
||||
"В бекап входит: telemt config, настройки goTelegram, ключи, отключённые ключи, сайт, шаблоны, SSL, бот, админка и история трафика."
|
||||
)
|
||||
else:
|
||||
text = (
|
||||
"<b>💾 Backups</b>\n\n"
|
||||
f"Files: <code>{len(backups)}</code>\n"
|
||||
f"Schedule: <b>{labels.get(schedule['frequency'], schedule['frequency'])}</b>\n\n"
|
||||
"Backups include telemt config, goTelegram settings, keys, disabled keys, site, templates, SSL, bot, admin panel and traffic history."
|
||||
)
|
||||
|
||||
buttons.append([InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")])
|
||||
|
||||
text = f"<b>💾 Backup Management</b>\n\nExisting backups: {len(backups)}"
|
||||
keyboard = InlineKeyboardMarkup(buttons)
|
||||
await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML")
|
||||
|
||||
@@ -2082,54 +2240,60 @@ async def cb_backup_create(update: Update, context: ContextTypes.DEFAULT_TYPE) -
|
||||
"""Create backup."""
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
user_id = _uid(update)
|
||||
|
||||
await safe_edit_message(query,"⏳ Creating backup...")
|
||||
await safe_edit_message(query, "⏳ Создаю полный бекап..." if get_user_lang(user_id) == "ru" else "⏳ Creating full backup...")
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
backup_file = os.path.join(BACKUP_DIR, f"backup_{timestamp}.tar.gz")
|
||||
|
||||
os.makedirs(BACKUP_DIR, exist_ok=True)
|
||||
code, _, stderr = await sh(
|
||||
"tar", "-czf", backup_file, GOTELEGRAM_CONFIG, TELEMT_CONFIG
|
||||
)
|
||||
|
||||
if code == 0:
|
||||
text = f"✅ Backup created:\n<code>{html.escape(backup_file)}</code>"
|
||||
ok, message = await run_full_backup()
|
||||
if ok:
|
||||
text = f"✅ Бекап создан:\n<code>{html.escape(message)}</code>" if get_user_lang(user_id) == "ru" else f"✅ Backup created:\n<code>{html.escape(message)}</code>"
|
||||
else:
|
||||
text = f"❌ Backup failed:\n<code>{html.escape(stderr[:500])}</code>"
|
||||
text = f"❌ Ошибка бекапа:\n<code>{html.escape(message[:500])}</code>" if get_user_lang(user_id) == "ru" else f"❌ Backup failed:\n<code>{html.escape(message[:500])}</code>"
|
||||
|
||||
keyboard = InlineKeyboardMarkup(
|
||||
[[InlineKeyboardButton("« Back", callback_data="menu_backup")]]
|
||||
[[InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_backup")]]
|
||||
)
|
||||
await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML")
|
||||
|
||||
|
||||
async def cb_backup_schedule_set(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
user_id = _uid(update)
|
||||
frequency = query.data.removeprefix("backup_schedule_")
|
||||
await safe_edit_message(query, "⏳ Сохраняю расписание..." if get_user_lang(user_id) == "ru" else "⏳ Saving schedule...")
|
||||
ok, message = await set_full_backup_schedule(frequency)
|
||||
if ok:
|
||||
text = "✅ Расписание обновлено." if get_user_lang(user_id) == "ru" else "✅ Backup schedule updated."
|
||||
else:
|
||||
text = f"❌ {html.escape(message[:500])}"
|
||||
await safe_edit_message(
|
||||
query,
|
||||
text,
|
||||
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_backup")]]),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
|
||||
async def cb_backup_list(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""List backups."""
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
user_id = _uid(update)
|
||||
|
||||
backups = []
|
||||
try:
|
||||
if os.path.exists(BACKUP_DIR):
|
||||
backups = sorted(
|
||||
[f for f in os.listdir(BACKUP_DIR) if f.endswith((".tar.gz", ".tar.gz.enc")) and not f.endswith(".sha256")],
|
||||
reverse=True,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
backups = list_backup_names()
|
||||
|
||||
if not backups:
|
||||
text = "No backups found"
|
||||
text = "Бекапов нет" if get_user_lang(user_id) == "ru" else "No backups found"
|
||||
else:
|
||||
text = "<b>📋 Available Backups</b>\n\n"
|
||||
text = "<b>📋 Доступные бекапы</b>\n\n" if get_user_lang(user_id) == "ru" else "<b>📋 Available Backups</b>\n\n"
|
||||
for backup in backups[:10]:
|
||||
path = os.path.join(BACKUP_DIR, backup)
|
||||
size = os.path.getsize(path) / (1024 * 1024)
|
||||
text += f"<code>{html.escape(backup)}</code> ({size:.2f} MB)\n"
|
||||
|
||||
keyboard = InlineKeyboardMarkup(
|
||||
[[InlineKeyboardButton("« Back", callback_data="menu_backup")]]
|
||||
[[InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_backup")]]
|
||||
)
|
||||
await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML")
|
||||
|
||||
@@ -2138,24 +2302,17 @@ async def cb_menu_restore(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
"""Restore menu."""
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
user_id = _uid(update)
|
||||
|
||||
backups = []
|
||||
try:
|
||||
if os.path.exists(BACKUP_DIR):
|
||||
backups = sorted(
|
||||
[f for f in os.listdir(BACKUP_DIR) if f.endswith((".tar.gz", ".tar.gz.enc")) and not f.endswith(".sha256")],
|
||||
reverse=True,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
backups = list_backup_names()
|
||||
|
||||
if not backups:
|
||||
text = "❌ No backups available"
|
||||
text = "❌ Нет доступных бекапов" if get_user_lang(user_id) == "ru" else "❌ No backups available"
|
||||
keyboard = InlineKeyboardMarkup(
|
||||
[[InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")]]
|
||||
[[InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_main")]]
|
||||
)
|
||||
else:
|
||||
text = "Select backup to restore:"
|
||||
text = "Выберите бекап для восстановления:" if get_user_lang(user_id) == "ru" else "Select backup to restore:"
|
||||
buttons = []
|
||||
for i, backup in enumerate(backups[:10]):
|
||||
buttons.append(
|
||||
@@ -2165,7 +2322,7 @@ async def cb_menu_restore(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
)
|
||||
]
|
||||
)
|
||||
buttons.append([InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")])
|
||||
buttons.append([InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_main")])
|
||||
keyboard = InlineKeyboardMarkup(buttons)
|
||||
# Store backup list in user_data for retrieval
|
||||
context.user_data["backup_list"] = backups[:10]
|
||||
@@ -2174,12 +2331,16 @@ async def cb_menu_restore(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
|
||||
|
||||
async def cb_restore_backup(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Execute backup restoration."""
|
||||
"""Confirm or execute backup restoration."""
|
||||
query = update.callback_query
|
||||
data = query.data
|
||||
user_id = _uid(update)
|
||||
|
||||
try:
|
||||
idx = int(data.removeprefix("restore_idx_"))
|
||||
if data.startswith("restore_yes_"):
|
||||
idx = int(data.removeprefix("restore_yes_"))
|
||||
else:
|
||||
idx = int(data.removeprefix("restore_idx_"))
|
||||
except ValueError:
|
||||
await query.answer("Invalid backup selection")
|
||||
return
|
||||
@@ -2193,24 +2354,40 @@ async def cb_restore_backup(update: Update, context: ContextTypes.DEFAULT_TYPE)
|
||||
backup_path = os.path.join(BACKUP_DIR, backup_name)
|
||||
|
||||
await query.answer()
|
||||
await safe_edit_message(query,f"⏳ Restoring from {html.escape(backup_name)}...")
|
||||
|
||||
if not os.path.exists(backup_path):
|
||||
text = "❌ Backup file not found"
|
||||
else:
|
||||
# Simple restore: extract tar to overwrite configs
|
||||
code, _, stderr = await sh(
|
||||
"tar", "-xzf", backup_path, "-C", "/", timeout=60
|
||||
if data.startswith("restore_idx_"):
|
||||
text = (
|
||||
f"Восстановить <code>{html.escape(backup_name)}</code>?\n\n"
|
||||
"Перед восстановлением будет создан свежий safety-бекап."
|
||||
) if get_user_lang(user_id) == "ru" else (
|
||||
f"Restore <code>{html.escape(backup_name)}</code>?\n\n"
|
||||
"A fresh safety backup will be created before restoring."
|
||||
)
|
||||
keyboard = InlineKeyboardMarkup([
|
||||
[InlineKeyboardButton("✅ Восстановить" if get_user_lang(user_id) == "ru" else "✅ Restore", callback_data=f"restore_yes_{idx}")],
|
||||
[InlineKeyboardButton(_t(user_id, "btn_cancel"), callback_data="menu_restore")],
|
||||
])
|
||||
await safe_edit_message(query, text, reply_markup=keyboard, parse_mode="HTML")
|
||||
return
|
||||
|
||||
await safe_edit_message(query, f"⏳ Восстановление запущено: {html.escape(backup_name)}..." if get_user_lang(user_id) == "ru" else f"⏳ Restore started: {html.escape(backup_name)}...")
|
||||
|
||||
safe_path = safe_backup_path(backup_name)
|
||||
if not safe_path:
|
||||
text = "❌ Файл бекапа не найден" if get_user_lang(user_id) == "ru" else "❌ Backup file not found"
|
||||
elif safe_path.endswith(".enc"):
|
||||
text = "❌ Зашифрованный бекап пока восстанавливается через CLI: gotelegram → Восстановить." if get_user_lang(user_id) == "ru" else "❌ Encrypted backups are restored from CLI for now: gotelegram → Restore."
|
||||
else:
|
||||
await launch_full_restore(safe_path)
|
||||
text = (
|
||||
f"✅ Восстановление <code>{html.escape(backup_name)}</code> запущено в фоне.\n"
|
||||
"Сервисы могут перезапуститься, через минуту откройте статус."
|
||||
) if get_user_lang(user_id) == "ru" else (
|
||||
f"✅ Restore for <code>{html.escape(backup_name)}</code> started in background.\n"
|
||||
"Services may restart; check status in about a minute."
|
||||
)
|
||||
if code == 0:
|
||||
# Restart services
|
||||
await sh("systemctl", "restart", TELEMT_SERVICE)
|
||||
text = f"✅ Restored from {html.escape(backup_name)}"
|
||||
else:
|
||||
text = f"❌ Restore failed:\n<code>{html.escape(stderr[:500])}</code>"
|
||||
|
||||
keyboard = InlineKeyboardMarkup(
|
||||
[[InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")]]
|
||||
[[InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_main")]]
|
||||
)
|
||||
await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML")
|
||||
|
||||
@@ -2814,6 +2991,10 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
"change_pro": cb_change_pro,
|
||||
"install_migrate": cb_install_migrate,
|
||||
"menu_stats": cb_menu_stats,
|
||||
"backup_schedule_off": cb_backup_schedule_set,
|
||||
"backup_schedule_daily": cb_backup_schedule_set,
|
||||
"backup_schedule_weekly": cb_backup_schedule_set,
|
||||
"backup_schedule_monthly": cb_backup_schedule_set,
|
||||
}
|
||||
|
||||
# Custom git template URL prompt
|
||||
@@ -2839,6 +3020,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_qr_"):
|
||||
await cb_user_qr(update, context)
|
||||
elif data.startswith("user_toggle_"):
|
||||
await cb_user_toggle(update, context)
|
||||
elif data.startswith("user_del_yes_"):
|
||||
@@ -2851,7 +3034,7 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
await cb_pro_template(update, context)
|
||||
elif data.startswith("pro_confirm_"):
|
||||
await cb_pro_confirm(update, context)
|
||||
elif data.startswith("restore_idx_"):
|
||||
elif data.startswith("restore_idx_") or data.startswith("restore_yes_"):
|
||||
await cb_restore_backup(update, context)
|
||||
elif data in handlers:
|
||||
await handlers[data](update, context)
|
||||
|
||||
Reference in New Issue
Block a user