mirror of
https://github.com/andrey271192/PCAtelegram_web.git
synced 2026-09-21 12:01:56 +00:00
feat: split out GoTelegram project
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
work/
|
||||||
|
dist/
|
||||||
59
README.md
Normal file
59
README.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# GoTelegram Pro
|
||||||
|
|
||||||
|
Отдельный проект GoTelegram Pro: MTProxy на базе `telemt`, Telegram-бот, локальная web-admin панель, статистика, backup/restore, шаблоны сайта.
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
На сервере под `root`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/andrey271192/gotelegram/main/bootstrap.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Если репозиторий private, первый `curl` тоже должен получить GitHub token:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||||
|
https://raw.githubusercontent.com/andrey271192/gotelegram/main/bootstrap.sh | \
|
||||||
|
GITHUB_TOKEN="$GITHUB_TOKEN" bash
|
||||||
|
```
|
||||||
|
|
||||||
|
После установки команда доступна как:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gotelegram
|
||||||
|
```
|
||||||
|
|
||||||
|
## Локальная проверка без GitHub
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rsync -a ./ root@SERVER:/opt/gotelegram/
|
||||||
|
ssh root@SERVER 'chmod +x /opt/gotelegram/install.sh /opt/gotelegram/install_gotelegram_bot.sh /opt/gotelegram/lib/*.sh && ln -sf /opt/gotelegram/install.sh /usr/local/bin/gotelegram && gotelegram'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Состав
|
||||||
|
|
||||||
|
| Путь | Назначение |
|
||||||
|
| --- | --- |
|
||||||
|
| `bootstrap.sh` | загружает файлы проекта в `/opt/gotelegram` и запускает меню |
|
||||||
|
| `install.sh` | основное CLI-меню GoTelegram |
|
||||||
|
| `install_gotelegram_bot.sh` | отдельная установка Telegram-бота |
|
||||||
|
| `lib/` | общие функции, telemt, nginx/site, stats, backup, i18n |
|
||||||
|
| `gotelegram-bot/` | Python Telegram bot |
|
||||||
|
| `admin-web/` | локальная web-admin панель |
|
||||||
|
| `templates_catalog.json` | каталог HTML-шаблонов |
|
||||||
|
|
||||||
|
## Переменные
|
||||||
|
|
||||||
|
| Переменная | По умолчанию | Описание |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `GOTELEGRAM_BASE` | `https://raw.githubusercontent.com/andrey271192/gotelegram/main` | база загрузки файлов |
|
||||||
|
| `GOTELEGRAM_INSTALL_DIR` | `/opt/gotelegram` | путь установки |
|
||||||
|
| `GITHUB_TOKEN` / `GH_TOKEN` | пусто | token для private GitHub raw downloads |
|
||||||
|
|
||||||
|
## Проверки
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -n bootstrap.sh install.sh install_gotelegram_bot.sh lib/*.sh lib/lang/*.sh
|
||||||
|
rg -i "old-brand-name" .
|
||||||
|
```
|
||||||
1665
admin-web/server.py
Normal file
1665
admin-web/server.py
Normal file
File diff suppressed because it is too large
Load Diff
1755
admin-web/static/app.js
Normal file
1755
admin-web/static/app.js
Normal file
File diff suppressed because it is too large
Load Diff
398
admin-web/static/index.html
Normal file
398
admin-web/static/index.html
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>goTelegram Pro Admin</title>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var stored = localStorage.getItem("gotelegram-theme");
|
||||||
|
var theme = stored || (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
|
||||||
|
document.documentElement.dataset.theme = theme;
|
||||||
|
}());
|
||||||
|
</script>
|
||||||
|
<link rel="stylesheet" href="/styles.css?v=2.5.0-admin18">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-shell">
|
||||||
|
<aside class="sidebar" id="sidebar">
|
||||||
|
<div class="brand">
|
||||||
|
<div class="brand-mark">GT</div>
|
||||||
|
<div>
|
||||||
|
<strong>goTelegram Pro</strong>
|
||||||
|
<span data-i18n="brandSubtitle">Local Admin</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav-tabs" aria-label="Admin sections" data-i18n-aria-label="ariaAdminSections">
|
||||||
|
<button type="button" class="nav-item active" data-nav="dashboard"><span class="nav-icon">⌁</span><span data-i18n="navDashboard">Dashboard</span></button>
|
||||||
|
<button type="button" class="nav-item" data-nav="traffic"><span class="nav-icon">⇅</span><span data-i18n="navTraffic">Traffic</span></button>
|
||||||
|
<button type="button" class="nav-item" data-nav="keys"><span class="nav-icon">⚿</span><span data-i18n="navKeys">Keys</span></button>
|
||||||
|
<button type="button" class="nav-item" data-nav="backups"><span class="nav-icon">▣</span><span data-i18n="navBackups">Backups</span></button>
|
||||||
|
<button type="button" class="nav-item" data-nav="logs"><span class="nav-icon">☰</span><span data-i18n="navLogs">Logs</span></button>
|
||||||
|
<button type="button" class="nav-item" data-nav="settings"><span class="nav-icon">⚙</span><span data-i18n="navSettings">Settings</span></button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="sidebar-foot">
|
||||||
|
<span id="sidebarVersion">v--</span>
|
||||||
|
<span id="sidebarBind">127.0.0.1:1984</span>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="workspace">
|
||||||
|
<header class="topbar">
|
||||||
|
<button id="menuBtn" class="icon-btn mobile-only" type="button" aria-label="Menu" data-i18n-aria-label="ariaMenu">☰</button>
|
||||||
|
<div class="title-block">
|
||||||
|
<p class="eyebrow" id="pageKicker">Local Admin</p>
|
||||||
|
<h1 id="pageTitle">Dashboard</h1>
|
||||||
|
<small id="lastRefresh">--</small>
|
||||||
|
</div>
|
||||||
|
<div class="top-actions">
|
||||||
|
<select id="languageSelect" class="language-select" aria-label="Language" data-i18n-aria-label="ariaLanguage">
|
||||||
|
<option value="en">EN</option>
|
||||||
|
<option value="ru">RU</option>
|
||||||
|
</select>
|
||||||
|
<button id="themeToggle" class="ghost" type="button">Theme</button>
|
||||||
|
<button id="refreshBtn" type="button" data-i18n="refresh">Refresh</button>
|
||||||
|
<button id="autoRefreshToggle" class="auto-refresh-toggle" type="button" aria-pressed="true" data-i18n-title="autoRefresh" title="Auto refresh">
|
||||||
|
<span class="auto-refresh-icon" aria-hidden="true">↻</span>
|
||||||
|
<span class="auto-refresh-track" aria-hidden="true"><span></span></span>
|
||||||
|
<span class="auto-refresh-state">5s</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="content">
|
||||||
|
<section class="page-panel active" data-page="dashboard">
|
||||||
|
<section class="visual-overview">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">goTelegram Pro</p>
|
||||||
|
<h2 id="visualTitle">Port 443</h2>
|
||||||
|
<p id="visualText">Website, MTProxy and local admin status in one operational view.</p>
|
||||||
|
</div>
|
||||||
|
<div class="port-map" id="port443Map">
|
||||||
|
<div class="port-map-head">
|
||||||
|
<div class="port-badge">
|
||||||
|
<span id="port443Number">443</span>
|
||||||
|
<small id="port443Configured">public</small>
|
||||||
|
</div>
|
||||||
|
<strong id="port443Summary">--</strong>
|
||||||
|
</div>
|
||||||
|
<div id="port443List" class="port-list"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="metric-grid">
|
||||||
|
<article class="metric-card accent-blue">
|
||||||
|
<span data-i18n="metricMode">Mode</span>
|
||||||
|
<strong id="metricMode">--</strong>
|
||||||
|
<small id="metricDomain">--</small>
|
||||||
|
<small id="siteStatus" class="metric-status">--</small>
|
||||||
|
</article>
|
||||||
|
<article class="metric-card accent-green">
|
||||||
|
<span data-i18n="metricKeys">Keys</span>
|
||||||
|
<strong id="metricUsers">0</strong>
|
||||||
|
<small data-i18n="configuredUsers">configured users</small>
|
||||||
|
</article>
|
||||||
|
<article class="metric-card accent-violet">
|
||||||
|
<span data-i18n="metricProxyTraffic">Proxy Traffic</span>
|
||||||
|
<strong id="metricProxyTraffic">0 B</strong>
|
||||||
|
<small id="metricProxyPackets">0 packets</small>
|
||||||
|
</article>
|
||||||
|
<article class="metric-card accent-amber">
|
||||||
|
<span data-i18n="metricSiteTraffic">Site Traffic</span>
|
||||||
|
<strong id="metricSiteTraffic">0 B</strong>
|
||||||
|
<small id="metricSitePackets">0 packets</small>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-two">
|
||||||
|
<section class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow" data-i18n="servicesEyebrow">Services</p>
|
||||||
|
<h2 class="with-help"><span data-i18n="servicesTitle">Service health</span><span class="info-hint" tabindex="0" data-i18n-title="servicesHelp" title="Service health">?</span></h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="service-grid" id="services"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow" data-i18n="runtimeEyebrow">Runtime</p>
|
||||||
|
<h2 class="with-help"><span data-i18n="runtimeTitle">telemt summary</span><span class="info-hint" tabindex="0" data-i18n-title="runtimeHelp" title="Runtime data">?</span></h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="runtimeCards" class="runtime-grid"></div>
|
||||||
|
<div id="runtimeIssues" class="issue-list"></div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="page-panel" data-page="traffic">
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow" data-i18n="trafficEyebrow">Traffic</p>
|
||||||
|
<h2 data-i18n="trafficTitle">History</h2>
|
||||||
|
</div>
|
||||||
|
<div class="panel-actions">
|
||||||
|
<span id="statsHealth" class="status-pill">--</span>
|
||||||
|
<button id="collectStatsBtn" class="ghost" type="button" data-i18n="collectStats" data-i18n-title="collectStatsHelp">Collect</button>
|
||||||
|
<button id="repairStatsBtn" type="button" data-i18n="repairStats" data-i18n-title="repairStatsHelp">Restart collector</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="traffic-summary">
|
||||||
|
<article>
|
||||||
|
<span data-i18n="collector">Collector</span>
|
||||||
|
<strong id="collectorState">--</strong>
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<span data-i18n="lastPoint">Last point</span>
|
||||||
|
<strong id="lastStatsPoint">--</strong>
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<span data-i18n="historyRows">History rows</span>
|
||||||
|
<strong id="historyRows">0</strong>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<div class="traffic-controls">
|
||||||
|
<div class="segmented" id="trafficRange" aria-label="Traffic range" data-i18n-aria-label="ariaTrafficRange">
|
||||||
|
<button type="button" data-traffic-range="15m" data-i18n="range15m">15 min</button>
|
||||||
|
<button type="button" data-traffic-range="1h" data-i18n="range1h">1 hour</button>
|
||||||
|
<button type="button" data-traffic-range="24h" data-i18n="range24h">24 hours</button>
|
||||||
|
<button type="button" data-traffic-range="month" data-i18n="rangeMonth">Month</button>
|
||||||
|
</div>
|
||||||
|
<div class="segmented" id="trafficView" aria-label="Traffic view" data-i18n-aria-label="ariaTrafficView">
|
||||||
|
<button type="button" data-traffic-view="chart" data-i18n="viewChart">Chart</button>
|
||||||
|
<button type="button" data-traffic-view="table" data-i18n="viewRows">Rows</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="trafficChart" class="traffic-chart"></div>
|
||||||
|
<div class="table-wrap" id="trafficTableWrap">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-i18n="tablePeriod">Period</th>
|
||||||
|
<th data-i18n="tableProxyDelta">Proxy delta</th>
|
||||||
|
<th data-i18n="tableSiteDelta">Site delta</th>
|
||||||
|
<th data-i18n="tableProxyTotal">Proxy total</th>
|
||||||
|
<th data-i18n="tableSiteTotal">Site total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="historyTable"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="page-panel" data-page="keys">
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow" data-i18n="keysEyebrow">Access</p>
|
||||||
|
<h2 data-i18n="keysTitle">User keys</h2>
|
||||||
|
</div>
|
||||||
|
<form id="addUserForm" class="inline-form">
|
||||||
|
<input id="userName" autocomplete="off" placeholder="client-name" data-i18n-placeholder="userPlaceholder">
|
||||||
|
<button type="submit" data-i18n="addKey">Add key</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="keys-wrap">
|
||||||
|
<div class="keys-list" id="usersTable" aria-live="polite"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel user-traffic-panel" id="userTrafficPanel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow" data-i18n="userTrafficEyebrow">Per user</p>
|
||||||
|
<h2 id="userTrafficTitle" data-i18n="userTrafficTitle">User traffic</h2>
|
||||||
|
</div>
|
||||||
|
<div class="panel-actions">
|
||||||
|
<span id="userTrafficHealth" class="status-pill">--</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="traffic-summary compact">
|
||||||
|
<article>
|
||||||
|
<span data-i18n="trafficTotal">Total</span>
|
||||||
|
<strong id="userTrafficTotal">--</strong>
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<span data-i18n="currentConnections">Connections</span>
|
||||||
|
<strong id="userTrafficConnections">--</strong>
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<span data-i18n="activeIps">Active IPs</span>
|
||||||
|
<strong id="userTrafficIps">--</strong>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<div class="traffic-controls">
|
||||||
|
<div class="segmented" id="userTrafficRange" aria-label="User traffic range" data-i18n-aria-label="ariaTrafficRange">
|
||||||
|
<button type="button" data-user-traffic-range="15m" data-i18n="range15m">15 min</button>
|
||||||
|
<button type="button" data-user-traffic-range="1h" data-i18n="range1h">1 hour</button>
|
||||||
|
<button type="button" data-user-traffic-range="24h" data-i18n="range24h">24 hours</button>
|
||||||
|
<button type="button" data-user-traffic-range="month" data-i18n="rangeMonth">Month</button>
|
||||||
|
</div>
|
||||||
|
<div class="segmented" id="userTrafficView" aria-label="User traffic view" data-i18n-aria-label="ariaTrafficView">
|
||||||
|
<button type="button" data-user-traffic-view="chart" data-i18n="viewChart">Chart</button>
|
||||||
|
<button type="button" data-user-traffic-view="table" data-i18n="viewRows">Rows</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="userTrafficChart" class="traffic-chart"></div>
|
||||||
|
<div class="table-wrap" id="userTrafficTableWrap">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-i18n="tablePeriod">Period</th>
|
||||||
|
<th data-i18n="tableTrafficDelta">Traffic delta</th>
|
||||||
|
<th data-i18n="tableTrafficTotal">Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="userTrafficTable"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="page-panel" data-page="backups">
|
||||||
|
<div class="grid-two">
|
||||||
|
<section class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow" data-i18n="backupsEyebrow">Snapshots</p>
|
||||||
|
<h2 data-i18n="backupsTitle">Backups</h2>
|
||||||
|
</div>
|
||||||
|
<button id="createBackupBtn" type="button" data-i18n="createBackup">Create backup</button>
|
||||||
|
</div>
|
||||||
|
<div class="backup-schedule">
|
||||||
|
<div>
|
||||||
|
<strong data-i18n="backupScheduleTitle">Automatic backups</strong>
|
||||||
|
<span id="backupScheduleMeta" data-i18n="backupScheduleLoading">Loading schedule...</span>
|
||||||
|
</div>
|
||||||
|
<div class="segmented compact" role="group" aria-label="Backup schedule">
|
||||||
|
<button type="button" data-backup-schedule="off" data-i18n="scheduleOff">Off</button>
|
||||||
|
<button type="button" data-backup-schedule="daily" data-i18n="scheduleDaily">Daily</button>
|
||||||
|
<button type="button" data-backup-schedule="weekly" data-i18n="scheduleWeekly">Weekly</button>
|
||||||
|
<button type="button" data-backup-schedule="monthly" data-i18n="scheduleMonthly">Monthly</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="backup-includes">
|
||||||
|
<strong data-i18n="backupIncludesTitle">Backup contents</strong>
|
||||||
|
<span data-i18n="backupIncludesText">telemt config, goTelegram settings, keys, disabled keys, site, templates, SSL certificates, bot, admin panel and traffic history.</span>
|
||||||
|
</div>
|
||||||
|
<div id="backupsList" class="backup-list"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<aside class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow" data-i18n="eventsEyebrow">Events</p>
|
||||||
|
<h2 data-i18n="eventsTitle">Activity</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="events" class="events-list"></div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="page-panel" data-page="logs">
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow" data-i18n="logsEyebrow">Journal</p>
|
||||||
|
<h2 data-i18n="logsTitle">Logs</h2>
|
||||||
|
</div>
|
||||||
|
<div class="inline-form">
|
||||||
|
<select id="logService">
|
||||||
|
<option value="telemt">telemt</option>
|
||||||
|
<option value="nginx">nginx</option>
|
||||||
|
<option value="gotelegram-bot">bot</option>
|
||||||
|
<option value="gotelegram-stats">stats</option>
|
||||||
|
<option value="gotelegram-admin">admin</option>
|
||||||
|
</select>
|
||||||
|
<button id="loadLogsBtn" type="button" data-i18n="loadLogs">Load</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="logsMeta" class="logs-meta"></div>
|
||||||
|
<pre id="logsBox" class="logs"></pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="page-panel" data-page="settings">
|
||||||
|
<div class="grid-two">
|
||||||
|
<section class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow" data-i18n="settingsEyebrow">Settings</p>
|
||||||
|
<h2 data-i18n="settingsTitle">Panel preferences</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-list">
|
||||||
|
<div>
|
||||||
|
<span data-i18n="panelLanguage">Panel language</span>
|
||||||
|
<strong id="settingsLanguage">--</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span data-i18n="theme">Theme</span>
|
||||||
|
<strong id="settingsTheme">--</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span data-i18n="bindAddress">Bind address</span>
|
||||||
|
<strong id="settingsBind">127.0.0.1:1984</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow" data-i18n="configEyebrow">Config</p>
|
||||||
|
<h2 data-i18n="configTitle">Installation state</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="configList" class="settings-list"></div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="toast" class="toast"></div>
|
||||||
|
<div id="qrModal" class="promo-modal" hidden>
|
||||||
|
<div class="promo-card qr-card" role="dialog" aria-modal="true" aria-labelledby="qrTitle">
|
||||||
|
<button id="qrClose" class="icon-btn ghost" type="button" aria-label="Close" data-i18n-aria-label="ariaClose">×</button>
|
||||||
|
<p class="eyebrow" data-i18n="qrEyebrow">QR import</p>
|
||||||
|
<h2 id="qrTitle" data-i18n="qrTitle">Scan Telegram proxy</h2>
|
||||||
|
<div class="qr-frame">
|
||||||
|
<img id="qrImage" alt="Telegram proxy QR">
|
||||||
|
</div>
|
||||||
|
<p id="qrMeta" class="modal-note"></p>
|
||||||
|
<button id="qrCopyBtn" type="button" class="soft" data-i18n="copyLink">Copy link</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="promoModal" class="promo-modal" hidden>
|
||||||
|
<div class="promo-card" role="dialog" aria-modal="true" aria-labelledby="promoTitle">
|
||||||
|
<button id="promoClose" class="icon-btn ghost" type="button" aria-label="Close" data-i18n-aria-label="ariaClose">×</button>
|
||||||
|
<p class="eyebrow" data-i18n="promoEyebrow">Promo</p>
|
||||||
|
<h2 id="promoTitle" data-i18n="promoTitle">Support goTelegram Pro</h2>
|
||||||
|
<div class="promo-grid">
|
||||||
|
<a href="https://vk.cc/ct29NQ" target="_blank" rel="noreferrer">
|
||||||
|
<strong data-i18n="promoHosting1">Hosting #1</strong>
|
||||||
|
<span>OFF60 · BONUS20 · BONUS6</span>
|
||||||
|
</a>
|
||||||
|
<a href="https://vk.cc/cUxAhj" target="_blank" rel="noreferrer">
|
||||||
|
<strong data-i18n="promoHosting2">Hosting #2</strong>
|
||||||
|
<span>OFF60</span>
|
||||||
|
</a>
|
||||||
|
<a href="https://pay.cloudtips.ru/p/7410814f" target="_blank" rel="noreferrer">
|
||||||
|
<strong data-i18n="promoTips">Tips</strong>
|
||||||
|
<span>CloudTips</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="/app.js?v=2.5.0-admin18" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1708
admin-web/static/styles.css
Normal file
1708
admin-web/static/styles.css
Normal file
File diff suppressed because it is too large
Load Diff
78
bootstrap.sh
Normal file
78
bootstrap.sh
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# GoTelegram Pro — bootstrap installer from this repository.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
GOTELEGRAM_BASE="${GOTELEGRAM_BASE:-https://raw.githubusercontent.com/andrey271192/gotelegram/main}"
|
||||||
|
INSTALL_DIR="${GOTELEGRAM_INSTALL_DIR:-/opt/gotelegram}"
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
echo -e " ${RED}✗${NC} Запустите от root"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for cmd in curl jq; do
|
||||||
|
command -v "$cmd" >/dev/null 2>&1 || {
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y -qq "$cmd" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
|
||||||
|
FILES=(
|
||||||
|
"install.sh" "install_gotelegram_bot.sh" "templates_catalog.json"
|
||||||
|
"lib/common.sh" "lib/telemt.sh" "lib/telemt_config.sh" "lib/backup.sh"
|
||||||
|
"lib/website.sh" "lib/templates_catalog.sh" "lib/stats.sh" "lib/i18n.sh"
|
||||||
|
"lib/lang/en.sh" "lib/lang/ru.sh"
|
||||||
|
"gotelegram-bot/bot.py" "gotelegram-bot/i18n.py"
|
||||||
|
"gotelegram-bot/lang/en.json" "gotelegram-bot/lang/ru.json"
|
||||||
|
"gotelegram-bot/config.example.env" "gotelegram-bot/requirements.txt" "gotelegram-bot/README.md"
|
||||||
|
"admin-web/server.py" "admin-web/static/index.html" "admin-web/static/styles.css" "admin-web/static/app.js"
|
||||||
|
)
|
||||||
|
|
||||||
|
curl_headers=()
|
||||||
|
if [ -n "${GITHUB_TOKEN:-${GH_TOKEN:-}}" ]; then
|
||||||
|
curl_headers=(-H "Authorization: Bearer ${GITHUB_TOKEN:-${GH_TOKEN:-}}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
download_file() {
|
||||||
|
local remote_path="$1"
|
||||||
|
local local_path="$2"
|
||||||
|
local attempt http_code
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$local_path")"
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
http_code=$(curl -sL "${curl_headers[@]}" -w "%{http_code}" -o "$local_path" "${GOTELEGRAM_BASE%/}/${remote_path}" 2>/dev/null || echo "000")
|
||||||
|
if [ "$http_code" = "200" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo -e " ${RED}✗${NC} Ошибка загрузки ${remote_path} (HTTP ${http_code})"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo -e " ${CYAN}↻${NC} Загрузка GoTelegram из ${GOTELEGRAM_BASE%/}..."
|
||||||
|
mkdir -p "${INSTALL_DIR}/lib/lang" "${INSTALL_DIR}/gotelegram-bot/lang" "${INSTALL_DIR}/admin-web/static"
|
||||||
|
|
||||||
|
failed=0
|
||||||
|
for f in "${FILES[@]}"; do
|
||||||
|
if download_file "$f" "${INSTALL_DIR}/${f}"; then
|
||||||
|
echo -e " ${GREEN}✓${NC} ${f}"
|
||||||
|
else
|
||||||
|
failed=$((failed + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[ "$failed" -eq 0 ] || exit 1
|
||||||
|
|
||||||
|
chmod +x "${INSTALL_DIR}/install.sh" "${INSTALL_DIR}/install_gotelegram_bot.sh"
|
||||||
|
chmod +x "${INSTALL_DIR}"/lib/*.sh
|
||||||
|
chmod +x "${INSTALL_DIR}/admin-web/server.py" 2>/dev/null || true
|
||||||
|
sed -i 's/\r$//' "${INSTALL_DIR}/install.sh" "${INSTALL_DIR}/install_gotelegram_bot.sh" "${INSTALL_DIR}"/lib/*.sh "${INSTALL_DIR}"/lib/lang/*.sh 2>/dev/null || true
|
||||||
|
ln -sf "${INSTALL_DIR}/install.sh" /usr/local/bin/gotelegram
|
||||||
|
|
||||||
|
exec bash "${INSTALL_DIR}/install.sh" "$@"
|
||||||
161
gotelegram-bot/README.md
Normal file
161
gotelegram-bot/README.md
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
# goTelegram Pro v2.5.0 Bot
|
||||||
|
|
||||||
|
Production-quality Telegram bot for managing MTProxy (telemt engine) on Linux servers.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Complete CLI Feature Parity** - All menu items from CLI version
|
||||||
|
- Install (Quick/Stealth modes)
|
||||||
|
- Status monitoring
|
||||||
|
- Proxy link generation
|
||||||
|
- Share with QR codes
|
||||||
|
- Service restart
|
||||||
|
- Logs viewing
|
||||||
|
- Mode/template changes
|
||||||
|
- Backup/restore
|
||||||
|
- telemt updates
|
||||||
|
- Website/SSL management
|
||||||
|
- Remove installation
|
||||||
|
- Promotional links
|
||||||
|
|
||||||
|
- **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
|
||||||
|
- **Async/Await** - Full async support via python-telegram-bot v21+
|
||||||
|
- **Inline Keyboards** - Modern UI with callback-based navigation
|
||||||
|
- **Shell Integration** - Executes system commands via asyncio subprocess
|
||||||
|
- **Error Handling** - Production-ready error handling
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Recommended installation path is the main CLI menu:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gotelegram
|
||||||
|
```
|
||||||
|
|
||||||
|
Then choose `12) Telegram-bot` → install/update. On repeat goTelegram Pro bootstrap/update, an already installed bot is refreshed automatically: code, i18n files and requirements are copied to `/opt/gotelegram-bot`, `.env` is preserved, dependencies are checked and `gotelegram-bot` is restarted.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Python 3.8+
|
||||||
|
- Linux system with systemd
|
||||||
|
- telemt installed and running
|
||||||
|
- Telegram Bot Token from @BotFather
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
1. Install dependencies:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create .env file:
|
||||||
|
```bash
|
||||||
|
cp config.example.env .env
|
||||||
|
# Edit .env and set your BOT_TOKEN
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
3. (Optional) Restrict access to specific users:
|
||||||
|
```bash
|
||||||
|
# Edit .env and uncomment ALLOWED_IDS
|
||||||
|
# ALLOWED_IDS=123456789,987654321
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running the Bot
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 bot.py
|
||||||
|
```
|
||||||
|
|
||||||
|
For systemd service:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[Unit]
|
||||||
|
Description=goTelegram Pro Bot
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=/opt/gotelegram-bot
|
||||||
|
ExecStart=/opt/gotelegram-bot/venv/bin/python /opt/gotelegram-bot/bot.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
Environment=PATH=/opt/gotelegram-bot/venv/bin:/usr/bin
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### .env Variables
|
||||||
|
|
||||||
|
- `BOT_TOKEN` - Telegram bot token (required)
|
||||||
|
- `ALLOWED_IDS` - Comma-separated user IDs (optional, all users allowed if empty)
|
||||||
|
- `BOT_LANG` - Default language inherited from goTelegram Pro install language
|
||||||
|
|
||||||
|
### System Paths
|
||||||
|
|
||||||
|
- `GOTELEGRAM_CONFIG` - `/opt/gotelegram/config.json`
|
||||||
|
- `TELEMT_CONFIG` - `/etc/telemt/config.toml`
|
||||||
|
- `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
|
||||||
|
|
||||||
|
### Single File Design
|
||||||
|
All functionality in one `bot.py` for simplicity and ease of deployment.
|
||||||
|
|
||||||
|
### Command Handlers
|
||||||
|
- `/start` - Main menu
|
||||||
|
- `/help` - Help text
|
||||||
|
- `/status` - Quick status
|
||||||
|
- `/logs` - Recent logs
|
||||||
|
|
||||||
|
### Callback Handlers
|
||||||
|
Organized by feature:
|
||||||
|
- Installation (quick/stealth modes)
|
||||||
|
- Status monitoring
|
||||||
|
- Backup/restore
|
||||||
|
- Backup schedules: off, daily, weekly, monthly
|
||||||
|
- SSL management
|
||||||
|
- Updates
|
||||||
|
- Removal
|
||||||
|
|
||||||
|
### Shell Integration
|
||||||
|
Async subprocess wrapper:
|
||||||
|
```python
|
||||||
|
code, stdout, stderr = await sh("command", "arg1", "arg2")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Callback Data Convention
|
||||||
|
|
||||||
|
- `menu_*` - Menu items
|
||||||
|
- `install_mode_*` - Install options
|
||||||
|
- `quick_dom_*` - Domain selection
|
||||||
|
- `stealth_cat_*` - Template categories
|
||||||
|
- `stealth_tpl_*` - Template selection
|
||||||
|
- `stealth_confirm_*` - Confirm installation
|
||||||
|
- `backup_*` - Backup operations
|
||||||
|
- `ssl_*` - SSL operations
|
||||||
|
- `restore_backup_*` - Restore operations
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- **telemt** - MTProxy engine foundation
|
||||||
|
- **HTML5UP** - Beautiful web templates
|
||||||
|
- **Learning Zone** - Educational resources
|
||||||
|
- **Start Bootstrap** - Bootstrap framework
|
||||||
|
- **Community** - Your feedback and support
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
goTelegram Pro v2.5.0 - Open source community project
|
||||||
3361
gotelegram-bot/bot.py
Normal file
3361
gotelegram-bot/bot.py
Normal file
File diff suppressed because it is too large
Load Diff
9
gotelegram-bot/config.example.env
Normal file
9
gotelegram-bot/config.example.env
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# goTelegram Pro v2.5.0 Bot Configuration
|
||||||
|
# Copy this to .env and fill in your values
|
||||||
|
|
||||||
|
# Telegram Bot Token from @BotFather
|
||||||
|
BOT_TOKEN=your_bot_token_from_@BotFather
|
||||||
|
|
||||||
|
# Comma-separated list of allowed Telegram user IDs
|
||||||
|
# Leave empty to allow all users
|
||||||
|
# ALLOWED_IDS=123456789,987654321
|
||||||
167
gotelegram-bot/i18n.py
Normal file
167
gotelegram-bot/i18n.py
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
"""
|
||||||
|
goTelegram Pro v2.5.0 Bot — i18n module
|
||||||
|
Provides per-user language preferences and a simple t()/tf() API.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from i18n import t, tf, set_user_lang, get_user_lang, get_language_name
|
||||||
|
|
||||||
|
msg = t(user_id, "menu_status")
|
||||||
|
msg = tf(user_id, "backup_created_fmt", filename)
|
||||||
|
|
||||||
|
Language files live next to this module in lang/<code>.json.
|
||||||
|
Per-user choices are persisted to USER_LANG_FILE (one JSON dict: user_id -> code).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# ── Paths ─────────────────────────────────────────────────────────────────
|
||||||
|
_MODULE_DIR = Path(__file__).resolve().parent
|
||||||
|
LANG_DIR = _MODULE_DIR / "lang"
|
||||||
|
USER_LANG_FILE = Path("/opt/gotelegram-bot/user_langs.json")
|
||||||
|
GOTELEGRAM_CONFIG = Path("/opt/gotelegram/config.json")
|
||||||
|
GOTELEGRAM_LANG_MARKER = Path("/opt/gotelegram/.language")
|
||||||
|
|
||||||
|
# Supported codes; keep in sync with lang/*.json
|
||||||
|
SUPPORTED_LANGS = ("en", "ru")
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_default_lang() -> str:
|
||||||
|
candidates = []
|
||||||
|
try:
|
||||||
|
if GOTELEGRAM_CONFIG.exists():
|
||||||
|
with open(GOTELEGRAM_CONFIG, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if isinstance(data, dict):
|
||||||
|
candidates.extend([data.get("language"), data.get("lang")])
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("failed to read goTelegram Pro language config: %s", e)
|
||||||
|
try:
|
||||||
|
if GOTELEGRAM_LANG_MARKER.exists():
|
||||||
|
candidates.append(GOTELEGRAM_LANG_MARKER.read_text(encoding="utf-8").strip()[:2])
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("failed to read goTelegram Pro language marker: %s", e)
|
||||||
|
candidates.append(os.getenv("BOT_LANG", ""))
|
||||||
|
for raw in candidates:
|
||||||
|
code = str(raw or "").strip().lower()
|
||||||
|
if code in SUPPORTED_LANGS:
|
||||||
|
return code
|
||||||
|
return "en"
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_LANG = _detect_default_lang()
|
||||||
|
|
||||||
|
LANG_NAMES = {
|
||||||
|
"en": "English",
|
||||||
|
"ru": "Русский",
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Caches ────────────────────────────────────────────────────────────────
|
||||||
|
_LANG_CACHE: Dict[str, Dict[str, str]] = {}
|
||||||
|
_USER_LANGS: Dict[int, str] = {}
|
||||||
|
_USER_LANGS_LOADED = False
|
||||||
|
|
||||||
|
|
||||||
|
def _load_lang_file(code: str) -> Dict[str, str]:
|
||||||
|
"""Load lang/<code>.json into the cache and return it."""
|
||||||
|
if code in _LANG_CACHE:
|
||||||
|
return _LANG_CACHE[code]
|
||||||
|
path = LANG_DIR / f"{code}.json"
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise ValueError("lang file must contain a top-level object")
|
||||||
|
_LANG_CACHE[code] = data
|
||||||
|
return data
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.warning("lang file not found: %s", path)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("failed to load %s: %s", path, e)
|
||||||
|
_LANG_CACHE[code] = {}
|
||||||
|
return _LANG_CACHE[code]
|
||||||
|
|
||||||
|
|
||||||
|
def _load_user_langs() -> None:
|
||||||
|
"""Load per-user language preferences from USER_LANG_FILE."""
|
||||||
|
global _USER_LANGS, _USER_LANGS_LOADED
|
||||||
|
_USER_LANGS_LOADED = True
|
||||||
|
try:
|
||||||
|
if USER_LANG_FILE.exists():
|
||||||
|
with open(USER_LANG_FILE, "r", encoding="utf-8") as f:
|
||||||
|
raw = json.load(f)
|
||||||
|
if isinstance(raw, dict):
|
||||||
|
_USER_LANGS = {
|
||||||
|
int(k): v for k, v in raw.items()
|
||||||
|
if isinstance(v, str) and v in SUPPORTED_LANGS
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("failed to load user_langs: %s", e)
|
||||||
|
_USER_LANGS = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _save_user_langs() -> None:
|
||||||
|
"""Persist per-user language preferences."""
|
||||||
|
try:
|
||||||
|
USER_LANG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(USER_LANG_FILE, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(
|
||||||
|
{str(k): v for k, v in _USER_LANGS.items()},
|
||||||
|
f, ensure_ascii=False, indent=2,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("failed to save user_langs: %s", e)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Public API ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def get_user_lang(user_id: Optional[int]) -> str:
|
||||||
|
"""Return the language code for the given user (or DEFAULT_LANG)."""
|
||||||
|
if not _USER_LANGS_LOADED:
|
||||||
|
_load_user_langs()
|
||||||
|
if user_id is None:
|
||||||
|
return _detect_default_lang()
|
||||||
|
return _USER_LANGS.get(int(user_id), _detect_default_lang())
|
||||||
|
|
||||||
|
|
||||||
|
def set_user_lang(user_id: int, code: str) -> bool:
|
||||||
|
"""Set the per-user language preference and persist it."""
|
||||||
|
if not _USER_LANGS_LOADED:
|
||||||
|
_load_user_langs()
|
||||||
|
code = (code or "").strip().lower()
|
||||||
|
if code not in SUPPORTED_LANGS:
|
||||||
|
return False
|
||||||
|
_USER_LANGS[int(user_id)] = code
|
||||||
|
_save_user_langs()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_language_name(code: str) -> str:
|
||||||
|
return LANG_NAMES.get(code, code)
|
||||||
|
|
||||||
|
|
||||||
|
def t(user_id: Optional[int], key: str, default: Optional[str] = None) -> str:
|
||||||
|
"""Translate key for the given user. Falls back to English, then default/key."""
|
||||||
|
code = get_user_lang(user_id)
|
||||||
|
table = _load_lang_file(code)
|
||||||
|
if key in table:
|
||||||
|
return table[key]
|
||||||
|
if code != "en":
|
||||||
|
en_table = _load_lang_file("en")
|
||||||
|
if key in en_table:
|
||||||
|
return en_table[key]
|
||||||
|
return default if default is not None else key
|
||||||
|
|
||||||
|
|
||||||
|
def tf(user_id: Optional[int], key: str, *args, default: Optional[str] = None) -> str:
|
||||||
|
"""Format a translated string with positional args using %-formatting."""
|
||||||
|
template = t(user_id, key, default=default)
|
||||||
|
try:
|
||||||
|
return template % args if args else template
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return template
|
||||||
118
gotelegram-bot/lang/en.json
Normal file
118
gotelegram-bot/lang/en.json
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
{
|
||||||
|
"lang_english": "English",
|
||||||
|
"lang_russian": "Русский",
|
||||||
|
"lang_title": "🌐 Language",
|
||||||
|
"lang_current": "Current language: %s",
|
||||||
|
"lang_saved": "Language saved: %s",
|
||||||
|
"lang_choose": "Choose your language:",
|
||||||
|
|
||||||
|
"welcome_title": "goTelegram Pro v%s",
|
||||||
|
"welcome_subtitle": "🤖 MTProxy Management Bot",
|
||||||
|
"welcome_powered": "Powered by telemt engine",
|
||||||
|
"welcome_prompt": "Select an action from the menu below:",
|
||||||
|
|
||||||
|
"waiting_admin_title": "👋 Hi, %s!",
|
||||||
|
"waiting_admin_body": "The bot is not configured yet.\nYour Telegram ID: <code>%s</code>\n\nAssign you as administrator?",
|
||||||
|
"btn_yes": "✅ Yes",
|
||||||
|
"btn_no": "❌ No",
|
||||||
|
|
||||||
|
"access_denied": "⛔ Access denied.\nYour ID: <code>%s</code>",
|
||||||
|
"help_title": "goTelegram Pro Bot — Commands",
|
||||||
|
"help_lines": "/start — Main menu\n/help — This help\n/status — Quick status\n/logs — Latest logs\n/lang — Change language\n/addadmin ID — Add admin\n/deladmin ID — Remove admin\n\nUse the menu buttons for other operations.",
|
||||||
|
|
||||||
|
"menu_install": "⚙️ Install",
|
||||||
|
"menu_status": "📊 Status",
|
||||||
|
"menu_link": "🔗 Link",
|
||||||
|
"menu_share": "📤 Share",
|
||||||
|
"menu_restart": "🔄 Restart",
|
||||||
|
"menu_logs": "📋 Logs",
|
||||||
|
"menu_change": "⚡ Change Mode/Template",
|
||||||
|
"menu_backup": "💾 Backup",
|
||||||
|
"menu_restore": "↩️ Restore",
|
||||||
|
"menu_update": "📡 Update telemt",
|
||||||
|
"menu_website": "🌐 Website/SSL",
|
||||||
|
"menu_promo": "🎁 Promo",
|
||||||
|
"menu_stats": "📊 Traffic Stats",
|
||||||
|
"menu_users": "🔑 Keys",
|
||||||
|
"menu_admin_web": "🖥 Web Admin",
|
||||||
|
"menu_remove": "🗑️ Remove",
|
||||||
|
"menu_admins": "👤 Admins",
|
||||||
|
"menu_credits": "ℹ️ Credits",
|
||||||
|
"menu_language": "🌐 Language",
|
||||||
|
"menu_close": "❌ Close",
|
||||||
|
"btn_back": "⬅️ Back",
|
||||||
|
"btn_refresh": "🔄 Refresh",
|
||||||
|
"btn_cancel": "❌ Cancel",
|
||||||
|
"btn_confirm": "✅ Confirm",
|
||||||
|
|
||||||
|
"status_checking": "⏳ Checking status...",
|
||||||
|
"status_title": "📊 Current Status",
|
||||||
|
"status_service": "Service",
|
||||||
|
"status_running": "✅ Running",
|
||||||
|
"status_stopped": "❌ Stopped",
|
||||||
|
"status_telemt": "Telemt",
|
||||||
|
"status_mode": "Mode",
|
||||||
|
"status_template": "Template",
|
||||||
|
"status_domain": "Domain",
|
||||||
|
"status_port": "Port",
|
||||||
|
"status_listen_port": "Listen Port",
|
||||||
|
"status_tls_domain": "TLS Domain",
|
||||||
|
|
||||||
|
"logs_failed": "Failed to retrieve logs",
|
||||||
|
"link_fetching": "⏳ Fetching link...",
|
||||||
|
"link_unavailable": "Link unavailable (proxy not installed?)",
|
||||||
|
"share_title": "📤 Share Proxy",
|
||||||
|
"share_body": "Send this link to your client:",
|
||||||
|
|
||||||
|
"restart_title": "🔄 Restart",
|
||||||
|
"restart_progress": "⏳ Restarting telemt...",
|
||||||
|
"restart_ok": "✅ telemt restarted",
|
||||||
|
"restart_fail": "❌ Restart failed",
|
||||||
|
|
||||||
|
"install_title": "⚙️ Install / Update",
|
||||||
|
"install_pick_mode": "Select installation mode:",
|
||||||
|
"install_mode_lite": "🚀 Lite (quick, no site)",
|
||||||
|
"install_mode_pro": "🎨 Pro (stealth + website)",
|
||||||
|
|
||||||
|
"backup_title": "💾 Backup",
|
||||||
|
"backup_creating": "⏳ Creating backup...",
|
||||||
|
"backup_created_fmt": "✅ Backup created: %s",
|
||||||
|
"backup_failed": "❌ Backup creation failed",
|
||||||
|
"backup_list_title": "Available backups:",
|
||||||
|
"backup_none": "No backups yet",
|
||||||
|
"backup_restore_title": "↩️ Restore backup",
|
||||||
|
"backup_restoring": "⏳ Restoring...",
|
||||||
|
"backup_restored": "✅ Backup restored",
|
||||||
|
|
||||||
|
"update_title": "📡 Update telemt",
|
||||||
|
"update_progress": "⏳ Updating telemt binary...",
|
||||||
|
"update_ok": "✅ telemt updated",
|
||||||
|
"update_fail": "❌ Update failed",
|
||||||
|
|
||||||
|
"website_title": "🌐 Website / SSL",
|
||||||
|
"ssl_renew_progress": "⏳ Renewing SSL...",
|
||||||
|
"ssl_renewed": "✅ SSL renewed",
|
||||||
|
"ssl_renew_fail": "❌ SSL renew failed",
|
||||||
|
"ssl_status_title": "🔒 SSL Status",
|
||||||
|
|
||||||
|
"remove_title": "🗑️ Remove",
|
||||||
|
"remove_warn": "⚠️ This will stop and remove telemt, nginx site and configs. Continue?",
|
||||||
|
"remove_progress": "⏳ Removing...",
|
||||||
|
"remove_done": "✅ Removed",
|
||||||
|
|
||||||
|
"admins_title": "👤 Administrators",
|
||||||
|
"admins_list": "Current admin IDs:",
|
||||||
|
"admins_empty": "No admins configured",
|
||||||
|
|
||||||
|
"promo_title": "🎁 Promo",
|
||||||
|
"credits_title": "ℹ️ Credits",
|
||||||
|
|
||||||
|
"cg_title": "🔗 Custom Git Template",
|
||||||
|
"cg_ask_url": "Send me the HTTPS git URL of a static site repository.\nOptionally append <code>@branch</code>.",
|
||||||
|
"cg_cloning": "⏳ Cloning %s ...",
|
||||||
|
"cg_invalid": "❌ Invalid URL. Only HTTPS git URLs are allowed.",
|
||||||
|
"cg_timeout": "❌ Clone timeout (repository too large or slow)",
|
||||||
|
"cg_too_big": "❌ Repository too large (>100MB)",
|
||||||
|
"cg_no_index": "❌ No index.html found in repository",
|
||||||
|
"cg_ok_fmt": "✅ Custom template downloaded: %s"
|
||||||
|
}
|
||||||
118
gotelegram-bot/lang/ru.json
Normal file
118
gotelegram-bot/lang/ru.json
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
{
|
||||||
|
"lang_english": "English",
|
||||||
|
"lang_russian": "Русский",
|
||||||
|
"lang_title": "🌐 Язык",
|
||||||
|
"lang_current": "Текущий язык: %s",
|
||||||
|
"lang_saved": "Язык сохранён: %s",
|
||||||
|
"lang_choose": "Выберите язык:",
|
||||||
|
|
||||||
|
"welcome_title": "goTelegram Pro v%s",
|
||||||
|
"welcome_subtitle": "🤖 Бот управления MTProxy",
|
||||||
|
"welcome_powered": "На базе движка telemt",
|
||||||
|
"welcome_prompt": "Выберите действие в меню ниже:",
|
||||||
|
|
||||||
|
"waiting_admin_title": "👋 Привет, %s!",
|
||||||
|
"waiting_admin_body": "Бот ещё не настроен.\nВаш Telegram ID: <code>%s</code>\n\nНазначить вас администратором?",
|
||||||
|
"btn_yes": "✅ Да",
|
||||||
|
"btn_no": "❌ Нет",
|
||||||
|
|
||||||
|
"access_denied": "⛔ Доступ запрещён.\nВаш ID: <code>%s</code>",
|
||||||
|
"help_title": "goTelegram Pro Bot — Команды",
|
||||||
|
"help_lines": "/start — Главное меню\n/help — Эта справка\n/status — Быстрый статус\n/logs — Последние логи\n/lang — Сменить язык\n/addadmin ID — Добавить админа\n/deladmin ID — Удалить админа\n\nИспользуйте кнопки меню для остальных операций.",
|
||||||
|
|
||||||
|
"menu_install": "⚙️ Установить",
|
||||||
|
"menu_status": "📊 Статус",
|
||||||
|
"menu_link": "🔗 Ссылка",
|
||||||
|
"menu_share": "📤 Поделиться",
|
||||||
|
"menu_restart": "🔄 Перезапуск",
|
||||||
|
"menu_logs": "📋 Логи",
|
||||||
|
"menu_change": "⚡ Сменить режим/шаблон",
|
||||||
|
"menu_backup": "💾 Бекап",
|
||||||
|
"menu_restore": "↩️ Восстановить",
|
||||||
|
"menu_update": "📡 Обновить telemt",
|
||||||
|
"menu_website": "🌐 Сайт/SSL",
|
||||||
|
"menu_promo": "🎁 Промо",
|
||||||
|
"menu_stats": "📊 Трафик",
|
||||||
|
"menu_users": "🔑 Ключи",
|
||||||
|
"menu_admin_web": "🖥 Веб-админка",
|
||||||
|
"menu_remove": "🗑️ Удалить",
|
||||||
|
"menu_admins": "👤 Админы",
|
||||||
|
"menu_credits": "ℹ️ О проекте",
|
||||||
|
"menu_language": "🌐 Язык",
|
||||||
|
"menu_close": "❌ Закрыть",
|
||||||
|
"btn_back": "⬅️ Назад",
|
||||||
|
"btn_refresh": "🔄 Обновить",
|
||||||
|
"btn_cancel": "❌ Отмена",
|
||||||
|
"btn_confirm": "✅ Подтвердить",
|
||||||
|
|
||||||
|
"status_checking": "⏳ Проверяю статус...",
|
||||||
|
"status_title": "📊 Текущий статус",
|
||||||
|
"status_service": "Сервис",
|
||||||
|
"status_running": "✅ Работает",
|
||||||
|
"status_stopped": "❌ Остановлен",
|
||||||
|
"status_telemt": "Telemt",
|
||||||
|
"status_mode": "Режим",
|
||||||
|
"status_template": "Шаблон",
|
||||||
|
"status_domain": "Домен",
|
||||||
|
"status_port": "Порт",
|
||||||
|
"status_listen_port": "Порт прослушивания",
|
||||||
|
"status_tls_domain": "TLS домен",
|
||||||
|
|
||||||
|
"logs_failed": "Не удалось получить логи",
|
||||||
|
"link_fetching": "⏳ Получаю ссылку...",
|
||||||
|
"link_unavailable": "Ссылка недоступна (прокси не установлен?)",
|
||||||
|
"share_title": "📤 Поделиться прокси",
|
||||||
|
"share_body": "Отправьте эту ссылку клиенту:",
|
||||||
|
|
||||||
|
"restart_title": "🔄 Перезапуск",
|
||||||
|
"restart_progress": "⏳ Перезапускаю telemt...",
|
||||||
|
"restart_ok": "✅ telemt перезапущен",
|
||||||
|
"restart_fail": "❌ Ошибка перезапуска",
|
||||||
|
|
||||||
|
"install_title": "⚙️ Установка / Обновление",
|
||||||
|
"install_pick_mode": "Выберите режим установки:",
|
||||||
|
"install_mode_lite": "🚀 Lite (быстро, без сайта)",
|
||||||
|
"install_mode_pro": "🎨 Pro (маскировка + сайт)",
|
||||||
|
|
||||||
|
"backup_title": "💾 Бекап",
|
||||||
|
"backup_creating": "⏳ Создаю бекап...",
|
||||||
|
"backup_created_fmt": "✅ Бекап создан: %s",
|
||||||
|
"backup_failed": "❌ Не удалось создать бекап",
|
||||||
|
"backup_list_title": "Доступные бекапы:",
|
||||||
|
"backup_none": "Бекапов пока нет",
|
||||||
|
"backup_restore_title": "↩️ Восстановление бекапа",
|
||||||
|
"backup_restoring": "⏳ Восстанавливаю...",
|
||||||
|
"backup_restored": "✅ Бекап восстановлен",
|
||||||
|
|
||||||
|
"update_title": "📡 Обновление telemt",
|
||||||
|
"update_progress": "⏳ Обновляю telemt...",
|
||||||
|
"update_ok": "✅ telemt обновлён",
|
||||||
|
"update_fail": "❌ Ошибка обновления",
|
||||||
|
|
||||||
|
"website_title": "🌐 Сайт / SSL",
|
||||||
|
"ssl_renew_progress": "⏳ Обновляю SSL...",
|
||||||
|
"ssl_renewed": "✅ SSL обновлён",
|
||||||
|
"ssl_renew_fail": "❌ Ошибка обновления SSL",
|
||||||
|
"ssl_status_title": "🔒 Статус SSL",
|
||||||
|
|
||||||
|
"remove_title": "🗑️ Удаление",
|
||||||
|
"remove_warn": "⚠️ Это остановит и удалит telemt, сайт nginx и конфиги. Продолжить?",
|
||||||
|
"remove_progress": "⏳ Удаляю...",
|
||||||
|
"remove_done": "✅ Удалено",
|
||||||
|
|
||||||
|
"admins_title": "👤 Администраторы",
|
||||||
|
"admins_list": "Текущие ID админов:",
|
||||||
|
"admins_empty": "Админы не настроены",
|
||||||
|
|
||||||
|
"promo_title": "🎁 Промо",
|
||||||
|
"credits_title": "ℹ️ О проекте",
|
||||||
|
|
||||||
|
"cg_title": "🔗 Свой git-шаблон",
|
||||||
|
"cg_ask_url": "Отправьте HTTPS git-URL репозитория со статическим сайтом.\nПри желании добавьте <code>@branch</code>.",
|
||||||
|
"cg_cloning": "⏳ Клонирую %s ...",
|
||||||
|
"cg_invalid": "❌ Неверный URL. Разрешены только HTTPS git-URL.",
|
||||||
|
"cg_timeout": "❌ Таймаут клонирования (репозиторий слишком большой или медленный)",
|
||||||
|
"cg_too_big": "❌ Репозиторий слишком большой (>100МБ)",
|
||||||
|
"cg_no_index": "❌ В репозитории не найден index.html",
|
||||||
|
"cg_ok_fmt": "✅ Свой шаблон загружен: %s"
|
||||||
|
}
|
||||||
3
gotelegram-bot/requirements.txt
Normal file
3
gotelegram-bot/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
python-telegram-bot>=21.0
|
||||||
|
python-dotenv>=1.0.0
|
||||||
|
toml>=0.10.2
|
||||||
1939
install.sh
Normal file
1939
install.sh
Normal file
File diff suppressed because it is too large
Load Diff
173
install_gotelegram_bot.sh
Normal file
173
install_gotelegram_bot.sh
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# GoTelegram v2.5.0 — Установка Telegram-бота
|
||||||
|
# Создаёт venv, ставит зависимости, настраивает systemd
|
||||||
|
|
||||||
|
set -e
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
BOT_DIR="/opt/gotelegram-bot"
|
||||||
|
SERVICE_NAME="gotelegram-bot"
|
||||||
|
GOTELEGRAM_DIR="/opt/gotelegram"
|
||||||
|
ADMIN_WEB_DIR="/opt/gotelegram-admin"
|
||||||
|
ADMIN_WEB_SERVICE="gotelegram-admin"
|
||||||
|
ADMIN_WEB_PORT="1984"
|
||||||
|
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
echo -e "${RED}Запустите с sudo.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${CYAN}╔═══════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${CYAN}║${NC} ${GREEN}GoTelegram v2.5.0 — Установка бота${NC} ${CYAN}║${NC}"
|
||||||
|
echo -e "${CYAN}╚═══════════════════════════════════════════╝${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── Python ───────────────────────────────────────────────────────────────────
|
||||||
|
if ! command -v python3 &>/dev/null; then
|
||||||
|
echo -e "${YELLOW}[*] Установка python3...${NC}"
|
||||||
|
if command -v apt-get &>/dev/null; then
|
||||||
|
apt-get update -qq && apt-get install -y -qq python3 python3-pip python3-venv
|
||||||
|
elif command -v dnf &>/dev/null; then
|
||||||
|
dnf install -y -q python3 python3-pip
|
||||||
|
elif command -v yum &>/dev/null; then
|
||||||
|
yum install -y -q python3 python3-pip
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Каталог бота ─────────────────────────────────────────────────────────────
|
||||||
|
mkdir -p "$BOT_DIR"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
[ -f "$SCRIPT_DIR/lib/common.sh" ] && source "$SCRIPT_DIR/lib/common.sh" || true
|
||||||
|
[ -f "$SCRIPT_DIR/lib/stats.sh" ] && source "$SCRIPT_DIR/lib/stats.sh" || true
|
||||||
|
|
||||||
|
if [ -f "$SCRIPT_DIR/gotelegram-bot/bot.py" ]; then
|
||||||
|
echo -e "${GREEN}[*] Копирование файлов бота...${NC}"
|
||||||
|
cp "$SCRIPT_DIR/gotelegram-bot/bot.py" "$BOT_DIR/"
|
||||||
|
cp "$SCRIPT_DIR/gotelegram-bot/requirements.txt" "$BOT_DIR/"
|
||||||
|
[ -f "$SCRIPT_DIR/gotelegram-bot/config.example.env" ] && cp "$SCRIPT_DIR/gotelegram-bot/config.example.env" "$BOT_DIR/"
|
||||||
|
else
|
||||||
|
echo -e "${RED}Файлы бота не найдены в $SCRIPT_DIR/gotelegram-bot/${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Копируем каталог шаблонов
|
||||||
|
if [ -f "$SCRIPT_DIR/templates_catalog.json" ]; then
|
||||||
|
mkdir -p "$GOTELEGRAM_DIR"
|
||||||
|
cp "$SCRIPT_DIR/templates_catalog.json" "$GOTELEGRAM_DIR/"
|
||||||
|
echo -e "${GREEN}[*] Каталог шаблонов скопирован${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Virtual environment ──────────────────────────────────────────────────────
|
||||||
|
if [ ! -d "$BOT_DIR/venv" ]; then
|
||||||
|
echo -e "${GREEN}[*] Создание виртуального окружения...${NC}"
|
||||||
|
python3 -m venv "$BOT_DIR/venv"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}[*] Установка зависимостей...${NC}"
|
||||||
|
"$BOT_DIR/venv/bin/pip" install -r "$BOT_DIR/requirements.txt" -q
|
||||||
|
|
||||||
|
# ── Конфигурация ─────────────────────────────────────────────────────────────
|
||||||
|
if [ ! -f "$BOT_DIR/.env" ]; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}Введите BOT_TOKEN от @BotFather:${NC}"
|
||||||
|
TOKEN=""
|
||||||
|
while [ -z "$TOKEN" ]; do
|
||||||
|
read -r TOKEN
|
||||||
|
TOKEN=$(echo "$TOKEN" | tr -d '[:space:]')
|
||||||
|
[ -z "$TOKEN" ] && echo -e "${RED}Токен не может быть пустым.${NC}"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo -ne "${YELLOW}ID администратора (Enter = доступ для всех):${NC} "
|
||||||
|
read -r ADMIN_ID
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "BOT_TOKEN=$TOKEN"
|
||||||
|
[ -n "$ADMIN_ID" ] && echo "ALLOWED_IDS=$ADMIN_ID"
|
||||||
|
} > "$BOT_DIR/.env"
|
||||||
|
|
||||||
|
chmod 600 "$BOT_DIR/.env"
|
||||||
|
echo -e "${GREEN}[*] .env создан${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}[*] .env уже существует${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Systemd ──────────────────────────────────────────────────────────────────
|
||||||
|
cat > "/etc/systemd/system/${SERVICE_NAME}.service" << EOF
|
||||||
|
[Unit]
|
||||||
|
Description=GoTelegram v2.5.0 Telegram Bot
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=$BOT_DIR
|
||||||
|
ExecStart=$BOT_DIR/venv/bin/python $BOT_DIR/bot.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
Environment=PATH=$BOT_DIR/venv/bin:/usr/bin
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable "$SERVICE_NAME"
|
||||||
|
systemctl restart "$SERVICE_NAME" 2>/dev/null || systemctl start "$SERVICE_NAME"
|
||||||
|
|
||||||
|
# ── Local Web Admin ──────────────────────────────────────────────────────────
|
||||||
|
if [ -f "$SCRIPT_DIR/admin-web/server.py" ]; then
|
||||||
|
echo -e "${GREEN}[*] Установка локальной web-админки...${NC}"
|
||||||
|
mkdir -p "$ADMIN_WEB_DIR/static"
|
||||||
|
cp "$SCRIPT_DIR/admin-web/server.py" "$ADMIN_WEB_DIR/server.py"
|
||||||
|
cp -a "$SCRIPT_DIR/admin-web/static/." "$ADMIN_WEB_DIR/static/"
|
||||||
|
chmod 700 "$ADMIN_WEB_DIR"
|
||||||
|
chmod 755 "$ADMIN_WEB_DIR/server.py" "$ADMIN_WEB_DIR/static"
|
||||||
|
rm -f "$ADMIN_WEB_DIR/token" 2>/dev/null || true
|
||||||
|
|
||||||
|
PYTHON_BIN=$(command -v python3)
|
||||||
|
cat > "/etc/systemd/system/${ADMIN_WEB_SERVICE}.service" << EOF
|
||||||
|
[Unit]
|
||||||
|
Description=GoTelegram v2.5.0 Local Web Admin
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=$ADMIN_WEB_DIR
|
||||||
|
ExecStart=$PYTHON_BIN $ADMIN_WEB_DIR/server.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
Environment=GOTELEGRAM_ADMIN_HOST=127.0.0.1
|
||||||
|
Environment=GOTELEGRAM_ADMIN_PORT=$ADMIN_WEB_PORT
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable "$ADMIN_WEB_SERVICE"
|
||||||
|
systemctl restart "$ADMIN_WEB_SERVICE" 2>/dev/null || systemctl start "$ADMIN_WEB_SERVICE"
|
||||||
|
if type install_stats_collector &>/dev/null; then
|
||||||
|
install_stats_collector >/dev/null 2>&1 || echo -e "${YELLOW}[!] Сборщик статистики не запущен; откройте Traffic в Web Admin и нажмите Repair.${NC}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}╔═══════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${GREEN}║ ✅ Бот установлен и запущен! ║${NC}"
|
||||||
|
echo -e "${GREEN}╚═══════════════════════════════════════════╝${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "Проверка: ${CYAN}systemctl status $SERVICE_NAME${NC}"
|
||||||
|
echo -e "Логи: ${CYAN}journalctl -u $SERVICE_NAME -f${NC}"
|
||||||
|
echo -e "Настройки: ${CYAN}$BOT_DIR/.env${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Благодарности
|
||||||
|
echo -e "${CYAN}─────────────────────────────────────────────${NC}"
|
||||||
|
echo -e "💜 Спасибо авторам открытых проектов:"
|
||||||
|
echo -e " ${CYAN}telemt${NC} — MTProxy engine (Rust)"
|
||||||
|
echo -e " ${CYAN}HTML5 UP${NC} — шаблоны сайтов (CC BY 3.0)"
|
||||||
|
echo -e " ${CYAN}learning-zone${NC} — 150+ HTML5 шаблонов"
|
||||||
|
echo -e " ${CYAN}Start Bootstrap${NC} — Bootstrap шаблоны (MIT)"
|
||||||
|
echo -e "${CYAN}─────────────────────────────────────────────${NC}"
|
||||||
589
lib/backup.sh
Normal file
589
lib/backup.sh
Normal file
@@ -0,0 +1,589 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# goTelegram Pro v2.5.0 — backup and restore (i18n-aware)
|
||||||
|
|
||||||
|
# ── Создание бекапа ──────────────────────────────────────────────────────────
|
||||||
|
create_backup() {
|
||||||
|
local password="$1"
|
||||||
|
local output_dir="${2:-$BACKUP_DIR}"
|
||||||
|
local timestamp
|
||||||
|
timestamp=$(date +%Y%m%d_%H%M%S)
|
||||||
|
local backup_name tmp_dir suffix=0
|
||||||
|
|
||||||
|
mkdir -p "$output_dir"
|
||||||
|
while true; do
|
||||||
|
if [ "$suffix" -eq 0 ]; then
|
||||||
|
backup_name="gotelegram_backup_${timestamp}"
|
||||||
|
else
|
||||||
|
backup_name="gotelegram_backup_${timestamp}_${suffix}"
|
||||||
|
fi
|
||||||
|
tmp_dir="/tmp/${backup_name}"
|
||||||
|
if [ ! -e "$tmp_dir" ] && \
|
||||||
|
[ ! -e "${output_dir}/${backup_name}.tar.gz" ] && \
|
||||||
|
[ ! -e "${output_dir}/${backup_name}.tar.gz.enc" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
suffix=$((suffix + 1))
|
||||||
|
done
|
||||||
|
mkdir -p "$tmp_dir"
|
||||||
|
|
||||||
|
# Собираем файлы
|
||||||
|
log_info "$(_t_or backup_collecting 'Собираю конфигурацию...')"
|
||||||
|
|
||||||
|
# telemt конфиг
|
||||||
|
if [ -f "$TELEMT_CONFIG" ]; then
|
||||||
|
cp "$TELEMT_CONFIG" "$tmp_dir/config.toml"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# goTelegram Pro конфиг
|
||||||
|
if [ -f "$GOTELEGRAM_CONFIG" ]; then
|
||||||
|
cp "$GOTELEGRAM_CONFIG" "$tmp_dir/gotelegram.json"
|
||||||
|
fi
|
||||||
|
if [ -f "$GOTELEGRAM_DIR/disabled_users.json" ]; then
|
||||||
|
cp "$GOTELEGRAM_DIR/disabled_users.json" "$tmp_dir/disabled_users.json" 2>/dev/null
|
||||||
|
fi
|
||||||
|
if [ -f "$GOTELEGRAM_DIR/backup_schedule.json" ]; then
|
||||||
|
cp "$GOTELEGRAM_DIR/backup_schedule.json" "$tmp_dir/backup_schedule.json" 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Language marker (i18n)
|
||||||
|
if [ -f "$GOTELEGRAM_DIR/.language" ]; then
|
||||||
|
cp "$GOTELEGRAM_DIR/.language" "$tmp_dir/.language"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# nginx конфиг (stealth mode)
|
||||||
|
if [ -f "$NGINX_SITE_CONF" ]; then
|
||||||
|
cp "$NGINX_SITE_CONF" "$tmp_dir/nginx.conf"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# SSL сертификаты и renewal metadata для переносов между VPS
|
||||||
|
local domain
|
||||||
|
domain=$(config_get domain 2>/dev/null)
|
||||||
|
if [ -n "$domain" ] && [ -d "/etc/letsencrypt/live/$domain" ]; then
|
||||||
|
mkdir -p "$tmp_dir/letsencrypt/live" "$tmp_dir/letsencrypt/archive" "$tmp_dir/letsencrypt/renewal"
|
||||||
|
cp -a "/etc/letsencrypt/live/$domain" "$tmp_dir/letsencrypt/live/" 2>/dev/null
|
||||||
|
[ -d "/etc/letsencrypt/archive/$domain" ] && \
|
||||||
|
cp -a "/etc/letsencrypt/archive/$domain" "$tmp_dir/letsencrypt/archive/" 2>/dev/null
|
||||||
|
[ -f "/etc/letsencrypt/renewal/$domain.conf" ] && \
|
||||||
|
cp -a "/etc/letsencrypt/renewal/$domain.conf" "$tmp_dir/letsencrypt/renewal/" 2>/dev/null
|
||||||
|
log_dim "SSL сертификаты включены"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Шаблон сайта (если есть)
|
||||||
|
if [ -d "$WEBSITE_ROOT" ] && [ -f "$WEBSITE_ROOT/index.html" ]; then
|
||||||
|
mkdir -p "$tmp_dir/site"
|
||||||
|
cp -a "$WEBSITE_ROOT/." "$tmp_dir/site/"
|
||||||
|
log_dim "$(_t_or backup_site_included 'Шаблон сайта включён')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Custom templates and catalog
|
||||||
|
if [ -d "$GOTELEGRAM_DIR/custom_templates" ]; then
|
||||||
|
mkdir -p "$tmp_dir/custom_templates"
|
||||||
|
cp -a "$GOTELEGRAM_DIR/custom_templates/." "$tmp_dir/custom_templates/" 2>/dev/null
|
||||||
|
fi
|
||||||
|
if [ -f "$GOTELEGRAM_DIR/templates_catalog.json" ]; then
|
||||||
|
cp "$GOTELEGRAM_DIR/templates_catalog.json" "$tmp_dir/templates_catalog.json" 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Bot state (.env has BotFather token, so encrypted backups are strongly recommended)
|
||||||
|
if [ -d "$BOT_DIR" ]; then
|
||||||
|
mkdir -p "$tmp_dir/bot"
|
||||||
|
[ -f "$BOT_DIR/.env" ] && cp "$BOT_DIR/.env" "$tmp_dir/bot/.env" 2>/dev/null
|
||||||
|
[ -f "$BOT_DIR/i18n.py" ] && cp "$BOT_DIR/i18n.py" "$tmp_dir/bot/i18n.py" 2>/dev/null
|
||||||
|
[ -d "$BOT_DIR/lang" ] && cp -a "$BOT_DIR/lang" "$tmp_dir/bot/" 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Local web admin state
|
||||||
|
if [ -d "$ADMIN_WEB_DIR" ]; then
|
||||||
|
mkdir -p "$tmp_dir/admin_web"
|
||||||
|
[ -f "$ADMIN_WEB_DIR/server.py" ] && cp "$ADMIN_WEB_DIR/server.py" "$tmp_dir/admin_web/server.py" 2>/dev/null
|
||||||
|
[ -d "$ADMIN_WEB_DIR/static" ] && cp -a "$ADMIN_WEB_DIR/static" "$tmp_dir/admin_web/" 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Traffic history
|
||||||
|
if [ -f "$GOTELEGRAM_DIR/stats_history.csv" ]; then
|
||||||
|
cp "$GOTELEGRAM_DIR/stats_history.csv" "$tmp_dir/stats_history.csv" 2>/dev/null
|
||||||
|
fi
|
||||||
|
if [ -f "$GOTELEGRAM_DIR/user_stats_history.csv" ]; then
|
||||||
|
cp "$GOTELEGRAM_DIR/user_stats_history.csv" "$tmp_dir/user_stats_history.csv" 2>/dev/null
|
||||||
|
fi
|
||||||
|
if [ -f "$GOTELEGRAM_DIR/shared-443.json" ]; then
|
||||||
|
cp "$GOTELEGRAM_DIR/shared-443.json" "$tmp_dir/shared-443.json" 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Метаданные
|
||||||
|
local ip mode engine lang port domain
|
||||||
|
ip=$(get_server_ip)
|
||||||
|
mode=$(config_get mode 2>/dev/null || echo "unknown")
|
||||||
|
engine=$(config_get engine 2>/dev/null || echo "telemt")
|
||||||
|
lang=$(type get_language &>/dev/null && get_language 2>/dev/null || echo "en")
|
||||||
|
port=$(config_get port 2>/dev/null || echo "443")
|
||||||
|
# Ensure port is numeric; fall back to 443 if garbage
|
||||||
|
[[ "$port" =~ ^[0-9]+$ ]] || port=443
|
||||||
|
domain=$(config_get domain 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
cat > "$tmp_dir/metadata.json" << EOMETA
|
||||||
|
{
|
||||||
|
"backup_version": "1.6",
|
||||||
|
"gotelegram_version": "$GOTELEGRAM_VERSION",
|
||||||
|
"created_at": "$(date -Iseconds)",
|
||||||
|
"hostname": "$(hostname)",
|
||||||
|
"ip": "$ip",
|
||||||
|
"engine": "$engine",
|
||||||
|
"mode": "$mode",
|
||||||
|
"language": "$lang",
|
||||||
|
"port": $port,
|
||||||
|
"domain": "$domain"
|
||||||
|
}
|
||||||
|
EOMETA
|
||||||
|
|
||||||
|
# Архивируем
|
||||||
|
local tar_file="/tmp/${backup_name}.tar.gz"
|
||||||
|
if ! tar czf "$tar_file" -C /tmp "$backup_name" 2>/dev/null; then
|
||||||
|
log_error "$(_t_or backup_archive_err 'Ошибка создания архива')"
|
||||||
|
rm -rf "$tmp_dir"
|
||||||
|
rm -f "$tar_file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$tar_file" ]; then
|
||||||
|
log_error "$(_t_or backup_archive_missing 'Архив не создан')"
|
||||||
|
rm -rf "$tmp_dir"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Шифруем если задан пароль
|
||||||
|
local final_file=""
|
||||||
|
if [ -n "$password" ]; then
|
||||||
|
final_file="${output_dir}/${backup_name}.tar.gz.enc"
|
||||||
|
openssl enc -aes-256-cbc -salt -pbkdf2 -in "$tar_file" -out "$final_file" -pass "pass:${password}" 2>/dev/null
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
log_error "$(_t_or backup_encrypt_err 'Ошибка шифрования')"
|
||||||
|
rm -f "$tar_file"
|
||||||
|
rm -rf "$tmp_dir"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
rm -f "$tar_file"
|
||||||
|
log_success "$(_t_or backup_encrypted 'Бекап зашифрован (AES-256-CBC)')"
|
||||||
|
else
|
||||||
|
final_file="${output_dir}/${backup_name}.tar.gz"
|
||||||
|
mv "$tar_file" "$final_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# SHA256 подпись
|
||||||
|
sha256sum "$final_file" > "${final_file}.sha256" 2>/dev/null
|
||||||
|
|
||||||
|
# Очистка
|
||||||
|
rm -rf "$tmp_dir"
|
||||||
|
|
||||||
|
local size
|
||||||
|
size=$(du -h "$final_file" | cut -f1)
|
||||||
|
if type tf &>/dev/null; then
|
||||||
|
log_success "$(tf backup_created_fmt "$final_file" "$size")"
|
||||||
|
else
|
||||||
|
log_success "Бекап создан: $final_file ($size)"
|
||||||
|
fi
|
||||||
|
echo "$final_file"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Восстановление из бекапа ────────────────────────────────────────────────
|
||||||
|
restore_backup() {
|
||||||
|
local backup_file="$1"
|
||||||
|
local password="$2"
|
||||||
|
local assume_yes="$3"
|
||||||
|
|
||||||
|
if [ ! -f "$backup_file" ]; then
|
||||||
|
if type tf &>/dev/null; then
|
||||||
|
log_error "$(tf backup_file_not_found_fmt "$backup_file")"
|
||||||
|
else
|
||||||
|
log_error "Файл не найден: $backup_file"
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local tmp_dir="/tmp/gotelegram_restore_$$"
|
||||||
|
mkdir -p "$tmp_dir"
|
||||||
|
|
||||||
|
# Расшифровываем если нужно
|
||||||
|
local tar_file=""
|
||||||
|
if echo "$backup_file" | grep -q '\.enc$'; then
|
||||||
|
if [ -z "$password" ]; then
|
||||||
|
echo -ne " $(_t_or backup_enter_pass 'Введите пароль от бекапа'): "
|
||||||
|
read -rs password
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
tar_file="/tmp/gotelegram_restore_$$.tar.gz"
|
||||||
|
openssl enc -aes-256-cbc -d -pbkdf2 -in "$backup_file" -out "$tar_file" -pass "pass:${password}" 2>/dev/null
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
log_error "$(_t_or backup_bad_pass 'Неверный пароль или повреждённый файл')"
|
||||||
|
rm -rf "$tmp_dir" "$tar_file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
tar_file="$backup_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Распаковываем
|
||||||
|
tar xzf "$tar_file" -C "$tmp_dir" 2>/dev/null
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
log_error "$(_t_or backup_extract_err 'Ошибка распаковки архива')"
|
||||||
|
rm -rf "$tmp_dir"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Находим папку бекапа
|
||||||
|
local backup_dir
|
||||||
|
backup_dir=$(find "$tmp_dir" -maxdepth 1 -type d -name "gotelegram_backup_*" | head -1)
|
||||||
|
[ -z "$backup_dir" ] && backup_dir="$tmp_dir"
|
||||||
|
|
||||||
|
# Legacy bot backups before v2.5.0 stored absolute paths directly in tar:
|
||||||
|
# opt/gotelegram/config.json and etc/telemt/config.toml.
|
||||||
|
if [ ! -f "$backup_dir/config.toml" ] && [ -f "$tmp_dir/etc/telemt/config.toml" ]; then
|
||||||
|
cp "$tmp_dir/etc/telemt/config.toml" "$backup_dir/config.toml" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
if [ ! -f "$backup_dir/gotelegram.json" ] && [ -f "$tmp_dir/opt/gotelegram/config.json" ]; then
|
||||||
|
cp "$tmp_dir/opt/gotelegram/config.json" "$backup_dir/gotelegram.json" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
if [ ! -f "$backup_dir/disabled_users.json" ] && [ -f "$tmp_dir/opt/gotelegram/disabled_users.json" ]; then
|
||||||
|
cp "$tmp_dir/opt/gotelegram/disabled_users.json" "$backup_dir/disabled_users.json" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Проверяем метаданные
|
||||||
|
if [ -f "$backup_dir/metadata.json" ]; then
|
||||||
|
local bk_version bk_mode bk_ip bk_lang bk_date
|
||||||
|
bk_version=$(jq -r '.gotelegram_version // "unknown"' "$backup_dir/metadata.json")
|
||||||
|
bk_mode=$(jq -r '.mode // "unknown"' "$backup_dir/metadata.json")
|
||||||
|
bk_ip=$(jq -r '.ip // "unknown"' "$backup_dir/metadata.json")
|
||||||
|
bk_lang=$(jq -r '.language // "-"' "$backup_dir/metadata.json")
|
||||||
|
bk_date=$(jq -r '.created_at // "-"' "$backup_dir/metadata.json")
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BOLD}${WHITE}📦 $(_t_or backup_label 'Бекап'):${NC}"
|
||||||
|
echo -e " $(_t_or backup_version_label 'Версия'): $bk_version | $(_t_or backup_mode_label 'Режим'): $bk_mode | IP: $bk_ip | $(_t_or backup_lang_label 'Язык'): $bk_lang"
|
||||||
|
echo -e " $(_t_or backup_date_label 'Дата'): $bk_date"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$assume_yes" != "yes" ] && ! confirm "$(_t_or backup_confirm_restore 'Восстановить конфигурацию? Текущие настройки будут перезаписаны.')"; then
|
||||||
|
rm -rf "$tmp_dir"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Останавливаем сервисы
|
||||||
|
stop_telemt 2>/dev/null
|
||||||
|
systemctl stop nginx 2>/dev/null
|
||||||
|
|
||||||
|
# Восстанавливаем telemt конфиг
|
||||||
|
if [ -f "$backup_dir/config.toml" ]; then
|
||||||
|
mkdir -p /etc/telemt
|
||||||
|
cp "$backup_dir/config.toml" "$TELEMT_CONFIG"
|
||||||
|
chmod 600 "$TELEMT_CONFIG"
|
||||||
|
log_success "$(_t_or backup_restored_telemt 'telemt конфиг восстановлен')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Восстанавливаем goTelegram Pro конфиг
|
||||||
|
if [ -f "$backup_dir/gotelegram.json" ]; then
|
||||||
|
mkdir -p "$GOTELEGRAM_DIR"
|
||||||
|
cp "$backup_dir/gotelegram.json" "$GOTELEGRAM_CONFIG"
|
||||||
|
log_success "$(_t_or backup_restored_gotelegram 'GoTelegram конфиг восстановлен')"
|
||||||
|
fi
|
||||||
|
if [ -f "$backup_dir/disabled_users.json" ]; then
|
||||||
|
mkdir -p "$GOTELEGRAM_DIR"
|
||||||
|
cp "$backup_dir/disabled_users.json" "$GOTELEGRAM_DIR/disabled_users.json"
|
||||||
|
chmod 600 "$GOTELEGRAM_DIR/disabled_users.json" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
if [ -f "$backup_dir/backup_schedule.json" ]; then
|
||||||
|
mkdir -p "$GOTELEGRAM_DIR"
|
||||||
|
cp "$backup_dir/backup_schedule.json" "$GOTELEGRAM_DIR/backup_schedule.json" 2>/dev/null
|
||||||
|
chmod 600 "$GOTELEGRAM_DIR/backup_schedule.json" 2>/dev/null || true
|
||||||
|
if command -v jq >/dev/null 2>&1; then
|
||||||
|
local restored_schedule
|
||||||
|
restored_schedule=$(jq -r '.frequency // "off"' "$GOTELEGRAM_DIR/backup_schedule.json" 2>/dev/null || echo "off")
|
||||||
|
case "$restored_schedule" in
|
||||||
|
off|daily|weekly|monthly) set_backup_schedule "$restored_schedule" >/dev/null 2>&1 || true ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Восстанавливаем language marker (i18n)
|
||||||
|
if [ -f "$backup_dir/.language" ]; then
|
||||||
|
mkdir -p "$GOTELEGRAM_DIR"
|
||||||
|
cp "$backup_dir/.language" "$GOTELEGRAM_DIR/.language"
|
||||||
|
log_success "$(_t_or backup_restored_lang 'Язык интерфейса восстановлен')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Восстанавливаем nginx конфиг
|
||||||
|
if [ -f "$backup_dir/nginx.conf" ]; then
|
||||||
|
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
|
||||||
|
cp "$backup_dir/nginx.conf" "$NGINX_SITE_CONF"
|
||||||
|
ln -sf "$NGINX_SITE_CONF" "$NGINX_SITE_LINK"
|
||||||
|
log_success "$(_t_or backup_restored_nginx 'nginx конфиг восстановлен')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Восстанавливаем SSL / Let's Encrypt structure
|
||||||
|
if [ -d "$backup_dir/letsencrypt" ]; then
|
||||||
|
mkdir -p /etc/letsencrypt/live /etc/letsencrypt/archive /etc/letsencrypt/renewal
|
||||||
|
[ -d "$backup_dir/letsencrypt/live" ] && cp -a "$backup_dir/letsencrypt/live/." /etc/letsencrypt/live/ 2>/dev/null
|
||||||
|
[ -d "$backup_dir/letsencrypt/archive" ] && cp -a "$backup_dir/letsencrypt/archive/." /etc/letsencrypt/archive/ 2>/dev/null
|
||||||
|
[ -d "$backup_dir/letsencrypt/renewal" ] && cp -a "$backup_dir/letsencrypt/renewal/." /etc/letsencrypt/renewal/ 2>/dev/null
|
||||||
|
log_success "$(_t_or backup_restored_ssl 'SSL сертификаты восстановлены')"
|
||||||
|
elif [ -d "$backup_dir/certs" ]; then
|
||||||
|
local domain
|
||||||
|
domain=$(config_get domain 2>/dev/null)
|
||||||
|
if [ -n "$domain" ]; then
|
||||||
|
local cert_dir="/etc/letsencrypt/live/$domain"
|
||||||
|
mkdir -p "$cert_dir"
|
||||||
|
cp "$backup_dir/certs/"* "$cert_dir/" 2>/dev/null
|
||||||
|
log_success "$(_t_or backup_restored_ssl 'SSL сертификаты восстановлены')"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Восстанавливаем шаблон сайта
|
||||||
|
if [ -d "$backup_dir/site" ]; then
|
||||||
|
mkdir -p "$WEBSITE_ROOT"
|
||||||
|
cp -a "$backup_dir/site/." "$WEBSITE_ROOT/"
|
||||||
|
chown -R www-data:www-data "$WEBSITE_ROOT" 2>/dev/null
|
||||||
|
log_success "$(_t_or backup_restored_site 'Шаблон сайта восстановлен')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Восстанавливаем custom templates/catalog/statistics
|
||||||
|
if [ -d "$backup_dir/custom_templates" ]; then
|
||||||
|
mkdir -p "$GOTELEGRAM_DIR/custom_templates"
|
||||||
|
cp -a "$backup_dir/custom_templates/." "$GOTELEGRAM_DIR/custom_templates/" 2>/dev/null
|
||||||
|
log_success "Пользовательские шаблоны восстановлены"
|
||||||
|
fi
|
||||||
|
if [ -f "$backup_dir/templates_catalog.json" ]; then
|
||||||
|
cp "$backup_dir/templates_catalog.json" "$GOTELEGRAM_DIR/templates_catalog.json" 2>/dev/null
|
||||||
|
fi
|
||||||
|
if [ -f "$backup_dir/stats_history.csv" ]; then
|
||||||
|
cp "$backup_dir/stats_history.csv" "$GOTELEGRAM_DIR/stats_history.csv" 2>/dev/null
|
||||||
|
log_success "История статистики восстановлена"
|
||||||
|
fi
|
||||||
|
if [ -f "$backup_dir/user_stats_history.csv" ]; then
|
||||||
|
cp "$backup_dir/user_stats_history.csv" "$GOTELEGRAM_DIR/user_stats_history.csv" 2>/dev/null
|
||||||
|
log_success "История статистики пользователей восстановлена"
|
||||||
|
fi
|
||||||
|
if [ -f "$backup_dir/shared-443.json" ]; then
|
||||||
|
cp "$backup_dir/shared-443.json" "$GOTELEGRAM_DIR/shared-443.json" 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Восстанавливаем состояние бота
|
||||||
|
if [ -d "$backup_dir/bot" ]; then
|
||||||
|
mkdir -p "$BOT_DIR"
|
||||||
|
[ -f "$backup_dir/bot/.env" ] && cp "$backup_dir/bot/.env" "$BOT_DIR/.env" 2>/dev/null && chmod 600 "$BOT_DIR/.env"
|
||||||
|
[ -d "$backup_dir/bot/lang" ] && cp -a "$backup_dir/bot/lang" "$BOT_DIR/" 2>/dev/null
|
||||||
|
[ -f "$backup_dir/bot/i18n.py" ] && cp "$backup_dir/bot/i18n.py" "$BOT_DIR/i18n.py" 2>/dev/null
|
||||||
|
log_success "Конфигурация Telegram-бота восстановлена"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Восстанавливаем состояние локальной web-админки
|
||||||
|
if [ -d "$backup_dir/admin_web" ]; then
|
||||||
|
mkdir -p "$ADMIN_WEB_DIR"
|
||||||
|
[ -f "$backup_dir/admin_web/server.py" ] && cp "$backup_dir/admin_web/server.py" "$ADMIN_WEB_DIR/server.py" 2>/dev/null
|
||||||
|
[ -d "$backup_dir/admin_web/static" ] && cp -a "$backup_dir/admin_web/static" "$ADMIN_WEB_DIR/" 2>/dev/null
|
||||||
|
rm -f "$ADMIN_WEB_DIR/token" 2>/dev/null || true
|
||||||
|
log_success "Конфигурация web-админки восстановлена"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Запускаем сервисы
|
||||||
|
if is_telemt_installed && [ ! -f "/etc/systemd/system/${TELEMT_SERVICE}.service" ]; then
|
||||||
|
install_telemt_service
|
||||||
|
fi
|
||||||
|
if is_telemt_installed; then
|
||||||
|
start_telemt
|
||||||
|
fi
|
||||||
|
command -v nginx &>/dev/null && systemctl start nginx 2>/dev/null
|
||||||
|
systemctl restart gotelegram-bot 2>/dev/null || true
|
||||||
|
systemctl restart gotelegram-admin 2>/dev/null || true
|
||||||
|
|
||||||
|
# Очистка
|
||||||
|
rm -rf "$tmp_dir"
|
||||||
|
[ "$tar_file" != "$backup_file" ] && rm -f "$tar_file"
|
||||||
|
|
||||||
|
log_success "$(_t_or backup_restore_done 'Восстановление завершено!')"
|
||||||
|
show_proxy_info
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Список бекапов ───────────────────────────────────────────────────────────
|
||||||
|
list_backups() {
|
||||||
|
if [ ! -d "$BACKUP_DIR" ] || [ -z "$(ls -A "$BACKUP_DIR" 2>/dev/null)" ]; then
|
||||||
|
log_info "$(_t_or backup_none 'Бекапов нет')"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BOLD}${WHITE}📦 $(_t_or backup_list_title 'Доступные бекапы'):${NC}"
|
||||||
|
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}"
|
||||||
|
|
||||||
|
local i=1
|
||||||
|
for f in "$BACKUP_DIR"/*.tar.gz*; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
[[ "$f" == *.sha256 ]] && continue
|
||||||
|
local size date_str name
|
||||||
|
size=$(du -h "$f" | cut -f1)
|
||||||
|
name=$(basename "$f")
|
||||||
|
date_str=$(echo "$name" | grep -oE '[0-9]{8}_[0-9]{6}' | head -1)
|
||||||
|
local encrypted=""
|
||||||
|
[[ "$f" == *.enc ]] && encrypted=" 🔒"
|
||||||
|
echo -e " ${CYAN}${i})${NC} ${name} (${size})${encrypted}"
|
||||||
|
((i++))
|
||||||
|
done
|
||||||
|
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Очистка старых бекапов ───────────────────────────────────────────────────
|
||||||
|
cleanup_old_backups() {
|
||||||
|
local keep="${1:-5}"
|
||||||
|
local count
|
||||||
|
count=$(find "$BACKUP_DIR" -name "*.tar.gz*" ! -name "*.sha256" 2>/dev/null | wc -l)
|
||||||
|
|
||||||
|
if [ "$count" -gt "$keep" ]; then
|
||||||
|
local to_delete=$((count - keep))
|
||||||
|
find "$BACKUP_DIR" -name "*.tar.gz*" ! -name "*.sha256" 2>/dev/null | sort | head -n "$to_delete" | while read -r f; do
|
||||||
|
rm -f "$f" "${f}.sha256"
|
||||||
|
done
|
||||||
|
if type tf &>/dev/null; then
|
||||||
|
log_dim "$(tf backup_cleanup_fmt "$to_delete" "$keep")"
|
||||||
|
else
|
||||||
|
log_dim "Удалено $to_delete старых бекапов (оставлено $keep)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Расписание бекапов ───────────────────────────────────────────────────────
|
||||||
|
backup_schedule_calendar() {
|
||||||
|
case "${1:-off}" in
|
||||||
|
off) echo "" ;;
|
||||||
|
daily) echo "*-*-* 03:20:00" ;;
|
||||||
|
weekly) echo "Sun 03:20:00" ;;
|
||||||
|
monthly) echo "*-*-01 03:20:00" ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
set_backup_schedule() {
|
||||||
|
local frequency="${1:-off}"
|
||||||
|
local calendar
|
||||||
|
if ! calendar=$(backup_schedule_calendar "$frequency"); then
|
||||||
|
log_error "Unsupported backup schedule: $frequency"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$GOTELEGRAM_DIR" "$BACKUP_DIR"
|
||||||
|
|
||||||
|
if [ "$frequency" = "off" ]; then
|
||||||
|
systemctl disable --now gotelegram-backup.timer >/dev/null 2>&1 || true
|
||||||
|
rm -f /etc/systemd/system/gotelegram-backup.timer /etc/systemd/system/gotelegram-backup.service
|
||||||
|
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||||
|
else
|
||||||
|
cat > /etc/systemd/system/gotelegram-backup.service << 'EOSVC'
|
||||||
|
[Unit]
|
||||||
|
Description=goTelegram Pro backup
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
Environment=GOTELEGRAM_BACKUP_KEEP=30
|
||||||
|
ExecStart=/bin/bash -lc '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 "${GOTELEGRAM_BACKUP_KEEP:-30}"'
|
||||||
|
EOSVC
|
||||||
|
|
||||||
|
cat > /etc/systemd/system/gotelegram-backup.timer << EOTIMER
|
||||||
|
[Unit]
|
||||||
|
Description=goTelegram Pro scheduled backup
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=$calendar
|
||||||
|
Persistent=true
|
||||||
|
RandomizedDelaySec=15m
|
||||||
|
Unit=gotelegram-backup.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
EOTIMER
|
||||||
|
systemctl daemon-reload >/dev/null 2>&1 || return 1
|
||||||
|
systemctl enable --now gotelegram-backup.timer >/dev/null 2>&1 || return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "$GOTELEGRAM_DIR/backup_schedule.json" << EOSCHEDULE
|
||||||
|
{
|
||||||
|
"frequency": "$frequency",
|
||||||
|
"calendar": "$calendar",
|
||||||
|
"keep": 30,
|
||||||
|
"updated_at": "$(date -Iseconds)"
|
||||||
|
}
|
||||||
|
EOSCHEDULE
|
||||||
|
chmod 600 "$GOTELEGRAM_DIR/backup_schedule.json" 2>/dev/null || true
|
||||||
|
log_success "Backup schedule: $frequency"
|
||||||
|
echo "$frequency"
|
||||||
|
}
|
||||||
|
|
||||||
|
backup_schedule_status() {
|
||||||
|
local frequency="off" calendar=""
|
||||||
|
if [ -f "$GOTELEGRAM_DIR/backup_schedule.json" ] && command -v jq >/dev/null 2>&1; then
|
||||||
|
frequency=$(jq -r '.frequency // "off"' "$GOTELEGRAM_DIR/backup_schedule.json" 2>/dev/null || echo "off")
|
||||||
|
calendar=$(jq -r '.calendar // ""' "$GOTELEGRAM_DIR/backup_schedule.json" 2>/dev/null || echo "")
|
||||||
|
fi
|
||||||
|
echo "frequency=$frequency calendar=$calendar"
|
||||||
|
systemctl list-timers gotelegram-backup.timer --no-pager 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Интерактивный бекап ──────────────────────────────────────────────────────
|
||||||
|
interactive_backup() {
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BOLD}${WHITE}💾 $(_t_or backup_create_title 'Создание бекапа')${NC}"
|
||||||
|
echo -ne " $(_t_or backup_encrypt_prompt 'Зашифровать бекап паролем?') [Y/n]: "
|
||||||
|
read -r use_pass
|
||||||
|
|
||||||
|
local password=""
|
||||||
|
if [[ ! "$use_pass" =~ ^[Nn] ]]; then
|
||||||
|
echo -ne " $(_t_or backup_enter_pass 'Введите пароль'): "
|
||||||
|
read -rs password
|
||||||
|
echo ""
|
||||||
|
echo -ne " $(_t_or backup_repeat_pass 'Повторите пароль'): "
|
||||||
|
read -rs password2
|
||||||
|
echo ""
|
||||||
|
if [ "$password" != "$password2" ]; then
|
||||||
|
log_error "$(_t_or backup_pass_mismatch 'Пароли не совпадают')"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [ ${#password} -lt 6 ]; then
|
||||||
|
log_error "$(_t_or backup_pass_short 'Пароль слишком короткий (минимум 6 символов)')"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
create_backup "$password"
|
||||||
|
cleanup_old_backups
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Интерактивное восстановление ─────────────────────────────────────────────
|
||||||
|
interactive_restore() {
|
||||||
|
list_backups || return 1
|
||||||
|
|
||||||
|
echo -ne " $(_t_or backup_pick_prompt 'Номер бекапа (или путь к файлу)'): "
|
||||||
|
read -r choice
|
||||||
|
|
||||||
|
local backup_file=""
|
||||||
|
if [[ "$choice" =~ ^[0-9]+$ ]]; then
|
||||||
|
local i=1
|
||||||
|
for f in "$BACKUP_DIR"/*.tar.gz*; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
[[ "$f" == *.sha256 ]] && continue
|
||||||
|
if [ "$i" -eq "$choice" ]; then
|
||||||
|
backup_file="$f"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
((i++))
|
||||||
|
done
|
||||||
|
elif [ -f "$choice" ]; then
|
||||||
|
backup_file="$choice"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$backup_file" ]; then
|
||||||
|
log_error "$(_t_or backup_not_found 'Бекап не найден')"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
restore_backup "$backup_file"
|
||||||
|
}
|
||||||
740
lib/common.sh
Normal file
740
lib/common.sh
Normal file
@@ -0,0 +1,740 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# goTelegram Pro v2.5.0 — common utilities
|
||||||
|
# Colors, logging, spinner, system helpers, v1 compat, i18n-aware
|
||||||
|
|
||||||
|
# ── Version ───────────────────────────────────────────────────────────────────
|
||||||
|
GOTELEGRAM_VERSION="2.5.0"
|
||||||
|
GOTELEGRAM_NAME="goTelegram Pro"
|
||||||
|
|
||||||
|
# ── Пути ──────────────────────────────────────────────────────────────────────
|
||||||
|
GOTELEGRAM_DIR="/opt/gotelegram"
|
||||||
|
GOTELEGRAM_CONFIG="$GOTELEGRAM_DIR/config.json"
|
||||||
|
TELEMT_CONFIG="/etc/telemt/config.toml"
|
||||||
|
TELEMT_BIN="/usr/local/bin/telemt"
|
||||||
|
TELEMT_SERVICE="telemt"
|
||||||
|
NGINX_SITE_CONF="/etc/nginx/sites-available/gotelegram"
|
||||||
|
NGINX_SITE_LINK="/etc/nginx/sites-enabled/gotelegram"
|
||||||
|
WEBSITE_ROOT="/var/www/gotelegram-site"
|
||||||
|
BACKUP_DIR="$GOTELEGRAM_DIR/backups"
|
||||||
|
LOG_FILE="/var/log/gotelegram.log"
|
||||||
|
BOT_DIR="/opt/gotelegram-bot"
|
||||||
|
ADMIN_WEB_DIR="/opt/gotelegram-admin"
|
||||||
|
ADMIN_WEB_SERVICE="gotelegram-admin"
|
||||||
|
ADMIN_WEB_HOST="127.0.0.1"
|
||||||
|
ADMIN_WEB_PORT="1984"
|
||||||
|
|
||||||
|
# ── V1 совместимость ─────────────────────────────────────────────────────────
|
||||||
|
V1_CONTAINER_NAME="mtproto-proxy"
|
||||||
|
V1_CONFIG_FILE="/opt/gotelegram-bot/proxy.json"
|
||||||
|
V1_SERVICE_NAME="gotelegram-bot"
|
||||||
|
|
||||||
|
# ── Цвета ────────────────────────────────────────────────────────────────────
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
MAGENTA='\033[0;35m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
WHITE='\033[1;37m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
DIM='\033[2m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# ── Логирование ──────────────────────────────────────────────────────────────
|
||||||
|
log_info() { echo -e " ${CYAN}ℹ${NC} $*" >&2; }
|
||||||
|
log_success() { echo -e " ${GREEN}✓${NC} $*" >&2; }
|
||||||
|
log_warning() { echo -e " ${YELLOW}⚠${NC} $*" >&2; }
|
||||||
|
log_error() { echo -e " ${RED}✗${NC} $*" >&2; }
|
||||||
|
log_step() { echo -e "\n${BOLD}${WHITE} $*${NC}" >&2; }
|
||||||
|
log_dim() { echo -e " ${DIM}$*${NC}" >&2; }
|
||||||
|
|
||||||
|
log_to_file() {
|
||||||
|
local ts; ts=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
echo "[$ts] $*" >> "$LOG_FILE" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Spinner ──────────────────────────────────────────────────────────────────
|
||||||
|
_spin_pid=""
|
||||||
|
spinner_start() {
|
||||||
|
local default_msg
|
||||||
|
default_msg=$(type t &>/dev/null && t wait || echo "Please wait...")
|
||||||
|
local msg="${1:-$default_msg}"
|
||||||
|
(
|
||||||
|
local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
|
||||||
|
local i=0
|
||||||
|
while true; do
|
||||||
|
printf "\r ${CYAN}${frames[$i]}${NC} ${msg}" >&2
|
||||||
|
i=$(( (i+1) % ${#frames[@]} ))
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
) &
|
||||||
|
_spin_pid=$!
|
||||||
|
}
|
||||||
|
|
||||||
|
spinner_stop() {
|
||||||
|
[ -n "$_spin_pid" ] && kill "$_spin_pid" 2>/dev/null && wait "$_spin_pid" 2>/dev/null
|
||||||
|
_spin_pid=""
|
||||||
|
printf "\r\033[K" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Прогресс-бар ─────────────────────────────────────────────────────────────
|
||||||
|
progress_bar() {
|
||||||
|
local current="$1" total="$2" label="${3:-}"
|
||||||
|
local pct=$(( current * 100 / total ))
|
||||||
|
local filled=$(( pct / 2 ))
|
||||||
|
local empty=$(( 50 - filled ))
|
||||||
|
local bar=""
|
||||||
|
for ((i=0; i<filled; i++)); do bar+="█"; done
|
||||||
|
for ((i=0; i<empty; i++)); do bar+="░"; done
|
||||||
|
printf "\r ${GREEN}[${bar}]${NC} ${pct}%% ${label}" >&2
|
||||||
|
[ "$current" -eq "$total" ] && echo "" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Выполнение с индикатором ─────────────────────────────────────────────────
|
||||||
|
run_with_spinner() {
|
||||||
|
local label="$1"; shift
|
||||||
|
local err_file="/tmp/.gotelegram_spinner_err_$$"
|
||||||
|
spinner_start "$label"
|
||||||
|
"$@" >/dev/null 2>"$err_file"
|
||||||
|
local rc=$?
|
||||||
|
spinner_stop
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
log_success "$label"
|
||||||
|
else
|
||||||
|
local err_label
|
||||||
|
err_label=$(type t &>/dev/null && t error || echo "error")
|
||||||
|
log_error "$label ${RED}(${err_label}, code: $rc)${NC}"
|
||||||
|
if [ -s "$err_file" ]; then
|
||||||
|
log_dim " $(head -3 "$err_file")"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
rm -f "$err_file"
|
||||||
|
return $rc
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Banner ───────────────────────────────────────────────────────────────────
|
||||||
|
show_banner() {
|
||||||
|
local line
|
||||||
|
line=$(printf '━%.0s' $(seq 1 60))
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}${line}${NC}"
|
||||||
|
if type tf &>/dev/null; then
|
||||||
|
echo -e " ${BOLD}${WHITE}🚀 $(tf banner_title "$GOTELEGRAM_VERSION")${NC}"
|
||||||
|
echo -e " ${DIM}$(t banner_subtitle)${NC}"
|
||||||
|
echo -e " ${DIM}$(t banner_features)${NC}"
|
||||||
|
else
|
||||||
|
echo -e " ${BOLD}${WHITE}🚀 goTelegram Pro v${GOTELEGRAM_VERSION}${NC}"
|
||||||
|
echo -e " ${DIM}MTProxy powered by telemt (Rust + Tokio)${NC}"
|
||||||
|
echo -e " ${DIM}Anti-DPI • Fake TLS • TCP Splice • JA3/JA4${NC}"
|
||||||
|
fi
|
||||||
|
echo -e "${CYAN}${line}${NC}"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Credits ──────────────────────────────────────────────────────────────────
|
||||||
|
show_credits() {
|
||||||
|
local line
|
||||||
|
line=$(printf '─%.0s' $(seq 1 60))
|
||||||
|
echo ""
|
||||||
|
echo -e "${MAGENTA}${line}${NC}"
|
||||||
|
echo -e " ${BOLD}$(type t &>/dev/null && t credits_title || echo 'Credits')${NC}"
|
||||||
|
echo -e "${MAGENTA}${line}${NC}"
|
||||||
|
echo -e " ${WHITE}telemt${NC} — MTProxy engine (Rust)"
|
||||||
|
echo -e " ${DIM}github.com/telemt/telemt${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${WHITE}HTML5 UP${NC} — responsive HTML/CSS templates"
|
||||||
|
echo -e " ${DIM}html5up.net • CC BY 3.0 • @ajlkn${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${WHITE}learning-zone${NC} — 150+ HTML5 templates"
|
||||||
|
echo -e " ${DIM}github.com/learning-zone/website-templates${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${WHITE}Start Bootstrap${NC} — MIT license"
|
||||||
|
echo -e " ${DIM}startbootstrap.com${NC}"
|
||||||
|
echo -e "${MAGENTA}${line}${NC}"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Системные утилиты ────────────────────────────────────────────────────────
|
||||||
|
_valid_ip() {
|
||||||
|
# Validate that each octet is 0-255
|
||||||
|
local ip="$1"
|
||||||
|
local IFS='.'
|
||||||
|
read -ra octets <<< "$ip"
|
||||||
|
[ ${#octets[@]} -ne 4 ] && return 1
|
||||||
|
for octet in "${octets[@]}"; do
|
||||||
|
[[ "$octet" =~ ^[0-9]+$ ]] || return 1
|
||||||
|
[ "$octet" -gt 255 ] && return 1
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
get_server_ip() {
|
||||||
|
local ip raw
|
||||||
|
for url in "https://api.ipify.org" "https://icanhazip.com" "https://ifconfig.me"; do
|
||||||
|
raw=$(curl -s -4 --max-time 5 "$url" 2>/dev/null)
|
||||||
|
ip=$(echo "$raw" | grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1)
|
||||||
|
if [ -n "$ip" ] && _valid_ip "$ip"; then
|
||||||
|
echo "$ip"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "0.0.0.0"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
_t_or() {
|
||||||
|
# Helper: translate if i18n available, otherwise return fallback
|
||||||
|
local key="$1" fallback="$2"
|
||||||
|
if type t &>/dev/null; then
|
||||||
|
t "$key"
|
||||||
|
else
|
||||||
|
echo "$fallback"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_root() {
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
log_error "$(_t_or err_need_root 'Run the script with sudo / as root')"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_os() {
|
||||||
|
if [ ! -f /etc/os-release ]; then
|
||||||
|
log_error "$(_t_or err_os_unknown 'Failed to detect OS. Linux is required.')"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
# Validate os-release before sourcing (reject command injection: ;, backticks, $())
|
||||||
|
if grep -qE '(;|`|\$\(|\$\{)' /etc/os-release 2>/dev/null; then
|
||||||
|
log_warning "/etc/os-release contains suspicious strings, skipping"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
. /etc/os-release
|
||||||
|
case "$ID" in
|
||||||
|
ubuntu|debian|centos|rocky|almalinux|fedora|rhel)
|
||||||
|
log_dim "OS: $PRETTY_NAME"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log_warning "OS $ID may be incompatible. Supported: Ubuntu, Debian, CentOS, Rocky."
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
get_arch() {
|
||||||
|
local arch
|
||||||
|
arch=$(uname -m)
|
||||||
|
case "$arch" in
|
||||||
|
x86_64|amd64) echo "amd64" ;;
|
||||||
|
aarch64|arm64) echo "arm64" ;;
|
||||||
|
armv7*|armhf) echo "armv7" ;;
|
||||||
|
*) echo "$arch" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
get_pkg_manager() {
|
||||||
|
if command -v apt-get &>/dev/null; then echo "apt"
|
||||||
|
elif command -v dnf &>/dev/null; then echo "dnf"
|
||||||
|
elif command -v yum &>/dev/null; then echo "yum"
|
||||||
|
else echo "unknown"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_pkg() {
|
||||||
|
local pkg="$1"
|
||||||
|
case "$(get_pkg_manager)" in
|
||||||
|
apt) apt_install "$pkg" ;;
|
||||||
|
dnf) dnf install -y -q "$pkg" ;;
|
||||||
|
yum) yum install -y -q "$pkg" ;;
|
||||||
|
*) log_error "$(_t_or err_bad_pkg_mgr 'Unknown package manager')"; return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── apt lock wait + install ─────────────────────────────────────────────────
|
||||||
|
# На свежих Ubuntu/Debian unattended-upgrades часто держит dpkg lock на старте
|
||||||
|
# → любой apt-get install падает с "Could not get lock /var/lib/dpkg/lock-frontend".
|
||||||
|
# Эти функции ждут освобождения лока до 300с, потом запускают apt с нативным
|
||||||
|
# таймаутом DPkg::Lock::Timeout. Использовать везде, где раньше был
|
||||||
|
# "apt-get install ...".
|
||||||
|
apt_lock_wait() {
|
||||||
|
local max_wait="${1:-300}"
|
||||||
|
local waited=0
|
||||||
|
local warned=0
|
||||||
|
while fuser /var/lib/dpkg/lock-frontend &>/dev/null \
|
||||||
|
|| fuser /var/lib/dpkg/lock &>/dev/null \
|
||||||
|
|| fuser /var/lib/apt/lists/lock &>/dev/null \
|
||||||
|
|| pgrep -f '^/usr/bin/unattended-upgrade' &>/dev/null; do
|
||||||
|
if [ "$warned" = "0" ]; then
|
||||||
|
log_warning "apt/dpkg locked by unattended-upgrades, waiting up to ${max_wait}s..."
|
||||||
|
warned=1
|
||||||
|
fi
|
||||||
|
sleep 3
|
||||||
|
waited=$((waited + 3))
|
||||||
|
if [ "$waited" -ge "$max_wait" ]; then
|
||||||
|
log_error "apt lock not released after ${max_wait}s"
|
||||||
|
log_dim "Manual fix: systemctl stop unattended-upgrades && killall -9 unattended-upgr 2>/dev/null; dpkg --configure -a"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[ "$warned" = "1" ] && log_success "apt lock released (waited ${waited}s)"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# apt_install <pkg> [pkg2 ...] — ждёт lock + ставит пакеты + показывает ошибку
|
||||||
|
apt_install() {
|
||||||
|
[ $# -eq 0 ] && return 0
|
||||||
|
apt_lock_wait || return 1
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
local opts="-o DPkg::Lock::Timeout=120"
|
||||||
|
local err_file; err_file=$(mktemp 2>/dev/null || echo /tmp/apt_err.$$)
|
||||||
|
if ! apt-get $opts install -y -qq "$@" 2>"$err_file"; then
|
||||||
|
log_error "apt-get install failed: $*"
|
||||||
|
[ -s "$err_file" ] && tail -n 5 "$err_file" | sed 's/^/ /' >&2
|
||||||
|
rm -f "$err_file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
rm -f "$err_file"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# apt_update — тихий update с ожиданием лока
|
||||||
|
apt_update() {
|
||||||
|
apt_lock_wait || return 1
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get -o DPkg::Lock::Timeout=120 update -qq 2>/dev/null || true
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Зависимости GoTelegram ──────────────────────────────────────────────────
|
||||||
|
# Полный список внешних команд, которые скрипт использует. Для каждой команды
|
||||||
|
# указан пакет на apt и dnf/yum (имена различаются: например dig = dnsutils на
|
||||||
|
# Debian, bind-utils на RHEL).
|
||||||
|
#
|
||||||
|
# КРИТИЧЕСКИЕ (без них скрипт просто не работает):
|
||||||
|
# jq — парсинг config.json, templates_catalog.json
|
||||||
|
# curl — скачивание telemt и проверки HTTPS
|
||||||
|
# openssl — генерация секретов, шифрование бекапов, SSL проверка
|
||||||
|
# git — клонирование шаблонов через download_template
|
||||||
|
# xxd — hex-encode домена для fake-TLS секрета (ee-prefix)
|
||||||
|
# tar — распаковка telemt архива и бекапы
|
||||||
|
# dig — DNS-проверка домена в Pro-режиме
|
||||||
|
#
|
||||||
|
# ЖЕЛАТЕЛЬНЫЕ (есть fallback, но с ними лучше):
|
||||||
|
# qrencode — QR-коды для прокси-ссылок
|
||||||
|
# bc — красивое форматирование чисел в статистике
|
||||||
|
#
|
||||||
|
# Pro-режим доустанавливает nginx/certbot через install_nginx/install_certbot
|
||||||
|
# (они большие и нужны только если пользователь выбрал Pro).
|
||||||
|
|
||||||
|
# Маппинг команды -> (apt_pkg, dnf_pkg). apt_pkg_for_cmd <cmd>
|
||||||
|
apt_pkg_for_cmd() {
|
||||||
|
case "$1" in
|
||||||
|
dig) echo "dnsutils" ;;
|
||||||
|
xxd) echo "xxd" ;; # Ubuntu 22+: отдельный пакет, fallback ниже
|
||||||
|
nslookup) echo "dnsutils" ;;
|
||||||
|
host) echo "dnsutils" ;;
|
||||||
|
ss) echo "iproute2" ;;
|
||||||
|
netstat) echo "net-tools" ;;
|
||||||
|
flock) echo "util-linux" ;;
|
||||||
|
iptables) echo "iptables" ;;
|
||||||
|
*) echo "$1" ;; # команда == имя пакета
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
dnf_pkg_for_cmd() {
|
||||||
|
case "$1" in
|
||||||
|
dig|nslookup|host) echo "bind-utils" ;;
|
||||||
|
xxd) echo "vim-common" ;;
|
||||||
|
ss) echo "iproute" ;;
|
||||||
|
netstat) echo "net-tools" ;;
|
||||||
|
flock) echo "util-linux" ;;
|
||||||
|
iptables) echo "iptables" ;;
|
||||||
|
*) echo "$1" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_deps() {
|
||||||
|
# Критические зависимости — без них скрипт не работает.
|
||||||
|
# flock используется bot_action_dispatch для сериализации параллельных
|
||||||
|
# вызовов (иначе гонка на config.json при одновременных change-template /
|
||||||
|
# change-lite-domain из бота).
|
||||||
|
local critical=(curl jq openssl git xxd tar dig flock)
|
||||||
|
# Желательные — есть fallback, устанавливать всё равно, но не падать если не смогли
|
||||||
|
local optional=(qrencode bc iptables)
|
||||||
|
|
||||||
|
local missing_critical=() missing_optional=() cmd
|
||||||
|
for cmd in "${critical[@]}"; do
|
||||||
|
command -v "$cmd" &>/dev/null || missing_critical+=("$cmd")
|
||||||
|
done
|
||||||
|
for cmd in "${optional[@]}"; do
|
||||||
|
command -v "$cmd" &>/dev/null || missing_optional+=("$cmd")
|
||||||
|
done
|
||||||
|
|
||||||
|
local all_missing=("${missing_critical[@]}" "${missing_optional[@]}")
|
||||||
|
[ ${#all_missing[@]} -eq 0 ] && return 0
|
||||||
|
|
||||||
|
# Собираем список пакетов для выбранного менеджера
|
||||||
|
local pkg_mgr pkg pkgs=()
|
||||||
|
pkg_mgr=$(get_pkg_manager)
|
||||||
|
|
||||||
|
for cmd in "${all_missing[@]}"; do
|
||||||
|
case "$pkg_mgr" in
|
||||||
|
apt) pkg=$(apt_pkg_for_cmd "$cmd") ;;
|
||||||
|
dnf|yum) pkg=$(dnf_pkg_for_cmd "$cmd") ;;
|
||||||
|
*) pkg="$cmd" ;;
|
||||||
|
esac
|
||||||
|
pkgs+=("$pkg")
|
||||||
|
done
|
||||||
|
|
||||||
|
# Убираем дубликаты (например dig+nslookup оба = dnsutils)
|
||||||
|
local uniq_pkgs=()
|
||||||
|
for pkg in "${pkgs[@]}"; do
|
||||||
|
local found=0 p
|
||||||
|
for p in "${uniq_pkgs[@]}"; do
|
||||||
|
[ "$p" = "$pkg" ] && { found=1; break; }
|
||||||
|
done
|
||||||
|
[ "$found" = "0" ] && uniq_pkgs+=("$pkg")
|
||||||
|
done
|
||||||
|
|
||||||
|
if type tf &>/dev/null; then
|
||||||
|
log_step "$(tf deps_installing "${all_missing[*]}")"
|
||||||
|
else
|
||||||
|
log_step "Installing dependencies: ${all_missing[*]} (packages: ${uniq_pkgs[*]})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$pkg_mgr" in
|
||||||
|
apt)
|
||||||
|
apt_update
|
||||||
|
apt_install "${uniq_pkgs[@]}" || true
|
||||||
|
;;
|
||||||
|
dnf) dnf install -y -q "${uniq_pkgs[@]}" 2>/dev/null ;;
|
||||||
|
yum) yum install -y -q "${uniq_pkgs[@]}" 2>/dev/null ;;
|
||||||
|
*)
|
||||||
|
log_error "Unknown package manager — install manually: ${uniq_pkgs[*]}"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Фолбэки для xxd: на некоторых системах нужен vim-common вместо xxd
|
||||||
|
if ! command -v xxd &>/dev/null && [ "$pkg_mgr" = "apt" ]; then
|
||||||
|
apt_install vim-common || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Повторная проверка критических команд
|
||||||
|
local still_missing=()
|
||||||
|
for cmd in "${critical[@]}"; do
|
||||||
|
command -v "$cmd" &>/dev/null || still_missing+=("$cmd")
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#still_missing[@]} -gt 0 ]; then
|
||||||
|
log_error "Critical dependencies still missing: ${still_missing[*]}"
|
||||||
|
log_error "Install manually and re-run gotelegram"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Опциональные — только предупреждение
|
||||||
|
local still_missing_opt=()
|
||||||
|
for cmd in "${optional[@]}"; do
|
||||||
|
command -v "$cmd" &>/dev/null || still_missing_opt+=("$cmd")
|
||||||
|
done
|
||||||
|
if [ ${#still_missing_opt[@]} -gt 0 ]; then
|
||||||
|
log_warning "Optional deps missing (features degraded): ${still_missing_opt[*]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Dependencies ready"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Быстрая проверка — только смотрит что критические установлены, ничего не ставит.
|
||||||
|
# Возвращает 0 если всё ок, 1 если что-то отсутствует. Используется на старте
|
||||||
|
# main() чтобы не дёргать apt-get update при каждом запуске меню.
|
||||||
|
check_deps_present() {
|
||||||
|
local cmd
|
||||||
|
for cmd in curl jq openssl git xxd tar dig flock; do
|
||||||
|
command -v "$cmd" &>/dev/null || return 1
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
check_port() {
|
||||||
|
local port="$1"
|
||||||
|
local line
|
||||||
|
line=$(ss -tlnp 2>/dev/null | grep -E ":${port}\b" | head -1)
|
||||||
|
[ -z "$line" ] && line=$(netstat -tlnp 2>/dev/null | grep -E ":${port}\b" | head -1)
|
||||||
|
if [ -n "$line" ]; then
|
||||||
|
echo "$line"
|
||||||
|
return 0 # порт занят
|
||||||
|
fi
|
||||||
|
return 1 # свободен
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_3xui() {
|
||||||
|
if systemctl list-unit-files 2>/dev/null | grep -Eq '^(x-ui|3x-ui)\.service'; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
[ -d /etc/x-ui ] || [ -d /usr/local/x-ui ] || [ -f /etc/x-ui/x-ui.db ]
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_3xui_443_listener() {
|
||||||
|
ss -ltnp 2>/dev/null | grep -E '(:|])443[[:space:]]' | grep -Eiq '(xray|x-ui|3x-ui)'
|
||||||
|
}
|
||||||
|
|
||||||
|
warn_3xui_443_conflict() {
|
||||||
|
detect_3xui_443_listener || return 1
|
||||||
|
log_warning "Обнаружен 3x-ui/Xray, который уже слушает TCP/443."
|
||||||
|
log_warning "goTelegram Pro не будет молча останавливать или переписывать 3x-ui."
|
||||||
|
log_dim "Для настоящего shared-443 нужен один фронтовой TLS/SNI-диспетчер и разные SNI-домены для Xray и goTelegram Pro."
|
||||||
|
mkdir -p "$GOTELEGRAM_DIR" 2>/dev/null
|
||||||
|
cat > "$GOTELEGRAM_DIR/shared-443-3xui.md" <<'EOF' 2>/dev/null || true
|
||||||
|
# goTelegram Pro + 3x-ui on one TCP/443
|
||||||
|
|
||||||
|
goTelegram Pro detected that 3x-ui/Xray already owns TCP/443. Two independent
|
||||||
|
processes cannot bind the same IP:port at the same time. A safe shared setup
|
||||||
|
needs one front TLS/SNI dispatcher on 443 and internal backends, for example:
|
||||||
|
|
||||||
|
- dispatcher: 0.0.0.0:443 (nginx stream ssl_preread)
|
||||||
|
- goTelegram Pro telemt: 127.0.0.1:7443
|
||||||
|
- 3x-ui/Xray inbound: 127.0.0.1:9443
|
||||||
|
- goTelegram Pro nginx mask site: 127.0.0.1:8443
|
||||||
|
|
||||||
|
The dispatcher routes Xray SNI domains to Xray. Everything else goes to telemt;
|
||||||
|
telemt then decides whether the session is MTProxy or regular HTTPS and forwards
|
||||||
|
the website to nginx through dns_overrides.
|
||||||
|
|
||||||
|
goTelegram Pro can generate the dispatcher with:
|
||||||
|
|
||||||
|
source /opt/gotelegram/lib/shared443.sh
|
||||||
|
shared443_enable <gotelegram-domain> <xray-sni-domain> 127.0.0.1:9443
|
||||||
|
|
||||||
|
Move the 3x-ui/Xray inbound from 0.0.0.0:443 to 127.0.0.1:9443 in the panel first,
|
||||||
|
or nginx will not be able to own the public 443 socket. goTelegram Pro intentionally
|
||||||
|
does not rewrite the 3x-ui SQLite database or generated Xray config without explicit
|
||||||
|
operator confirmation, because 3x-ui can overwrite manual JSON edits on the next
|
||||||
|
panel change.
|
||||||
|
EOF
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
check_disk_space() {
|
||||||
|
local min_mb="${1:-500}"
|
||||||
|
local avail_mb
|
||||||
|
avail_mb=$(df -m / | awk 'NR==2 {print $4}')
|
||||||
|
if [ "$avail_mb" -lt "$min_mb" ]; then
|
||||||
|
if type tf &>/dev/null; then
|
||||||
|
log_error "$(tf err_low_disk "$avail_mb" "$min_mb")"
|
||||||
|
else
|
||||||
|
log_error "Low disk space: ${avail_mb}MB (need ${min_mb}MB+)"
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Конфигурация GoTelegram (JSON) ──────────────────────────────────────────
|
||||||
|
save_gotelegram_config() {
|
||||||
|
mkdir -p "$(dirname "$GOTELEGRAM_CONFIG")"
|
||||||
|
local cur_lang
|
||||||
|
cur_lang=$(type get_language &>/dev/null && get_language || echo en)
|
||||||
|
cat > "$GOTELEGRAM_CONFIG" << EOJSON
|
||||||
|
{
|
||||||
|
"version": "$GOTELEGRAM_VERSION",
|
||||||
|
"engine": "${1:-telemt}",
|
||||||
|
"mode": "${2:-lite}",
|
||||||
|
"port": ${3:-443},
|
||||||
|
"secret": "${4:-}",
|
||||||
|
"mask_host": "${5:-google.com}",
|
||||||
|
"domain": "${6:-}",
|
||||||
|
"template_id": "${7:-}",
|
||||||
|
"language": "${cur_lang}",
|
||||||
|
"installed_at": "$(date -Iseconds)",
|
||||||
|
"updated_at": "$(date -Iseconds)"
|
||||||
|
}
|
||||||
|
EOJSON
|
||||||
|
chmod 600 "$GOTELEGRAM_CONFIG"
|
||||||
|
}
|
||||||
|
|
||||||
|
load_gotelegram_config() {
|
||||||
|
if [ -f "$GOTELEGRAM_CONFIG" ]; then
|
||||||
|
cat "$GOTELEGRAM_CONFIG"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "{}"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
config_get() {
|
||||||
|
local key="$1"
|
||||||
|
if [ ! -f "$GOTELEGRAM_CONFIG" ]; then
|
||||||
|
return 2 # file missing
|
||||||
|
fi
|
||||||
|
local val
|
||||||
|
val=$(jq -r ".$key // empty" "$GOTELEGRAM_CONFIG" 2>/dev/null)
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
return 3 # invalid JSON
|
||||||
|
fi
|
||||||
|
if [ -z "$val" ]; then
|
||||||
|
return 1 # key missing or empty
|
||||||
|
fi
|
||||||
|
echo "$val"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── V1 совместимость ─────────────────────────────────────────────────────────
|
||||||
|
detect_v1_installation() {
|
||||||
|
# Проверяем наличие mtg Docker контейнера (v1)
|
||||||
|
if command -v docker &>/dev/null; then
|
||||||
|
if docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^${V1_CONTAINER_NAME}$"; then
|
||||||
|
return 0 # v1 обнаружена
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# Проверяем наличие конфига v1
|
||||||
|
if [ -f "$V1_CONFIG_FILE" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get_v1_config() {
|
||||||
|
# Извлекаем данные из работающего v1 контейнера
|
||||||
|
if ! command -v docker &>/dev/null; then
|
||||||
|
echo "{}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local running
|
||||||
|
running=$(docker ps --format '{{.Names}}' 2>/dev/null | grep "^${V1_CONTAINER_NAME}$")
|
||||||
|
|
||||||
|
if [ -z "$running" ]; then
|
||||||
|
# Пробуем из сохранённого конфига
|
||||||
|
if [ -f "$V1_CONFIG_FILE" ]; then
|
||||||
|
cat "$V1_CONFIG_FILE"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "{}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Достаём из Docker
|
||||||
|
local cmd_str port secret ip
|
||||||
|
cmd_str=$(docker inspect "$V1_CONTAINER_NAME" --format='{{range .Config.Cmd}}{{.}} {{end}}' 2>/dev/null)
|
||||||
|
secret=$(echo "$cmd_str" | awk '{print $NF}')
|
||||||
|
port=$(docker inspect "$V1_CONTAINER_NAME" --format='{{range $p,$c := .HostConfig.PortBindings}}{{(index $c 0).HostPort}}{{end}}' 2>/dev/null)
|
||||||
|
ip=$(get_server_ip)
|
||||||
|
|
||||||
|
jq -n \
|
||||||
|
--arg secret "$secret" \
|
||||||
|
--arg port "${port:-443}" \
|
||||||
|
--arg ip "$ip" \
|
||||||
|
'{secret: $secret, port: ($port | tonumber), ip: $ip, engine: "mtg"}'
|
||||||
|
}
|
||||||
|
|
||||||
|
migrate_v1_to_v2() {
|
||||||
|
log_step "$(_t_or v1_migration_step 'Migrating from v1 (mtg) to v2 (telemt)')"
|
||||||
|
|
||||||
|
local v1_config
|
||||||
|
v1_config=$(get_v1_config)
|
||||||
|
|
||||||
|
local old_port old_secret
|
||||||
|
old_port=$(echo "$v1_config" | jq -r '.port // 443')
|
||||||
|
old_secret=$(echo "$v1_config" | jq -r '.secret // empty')
|
||||||
|
|
||||||
|
if [ -z "$old_secret" ]; then
|
||||||
|
log_warning "Failed to extract secret from v1. A new one will be generated."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e " ${WHITE}$(_t_or v1_found_title 'Found v1 (mtg) installation:')${NC}"
|
||||||
|
if type tf &>/dev/null; then
|
||||||
|
echo -e " $(tf v1_port "$old_port")"
|
||||||
|
echo -e " $(tf v1_secret "${old_secret:0:16}")"
|
||||||
|
else
|
||||||
|
echo -e " Port: ${CYAN}${old_port}${NC}"
|
||||||
|
echo -e " Secret: ${CYAN}${old_secret:0:16}...${NC}"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
echo -e " ${YELLOW}$(_t_or warning 'Warning'):${NC} $(_t_or v1_incompatible 'mtg secret is NOT directly compatible with telemt.')"
|
||||||
|
echo -e " $(_t_or v1_new_link 'Clients will need a new link.')"
|
||||||
|
echo ""
|
||||||
|
echo -ne " $(_t_or v1_stop_migrate 'Stop v1 container and migrate to v2? [Y/n]:') "
|
||||||
|
read -r ans
|
||||||
|
if [[ "$ans" =~ ^[Nn] ]]; then
|
||||||
|
log_info "$(_t_or v1_migration_cancelled 'Migration cancelled. v1 left intact.')"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stop v1
|
||||||
|
log_info "$(_t_or v1_stopping 'Stopping v1 container...')"
|
||||||
|
docker stop "$V1_CONTAINER_NAME" 2>/dev/null
|
||||||
|
docker rm "$V1_CONTAINER_NAME" 2>/dev/null
|
||||||
|
|
||||||
|
# Backup v1 config
|
||||||
|
if [ -f "$V1_CONFIG_FILE" ]; then
|
||||||
|
mkdir -p "$GOTELEGRAM_DIR"
|
||||||
|
cp "$V1_CONFIG_FILE" "$GOTELEGRAM_DIR/v1_backup_proxy.json" 2>/dev/null
|
||||||
|
if type tf &>/dev/null; then
|
||||||
|
log_success "$(tf v1_config_saved "$GOTELEGRAM_DIR/v1_backup_proxy.json")"
|
||||||
|
else
|
||||||
|
log_success "v1 config saved to $GOTELEGRAM_DIR/v1_backup_proxy.json"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if type tf &>/dev/null; then
|
||||||
|
log_success "$(tf v1_port_freed "$old_port")"
|
||||||
|
else
|
||||||
|
log_success "v1 stopped. Port $old_port freed."
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Confirm prompt ───────────────────────────────────────────────────────────
|
||||||
|
confirm() {
|
||||||
|
local default_msg
|
||||||
|
default_msg=$(_t_or install_continue_anyway 'Continue?')
|
||||||
|
local msg="${1:-$default_msg}"
|
||||||
|
echo -ne " ${msg} [Y/n]: " >&2
|
||||||
|
read -r ans
|
||||||
|
[[ ! "$ans" =~ ^[Nn] ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Выбор из списка ──────────────────────────────────────────────────────────
|
||||||
|
select_option() {
|
||||||
|
local title="$1"
|
||||||
|
shift
|
||||||
|
local options=("$@")
|
||||||
|
|
||||||
|
echo "" >&2
|
||||||
|
echo -e " ${BOLD}${WHITE}${title}${NC}" >&2
|
||||||
|
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}" >&2
|
||||||
|
local i=1
|
||||||
|
for opt in "${options[@]}"; do
|
||||||
|
echo -e " ${CYAN}${i})${NC} ${opt}" >&2
|
||||||
|
((i++))
|
||||||
|
done
|
||||||
|
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}" >&2
|
||||||
|
echo -ne " ${WHITE}$(_t_or choose 'Choose'):${NC} " >&2
|
||||||
|
read -r choice
|
||||||
|
echo "$choice"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Генерация случайного hex ─────────────────────────────────────────────────
|
||||||
|
generate_hex() {
|
||||||
|
local len="${1:-32}"
|
||||||
|
openssl rand -hex "$((len/2))" 2>/dev/null || head -c "$((len/2))" /dev/urandom | xxd -p | tr -d '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Проверка домена ──────────────────────────────────────────────────────────
|
||||||
|
validate_domain() {
|
||||||
|
local domain="$1"
|
||||||
|
if echo "$domain" | grep -qE '^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Init: создание директорий ────────────────────────────────────────────────
|
||||||
|
init_dirs() {
|
||||||
|
mkdir -p "$GOTELEGRAM_DIR" "$BACKUP_DIR" /etc/telemt 2>/dev/null
|
||||||
|
touch "$LOG_FILE" 2>/dev/null
|
||||||
|
}
|
||||||
140
lib/i18n.sh
Normal file
140
lib/i18n.sh
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# GoTelegram v2.5.0 — i18n engine
|
||||||
|
# Internationalization support: EN (English) / RU (Русский)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# source lib/i18n.sh
|
||||||
|
# load_language "ru" # or "en"
|
||||||
|
# echo "$(t menu_install)" # translated string
|
||||||
|
# printf "$(t greeting)\n" "$name" # with format args
|
||||||
|
|
||||||
|
# ── Global i18n state ──
|
||||||
|
declare -gA I18N
|
||||||
|
LANG_CODE="${LANG_CODE:-en}"
|
||||||
|
LANG_FILE=""
|
||||||
|
|
||||||
|
# ── Load a language ──
|
||||||
|
# Sources lib/lang/${lang}.sh into the I18N associative array.
|
||||||
|
# Falls back to English if requested language file is missing.
|
||||||
|
load_language() {
|
||||||
|
local lang="${1:-en}"
|
||||||
|
# Sanitize: only allow [a-z]{2} codes
|
||||||
|
if ! [[ "$lang" =~ ^[a-z]{2}$ ]]; then
|
||||||
|
lang="en"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local lang_dir
|
||||||
|
lang_dir="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/lang"
|
||||||
|
local lang_file="${lang_dir}/${lang}.sh"
|
||||||
|
|
||||||
|
if [ ! -f "$lang_file" ]; then
|
||||||
|
lang_file="${lang_dir}/en.sh"
|
||||||
|
lang="en"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$lang_file" ]; then
|
||||||
|
# Clear previous keys then source the new language
|
||||||
|
I18N=()
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$lang_file"
|
||||||
|
LANG_CODE="$lang"
|
||||||
|
LANG_FILE="$lang_file"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Translate: fetch value by key ──
|
||||||
|
# t <key> → echoes translation (or key if missing)
|
||||||
|
t() {
|
||||||
|
local key="$1"
|
||||||
|
local val="${I18N[$key]:-}"
|
||||||
|
if [ -z "$val" ]; then
|
||||||
|
# Fallback to key name so missing translations are visible
|
||||||
|
echo "$key"
|
||||||
|
else
|
||||||
|
echo "$val"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Translate + printf-style formatting ──
|
||||||
|
# tf <key> <arg1> <arg2> ...
|
||||||
|
tf() {
|
||||||
|
local key="$1"
|
||||||
|
shift
|
||||||
|
local fmt="${I18N[$key]:-$key}"
|
||||||
|
# shellcheck disable=SC2059
|
||||||
|
printf "$fmt" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Get current language code ──
|
||||||
|
get_language() {
|
||||||
|
echo "$LANG_CODE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Detect saved language from config.json, default en ──
|
||||||
|
detect_language() {
|
||||||
|
local cfg="${GOTELEGRAM_CONFIG:-/opt/gotelegram/config.json}"
|
||||||
|
local lang=""
|
||||||
|
if [ -f "$cfg" ] && command -v jq >/dev/null 2>&1; then
|
||||||
|
lang=$(jq -r '.language // empty' "$cfg" 2>/dev/null)
|
||||||
|
fi
|
||||||
|
# Also check marker file (language set before config.json exists)
|
||||||
|
if [ -z "$lang" ]; then
|
||||||
|
local marker="${GOTELEGRAM_DIR:-/opt/gotelegram}/.language"
|
||||||
|
if [ -f "$marker" ]; then
|
||||||
|
lang=$(head -c 2 "$marker" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# Sanitize
|
||||||
|
if ! [[ "$lang" =~ ^(en|ru)$ ]]; then
|
||||||
|
lang="en"
|
||||||
|
fi
|
||||||
|
echo "$lang"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Persist selected language ──
|
||||||
|
# Saves to config.json if present, otherwise to marker file
|
||||||
|
save_language() {
|
||||||
|
local lang="$1"
|
||||||
|
if ! [[ "$lang" =~ ^(en|ru)$ ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
mkdir -p "${GOTELEGRAM_DIR:-/opt/gotelegram}" 2>/dev/null
|
||||||
|
# Always write marker for early-access (before config.json exists)
|
||||||
|
echo "$lang" > "${GOTELEGRAM_DIR:-/opt/gotelegram}/.language" 2>/dev/null
|
||||||
|
|
||||||
|
local cfg="${GOTELEGRAM_CONFIG:-/opt/gotelegram/config.json}"
|
||||||
|
if [ -f "$cfg" ] && command -v jq >/dev/null 2>&1; then
|
||||||
|
local tmp
|
||||||
|
tmp=$(mktemp) || return 1
|
||||||
|
if jq --arg lang "$lang" '. + {language: $lang}' "$cfg" > "$tmp" 2>/dev/null; then
|
||||||
|
mv "$tmp" "$cfg"
|
||||||
|
chmod 600 "$cfg"
|
||||||
|
else
|
||||||
|
rm -f "$tmp"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── First-run interactive language picker ──
|
||||||
|
# Shows a minimal, language-agnostic picker (keeps it culture-neutral).
|
||||||
|
# Returns the chosen code via echo.
|
||||||
|
pick_language_interactive() {
|
||||||
|
echo "" >&2
|
||||||
|
echo " ┌──────────────────────────────────────────┐" >&2
|
||||||
|
echo " │ Select language / Выберите язык │" >&2
|
||||||
|
echo " ├──────────────────────────────────────────┤" >&2
|
||||||
|
echo " │ 1) English │" >&2
|
||||||
|
echo " │ 2) Русский │" >&2
|
||||||
|
echo " └──────────────────────────────────────────┘" >&2
|
||||||
|
echo -n " > " >&2
|
||||||
|
local ch
|
||||||
|
read -r ch
|
||||||
|
case "$ch" in
|
||||||
|
1|en|EN|english|English) echo "en" ;;
|
||||||
|
2|ru|RU|russian|Russian|русский) echo "ru" ;;
|
||||||
|
*) echo "en" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
377
lib/lang/en.sh
Normal file
377
lib/lang/en.sh
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# goTelegram Pro v2.5.0 — English translations
|
||||||
|
# shellcheck disable=SC2034,SC2148
|
||||||
|
|
||||||
|
# ── Common words ────────────────────────────────────────────────────────
|
||||||
|
I18N[yes]="Yes"
|
||||||
|
I18N[no]="No"
|
||||||
|
I18N[ok]="OK"
|
||||||
|
I18N[cancel]="Cancel"
|
||||||
|
I18N[back]="« Back"
|
||||||
|
I18N[exit]="Exit"
|
||||||
|
I18N[skip]="Skip"
|
||||||
|
I18N[choose]="Choose"
|
||||||
|
I18N[press_enter]="Press Enter..."
|
||||||
|
I18N[press_enter_to_return]="Press Enter to return to menu..."
|
||||||
|
I18N[invalid_choice]="Invalid choice"
|
||||||
|
I18N[running]="running"
|
||||||
|
I18N[stopped]="stopped"
|
||||||
|
I18N[not_installed]="not installed"
|
||||||
|
I18N[unknown]="unknown"
|
||||||
|
I18N[error]="Error"
|
||||||
|
I18N[warning]="Warning"
|
||||||
|
I18N[info]="Info"
|
||||||
|
I18N[success]="Done"
|
||||||
|
I18N[wait]="Please wait..."
|
||||||
|
|
||||||
|
# ── Banner ──────────────────────────────────────────────────────────────
|
||||||
|
I18N[banner_title]="goTelegram Pro v%s"
|
||||||
|
I18N[banner_subtitle]="MTProxy powered by telemt (Rust + Tokio)"
|
||||||
|
I18N[banner_features]="Anti-DPI • Fake TLS • TCP Splice • JA3/JA4"
|
||||||
|
I18N[credits_title]="Credits / Thanks"
|
||||||
|
|
||||||
|
# ── Main menu (dashboard) ───────────────────────────────────────────────
|
||||||
|
I18N[dashboard_title]="Control panel"
|
||||||
|
I18N[svc_proxy]="Proxy"
|
||||||
|
I18N[svc_nginx]="nginx"
|
||||||
|
I18N[svc_site]="Site"
|
||||||
|
I18N[svc_ssl]="SSL"
|
||||||
|
I18N[svc_bot]="Bot"
|
||||||
|
I18N[ssl_until]="until %s"
|
||||||
|
I18N[net_ip]="IP:"
|
||||||
|
I18N[net_port]="Port:"
|
||||||
|
I18N[net_mode]="Mode:"
|
||||||
|
I18N[net_domain]="Domain:"
|
||||||
|
I18N[connection_link]="Telegram connection link:"
|
||||||
|
I18N[proxy_not_configured]="Proxy is not configured. Select option 1."
|
||||||
|
I18N[menu_proxy]="Proxy ▸"
|
||||||
|
I18N[menu_stats]="Statistics ▸"
|
||||||
|
I18N[menu_manage]="Management ▸"
|
||||||
|
I18N[menu_telegram_bot]="Telegram bot ▸"
|
||||||
|
I18N[menu_about]="About ▸"
|
||||||
|
I18N[auto_refresh_30s]="Refresh in 30 sec"
|
||||||
|
|
||||||
|
# ── Submenu: Proxy ──────────────────────────────────────────────────────
|
||||||
|
I18N[submenu_proxy_title]="🚀 PROXY"
|
||||||
|
I18N[proxy_install_update]="Install / Update"
|
||||||
|
I18N[proxy_status_detail]="Detailed status"
|
||||||
|
I18N[proxy_copy_link]="Copy link"
|
||||||
|
I18N[proxy_share]="Share key"
|
||||||
|
I18N[proxy_restart]="Restart"
|
||||||
|
I18N[proxy_logs]="Logs"
|
||||||
|
I18N[proxy_change_mode]="Change mode / template"
|
||||||
|
|
||||||
|
# ── Submenu: Manage ─────────────────────────────────────────────────────
|
||||||
|
I18N[submenu_manage_title]="⚙️ MANAGEMENT"
|
||||||
|
I18N[manage_backup]="Backup"
|
||||||
|
I18N[manage_restore]="Restore"
|
||||||
|
I18N[manage_update_telemt]="Update telemt"
|
||||||
|
I18N[manage_site_ssl]="Site / SSL"
|
||||||
|
I18N[manage_remove]="Remove"
|
||||||
|
I18N[manage_language]="Language / Язык"
|
||||||
|
|
||||||
|
# ── Submenu: About ──────────────────────────────────────────────────────
|
||||||
|
I18N[submenu_about_title]="ℹ️ ABOUT"
|
||||||
|
I18N[about_version_info]="Version info"
|
||||||
|
I18N[about_promo]="Promo / Donate"
|
||||||
|
I18N[version_title]="🔍 Information"
|
||||||
|
I18N[version_label]="goTelegram Pro:"
|
||||||
|
I18N[version_engine]="Engine:"
|
||||||
|
I18N[version_tech]="Technology:"
|
||||||
|
I18N[version_license]="License:"
|
||||||
|
|
||||||
|
# ── Install flow ────────────────────────────────────────────────────────
|
||||||
|
I18N[install_select_mode]="🎭 Select masquerade mode:"
|
||||||
|
I18N[install_lite_title]="⚡ Lite — masquerade as popular website"
|
||||||
|
I18N[install_lite_desc1]="Fast, no domain needed. telemt disguises traffic"
|
||||||
|
I18N[install_lite_desc2]="as the chosen site (google.com etc.)"
|
||||||
|
I18N[install_pro_title]="🛡 Pro — your own site + full masquerade"
|
||||||
|
I18N[install_pro_desc1]="nginx + SSL + HTML template + telemt."
|
||||||
|
I18N[install_pro_desc2]="DPI sees a real website with a real certificate."
|
||||||
|
I18N[install_pro_desc3]="Requires: a domain pointing to this server."
|
||||||
|
I18N[install_mode_choice]="Choice (1/2):"
|
||||||
|
I18N[install_bad_choice]="Invalid choice: %s"
|
||||||
|
I18N[install_lite_step]="Installing Lite mode"
|
||||||
|
I18N[install_pro_step]="Installing Pro mode"
|
||||||
|
I18N[install_enter_domain]="Enter your domain (e.g. example.com):"
|
||||||
|
I18N[install_bad_domain]="Invalid domain: %s"
|
||||||
|
I18N[install_dns_mismatch]="Domain %s points to %s, not to %s"
|
||||||
|
I18N[install_continue_anyway]="Continue anyway?"
|
||||||
|
I18N[install_enter_email]="Email for SSL (Enter = no email):"
|
||||||
|
I18N[install_config_title]="📋 Configuration:"
|
||||||
|
I18N[install_cfg_ip]="IP:"
|
||||||
|
I18N[install_cfg_port]="Port:"
|
||||||
|
I18N[install_cfg_mask]="Masquerade:"
|
||||||
|
I18N[install_cfg_mode]="Mode:"
|
||||||
|
I18N[install_cfg_domain]="Domain:"
|
||||||
|
I18N[install_confirm_proxy]="Install proxy?"
|
||||||
|
I18N[install_confirm_proxy_site]="Install proxy + website?"
|
||||||
|
I18N[install_done]="goTelegram Pro v%s installed! (%s mode)"
|
||||||
|
I18N[install_arch_desc1]="telemt accepts all traffic on 443 (HTTPS masquerade)"
|
||||||
|
I18N[install_arch_desc2]="nginx serves the site on internal port %s"
|
||||||
|
I18N[install_arch_desc3]="ISP only sees HTTPS traffic to %s:443"
|
||||||
|
|
||||||
|
# ── Change mode/template ────────────────────────────────────────────────
|
||||||
|
I18N[change_current_mode]="Current mode:"
|
||||||
|
I18N[change_template]="Change site template (pro only)"
|
||||||
|
I18N[change_mode_switch]="Switch mode (lite ↔ pro)"
|
||||||
|
I18N[change_only_pro]="Template change is available in pro mode only"
|
||||||
|
I18N[change_requires_reinstall]="Mode switch requires reinstall."
|
||||||
|
I18N[change_reinstall_confirm]="Reinstall proxy?"
|
||||||
|
|
||||||
|
# ── Logs ────────────────────────────────────────────────────────────────
|
||||||
|
I18N[logs_telemt_title]="📋 telemt logs (last %s lines):"
|
||||||
|
|
||||||
|
# ── Link / Share ────────────────────────────────────────────────────────
|
||||||
|
I18N[link_title]="🔗 Connection link:"
|
||||||
|
I18N[share_title]="📤 Forward this message:"
|
||||||
|
I18N[share_line1]="🔐 MTProxy for Telegram (goTelegram Pro v%s)"
|
||||||
|
I18N[share_server]="🌍 Server: %s"
|
||||||
|
I18N[share_port]="🔌 Port: %s"
|
||||||
|
I18N[share_connect_cta]="👉 Connect with one tap:"
|
||||||
|
I18N[share_footer]="Just tap the link or configure manually."
|
||||||
|
|
||||||
|
# ── Website ─────────────────────────────────────────────────────────────
|
||||||
|
I18N[website_title]="🌐 Website management"
|
||||||
|
I18N[website_domain]="Domain:"
|
||||||
|
I18N[website_ssl_until]="SSL until:"
|
||||||
|
I18N[website_only_pro]="Website management is available in pro mode only"
|
||||||
|
I18N[website_renew_ssl]="Renew SSL certificate"
|
||||||
|
I18N[website_restart_nginx]="Restart nginx"
|
||||||
|
I18N[website_change_template]="Change template"
|
||||||
|
|
||||||
|
# ── Remove ──────────────────────────────────────────────────────────────
|
||||||
|
I18N[remove_title]="🗑 Remove goTelegram Pro"
|
||||||
|
I18N[remove_proxy_only]="Remove proxy only (telemt)"
|
||||||
|
I18N[remove_bot_only]="Remove Telegram bot only"
|
||||||
|
I18N[remove_all]="Remove everything (proxy + bot + settings)"
|
||||||
|
I18N[remove_warn_proxy]="This will remove the proxy and all its settings."
|
||||||
|
I18N[remove_confirm_proxy]="Remove proxy?"
|
||||||
|
I18N[remove_backup_before]="Create a backup before removal?"
|
||||||
|
I18N[remove_warn_all]="This will remove EVERYTHING: proxy, bot, site, settings."
|
||||||
|
I18N[remove_confirm_all]="Are you absolutely sure?"
|
||||||
|
I18N[remove_proxy_done]="Proxy removed"
|
||||||
|
I18N[remove_all_done]="goTelegram Pro fully removed (proxy + bot)"
|
||||||
|
|
||||||
|
# ── Telegram bot submenu ────────────────────────────────────────────────
|
||||||
|
I18N[bot_title]="🤖 Telegram bot"
|
||||||
|
I18N[bot_status_running]="● Running"
|
||||||
|
I18N[bot_status_stopped]="○ Stopped"
|
||||||
|
I18N[bot_status_not_installed]="✗ Not installed"
|
||||||
|
I18N[bot_menu_status]="📊 Bot status"
|
||||||
|
I18N[bot_menu_logs]="📋 Bot logs"
|
||||||
|
I18N[bot_menu_restart]="🔄 Restart bot"
|
||||||
|
I18N[bot_menu_stop]="⏹ Stop bot"
|
||||||
|
I18N[bot_menu_start]="▶️ Start bot"
|
||||||
|
I18N[bot_menu_settings]="⚙️ Settings (.env)"
|
||||||
|
I18N[bot_menu_remove]="🗑 Remove bot"
|
||||||
|
I18N[bot_menu_install]="🔧 Install bot"
|
||||||
|
I18N[bot_intro1]="The bot lets you manage the proxy from Telegram:"
|
||||||
|
I18N[bot_intro2]="status, restart, change mode, backup, QR code."
|
||||||
|
I18N[bot_install_step]="Installing Telegram bot"
|
||||||
|
I18N[bot_install_python]="Installing Python3..."
|
||||||
|
I18N[bot_files_not_found]="Bot files not found in %s"
|
||||||
|
I18N[bot_create_venv]="Creating virtual environment..."
|
||||||
|
I18N[bot_install_deps]="Installing dependencies..."
|
||||||
|
I18N[bot_enter_token]="Enter BOT_TOKEN from @BotFather:"
|
||||||
|
I18N[bot_token_empty]="Token cannot be empty"
|
||||||
|
I18N[bot_token]="Token:"
|
||||||
|
I18N[bot_add_admin_how]="How to add the administrator?"
|
||||||
|
I18N[bot_admin_auto]="Auto — bot will capture the ID on first /start"
|
||||||
|
I18N[bot_admin_manual]="Manual — enter the ID now"
|
||||||
|
I18N[bot_admin_ids_prompt]="Admin IDs (space or comma separated):"
|
||||||
|
I18N[bot_env_created]=".env created"
|
||||||
|
I18N[bot_env_exists]=".env already exists, settings preserved"
|
||||||
|
I18N[bot_wait_admin_title]="Waiting for administrator"
|
||||||
|
I18N[bot_wait_admin_msg1]="Open the bot in Telegram and send"
|
||||||
|
I18N[bot_wait_admin_msg2]="The bot will automatically make you an admin"
|
||||||
|
I18N[bot_wait_admin_skip]="Press Ctrl+C to skip"
|
||||||
|
I18N[bot_wait_spinner]="Waiting... send /start to the bot (%d sec)"
|
||||||
|
I18N[bot_admin_assigned]="Administrator assigned!"
|
||||||
|
I18N[bot_wait_skipped]="Skipped. Add admin later via: menu → Telegram bot → Settings"
|
||||||
|
I18N[bot_wait_timeout]="Timeout (5 min). Add admin via: menu → Telegram bot → Settings"
|
||||||
|
I18N[bot_installed]="Bot installed and running!"
|
||||||
|
I18N[bot_status_title]="📊 Telegram bot status"
|
||||||
|
I18N[bot_token_configured]="configured"
|
||||||
|
I18N[bot_access_open]="all users"
|
||||||
|
I18N[bot_logs_title]="📋 Bot logs (last 30 lines):"
|
||||||
|
I18N[bot_settings_title]="⚙️ Bot settings"
|
||||||
|
I18N[bot_current_env]="Current .env:"
|
||||||
|
I18N[bot_change_token]="Change BOT_TOKEN"
|
||||||
|
I18N[bot_change_allowed]="Change ALLOWED_IDS"
|
||||||
|
I18N[bot_new_token]="New BOT_TOKEN:"
|
||||||
|
I18N[bot_token_empty_err]="Empty token"
|
||||||
|
I18N[bot_token_updated]="Token updated, bot restarted"
|
||||||
|
I18N[bot_allowed_prompt]="ALLOWED_IDS (space or comma separated, empty = auto):"
|
||||||
|
I18N[bot_access_updated]="Access updated, bot restarted"
|
||||||
|
I18N[bot_remove_warn]="This will remove the Telegram bot and all its settings."
|
||||||
|
I18N[bot_remove_confirm]="Remove bot?"
|
||||||
|
I18N[bot_removed]="Bot fully removed"
|
||||||
|
I18N[bot_restarted]="Bot restarted"
|
||||||
|
I18N[bot_stopped]="Bot stopped"
|
||||||
|
I18N[bot_started]="Bot started"
|
||||||
|
I18N[bot_status_colon]="Status:"
|
||||||
|
I18N[bot_access_colon]="Access:"
|
||||||
|
I18N[bot_access_ids_fmt]="ID: %s"
|
||||||
|
|
||||||
|
# ── Promo / Donate ──────────────────────────────────────────────────────
|
||||||
|
I18N[promo_host1_title]="💰 HOSTING #1 — UP TO 60% OFF"
|
||||||
|
I18N[promo_host2_title]="💰 HOSTING #2 — UP TO 60% OFF"
|
||||||
|
I18N[promo_tips_title]="☕ Donate / Tips"
|
||||||
|
I18N[promo_youtube_title]="▶ YouTube Channel"
|
||||||
|
I18N[promo_link_label]="Link:"
|
||||||
|
I18N[promo_off60]="60%% discount on the first month"
|
||||||
|
I18N[promo_ant20]="20%% + 3%% when paid for 3 months"
|
||||||
|
I18N[promo_ant6]="15%% + 5%% when paid for 6 months"
|
||||||
|
I18N[promo_qr_host1]="── QR: Hosting #1 ──"
|
||||||
|
I18N[promo_qr_host2]="── QR: Hosting #2 ──"
|
||||||
|
I18N[promo_qr_tips]="── QR: Donate / Tips ──"
|
||||||
|
I18N[promo_qr_youtube]="── QR: YouTube Channel ──"
|
||||||
|
I18N[promo_menu_in]="Menu in %d sec..."
|
||||||
|
|
||||||
|
# ── Stats ───────────────────────────────────────────────────────────────
|
||||||
|
I18N[stats_title]="📊 Traffic statistics"
|
||||||
|
I18N[stats_module_missing]="Statistics module not loaded."
|
||||||
|
I18N[stats_file_missing]="File lib/stats.sh not found."
|
||||||
|
I18N[stats_toggle]="Toggle counter (now: %s)"
|
||||||
|
I18N[stats_install_collector]="Install/update stats collector"
|
||||||
|
I18N[stats_auto_refresh]="Refresh every 3 sec"
|
||||||
|
I18N[stats_on]="on"
|
||||||
|
I18N[stats_off]="off"
|
||||||
|
|
||||||
|
# ── Templates catalog ───────────────────────────────────────────────────
|
||||||
|
I18N[templates_categories]="📂 Site template categories:"
|
||||||
|
I18N[templates_custom_git]="📎 Custom template from git URL"
|
||||||
|
I18N[templates_random]="🎲 Random template"
|
||||||
|
I18N[templates_count_fmt]="(%d templates)"
|
||||||
|
I18N[templates_list]="📋 %s — available templates:"
|
||||||
|
I18N[templates_preview_title]="🔍 Template preview:"
|
||||||
|
I18N[templates_name]="Name:"
|
||||||
|
I18N[templates_source]="Source:"
|
||||||
|
I18N[templates_description]="Description:"
|
||||||
|
I18N[templates_preview]="👁 Preview:"
|
||||||
|
I18N[templates_preview_hint]="Open the link in a browser to preview the template"
|
||||||
|
I18N[templates_repo]="📦 Repo:"
|
||||||
|
I18N[templates_thanks]="💜 Thanks to the authors of %s for the open source code!"
|
||||||
|
I18N[templates_install_this]="Install this template?"
|
||||||
|
I18N[templates_cat_empty]="No templates in this category"
|
||||||
|
I18N[templates_downloading]="Downloading template \"%s\"..."
|
||||||
|
I18N[templates_downloaded]="Template \"%s\" downloaded"
|
||||||
|
I18N[templates_downloaded_subfolder]="Template \"%s\" downloaded (from subfolder)"
|
||||||
|
I18N[templates_no_index]="Template does not contain index.html"
|
||||||
|
I18N[templates_path]="Path: %s"
|
||||||
|
I18N[templates_catalog_not_found]="Templates catalog not found: %s"
|
||||||
|
|
||||||
|
# ── Custom git template ─────────────────────────────────────────────────
|
||||||
|
I18N[custom_git_title]="📎 CUSTOM TEMPLATE FROM GIT URL"
|
||||||
|
I18N[custom_git_help_1]="You can use ANY public static HTML repository as a template."
|
||||||
|
I18N[custom_git_help_2]="The repository must be public and contain a ready-made"
|
||||||
|
I18N[custom_git_help_3]="index.html (build via npm is NOT performed)."
|
||||||
|
I18N[custom_git_formats]="Supported URL formats:"
|
||||||
|
I18N[custom_git_fmt_github]=" • https://github.com/user/repo"
|
||||||
|
I18N[custom_git_fmt_gitlab]=" • https://gitlab.com/user/repo"
|
||||||
|
I18N[custom_git_fmt_gitext]=" • https://example.com/user/repo.git"
|
||||||
|
I18N[custom_git_fmt_branch]=" • https://github.com/user/repo@branch (branch after @)"
|
||||||
|
I18N[custom_git_auto_detect]="Repository structure (auto-detection):"
|
||||||
|
I18N[custom_git_auto_1]=" 1. index.html in repo root"
|
||||||
|
I18N[custom_git_auto_2]=" 2. dist/index.html (StartBootstrap, Vite, webpack)"
|
||||||
|
I18N[custom_git_auto_3]=" 3. public/ or build/ or _site/ or site/ or docs/"
|
||||||
|
I18N[custom_git_auto_4]=" 4. Fallback: search index.html across whole repo"
|
||||||
|
I18N[custom_git_requirements]="Requirements:"
|
||||||
|
I18N[custom_git_req_1]=" • HTTPS only (ssh:// and git:// are blocked)"
|
||||||
|
I18N[custom_git_req_2]=" • Public repositories only"
|
||||||
|
I18N[custom_git_req_3]=" • Repo size up to 100 MB"
|
||||||
|
I18N[custom_git_req_4]=" • Static HTML (no PHP/Python/Node server code)"
|
||||||
|
I18N[custom_git_examples]="Tested example repos:"
|
||||||
|
I18N[custom_git_ex_1]=" • https://github.com/html5up-collective/strata"
|
||||||
|
I18N[custom_git_ex_2]=" • https://github.com/StartBootstrap/startbootstrap-landing-page"
|
||||||
|
I18N[custom_git_enter_url]="Paste git URL (or Enter to cancel):"
|
||||||
|
I18N[custom_git_empty]="No URL provided, cancelled"
|
||||||
|
I18N[custom_git_bad_url]="Invalid URL. Only https:// addresses are accepted"
|
||||||
|
I18N[custom_git_cloning]="Cloning repository..."
|
||||||
|
I18N[custom_git_clone_failed]="Failed to clone repository: %s"
|
||||||
|
I18N[custom_git_too_big]="Repository is too large: %s (limit 100MB)"
|
||||||
|
I18N[custom_git_scanning]="Scanning for index.html..."
|
||||||
|
I18N[custom_git_found_at]="✓ Found index.html in: %s"
|
||||||
|
I18N[custom_git_no_index]="index.html not found in the repository"
|
||||||
|
I18N[custom_git_installed]="Custom template installed from %s"
|
||||||
|
I18N[custom_git_saved]="Template URL saved in config (menu → Site → Update from git)"
|
||||||
|
|
||||||
|
# ── First-run language picker ───────────────────────────────────────────
|
||||||
|
I18N[lang_picker_title]="Select language / Выберите язык"
|
||||||
|
I18N[lang_english]="English"
|
||||||
|
I18N[lang_russian]="Русский"
|
||||||
|
I18N[lang_saved]="Language saved: %s"
|
||||||
|
I18N[lang_change_prompt]="Select a new language:"
|
||||||
|
|
||||||
|
# ── Backup ──────────────────────────────────────────────────────────────
|
||||||
|
I18N[backup_title]="💾 Backup"
|
||||||
|
I18N[backup_creating]="Creating backup..."
|
||||||
|
I18N[backup_created]="Backup created: %s"
|
||||||
|
I18N[backup_failed]="Backup creation failed"
|
||||||
|
I18N[backup_restore_title]="↩️ Restore from backup"
|
||||||
|
I18N[backup_no_files]="No backup files"
|
||||||
|
I18N[backup_select]="Select a backup to restore:"
|
||||||
|
I18N[backup_restoring]="Restoring..."
|
||||||
|
I18N[backup_restored]="Backup restored"
|
||||||
|
I18N[backup_collecting]="Collecting configuration..."
|
||||||
|
I18N[backup_site_included]="Website template included"
|
||||||
|
I18N[backup_archive_err]="Archive creation failed"
|
||||||
|
I18N[backup_archive_missing]="Archive not created"
|
||||||
|
I18N[backup_encrypt_err]="Encryption failed"
|
||||||
|
I18N[backup_encrypted]="Backup encrypted (AES-256-CBC)"
|
||||||
|
I18N[backup_created_fmt]="Backup created: %s (%s)"
|
||||||
|
I18N[backup_file_not_found_fmt]="File not found: %s"
|
||||||
|
I18N[backup_enter_pass]="Enter backup password"
|
||||||
|
I18N[backup_bad_pass]="Wrong password or corrupted file"
|
||||||
|
I18N[backup_extract_err]="Archive extraction failed"
|
||||||
|
I18N[backup_label]="Backup"
|
||||||
|
I18N[backup_version_label]="Version"
|
||||||
|
I18N[backup_mode_label]="Mode"
|
||||||
|
I18N[backup_lang_label]="Language"
|
||||||
|
I18N[backup_date_label]="Date"
|
||||||
|
I18N[backup_confirm_restore]="Restore configuration? Current settings will be overwritten."
|
||||||
|
I18N[backup_restored_telemt]="telemt config restored"
|
||||||
|
I18N[backup_restored_gotelegram]="goTelegram Pro config restored"
|
||||||
|
I18N[backup_restored_lang]="Interface language restored"
|
||||||
|
I18N[backup_restored_nginx]="nginx config restored"
|
||||||
|
I18N[backup_restored_ssl]="SSL certificates restored"
|
||||||
|
I18N[backup_restored_site]="Website template restored"
|
||||||
|
I18N[backup_restore_done]="Restore completed!"
|
||||||
|
I18N[backup_none]="No backups"
|
||||||
|
I18N[backup_list_title]="Available backups"
|
||||||
|
I18N[backup_cleanup_fmt]="Removed %s old backups (kept %s)"
|
||||||
|
I18N[backup_create_title]="Create backup"
|
||||||
|
I18N[backup_encrypt_prompt]="Encrypt backup with a password?"
|
||||||
|
I18N[backup_repeat_pass]="Repeat password"
|
||||||
|
I18N[backup_pass_mismatch]="Passwords do not match"
|
||||||
|
I18N[backup_pass_short]="Password too short (minimum 6 characters)"
|
||||||
|
I18N[backup_pick_prompt]="Backup number (or path to file)"
|
||||||
|
I18N[backup_not_found]="Backup not found"
|
||||||
|
|
||||||
|
# ── Errors / misc ───────────────────────────────────────────────────────
|
||||||
|
I18N[err_need_root]="Run the script with sudo / as root"
|
||||||
|
I18N[err_os_unknown]="Failed to detect OS. Linux is required."
|
||||||
|
I18N[err_low_disk]="Low disk space: %sMB (need %sMB+)"
|
||||||
|
I18N[err_bad_pkg_mgr]="Unknown package manager"
|
||||||
|
I18N[err_unexpected]="Unexpected error"
|
||||||
|
I18N[bye]="See you later! 👋"
|
||||||
|
I18N[auto_refresh]="Refresh in 30 sec"
|
||||||
|
|
||||||
|
# ── Deps ────────────────────────────────────────────────────────────────
|
||||||
|
I18N[deps_installing]="Installing dependencies: %s"
|
||||||
|
|
||||||
|
# ── Migration ───────────────────────────────────────────────────────────
|
||||||
|
I18N[v1_detected]="⚠️ goTelegram Pro v1 (mtg) installation detected"
|
||||||
|
I18N[v1_container]="Container: %s"
|
||||||
|
I18N[v1_migration_step]="Migrating from v1 (mtg) to v2 (telemt)"
|
||||||
|
I18N[v1_found_title]="Found v1 (mtg) installation:"
|
||||||
|
I18N[v1_port]="Port: %s"
|
||||||
|
I18N[v1_secret]="Secret: %s..."
|
||||||
|
I18N[v1_incompatible]="mtg secret is NOT directly compatible with telemt."
|
||||||
|
I18N[v1_new_link]="Clients will need a new link."
|
||||||
|
I18N[v1_stop_migrate]="Stop v1 container and migrate to v2? [Y/n]:"
|
||||||
|
I18N[v1_migration_cancelled]="Migration cancelled. v1 left intact."
|
||||||
|
I18N[v1_stopping]="Stopping v1 container..."
|
||||||
|
I18N[v1_config_saved]="v1 config saved to %s"
|
||||||
|
I18N[v1_port_freed]="v1 stopped. Port %s freed."
|
||||||
377
lib/lang/ru.sh
Normal file
377
lib/lang/ru.sh
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# goTelegram Pro v2.5.0 — Russian translations
|
||||||
|
# shellcheck disable=SC2034,SC2148
|
||||||
|
|
||||||
|
# ── Common words ────────────────────────────────────────────────────────
|
||||||
|
I18N[yes]="Да"
|
||||||
|
I18N[no]="Нет"
|
||||||
|
I18N[ok]="OK"
|
||||||
|
I18N[cancel]="Отмена"
|
||||||
|
I18N[back]="« Назад"
|
||||||
|
I18N[exit]="Выход"
|
||||||
|
I18N[skip]="Пропустить"
|
||||||
|
I18N[choose]="Выбор"
|
||||||
|
I18N[press_enter]="Нажмите Enter..."
|
||||||
|
I18N[press_enter_to_return]="Нажмите Enter для возврата в меню..."
|
||||||
|
I18N[invalid_choice]="Неверный выбор"
|
||||||
|
I18N[running]="работает"
|
||||||
|
I18N[stopped]="остановлен"
|
||||||
|
I18N[not_installed]="не установлен"
|
||||||
|
I18N[unknown]="неизвестно"
|
||||||
|
I18N[error]="Ошибка"
|
||||||
|
I18N[warning]="Внимание"
|
||||||
|
I18N[info]="Инфо"
|
||||||
|
I18N[success]="Готово"
|
||||||
|
I18N[wait]="Подождите..."
|
||||||
|
|
||||||
|
# ── Banner ──────────────────────────────────────────────────────────────
|
||||||
|
I18N[banner_title]="goTelegram Pro v%s"
|
||||||
|
I18N[banner_subtitle]="MTProxy на ядре telemt (Rust + Tokio)"
|
||||||
|
I18N[banner_features]="Anti-DPI • Fake TLS • TCP Splice • JA3/JA4"
|
||||||
|
I18N[credits_title]="Благодарности / Credits"
|
||||||
|
|
||||||
|
# ── Main menu (dashboard) ───────────────────────────────────────────────
|
||||||
|
I18N[dashboard_title]="Панель управления"
|
||||||
|
I18N[svc_proxy]="Прокси"
|
||||||
|
I18N[svc_nginx]="nginx"
|
||||||
|
I18N[svc_site]="Сайт"
|
||||||
|
I18N[svc_ssl]="SSL"
|
||||||
|
I18N[svc_bot]="Бот"
|
||||||
|
I18N[ssl_until]="до %s"
|
||||||
|
I18N[net_ip]="IP:"
|
||||||
|
I18N[net_port]="Порт:"
|
||||||
|
I18N[net_mode]="Режим:"
|
||||||
|
I18N[net_domain]="Домен:"
|
||||||
|
I18N[connection_link]="Ссылка для Telegram:"
|
||||||
|
I18N[proxy_not_configured]="Прокси не настроен. Выберите пункт 1."
|
||||||
|
I18N[menu_proxy]="Прокси ▸"
|
||||||
|
I18N[menu_stats]="Статистика ▸"
|
||||||
|
I18N[menu_manage]="Управление ▸"
|
||||||
|
I18N[menu_telegram_bot]="Telegram-бот ▸"
|
||||||
|
I18N[menu_about]="О программе ▸"
|
||||||
|
I18N[auto_refresh_30s]="Обновление через 30 сек"
|
||||||
|
|
||||||
|
# ── Submenu: Proxy ──────────────────────────────────────────────────────
|
||||||
|
I18N[submenu_proxy_title]="🚀 ПРОКСИ"
|
||||||
|
I18N[proxy_install_update]="Установить / Обновить"
|
||||||
|
I18N[proxy_status_detail]="Статус подробно"
|
||||||
|
I18N[proxy_copy_link]="Скопировать ссылку"
|
||||||
|
I18N[proxy_share]="Поделиться ключом"
|
||||||
|
I18N[proxy_restart]="Перезапуск"
|
||||||
|
I18N[proxy_logs]="Логи"
|
||||||
|
I18N[proxy_change_mode]="Сменить режим / шаблон"
|
||||||
|
|
||||||
|
# ── Submenu: Manage ─────────────────────────────────────────────────────
|
||||||
|
I18N[submenu_manage_title]="⚙️ УПРАВЛЕНИЕ"
|
||||||
|
I18N[manage_backup]="Бекап"
|
||||||
|
I18N[manage_restore]="Восстановить"
|
||||||
|
I18N[manage_update_telemt]="Обновить telemt"
|
||||||
|
I18N[manage_site_ssl]="Сайт / SSL"
|
||||||
|
I18N[manage_remove]="Удалить"
|
||||||
|
I18N[manage_language]="Язык / Language"
|
||||||
|
|
||||||
|
# ── Submenu: About ──────────────────────────────────────────────────────
|
||||||
|
I18N[submenu_about_title]="ℹ️ О ПРОГРАММЕ"
|
||||||
|
I18N[about_version_info]="Информация о версии"
|
||||||
|
I18N[about_promo]="Промо / Донат"
|
||||||
|
I18N[version_title]="🔍 Информация"
|
||||||
|
I18N[version_label]="goTelegram Pro:"
|
||||||
|
I18N[version_engine]="Ядро:"
|
||||||
|
I18N[version_tech]="Технология:"
|
||||||
|
I18N[version_license]="Лицензия:"
|
||||||
|
|
||||||
|
# ── Install flow ────────────────────────────────────────────────────────
|
||||||
|
I18N[install_select_mode]="🎭 Выберите режим маскировки:"
|
||||||
|
I18N[install_lite_title]="⚡ Lite — маскировка под популярный сайт"
|
||||||
|
I18N[install_lite_desc1]="Быстро, без домена. telemt маскирует трафик"
|
||||||
|
I18N[install_lite_desc2]="под выбранный сайт (google.com и т.д.)"
|
||||||
|
I18N[install_pro_title]="🛡 Pro — свой сайт + полная маскировка"
|
||||||
|
I18N[install_pro_desc1]="nginx + SSL + HTML-шаблон + telemt."
|
||||||
|
I18N[install_pro_desc2]="DPI видит реальный сайт с реальным сертификатом."
|
||||||
|
I18N[install_pro_desc3]="Требует: домен, направленный на этот сервер."
|
||||||
|
I18N[install_mode_choice]="Выбор (1/2):"
|
||||||
|
I18N[install_bad_choice]="Неверный выбор: %s"
|
||||||
|
I18N[install_lite_step]="Установка Lite-режима"
|
||||||
|
I18N[install_pro_step]="Установка Pro-режима"
|
||||||
|
I18N[install_enter_domain]="Введите ваш домен (например, example.com):"
|
||||||
|
I18N[install_bad_domain]="Некорректный домен: %s"
|
||||||
|
I18N[install_dns_mismatch]="Домен %s указывает на %s, а не на %s"
|
||||||
|
I18N[install_continue_anyway]="Продолжить всё равно?"
|
||||||
|
I18N[install_enter_email]="Email для SSL (Enter = без email):"
|
||||||
|
I18N[install_config_title]="📋 Конфигурация:"
|
||||||
|
I18N[install_cfg_ip]="IP:"
|
||||||
|
I18N[install_cfg_port]="Порт:"
|
||||||
|
I18N[install_cfg_mask]="Маскировка:"
|
||||||
|
I18N[install_cfg_mode]="Режим:"
|
||||||
|
I18N[install_cfg_domain]="Домен:"
|
||||||
|
I18N[install_confirm_proxy]="Установить прокси?"
|
||||||
|
I18N[install_confirm_proxy_site]="Установить прокси + сайт?"
|
||||||
|
I18N[install_done]="goTelegram Pro v%s установлен! (%s-режим)"
|
||||||
|
I18N[install_arch_desc1]="telemt принимает весь трафик на 443 (маскировка под HTTPS)"
|
||||||
|
I18N[install_arch_desc2]="nginx обслуживает сайт на внутреннем порту %s"
|
||||||
|
I18N[install_arch_desc3]="Провайдер видит только HTTPS-трафик к %s:443"
|
||||||
|
|
||||||
|
# ── Change mode/template ────────────────────────────────────────────────
|
||||||
|
I18N[change_current_mode]="Текущий режим:"
|
||||||
|
I18N[change_template]="Сменить шаблон сайта (только pro)"
|
||||||
|
I18N[change_mode_switch]="Переключить режим (lite ↔ pro)"
|
||||||
|
I18N[change_only_pro]="Смена шаблона доступна только в pro-режиме"
|
||||||
|
I18N[change_requires_reinstall]="Переключение режима требует переустановки."
|
||||||
|
I18N[change_reinstall_confirm]="Переустановить прокси?"
|
||||||
|
|
||||||
|
# ── Logs ────────────────────────────────────────────────────────────────
|
||||||
|
I18N[logs_telemt_title]="📋 Логи telemt (последние %s строк):"
|
||||||
|
|
||||||
|
# ── Link / Share ────────────────────────────────────────────────────────
|
||||||
|
I18N[link_title]="🔗 Ссылка для подключения:"
|
||||||
|
I18N[share_title]="📤 Перешлите это сообщение:"
|
||||||
|
I18N[share_line1]="🔐 MTProxy для Telegram (goTelegram Pro v%s)"
|
||||||
|
I18N[share_server]="🌍 Сервер: %s"
|
||||||
|
I18N[share_port]="🔌 Порт: %s"
|
||||||
|
I18N[share_connect_cta]="👉 Подключиться одним нажатием:"
|
||||||
|
I18N[share_footer]="Просто нажмите на ссылку или настройте вручную."
|
||||||
|
|
||||||
|
# ── Website ─────────────────────────────────────────────────────────────
|
||||||
|
I18N[website_title]="🌐 Управление сайтом"
|
||||||
|
I18N[website_domain]="Домен:"
|
||||||
|
I18N[website_ssl_until]="SSL до:"
|
||||||
|
I18N[website_only_pro]="Управление сайтом доступно только в pro-режиме"
|
||||||
|
I18N[website_renew_ssl]="Обновить SSL сертификат"
|
||||||
|
I18N[website_restart_nginx]="Перезапустить nginx"
|
||||||
|
I18N[website_change_template]="Сменить шаблон"
|
||||||
|
|
||||||
|
# ── Remove ──────────────────────────────────────────────────────────────
|
||||||
|
I18N[remove_title]="🗑 Удаление goTelegram Pro"
|
||||||
|
I18N[remove_proxy_only]="Удалить только прокси (telemt)"
|
||||||
|
I18N[remove_bot_only]="Удалить только Telegram-бота"
|
||||||
|
I18N[remove_all]="Удалить всё (прокси + бот + настройки)"
|
||||||
|
I18N[remove_warn_proxy]="Это удалит прокси и все его настройки."
|
||||||
|
I18N[remove_confirm_proxy]="Удалить прокси?"
|
||||||
|
I18N[remove_backup_before]="Сделать бекап перед удалением?"
|
||||||
|
I18N[remove_warn_all]="Это удалит ВСЁ: прокси, бот, сайт, настройки."
|
||||||
|
I18N[remove_confirm_all]="Вы точно уверены?"
|
||||||
|
I18N[remove_proxy_done]="Прокси удалён"
|
||||||
|
I18N[remove_all_done]="goTelegram Pro полностью удалён (прокси + бот)"
|
||||||
|
|
||||||
|
# ── Telegram bot submenu ────────────────────────────────────────────────
|
||||||
|
I18N[bot_title]="🤖 Telegram-бот"
|
||||||
|
I18N[bot_status_running]="● Работает"
|
||||||
|
I18N[bot_status_stopped]="○ Остановлен"
|
||||||
|
I18N[bot_status_not_installed]="✗ Не установлен"
|
||||||
|
I18N[bot_menu_status]="📊 Статус бота"
|
||||||
|
I18N[bot_menu_logs]="📋 Логи бота"
|
||||||
|
I18N[bot_menu_restart]="🔄 Перезапустить бота"
|
||||||
|
I18N[bot_menu_stop]="⏹ Остановить бота"
|
||||||
|
I18N[bot_menu_start]="▶️ Запустить бота"
|
||||||
|
I18N[bot_menu_settings]="⚙️ Настройки (.env)"
|
||||||
|
I18N[bot_menu_remove]="🗑 Удалить бота"
|
||||||
|
I18N[bot_menu_install]="🔧 Установить бота"
|
||||||
|
I18N[bot_intro1]="Бот позволяет управлять прокси прямо из Telegram:"
|
||||||
|
I18N[bot_intro2]="статус, перезапуск, смена режима, бекап, QR-код."
|
||||||
|
I18N[bot_install_step]="Установка Telegram-бота"
|
||||||
|
I18N[bot_install_python]="Установка Python3..."
|
||||||
|
I18N[bot_files_not_found]="Файлы бота не найдены в %s"
|
||||||
|
I18N[bot_create_venv]="Создание виртуального окружения..."
|
||||||
|
I18N[bot_install_deps]="Установка зависимостей..."
|
||||||
|
I18N[bot_enter_token]="Введите BOT_TOKEN от @BotFather:"
|
||||||
|
I18N[bot_token_empty]="Токен не может быть пустым"
|
||||||
|
I18N[bot_token]="Token:"
|
||||||
|
I18N[bot_add_admin_how]="Как добавить администратора?"
|
||||||
|
I18N[bot_admin_auto]="Автоматически — бот определит ID при первом /start"
|
||||||
|
I18N[bot_admin_manual]="Вручную — ввести ID сейчас"
|
||||||
|
I18N[bot_admin_ids_prompt]="ID администраторов (через пробел/запятую):"
|
||||||
|
I18N[bot_env_created]=".env создан"
|
||||||
|
I18N[bot_env_exists]=".env уже существует, настройки сохранены"
|
||||||
|
I18N[bot_wait_admin_title]="Ожидание администратора"
|
||||||
|
I18N[bot_wait_admin_msg1]="Откройте бота в Telegram и отправьте"
|
||||||
|
I18N[bot_wait_admin_msg2]="Бот автоматически назначит вас администратором"
|
||||||
|
I18N[bot_wait_admin_skip]="Нажмите Ctrl+C чтобы пропустить"
|
||||||
|
I18N[bot_wait_spinner]="Ожидание... напишите /start боту (%d сек)"
|
||||||
|
I18N[bot_admin_assigned]="Администратор назначен!"
|
||||||
|
I18N[bot_wait_skipped]="Пропущено. Добавить админа позже: меню → Telegram-бот → Настройки"
|
||||||
|
I18N[bot_wait_timeout]="Таймаут (5 мин). Добавить админа: меню → Telegram-бот → Настройки"
|
||||||
|
I18N[bot_installed]="Бот установлен и запущен!"
|
||||||
|
I18N[bot_status_title]="📊 Статус Telegram-бота"
|
||||||
|
I18N[bot_token_configured]="настроен"
|
||||||
|
I18N[bot_access_open]="все пользователи"
|
||||||
|
I18N[bot_logs_title]="📋 Логи бота (последние 30 строк):"
|
||||||
|
I18N[bot_settings_title]="⚙️ Настройки бота"
|
||||||
|
I18N[bot_current_env]="Текущий .env:"
|
||||||
|
I18N[bot_change_token]="Сменить BOT_TOKEN"
|
||||||
|
I18N[bot_change_allowed]="Изменить ALLOWED_IDS"
|
||||||
|
I18N[bot_new_token]="Новый BOT_TOKEN:"
|
||||||
|
I18N[bot_token_empty_err]="Пустой токен"
|
||||||
|
I18N[bot_token_updated]="Токен обновлён, бот перезапущен"
|
||||||
|
I18N[bot_allowed_prompt]="ALLOWED_IDS (через пробел/запятую, пусто = авто):"
|
||||||
|
I18N[bot_access_updated]="Доступ обновлён, бот перезапущен"
|
||||||
|
I18N[bot_remove_warn]="Это удалит Telegram-бота и все его настройки."
|
||||||
|
I18N[bot_remove_confirm]="Удалить бота?"
|
||||||
|
I18N[bot_removed]="Бот полностью удалён"
|
||||||
|
I18N[bot_restarted]="Бот перезапущен"
|
||||||
|
I18N[bot_stopped]="Бот остановлен"
|
||||||
|
I18N[bot_started]="Бот запущен"
|
||||||
|
I18N[bot_status_colon]="Статус:"
|
||||||
|
I18N[bot_access_colon]="Доступ:"
|
||||||
|
I18N[bot_access_ids_fmt]="ID: %s"
|
||||||
|
|
||||||
|
# ── Promo / Donate ──────────────────────────────────────────────────────
|
||||||
|
I18N[promo_host1_title]="💰 ХОСТИНГ #1 — СКИДКА ДО 60%"
|
||||||
|
I18N[promo_host2_title]="💰 ХОСТИНГ #2 — СКИДКА ДО 60%"
|
||||||
|
I18N[promo_tips_title]="☕ Донат / Чаевые"
|
||||||
|
I18N[promo_youtube_title]="▶ YouTube-канал"
|
||||||
|
I18N[promo_link_label]="Ссылка:"
|
||||||
|
I18N[promo_off60]="60%% скидки на первый месяц"
|
||||||
|
I18N[promo_ant20]="20%% + 3%% при оплате за 3 месяца"
|
||||||
|
I18N[promo_ant6]="15%% + 5%% при оплате за 6 месяцев"
|
||||||
|
I18N[promo_qr_host1]="── QR: Хостинг #1 ──"
|
||||||
|
I18N[promo_qr_host2]="── QR: Хостинг #2 ──"
|
||||||
|
I18N[promo_qr_tips]="── QR: Чаевые / Донат ──"
|
||||||
|
I18N[promo_qr_youtube]="── QR: YouTube-канал ──"
|
||||||
|
I18N[promo_menu_in]="Меню через %d сек..."
|
||||||
|
|
||||||
|
# ── Stats ───────────────────────────────────────────────────────────────
|
||||||
|
I18N[stats_title]="📊 Статистика трафика"
|
||||||
|
I18N[stats_module_missing]="Модуль статистики не загружен."
|
||||||
|
I18N[stats_file_missing]="Файл lib/stats.sh не найден."
|
||||||
|
I18N[stats_toggle]="Вкл/Выкл подсчёт (сейчас: %s)"
|
||||||
|
I18N[stats_install_collector]="Установить/обновить сборщик статистики"
|
||||||
|
I18N[stats_auto_refresh]="Обновление каждые 3 сек"
|
||||||
|
I18N[stats_on]="вкл"
|
||||||
|
I18N[stats_off]="выкл"
|
||||||
|
|
||||||
|
# ── Templates catalog ───────────────────────────────────────────────────
|
||||||
|
I18N[templates_categories]="📂 Категории шаблонов сайтов:"
|
||||||
|
I18N[templates_custom_git]="📎 Свой шаблон по git URL"
|
||||||
|
I18N[templates_random]="🎲 Случайный шаблон"
|
||||||
|
I18N[templates_count_fmt]="(%d шаблонов)"
|
||||||
|
I18N[templates_list]="📋 %s — доступные шаблоны:"
|
||||||
|
I18N[templates_preview_title]="🔍 Превью шаблона:"
|
||||||
|
I18N[templates_name]="Название:"
|
||||||
|
I18N[templates_source]="Источник:"
|
||||||
|
I18N[templates_description]="Описание:"
|
||||||
|
I18N[templates_preview]="👁 Превью:"
|
||||||
|
I18N[templates_preview_hint]="Откройте ссылку в браузере для просмотра шаблона"
|
||||||
|
I18N[templates_repo]="📦 Репо:"
|
||||||
|
I18N[templates_thanks]="💜 Спасибо авторам %s за открытый код!"
|
||||||
|
I18N[templates_install_this]="Установить этот шаблон?"
|
||||||
|
I18N[templates_cat_empty]="В этой категории нет шаблонов"
|
||||||
|
I18N[templates_downloading]="Скачивание шаблона \"%s\"..."
|
||||||
|
I18N[templates_downloaded]="Шаблон \"%s\" скачан"
|
||||||
|
I18N[templates_downloaded_subfolder]="Шаблон \"%s\" скачан (из подпапки)"
|
||||||
|
I18N[templates_no_index]="Шаблон не содержит index.html"
|
||||||
|
I18N[templates_path]="Путь: %s"
|
||||||
|
I18N[templates_catalog_not_found]="Каталог шаблонов не найден: %s"
|
||||||
|
|
||||||
|
# ── Custom git template ─────────────────────────────────────────────────
|
||||||
|
I18N[custom_git_title]="📎 СВОЙ ШАБЛОН ПО GIT URL"
|
||||||
|
I18N[custom_git_help_1]="Вы можете использовать ЛЮБОЙ репозиторий со статическим HTML-сайтом"
|
||||||
|
I18N[custom_git_help_2]="в качестве шаблона. Репозиторий должен быть публичным и содержать"
|
||||||
|
I18N[custom_git_help_3]="готовый index.html (сборка через npm НЕ выполняется)."
|
||||||
|
I18N[custom_git_formats]="Поддерживаемые форматы URL:"
|
||||||
|
I18N[custom_git_fmt_github]=" • https://github.com/user/repo"
|
||||||
|
I18N[custom_git_fmt_gitlab]=" • https://gitlab.com/user/repo"
|
||||||
|
I18N[custom_git_fmt_gitext]=" • https://example.com/user/repo.git"
|
||||||
|
I18N[custom_git_fmt_branch]=" • https://github.com/user/repo@branch (ветка после @)"
|
||||||
|
I18N[custom_git_auto_detect]="Структура репозитория (авто-определение):"
|
||||||
|
I18N[custom_git_auto_1]=" 1. index.html в корне репозитория"
|
||||||
|
I18N[custom_git_auto_2]=" 2. dist/index.html (StartBootstrap, Vite, webpack)"
|
||||||
|
I18N[custom_git_auto_3]=" 3. public/ или build/ или _site/ или site/ или docs/"
|
||||||
|
I18N[custom_git_auto_4]=" 4. Fallback: поиск index.html по всему репозиторию"
|
||||||
|
I18N[custom_git_requirements]="Требования:"
|
||||||
|
I18N[custom_git_req_1]=" • Только HTTPS (ssh:// и git:// блокируются)"
|
||||||
|
I18N[custom_git_req_2]=" • Только публичные репозитории"
|
||||||
|
I18N[custom_git_req_3]=" • Размер репо не более 100 МБ"
|
||||||
|
I18N[custom_git_req_4]=" • Статический HTML (без серверного кода PHP/Python/Node)"
|
||||||
|
I18N[custom_git_examples]="Примеры проверенных репо:"
|
||||||
|
I18N[custom_git_ex_1]=" • https://github.com/html5up-collective/strata"
|
||||||
|
I18N[custom_git_ex_2]=" • https://github.com/StartBootstrap/startbootstrap-landing-page"
|
||||||
|
I18N[custom_git_enter_url]="Вставьте git URL (или Enter для отмены):"
|
||||||
|
I18N[custom_git_empty]="URL не указан, отмена"
|
||||||
|
I18N[custom_git_bad_url]="Недопустимый URL. Принимаются только https:// адреса"
|
||||||
|
I18N[custom_git_cloning]="Клонирование репозитория..."
|
||||||
|
I18N[custom_git_clone_failed]="Не удалось клонировать репозиторий: %s"
|
||||||
|
I18N[custom_git_too_big]="Репозиторий слишком большой: %s (лимит 100MB)"
|
||||||
|
I18N[custom_git_scanning]="Поиск index.html в структуре..."
|
||||||
|
I18N[custom_git_found_at]="✓ Найден index.html в: %s"
|
||||||
|
I18N[custom_git_no_index]="index.html не найден в репозитории"
|
||||||
|
I18N[custom_git_installed]="Свой шаблон установлен из %s"
|
||||||
|
I18N[custom_git_saved]="URL шаблона сохранён в конфиге (меню → Сайт → Обновить из git)"
|
||||||
|
|
||||||
|
# ── First-run language picker ───────────────────────────────────────────
|
||||||
|
I18N[lang_picker_title]="Выберите язык / Select language"
|
||||||
|
I18N[lang_english]="English"
|
||||||
|
I18N[lang_russian]="Русский"
|
||||||
|
I18N[lang_saved]="Язык сохранён: %s"
|
||||||
|
I18N[lang_change_prompt]="Выберите новый язык:"
|
||||||
|
|
||||||
|
# ── Backup ──────────────────────────────────────────────────────────────
|
||||||
|
I18N[backup_title]="💾 Бекап"
|
||||||
|
I18N[backup_creating]="Создание бекапа..."
|
||||||
|
I18N[backup_created]="Бекап создан: %s"
|
||||||
|
I18N[backup_failed]="Ошибка создания бекапа"
|
||||||
|
I18N[backup_restore_title]="↩️ Восстановление из бекапа"
|
||||||
|
I18N[backup_no_files]="Нет файлов бекапа"
|
||||||
|
I18N[backup_select]="Выберите бекап для восстановления:"
|
||||||
|
I18N[backup_restoring]="Восстановление..."
|
||||||
|
I18N[backup_restored]="Бекап восстановлен"
|
||||||
|
I18N[backup_collecting]="Собираю конфигурацию..."
|
||||||
|
I18N[backup_site_included]="Шаблон сайта включён"
|
||||||
|
I18N[backup_archive_err]="Ошибка создания архива"
|
||||||
|
I18N[backup_archive_missing]="Архив не создан"
|
||||||
|
I18N[backup_encrypt_err]="Ошибка шифрования"
|
||||||
|
I18N[backup_encrypted]="Бекап зашифрован (AES-256-CBC)"
|
||||||
|
I18N[backup_created_fmt]="Бекап создан: %s (%s)"
|
||||||
|
I18N[backup_file_not_found_fmt]="Файл не найден: %s"
|
||||||
|
I18N[backup_enter_pass]="Введите пароль от бекапа"
|
||||||
|
I18N[backup_bad_pass]="Неверный пароль или повреждённый файл"
|
||||||
|
I18N[backup_extract_err]="Ошибка распаковки архива"
|
||||||
|
I18N[backup_label]="Бекап"
|
||||||
|
I18N[backup_version_label]="Версия"
|
||||||
|
I18N[backup_mode_label]="Режим"
|
||||||
|
I18N[backup_lang_label]="Язык"
|
||||||
|
I18N[backup_date_label]="Дата"
|
||||||
|
I18N[backup_confirm_restore]="Восстановить конфигурацию? Текущие настройки будут перезаписаны."
|
||||||
|
I18N[backup_restored_telemt]="telemt конфиг восстановлен"
|
||||||
|
I18N[backup_restored_gotelegram]="goTelegram Pro конфиг восстановлен"
|
||||||
|
I18N[backup_restored_lang]="Язык интерфейса восстановлен"
|
||||||
|
I18N[backup_restored_nginx]="nginx конфиг восстановлен"
|
||||||
|
I18N[backup_restored_ssl]="SSL сертификаты восстановлены"
|
||||||
|
I18N[backup_restored_site]="Шаблон сайта восстановлен"
|
||||||
|
I18N[backup_restore_done]="Восстановление завершено!"
|
||||||
|
I18N[backup_none]="Бекапов нет"
|
||||||
|
I18N[backup_list_title]="Доступные бекапы"
|
||||||
|
I18N[backup_cleanup_fmt]="Удалено %s старых бекапов (оставлено %s)"
|
||||||
|
I18N[backup_create_title]="Создание бекапа"
|
||||||
|
I18N[backup_encrypt_prompt]="Зашифровать бекап паролем?"
|
||||||
|
I18N[backup_repeat_pass]="Повторите пароль"
|
||||||
|
I18N[backup_pass_mismatch]="Пароли не совпадают"
|
||||||
|
I18N[backup_pass_short]="Пароль слишком короткий (минимум 6 символов)"
|
||||||
|
I18N[backup_pick_prompt]="Номер бекапа (или путь к файлу)"
|
||||||
|
I18N[backup_not_found]="Бекап не найден"
|
||||||
|
|
||||||
|
# ── Errors / misc ───────────────────────────────────────────────────────
|
||||||
|
I18N[err_need_root]="Запустите скрипт с sudo / от root"
|
||||||
|
I18N[err_os_unknown]="Не удалось определить ОС. Требуется Linux."
|
||||||
|
I18N[err_low_disk]="Мало места на диске: %sMB (нужно %sMB+)"
|
||||||
|
I18N[err_bad_pkg_mgr]="Неизвестный пакетный менеджер"
|
||||||
|
I18N[err_unexpected]="Неожиданная ошибка"
|
||||||
|
I18N[bye]="До встречи! 👋"
|
||||||
|
I18N[auto_refresh]="Обновление через 30 сек"
|
||||||
|
|
||||||
|
# ── Deps ────────────────────────────────────────────────────────────────
|
||||||
|
I18N[deps_installing]="Установка зависимостей: %s"
|
||||||
|
|
||||||
|
# ── Migration ───────────────────────────────────────────────────────────
|
||||||
|
I18N[v1_detected]="⚠️ Обнаружена установка goTelegram Pro v1 (mtg)"
|
||||||
|
I18N[v1_container]="Контейнер: %s"
|
||||||
|
I18N[v1_migration_step]="Миграция с v1 (mtg) на v2 (telemt)"
|
||||||
|
I18N[v1_found_title]="Найдена установка v1 (mtg):"
|
||||||
|
I18N[v1_port]="Порт: %s"
|
||||||
|
I18N[v1_secret]="Secret: %s..."
|
||||||
|
I18N[v1_incompatible]="секрет mtg НЕ совместим с telemt напрямую."
|
||||||
|
I18N[v1_new_link]="Клиентам потребуется новая ссылка."
|
||||||
|
I18N[v1_stop_migrate]="Остановить v1 контейнер и перейти на v2? [Y/n]:"
|
||||||
|
I18N[v1_migration_cancelled]="Миграция отменена. v1 оставлен без изменений."
|
||||||
|
I18N[v1_stopping]="Остановка v1 контейнера..."
|
||||||
|
I18N[v1_config_saved]="Конфиг v1 сохранён в %s"
|
||||||
|
I18N[v1_port_freed]="v1 остановлен. Порт %s освобождён."
|
||||||
594
lib/stats.sh
Normal file
594
lib/stats.sh
Normal file
@@ -0,0 +1,594 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# stats.sh — Traffic statistics module for GoTelegram v2.5.0
|
||||||
|
# Tracks proxy (telemt port 443) and site (nginx port 8443) traffic
|
||||||
|
# Uses iptables counters + real-time snapshots + historical CSV
|
||||||
|
|
||||||
|
# Color codes (from common.sh)
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
STATS_DIR="/run/gotelegram"
|
||||||
|
HISTORY_FILE="/opt/gotelegram/stats_history.csv"
|
||||||
|
USER_HISTORY_FILE="/opt/gotelegram/user_stats_history.csv"
|
||||||
|
SNAPSHOTS_DIR="$STATS_DIR/snapshots"
|
||||||
|
CURRENT_SNAPSHOT="$STATS_DIR/stats_current.json"
|
||||||
|
CONFIG_FILE="/opt/gotelegram/config.json"
|
||||||
|
TELEMT_CONFIG_FILE="/etc/telemt/config.toml"
|
||||||
|
STATS_RETENTION_DAYS="${STATS_RETENTION_DAYS:-365}"
|
||||||
|
STATS_MINUTE_RETENTION_DAYS="${STATS_MINUTE_RETENTION_DAYS:-31}"
|
||||||
|
STATS_CLEANUP_INTERVAL="${STATS_CLEANUP_INTERVAL:-3600}"
|
||||||
|
STATS_CLEANUP_STAMP="$STATS_DIR/last_history_cleanup"
|
||||||
|
USER_STATS_COLLECT_STAMP="$STATS_DIR/last_user_stats_minute"
|
||||||
|
|
||||||
|
# Initialize stats infrastructure
|
||||||
|
stats_init() {
|
||||||
|
if ! command -v iptables &>/dev/null; then
|
||||||
|
log_warning "iptables не найден: установите пакет iptables или запустите установку зависимостей"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create runtime directory
|
||||||
|
mkdir -p "$STATS_DIR" "$SNAPSHOTS_DIR" 2>/dev/null
|
||||||
|
chmod 755 "$STATS_DIR" "$SNAPSHOTS_DIR" 2>/dev/null
|
||||||
|
|
||||||
|
# Create iptables chain if not exists
|
||||||
|
if ! iptables -L GOTELEGRAM_STATS -n >/dev/null 2>&1; then
|
||||||
|
iptables -N GOTELEGRAM_STATS 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add chain to INPUT if not already present
|
||||||
|
if ! iptables -C INPUT -j GOTELEGRAM_STATS 2>/dev/null; then
|
||||||
|
iptables -I INPUT -j GOTELEGRAM_STATS 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add rule for proxy traffic (port 443, TCP)
|
||||||
|
if ! iptables -C GOTELEGRAM_STATS -p tcp --dport 443 2>/dev/null; then
|
||||||
|
iptables -A GOTELEGRAM_STATS -p tcp --dport 443 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add rule for site traffic (loopback, port 8443, TCP)
|
||||||
|
if ! iptables -C GOTELEGRAM_STATS -i lo -p tcp --dport 8443 2>/dev/null; then
|
||||||
|
iptables -A GOTELEGRAM_STATS -i lo -p tcp --dport 8443 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Initialize CSV header if file doesn't exist
|
||||||
|
if [[ ! -f "$HISTORY_FILE" ]]; then
|
||||||
|
echo "epoch,proxy_bytes,site_bytes" > "$HISTORY_FILE" 2>/dev/null
|
||||||
|
fi
|
||||||
|
if [[ ! -f "$USER_HISTORY_FILE" ]]; then
|
||||||
|
echo "epoch,user,total_octets,current_connections,active_unique_ips,recent_unique_ips" > "$USER_HISTORY_FILE" 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Write initial snapshot
|
||||||
|
stats_collect
|
||||||
|
}
|
||||||
|
|
||||||
|
# Collect current traffic statistics from iptables
|
||||||
|
stats_collect() {
|
||||||
|
local proxy_bytes=0 proxy_pkts=0 site_bytes=0 site_pkts=0
|
||||||
|
local ts=$(date +%s)
|
||||||
|
local temp_file=$(mktemp)
|
||||||
|
|
||||||
|
if ! command -v iptables &>/dev/null; then
|
||||||
|
mkdir -p "$STATS_DIR" 2>/dev/null
|
||||||
|
echo "{\"ts\":$ts,\"proxy_bytes\":0,\"proxy_pkts\":0,\"site_bytes\":0,\"site_pkts\":0,\"error\":\"iptables_missing\"}" > "$CURRENT_SNAPSHOT" 2>/dev/null
|
||||||
|
rm -f "$temp_file" 2>/dev/null
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Parse iptables output: format is "pkts bytes target"
|
||||||
|
# We need to extract bytes (2nd column) for each rule
|
||||||
|
local iptables_output=$(iptables -L GOTELEGRAM_STATS -v -n -x 2>/dev/null)
|
||||||
|
|
||||||
|
# Extract counters for port 443 (proxy)
|
||||||
|
proxy_bytes=$(echo "$iptables_output" | grep "dpt:443" | grep -v "lo" | awk '{print $2}')
|
||||||
|
proxy_pkts=$(echo "$iptables_output" | grep "dpt:443" | grep -v "lo" | awk '{print $1}')
|
||||||
|
|
||||||
|
# Extract counters for port 8443 on loopback (site)
|
||||||
|
site_bytes=$(echo "$iptables_output" | grep "dpt:8443" | awk '{print $2}')
|
||||||
|
site_pkts=$(echo "$iptables_output" | grep "dpt:8443" | awk '{print $1}')
|
||||||
|
|
||||||
|
# Default to 0 if not found
|
||||||
|
proxy_bytes=${proxy_bytes:-0}
|
||||||
|
proxy_pkts=${proxy_pkts:-0}
|
||||||
|
site_bytes=${site_bytes:-0}
|
||||||
|
site_pkts=${site_pkts:-0}
|
||||||
|
|
||||||
|
# Write current snapshot as JSON
|
||||||
|
if command -v jq &>/dev/null; then
|
||||||
|
echo "{\"ts\":$ts,\"proxy_bytes\":$proxy_bytes,\"proxy_pkts\":$proxy_pkts,\"site_bytes\":$site_bytes,\"site_pkts\":$site_pkts}" > "$CURRENT_SNAPSHOT" 2>/dev/null
|
||||||
|
else
|
||||||
|
cat > "$CURRENT_SNAPSHOT" 2>/dev/null <<EOF
|
||||||
|
{"ts":$ts,"proxy_bytes":$proxy_bytes,"proxy_pkts":$proxy_pkts,"site_bytes":$site_bytes,"site_pkts":$site_pkts}
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Save snapshot for rate calculation (one per minute)
|
||||||
|
local minute_key
|
||||||
|
minute_key=$(date +%Y%m%d%H%M 2>/dev/null)
|
||||||
|
local snapshot_file="$SNAPSHOTS_DIR/snap_${minute_key}.json"
|
||||||
|
cp "$CURRENT_SNAPSHOT" "$snapshot_file" 2>/dev/null
|
||||||
|
|
||||||
|
# Append to history CSV (once per minute, check if last entry is fresh)
|
||||||
|
# Auto-recreate the file with header if it was deleted — otherwise the
|
||||||
|
# collector would silently stop writing history after any wipe (v2.4.1 fix).
|
||||||
|
if [[ ! -f "$HISTORY_FILE" ]]; then
|
||||||
|
mkdir -p "$(dirname "$HISTORY_FILE")" 2>/dev/null
|
||||||
|
echo "epoch,proxy_bytes,site_bytes" > "$HISTORY_FILE" 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$HISTORY_FILE" ]]; then
|
||||||
|
local last_ts
|
||||||
|
last_ts=$(grep -E '^[0-9]' "$HISTORY_FILE" 2>/dev/null | tail -1 | cut -d, -f1)
|
||||||
|
last_ts="${last_ts:-0}"
|
||||||
|
local current_minute=$((ts - (ts % 60)))
|
||||||
|
|
||||||
|
if [[ "$last_ts" -eq 0 ]] || [[ $((current_minute - last_ts)) -ge 60 ]]; then
|
||||||
|
echo "$current_minute,$proxy_bytes,$site_bytes" >> "$HISTORY_FILE" 2>/dev/null
|
||||||
|
|
||||||
|
# Cleanup/compact history at most once per hour.
|
||||||
|
stats_cleanup_history
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
stats_collect_users "$ts"
|
||||||
|
|
||||||
|
rm -f "$temp_file" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print active telemt usernames from [access.users]. Usernames are restricted by
|
||||||
|
# goTelegram to A-Z/a-z/0-9/_.- so they are safe in URLs and CSV fields.
|
||||||
|
stats_active_users() {
|
||||||
|
[[ -f "$TELEMT_CONFIG_FILE" ]] || return 0
|
||||||
|
awk '
|
||||||
|
/^\[access\.users\]$/ { in_users=1; next }
|
||||||
|
in_users && /^\[/ { exit }
|
||||||
|
in_users && /^[[:space:]]*#/ { next }
|
||||||
|
in_users && /=/ {
|
||||||
|
key=$1
|
||||||
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
|
||||||
|
gsub(/^"|"$/, "", key)
|
||||||
|
if (key ~ /^[A-Za-z0-9_.-]{1,48}$/) print key
|
||||||
|
}
|
||||||
|
' "$TELEMT_CONFIG_FILE" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
stats_collect_users() {
|
||||||
|
local ts="${1:-$(date +%s)}"
|
||||||
|
local current_minute=$((ts - (ts % 60)))
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$USER_HISTORY_FILE")" 2>/dev/null
|
||||||
|
if [[ ! -f "$USER_HISTORY_FILE" ]]; then
|
||||||
|
echo "epoch,user,total_octets,current_connections,active_unique_ips,recent_unique_ips" > "$USER_HISTORY_FILE" 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
command -v curl &>/dev/null || return 0
|
||||||
|
command -v jq &>/dev/null || return 0
|
||||||
|
|
||||||
|
if [[ -f "$USER_STATS_COLLECT_STAMP" ]] && [[ "$(cat "$USER_STATS_COLLECT_STAMP" 2>/dev/null)" == "$current_minute" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local existing_users=""
|
||||||
|
existing_users=$(awk -F, -v ts="$current_minute" '$1 == ts { print $2 }' "$USER_HISTORY_FILE" 2>/dev/null || true)
|
||||||
|
|
||||||
|
local user payload total conns active_ips recent_ips
|
||||||
|
while IFS= read -r user; do
|
||||||
|
[[ -n "$user" ]] || continue
|
||||||
|
if printf '%s\n' "$existing_users" | grep -Fxq "$user"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
payload=$(curl -sS --max-time 2 "http://127.0.0.1:9091/v1/users/${user}" 2>/dev/null || true)
|
||||||
|
[[ -n "$payload" ]] || continue
|
||||||
|
total=$(echo "$payload" | jq -r '.data.total_octets // .total_octets // 0' 2>/dev/null)
|
||||||
|
conns=$(echo "$payload" | jq -r '.data.current_connections // .current_connections // 0' 2>/dev/null)
|
||||||
|
active_ips=$(echo "$payload" | jq -r '.data.active_unique_ips // .active_unique_ips // 0' 2>/dev/null)
|
||||||
|
recent_ips=$(echo "$payload" | jq -r '.data.recent_unique_ips // .recent_unique_ips // 0' 2>/dev/null)
|
||||||
|
[[ "$total" =~ ^[0-9]+$ ]] || total=0
|
||||||
|
[[ "$conns" =~ ^[0-9]+$ ]] || conns=0
|
||||||
|
[[ "$active_ips" =~ ^[0-9]+$ ]] || active_ips=0
|
||||||
|
[[ "$recent_ips" =~ ^[0-9]+$ ]] || recent_ips=0
|
||||||
|
echo "$current_minute,$user,$total,$conns,$active_ips,$recent_ips" >> "$USER_HISTORY_FILE" 2>/dev/null
|
||||||
|
done < <(stats_active_users)
|
||||||
|
|
||||||
|
echo "$current_minute" > "$USER_STATS_COLLECT_STAMP" 2>/dev/null || true
|
||||||
|
stats_cleanup_user_history
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read current snapshot as JSON
|
||||||
|
stats_read_current() {
|
||||||
|
if [[ -f "$CURRENT_SNAPSHOT" ]]; then
|
||||||
|
cat "$CURRENT_SNAPSHOT"
|
||||||
|
else
|
||||||
|
echo "{}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract value from JSON (fallback if jq not available)
|
||||||
|
json_get() {
|
||||||
|
local json="$1"
|
||||||
|
local key="$2"
|
||||||
|
|
||||||
|
if command -v jq &>/dev/null; then
|
||||||
|
echo "$json" | jq -r ".${key}" 2>/dev/null || echo "0"
|
||||||
|
else
|
||||||
|
echo "$json" | grep -o "\"$key\":[^,}]*" | cut -d: -f2 | tr -d ' "' || echo "0"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Convert bytes to human-readable format
|
||||||
|
format_bytes() {
|
||||||
|
local bytes=$1
|
||||||
|
|
||||||
|
if (( bytes < 1024 )); then
|
||||||
|
printf "%.0f B" "$bytes"
|
||||||
|
elif (( bytes < 1024 * 1024 )); then
|
||||||
|
printf "%.1f KB" "$(echo "scale=1; $bytes / 1024" | bc 2>/dev/null || echo "$((bytes / 1024))")"
|
||||||
|
elif (( bytes < 1024 * 1024 * 1024 )); then
|
||||||
|
printf "%.1f MB" "$(echo "scale=1; $bytes / 1024 / 1024" | bc 2>/dev/null || echo "$((bytes / 1024 / 1024))")"
|
||||||
|
elif (( bytes < 1024 * 1024 * 1024 * 1024 )); then
|
||||||
|
printf "%.1f GB" "$(echo "scale=1; $bytes / 1024 / 1024 / 1024" | bc 2>/dev/null || echo "$((bytes / 1024 / 1024 / 1024))")"
|
||||||
|
else
|
||||||
|
printf "%.1f TB" "$(echo "scale=1; $bytes / 1024 / 1024 / 1024 / 1024" | bc 2>/dev/null || echo "$((bytes / 1024 / 1024 / 1024 / 1024))")"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Convert bytes/sec to human-readable rate
|
||||||
|
format_rate() {
|
||||||
|
local bytes_per_sec=$1
|
||||||
|
|
||||||
|
if (( bytes_per_sec < 1024 )); then
|
||||||
|
printf "%.0f B/s" "$bytes_per_sec"
|
||||||
|
elif (( bytes_per_sec < 1024 * 1024 )); then
|
||||||
|
printf "%.1f KB/s" "$(echo "scale=1; $bytes_per_sec / 1024" | bc 2>/dev/null || echo "$((bytes_per_sec / 1024))")"
|
||||||
|
elif (( bytes_per_sec < 1024 * 1024 * 1024 )); then
|
||||||
|
printf "%.1f MB/s" "$(echo "scale=1; $bytes_per_sec / 1024 / 1024" | bc 2>/dev/null || echo "$((bytes_per_sec / 1024 / 1024))")"
|
||||||
|
else
|
||||||
|
printf "%.1f GB/s" "$(echo "scale=1; $bytes_per_sec / 1024 / 1024 / 1024" | bc 2>/dev/null || echo "$((bytes_per_sec / 1024 / 1024 / 1024))")"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Safely convert value to integer (returns 0 for empty/non-numeric)
|
||||||
|
_to_int() {
|
||||||
|
local val="${1:-0}"
|
||||||
|
# Strip non-numeric chars, default to 0
|
||||||
|
val="${val//[^0-9]/}"
|
||||||
|
echo "${val:-0}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Calculate diff safely (never negative, never crashes on empty)
|
||||||
|
_safe_diff() {
|
||||||
|
local a=$(_to_int "$1")
|
||||||
|
local b=$(_to_int "$2")
|
||||||
|
local d=$((a - b))
|
||||||
|
(( d < 0 )) && d=0
|
||||||
|
echo "$d"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Calculate traffic rates and totals from history
|
||||||
|
stats_calculate_rates() {
|
||||||
|
local traffic_type="$1" # "proxy" or "site"
|
||||||
|
local col_idx=2 # proxy_bytes is column 2
|
||||||
|
[[ "$traffic_type" == "site" ]] && col_idx=3
|
||||||
|
|
||||||
|
local now
|
||||||
|
now=$(date +%s)
|
||||||
|
|
||||||
|
# Get latest data line (skip header with grep -E '^[0-9]')
|
||||||
|
local bytes_now
|
||||||
|
bytes_now=$(_to_int "$(grep -E '^[0-9]' "$HISTORY_FILE" 2>/dev/null | tail -1 | cut -d, -f"$col_idx")")
|
||||||
|
|
||||||
|
local periods="60 300 3600 86400 604800 2592000 31536000"
|
||||||
|
local results=""
|
||||||
|
|
||||||
|
for secs in $periods; do
|
||||||
|
local target_ts=$((now - secs))
|
||||||
|
# Find closest entry at or after target timestamp (skip header)
|
||||||
|
local old_val
|
||||||
|
old_val=$(_to_int "$(awk -F, -v ts="$target_ts" '$1 ~ /^[0-9]/ && $1 <= ts' "$HISTORY_FILE" 2>/dev/null | tail -1 | cut -d, -f"$col_idx")")
|
||||||
|
|
||||||
|
local diff
|
||||||
|
diff=$(_safe_diff "$bytes_now" "$old_val")
|
||||||
|
local rate=$(( secs > 0 ? diff / secs : 0 ))
|
||||||
|
|
||||||
|
local bytes_fmt rate_fmt
|
||||||
|
bytes_fmt=$(format_bytes "$diff")
|
||||||
|
rate_fmt=$(format_rate "$rate")
|
||||||
|
|
||||||
|
if [ -z "$results" ]; then
|
||||||
|
results="${bytes_fmt}|${rate_fmt}"
|
||||||
|
else
|
||||||
|
results="${results}|${bytes_fmt}|${rate_fmt}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "$results"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main display function for traffic statistics
|
||||||
|
show_traffic_stats() {
|
||||||
|
# Ensure stats are collected
|
||||||
|
stats_collect
|
||||||
|
|
||||||
|
# Get current counters
|
||||||
|
local current_json=$(stats_read_current)
|
||||||
|
local proxy_pkts=$(json_get "$current_json" "proxy_pkts")
|
||||||
|
local site_pkts=$(json_get "$current_json" "site_pkts")
|
||||||
|
|
||||||
|
# Calculate rates for proxy
|
||||||
|
local proxy_rates=$(stats_calculate_rates "proxy")
|
||||||
|
IFS='|' read -r p1m p1mr p5m p5mr p60m p60mr p1d p1dr p7d p7dr p30d p30dr p365d p365dr <<< "$proxy_rates"
|
||||||
|
|
||||||
|
# Calculate rates for site
|
||||||
|
local site_rates=$(stats_calculate_rates "site")
|
||||||
|
IFS='|' read -r s1m s1mr s5m s5mr s60m s60mr s1d s1dr s7d s7dr s30d s30dr s365d s365dr <<< "$site_rates"
|
||||||
|
|
||||||
|
# Display proxy stats
|
||||||
|
{
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE} Proxy (telemt, порт 443):${NC}"
|
||||||
|
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||||
|
echo -e "${BLUE} Период │ Входящий │ Скорость${NC}"
|
||||||
|
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||||
|
printf " %-9s │ %14s │ %s\n" "1 мин" "$p1m" "$p1mr"
|
||||||
|
printf " %-9s │ %14s │ %s\n" "5 мин" "$p5m" "$p5mr"
|
||||||
|
printf " %-9s │ %14s │ %s\n" "60 мин" "$p60m" "$p60mr"
|
||||||
|
printf " %-9s │ %14s │ %s\n" "1 день" "$p1d" "$p1dr"
|
||||||
|
printf " %-9s │ %14s │ %s\n" "7 дней" "$p7d" "$p7dr"
|
||||||
|
printf " %-9s │ %14s │ %s\n" "30 дней" "$p30d" "$p30dr"
|
||||||
|
printf " %-9s │ %14s │ %s\n" "365 дней" "$p365d" "$p365dr"
|
||||||
|
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||||
|
printf " Пакетов: %d\n\n" "$proxy_pkts"
|
||||||
|
|
||||||
|
echo -e "${BLUE} Сайт (nginx, порт 8443):${NC}"
|
||||||
|
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||||
|
echo -e "${BLUE} Период │ Входящий │ Скорость${NC}"
|
||||||
|
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||||
|
printf " %-9s │ %14s │ %s\n" "1 мин" "$s1m" "$s1mr"
|
||||||
|
printf " %-9s │ %14s │ %s\n" "5 мин" "$s5m" "$s5mr"
|
||||||
|
printf " %-9s │ %14s │ %s\n" "60 мин" "$s60m" "$s60mr"
|
||||||
|
printf " %-9s │ %14s │ %s\n" "1 день" "$s1d" "$s1dr"
|
||||||
|
printf " %-9s │ %14s │ %s\n" "7 дней" "$s7d" "$s7dr"
|
||||||
|
printf " %-9s │ %14s │ %s\n" "30 дней" "$s30d" "$s30dr"
|
||||||
|
printf " %-9s │ %14s │ %s\n" "365 дней" "$s365d" "$s365dr"
|
||||||
|
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||||
|
printf " Пакетов: %d\n" "$site_pkts"
|
||||||
|
echo ""
|
||||||
|
} >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
_stats_positive_int() {
|
||||||
|
local value="${1:-0}"
|
||||||
|
[[ "$value" =~ ^[0-9]+$ ]] && [[ "$value" -gt 0 ]] && echo "$value" || echo "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
stats_should_cleanup() {
|
||||||
|
local stamp="$1"
|
||||||
|
local now last interval
|
||||||
|
mkdir -p "$STATS_DIR" 2>/dev/null || true
|
||||||
|
now=$(date +%s)
|
||||||
|
interval=$(_stats_positive_int "$STATS_CLEANUP_INTERVAL" 3600)
|
||||||
|
last=$(cat "$stamp" 2>/dev/null || echo 0)
|
||||||
|
[[ "$last" =~ ^[0-9]+$ ]] || last=0
|
||||||
|
if (( now - last < interval )); then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "$now" > "$stamp" 2>/dev/null || true
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
stats_retention_cutoffs() {
|
||||||
|
local now retention_days minute_days
|
||||||
|
now=$(date +%s)
|
||||||
|
retention_days=$(_stats_positive_int "$STATS_RETENTION_DAYS" 365)
|
||||||
|
minute_days=$(_stats_positive_int "$STATS_MINUTE_RETENTION_DAYS" 31)
|
||||||
|
if (( minute_days > retention_days )); then
|
||||||
|
minute_days="$retention_days"
|
||||||
|
fi
|
||||||
|
echo "$((now - retention_days * 86400)) $((now - minute_days * 86400))"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Keep history for at most one year. Recent points stay per-minute; older
|
||||||
|
# points are compacted to one last cumulative snapshot per hour.
|
||||||
|
stats_cleanup_history() {
|
||||||
|
if [[ ! -f "$HISTORY_FILE" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
stats_should_cleanup "$STATS_CLEANUP_STAMP" || return 0
|
||||||
|
|
||||||
|
local retention_cutoff minute_cutoff temp_file
|
||||||
|
read -r retention_cutoff minute_cutoff <<< "$(stats_retention_cutoffs)"
|
||||||
|
temp_file=$(mktemp)
|
||||||
|
|
||||||
|
{
|
||||||
|
head -1 "$HISTORY_FILE"
|
||||||
|
awk -F, -v keep="$retention_cutoff" -v minute="$minute_cutoff" '
|
||||||
|
BEGIN { OFS="," }
|
||||||
|
NR == 1 { next }
|
||||||
|
$1 !~ /^[0-9]+$/ { next }
|
||||||
|
$1 < keep { next }
|
||||||
|
$1 >= minute { print $1, $2, $3; next }
|
||||||
|
{
|
||||||
|
bucket = int($1 / 3600)
|
||||||
|
compact[bucket] = $1 OFS $2 OFS $3
|
||||||
|
}
|
||||||
|
END {
|
||||||
|
for (bucket in compact) print compact[bucket]
|
||||||
|
}
|
||||||
|
' "$HISTORY_FILE" | sort -t, -k1,1n
|
||||||
|
} > "$temp_file" 2>/dev/null
|
||||||
|
|
||||||
|
mv "$temp_file" "$HISTORY_FILE" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
stats_cleanup_user_history() {
|
||||||
|
if [[ ! -f "$USER_HISTORY_FILE" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
stats_should_cleanup "${STATS_CLEANUP_STAMP}.users" || return 0
|
||||||
|
|
||||||
|
local retention_cutoff minute_cutoff temp_file
|
||||||
|
read -r retention_cutoff minute_cutoff <<< "$(stats_retention_cutoffs)"
|
||||||
|
temp_file=$(mktemp)
|
||||||
|
|
||||||
|
{
|
||||||
|
head -1 "$USER_HISTORY_FILE"
|
||||||
|
awk -F, -v keep="$retention_cutoff" -v minute="$minute_cutoff" '
|
||||||
|
BEGIN { OFS="," }
|
||||||
|
NR == 1 { next }
|
||||||
|
$1 !~ /^[0-9]+$/ { next }
|
||||||
|
$1 < keep { next }
|
||||||
|
$1 >= minute { print $1, $2, $3, $4, $5, $6; next }
|
||||||
|
{
|
||||||
|
bucket = $2 SUBSEP int($1 / 3600)
|
||||||
|
compact[bucket] = $1 OFS $2 OFS $3 OFS $4 OFS $5 OFS $6
|
||||||
|
}
|
||||||
|
END {
|
||||||
|
for (bucket in compact) print compact[bucket]
|
||||||
|
}
|
||||||
|
' "$USER_HISTORY_FILE" | sort -t, -k1,1n -k2,2
|
||||||
|
} > "$temp_file" 2>/dev/null
|
||||||
|
|
||||||
|
mv "$temp_file" "$USER_HISTORY_FILE" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Toggle stats collection on/off
|
||||||
|
toggle_stats() {
|
||||||
|
local current_state="false"
|
||||||
|
|
||||||
|
# Read current state from config
|
||||||
|
if [[ -f "$CONFIG_FILE" ]] && command -v jq &>/dev/null; then
|
||||||
|
current_state=$(jq -r '.stats_enabled // false' "$CONFIG_FILE" 2>/dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Toggle
|
||||||
|
if [[ "$current_state" == "true" ]]; then
|
||||||
|
# Disable stats
|
||||||
|
if [[ -f "$CONFIG_FILE" ]]; then
|
||||||
|
if command -v jq &>/dev/null; then
|
||||||
|
jq '.stats_enabled = false' "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" 2>/dev/null
|
||||||
|
mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove iptables rules
|
||||||
|
iptables -D INPUT -j GOTELEGRAM_STATS 2>/dev/null
|
||||||
|
iptables -F GOTELEGRAM_STATS 2>/dev/null
|
||||||
|
iptables -X GOTELEGRAM_STATS 2>/dev/null
|
||||||
|
|
||||||
|
# Clean up directories
|
||||||
|
rm -rf "$STATS_DIR" 2>/dev/null
|
||||||
|
|
||||||
|
echo "Сбор статистики ОТКЛЮЧЕН" >&2
|
||||||
|
else
|
||||||
|
# Enable stats
|
||||||
|
if [[ -f "$CONFIG_FILE" ]]; then
|
||||||
|
if command -v jq &>/dev/null; then
|
||||||
|
jq '.stats_enabled = true' "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" 2>/dev/null
|
||||||
|
mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Initialize stats collection
|
||||||
|
stats_init
|
||||||
|
|
||||||
|
echo "Сбор статистики ВКЛЮЧЕН" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install systemd service for stats collection
|
||||||
|
install_stats_collector() {
|
||||||
|
local service_file="/etc/systemd/system/gotelegram-stats.service"
|
||||||
|
|
||||||
|
# Check if running as root
|
||||||
|
if [[ $EUID -ne 0 ]]; then
|
||||||
|
echo "Требуется root для установки сервиса" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v iptables &>/dev/null; then
|
||||||
|
log_info "Установка iptables для подсчёта трафика..."
|
||||||
|
install_pkg "$(apt_pkg_for_cmd iptables)" || {
|
||||||
|
echo "Не удалось установить iptables" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get script directory (resolve symlinks)
|
||||||
|
local script_dir=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")
|
||||||
|
local lib_dir=$(dirname "$script_dir")
|
||||||
|
|
||||||
|
# Create systemd service file
|
||||||
|
cat > "$service_file" <<'EOF'
|
||||||
|
[Unit]
|
||||||
|
Description=goTelegram Pro Traffic Stats Collector
|
||||||
|
After=network.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
ExecStart=/bin/bash -c 'source /opt/gotelegram/lib/common.sh; source /opt/gotelegram/lib/stats.sh; stats_init; while true; do stats_collect; sleep 1; done'
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 644 "$service_file"
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable gotelegram-stats.service
|
||||||
|
systemctl restart gotelegram-stats.service
|
||||||
|
|
||||||
|
if [[ -f "$CONFIG_FILE" ]] && command -v jq &>/dev/null; then
|
||||||
|
local tmp
|
||||||
|
tmp=$(mktemp)
|
||||||
|
if jq '.stats_enabled = true' "$CONFIG_FILE" > "$tmp" 2>/dev/null; then
|
||||||
|
mv "$tmp" "$CONFIG_FILE"
|
||||||
|
chmod 600 "$CONFIG_FILE" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
rm -f "$tmp" 2>/dev/null
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Сервис gotelegram-stats установлен и запущен" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove stats collector service
|
||||||
|
remove_stats_collector() {
|
||||||
|
if [[ $EUID -ne 0 ]]; then
|
||||||
|
echo "Требуется root для удаления сервиса" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
systemctl stop gotelegram-stats.service 2>/dev/null
|
||||||
|
systemctl disable gotelegram-stats.service 2>/dev/null
|
||||||
|
rm -f /etc/systemd/system/gotelegram-stats.service
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
# Remove iptables rules
|
||||||
|
iptables -D INPUT -j GOTELEGRAM_STATS 2>/dev/null
|
||||||
|
iptables -F GOTELEGRAM_STATS 2>/dev/null
|
||||||
|
iptables -X GOTELEGRAM_STATS 2>/dev/null
|
||||||
|
|
||||||
|
# Clean up directories and files
|
||||||
|
rm -rf "$STATS_DIR" 2>/dev/null
|
||||||
|
rm -f "$HISTORY_FILE" "$USER_HISTORY_FILE" 2>/dev/null
|
||||||
|
|
||||||
|
echo "Сервис статистики удалён" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# Export functions for external use
|
||||||
|
export -f stats_init stats_collect stats_collect_users stats_active_users stats_read_current stats_calculate_rates
|
||||||
|
export -f show_traffic_stats format_bytes format_rate toggle_stats
|
||||||
|
export -f stats_cleanup_history stats_cleanup_user_history stats_should_cleanup stats_retention_cutoffs install_stats_collector remove_stats_collector
|
||||||
|
export -f json_get
|
||||||
366
lib/telemt.sh
Normal file
366
lib/telemt.sh
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# GoTelegram v2.5.0 — Управление telemt binary
|
||||||
|
# Скачивание, обновление, запуск, остановка через systemd
|
||||||
|
|
||||||
|
TELEMT_GITHUB="telemt/telemt"
|
||||||
|
TELEMT_RELEASE_API="https://api.github.com/repos/${TELEMT_GITHUB}/releases/latest"
|
||||||
|
TELEMT_USER="telemt"
|
||||||
|
TELEMT_GROUP="telemt"
|
||||||
|
|
||||||
|
# ── Получение последней версии ───────────────────────────────────────────────
|
||||||
|
get_latest_telemt_version() {
|
||||||
|
local resp
|
||||||
|
resp=$(curl -s --max-time 10 "$TELEMT_RELEASE_API" 2>/dev/null)
|
||||||
|
if [ $? -ne 0 ] || [ -z "$resp" ]; then
|
||||||
|
log_error "Не удалось получить информацию о релизах telemt"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "$resp" | jq -r '.tag_name // empty'
|
||||||
|
}
|
||||||
|
|
||||||
|
get_telemt_download_url() {
|
||||||
|
local arch
|
||||||
|
arch=$(get_arch)
|
||||||
|
local resp
|
||||||
|
resp=$(curl -s --max-time 10 "$TELEMT_RELEASE_API" 2>/dev/null)
|
||||||
|
if [ -z "$resp" ]; then return 1; fi
|
||||||
|
|
||||||
|
# URL format: telemt-x86_64-linux-gnu.tar.gz (arch BEFORE linux)
|
||||||
|
local arch_pattern
|
||||||
|
case "$arch" in
|
||||||
|
amd64) arch_pattern="(amd64|x86_64)" ;;
|
||||||
|
arm64) arch_pattern="(arm64|aarch64)" ;;
|
||||||
|
armv7) arch_pattern="(armv7|arm)" ;;
|
||||||
|
*) arch_pattern="${arch}" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "$resp" | jq -r ".assets[].browser_download_url" 2>/dev/null \
|
||||||
|
| grep -iE "$arch_pattern" \
|
||||||
|
| grep -i "linux" \
|
||||||
|
| grep -v "sha256" \
|
||||||
|
| grep "gnu" \
|
||||||
|
| head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Установленная версия ─────────────────────────────────────────────────────
|
||||||
|
get_installed_telemt_version() {
|
||||||
|
if [ -x "$TELEMT_BIN" ]; then
|
||||||
|
"$TELEMT_BIN" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
is_telemt_installed() {
|
||||||
|
[ -x "$TELEMT_BIN" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Скачивание и установка ───────────────────────────────────────────────────
|
||||||
|
download_telemt() {
|
||||||
|
local url
|
||||||
|
url=$(get_telemt_download_url)
|
||||||
|
if [ -z "$url" ]; then
|
||||||
|
log_error "Не найден бинарник telemt для архитектуры $(get_arch)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local tmp_file="/tmp/telemt_download_$$"
|
||||||
|
local extract_dir="/tmp/telemt_extract_$$"
|
||||||
|
log_info "Скачивание: $url"
|
||||||
|
|
||||||
|
if ! curl -L -s --max-time 120 -o "$tmp_file" "$url"; then
|
||||||
|
log_error "Ошибка скачивания telemt"
|
||||||
|
rm -f "$tmp_file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Проверяем что файл не пустой и не HTML
|
||||||
|
local file_size
|
||||||
|
file_size=$(stat -c%s "$tmp_file" 2>/dev/null || echo 0)
|
||||||
|
if [ "$file_size" -lt 1000 ]; then
|
||||||
|
log_error "Скачанный файл слишком маленький ($file_size байт) — возможна ошибка сети"
|
||||||
|
rm -f "$tmp_file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Определяем тип файла и распаковываем
|
||||||
|
local mime extracted=""
|
||||||
|
mime=$(file -b --mime-type "$tmp_file" 2>/dev/null)
|
||||||
|
rm -rf "$extract_dir"
|
||||||
|
mkdir -p "$extract_dir"
|
||||||
|
|
||||||
|
case "$mime" in
|
||||||
|
application/gzip|application/x-gzip)
|
||||||
|
tar xzf "$tmp_file" -C "$extract_dir" 2>/dev/null
|
||||||
|
extracted=$(find "$extract_dir" -name "telemt" -type f 2>/dev/null | head -1)
|
||||||
|
if [ -z "$extracted" ]; then
|
||||||
|
# Может быть просто gzip без tar
|
||||||
|
gunzip -c "$tmp_file" > "$extract_dir/telemt_bin" 2>/dev/null
|
||||||
|
extracted="$extract_dir/telemt_bin"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
application/x-tar)
|
||||||
|
tar xf "$tmp_file" -C "$extract_dir" 2>/dev/null
|
||||||
|
extracted=$(find "$extract_dir" -name "telemt" -type f 2>/dev/null | head -1)
|
||||||
|
;;
|
||||||
|
application/zip)
|
||||||
|
unzip -o "$tmp_file" -d "$extract_dir" 2>/dev/null
|
||||||
|
extracted=$(find "$extract_dir" -name "telemt" -type f 2>/dev/null | head -1)
|
||||||
|
;;
|
||||||
|
application/octet-stream|application/x-executable)
|
||||||
|
extracted="$tmp_file"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# Пробуем определить по содержимому
|
||||||
|
if file "$tmp_file" 2>/dev/null | grep -q "ELF"; then
|
||||||
|
extracted="$tmp_file"
|
||||||
|
else
|
||||||
|
# Пробуем как tar.gz
|
||||||
|
tar xzf "$tmp_file" -C "$extract_dir" 2>/dev/null
|
||||||
|
extracted=$(find "$extract_dir" -name "telemt" -type f 2>/dev/null | head -1)
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -z "$extracted" ] || [ ! -f "$extracted" ]; then
|
||||||
|
log_error "Не удалось извлечь бинарник telemt (mime: $mime)"
|
||||||
|
rm -f "$tmp_file"
|
||||||
|
rm -rf "$extract_dir"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Устанавливаем
|
||||||
|
cp "$extracted" "$TELEMT_BIN"
|
||||||
|
chmod 755 "$TELEMT_BIN"
|
||||||
|
rm -f "$tmp_file"
|
||||||
|
rm -rf "$extract_dir"
|
||||||
|
|
||||||
|
# Проверяем
|
||||||
|
if "$TELEMT_BIN" --version &>/dev/null; then
|
||||||
|
log_success "telemt $(get_installed_telemt_version) установлен в $TELEMT_BIN"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log_error "Бинарник telemt не запускается ($(file -b "$TELEMT_BIN" 2>/dev/null))"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Системный пользователь ───────────────────────────────────────────────────
|
||||||
|
create_telemt_user() {
|
||||||
|
if ! id "$TELEMT_USER" &>/dev/null; then
|
||||||
|
useradd -r -s /usr/sbin/nologin -d /etc/telemt "$TELEMT_USER" 2>/dev/null
|
||||||
|
log_dim "Создан системный пользователь: $TELEMT_USER"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Systemd сервис ───────────────────────────────────────────────────────────
|
||||||
|
install_telemt_service() {
|
||||||
|
local config_path="${1:-$TELEMT_CONFIG}"
|
||||||
|
|
||||||
|
cat > "/etc/systemd/system/${TELEMT_SERVICE}.service" << EOF
|
||||||
|
[Unit]
|
||||||
|
Description=goTelegram Pro MTProxy (telemt engine)
|
||||||
|
Documentation=https://github.com/telemt/telemt
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
ExecStart=$TELEMT_BIN run $config_path
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
LimitNOFILE=65535
|
||||||
|
|
||||||
|
# Безопасность
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ProtectSystem=full
|
||||||
|
ProtectHome=true
|
||||||
|
PrivateTmp=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
log_success "Systemd сервис $TELEMT_SERVICE создан"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Управление сервисом ──────────────────────────────────────────────────────
|
||||||
|
# start_telemt ensures telemt is running with the CURRENT on-disk config.
|
||||||
|
# If the service is already active we must restart (not plain start) — otherwise
|
||||||
|
# the running process keeps its old in-memory config and the freshly generated
|
||||||
|
# /etc/telemt/config.toml is silently ignored. This was the root cause of the
|
||||||
|
# "lite-mode key doesn't work after reinstall" bug: telemt had loaded the
|
||||||
|
# previous Pro config (tls_domain=legacy-upstream-domain) and was rejecting SNI=google.com
|
||||||
|
# clients with unknown_sni_action=Drop even though the on-disk config said
|
||||||
|
# tls_domain=google.com.
|
||||||
|
wait_telemt_ready() {
|
||||||
|
local timeout="${1:-90}"
|
||||||
|
local port elapsed=0
|
||||||
|
port=$(awk '
|
||||||
|
/^\[server\]/ { in_server=1; next }
|
||||||
|
/^\[/ && in_server { exit }
|
||||||
|
in_server && $1 == "port" {
|
||||||
|
sub(/^[^=]*=[[:space:]]*/, "")
|
||||||
|
gsub(/[[:space:]]/, "")
|
||||||
|
print
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
' "$TELEMT_CONFIG" 2>/dev/null)
|
||||||
|
[[ "$port" =~ ^[0-9]+$ ]] || port=443
|
||||||
|
|
||||||
|
while [ "$elapsed" -lt "$timeout" ]; do
|
||||||
|
if ! systemctl is-active --quiet "$TELEMT_SERVICE" 2>/dev/null; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ss -ltnp 2>/dev/null | grep -E ":${port}\b" | grep -q "telemt"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
elapsed=$((elapsed + 1))
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
start_telemt() {
|
||||||
|
if systemctl is-active --quiet "$TELEMT_SERVICE" 2>/dev/null; then
|
||||||
|
systemctl restart "$TELEMT_SERVICE" 2>/dev/null
|
||||||
|
else
|
||||||
|
systemctl start "$TELEMT_SERVICE" 2>/dev/null
|
||||||
|
fi
|
||||||
|
if wait_telemt_ready 90; then
|
||||||
|
log_success "telemt запущен"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log_error "telemt не запустился или не открыл порт"
|
||||||
|
journalctl -u "$TELEMT_SERVICE" --no-pager -n 10 2>/dev/null
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_telemt() {
|
||||||
|
if systemctl is-active --quiet "$TELEMT_SERVICE" 2>/dev/null; then
|
||||||
|
systemctl stop "$TELEMT_SERVICE"
|
||||||
|
log_success "telemt остановлен"
|
||||||
|
else
|
||||||
|
log_dim "telemt уже остановлен"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
restart_telemt() {
|
||||||
|
systemctl restart "$TELEMT_SERVICE" 2>/dev/null
|
||||||
|
if wait_telemt_ready 90; then
|
||||||
|
log_success "telemt перезапущен"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log_error "telemt не перезапустился или не открыл порт"
|
||||||
|
journalctl -u "$TELEMT_SERVICE" --no-pager -n 10 2>/dev/null
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
enable_telemt() {
|
||||||
|
systemctl enable "$TELEMT_SERVICE" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
telemt_status() {
|
||||||
|
if ! is_telemt_installed; then
|
||||||
|
echo "not_installed"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if systemctl is-active --quiet "$TELEMT_SERVICE" 2>/dev/null; then
|
||||||
|
echo "running"
|
||||||
|
elif systemctl is-enabled --quiet "$TELEMT_SERVICE" 2>/dev/null; then
|
||||||
|
echo "stopped"
|
||||||
|
else
|
||||||
|
echo "disabled"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
telemt_logs() {
|
||||||
|
local lines="${1:-40}"
|
||||||
|
journalctl -u "$TELEMT_SERVICE" --no-pager -n "$lines" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
telemt_uptime() {
|
||||||
|
local started
|
||||||
|
started=$(systemctl show "$TELEMT_SERVICE" --property=ActiveEnterTimestamp --value 2>/dev/null)
|
||||||
|
if [ -n "$started" ] && [ "$started" != "" ]; then
|
||||||
|
echo "$started"
|
||||||
|
else
|
||||||
|
echo "N/A"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Обновление ───────────────────────────────────────────────────────────────
|
||||||
|
check_telemt_update() {
|
||||||
|
local current latest
|
||||||
|
current=$(get_installed_telemt_version)
|
||||||
|
latest=$(get_latest_telemt_version)
|
||||||
|
|
||||||
|
if [ -z "$current" ] || [ -z "$latest" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$current" != "$latest" ]; then
|
||||||
|
echo "$latest"
|
||||||
|
return 0 # есть обновление
|
||||||
|
fi
|
||||||
|
return 1 # актуально
|
||||||
|
}
|
||||||
|
|
||||||
|
update_telemt() {
|
||||||
|
local latest
|
||||||
|
latest=$(check_telemt_update)
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
log_info "telemt уже последней версии ($(get_installed_telemt_version))"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Доступно обновление: $(get_installed_telemt_version) → $latest"
|
||||||
|
if ! confirm "Обновить telemt?"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
stop_telemt
|
||||||
|
if download_telemt; then
|
||||||
|
start_telemt
|
||||||
|
log_success "telemt обновлён до $latest"
|
||||||
|
else
|
||||||
|
start_telemt # запускаем старую версию обратно
|
||||||
|
log_error "Обновление не удалось"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Полная установка telemt ──────────────────────────────────────────────────
|
||||||
|
install_telemt_full() {
|
||||||
|
log_step "Установка telemt"
|
||||||
|
|
||||||
|
# Создаём директории
|
||||||
|
mkdir -p /etc/telemt
|
||||||
|
|
||||||
|
# Скачиваем бинарник
|
||||||
|
run_with_spinner "Скачивание telemt" download_telemt || return 1
|
||||||
|
|
||||||
|
# Устанавливаем systemd сервис
|
||||||
|
install_telemt_service
|
||||||
|
|
||||||
|
# Включаем автозапуск
|
||||||
|
enable_telemt
|
||||||
|
|
||||||
|
log_success "telemt готов к работе"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Удаление telemt ──────────────────────────────────────────────────────────
|
||||||
|
remove_telemt() {
|
||||||
|
stop_telemt
|
||||||
|
systemctl disable "$TELEMT_SERVICE" 2>/dev/null
|
||||||
|
rm -f "/etc/systemd/system/${TELEMT_SERVICE}.service"
|
||||||
|
systemctl daemon-reload
|
||||||
|
rm -f "$TELEMT_BIN"
|
||||||
|
rm -rf /etc/telemt
|
||||||
|
log_success "telemt полностью удалён"
|
||||||
|
}
|
||||||
501
lib/telemt_config.sh
Normal file
501
lib/telemt_config.sh
Normal file
@@ -0,0 +1,501 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# GoTelegram v2.5.0 — Генерация TOML конфигурации для telemt
|
||||||
|
|
||||||
|
# ── Популярные домены (не заблокированные в РФ) ──────────────────────────────
|
||||||
|
QUICK_DOMAINS=(
|
||||||
|
"google.com"
|
||||||
|
"microsoft.com"
|
||||||
|
"cloudflare.com"
|
||||||
|
"apple.com"
|
||||||
|
"amazon.com"
|
||||||
|
"github.com"
|
||||||
|
"stackoverflow.com"
|
||||||
|
"medium.com"
|
||||||
|
"wikipedia.org"
|
||||||
|
"coursera.org"
|
||||||
|
"udemy.com"
|
||||||
|
"habr.com"
|
||||||
|
"stepik.org"
|
||||||
|
"duolingo.com"
|
||||||
|
"khanacademy.org"
|
||||||
|
"bbc.com"
|
||||||
|
"reuters.com"
|
||||||
|
"nytimes.com"
|
||||||
|
"ted.com"
|
||||||
|
"zoom.us"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Генерация TOML конфига (telemt v3 формат) ───────────────────────────────
|
||||||
|
generate_telemt_toml() {
|
||||||
|
local secret="$1"
|
||||||
|
local port="${2:-443}"
|
||||||
|
local mask_mode="${3:-lite}" # lite | pro
|
||||||
|
local mask_domain="${4:-google.com}"
|
||||||
|
local mask_port="${5:-443}"
|
||||||
|
local output="${6:-$TELEMT_CONFIG}"
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$output")"
|
||||||
|
|
||||||
|
# DNS override для pro: домен резолвится в 127.0.0.1
|
||||||
|
# чтобы mask-трафик шёл на локальный nginx, а не в интернет
|
||||||
|
local dns_line=""
|
||||||
|
if [ "$mask_mode" = "pro" ]; then
|
||||||
|
dns_line="dns_overrides = [\"${mask_domain}:${mask_port}:127.0.0.1\"]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "$output" << EOTOML
|
||||||
|
# GoTelegram v${GOTELEGRAM_VERSION} — telemt v3 configuration
|
||||||
|
# Сгенерировано: $(date -Iseconds)
|
||||||
|
# Режим: ${mask_mode}
|
||||||
|
|
||||||
|
[general]
|
||||||
|
use_middle_proxy = true
|
||||||
|
log_level = "normal"
|
||||||
|
|
||||||
|
[general.modes]
|
||||||
|
classic = false
|
||||||
|
secure = false
|
||||||
|
tls = true
|
||||||
|
|
||||||
|
[general.links]
|
||||||
|
show = "*"
|
||||||
|
public_port = ${port}
|
||||||
|
|
||||||
|
[server]
|
||||||
|
port = ${port}
|
||||||
|
listen_addr_ipv4 = "0.0.0.0"
|
||||||
|
metrics_listen = "127.0.0.1:9090"
|
||||||
|
metrics_whitelist = ["127.0.0.1/32", "::1/128"]
|
||||||
|
|
||||||
|
[server.api]
|
||||||
|
enabled = true
|
||||||
|
listen = "127.0.0.1:9091"
|
||||||
|
whitelist = ["127.0.0.1/32", "::1/128"]
|
||||||
|
minimal_runtime_enabled = false
|
||||||
|
minimal_runtime_cache_ttl_ms = 1000
|
||||||
|
|
||||||
|
[censorship]
|
||||||
|
tls_domain = "${mask_domain}"
|
||||||
|
mask = true
|
||||||
|
mask_port = ${mask_port}
|
||||||
|
tls_emulation = $([ "$mask_mode" = "pro" ] && echo "false" || echo "true")
|
||||||
|
unknown_sni_action = "mask"
|
||||||
|
|
||||||
|
[access.users]
|
||||||
|
main = "${secret}"
|
||||||
|
|
||||||
|
[network]
|
||||||
|
${dns_line}
|
||||||
|
EOTOML
|
||||||
|
|
||||||
|
chmod 600 "$output"
|
||||||
|
log_success "Конфиг telemt записан: $output"
|
||||||
|
log_dim "Режим: $mask_mode, домен: $mask_domain, порт mask: $mask_port"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Добавление дополнительного секрета ───────────────────────────────────────
|
||||||
|
add_secret_to_config() {
|
||||||
|
local name="$1"
|
||||||
|
local secret="$2"
|
||||||
|
local config="${3:-$TELEMT_CONFIG}"
|
||||||
|
|
||||||
|
if [ ! -f "$config" ]; then
|
||||||
|
log_error "Конфиг не найден: $config"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# telemt v3: добавляем ключ в секцию [access.users]
|
||||||
|
# Формат: name = "secret" под блоком [access.users]
|
||||||
|
if grep -q '\[access\.users\]' "$config"; then
|
||||||
|
sed -i "/\[access\.users\]/a ${name} = \"${secret}\"" "$config"
|
||||||
|
else
|
||||||
|
cat >> "$config" << EOSECRET
|
||||||
|
|
||||||
|
[access.users]
|
||||||
|
${name} = "${secret}"
|
||||||
|
EOSECRET
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Добавлен секрет: $name"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Чтение текущего конфига (telemt v3 формат) ──────────────────────────────
|
||||||
|
get_config_value() {
|
||||||
|
local key="$1"
|
||||||
|
local config="${2:-$TELEMT_CONFIG}"
|
||||||
|
|
||||||
|
if [ ! -f "$config" ]; then return 1; fi
|
||||||
|
|
||||||
|
case "$key" in
|
||||||
|
secret)
|
||||||
|
# [access.users] main = "..."
|
||||||
|
awk '
|
||||||
|
/^\[access\.users\]/ { in_users=1; next }
|
||||||
|
/^\[/ && in_users { exit }
|
||||||
|
in_users && /^[[:space:]]*[^#[:space:]][^=]*=/ {
|
||||||
|
user_key=$1
|
||||||
|
gsub(/"/, "", user_key)
|
||||||
|
if (user_key != "main") next
|
||||||
|
|
||||||
|
value=$0
|
||||||
|
sub(/^[^=]*=[[:space:]]*/, "", value)
|
||||||
|
sub(/^"/, "", value)
|
||||||
|
sub(/".*$/, "", value)
|
||||||
|
gsub(/[[:space:]]/, "", value)
|
||||||
|
if (value != "") {
|
||||||
|
print value
|
||||||
|
found=1
|
||||||
|
}
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
END { exit found ? 0 : 1 }
|
||||||
|
' "$config"
|
||||||
|
;;
|
||||||
|
port)
|
||||||
|
# [server] port = 443
|
||||||
|
awk '
|
||||||
|
/^\[server\]/ { in_server=1; next }
|
||||||
|
/^\[/ && in_server { exit }
|
||||||
|
in_server && $1 == "port" {
|
||||||
|
sub(/^[^=]*=[[:space:]]*/, "")
|
||||||
|
gsub(/[[:space:]]/, "")
|
||||||
|
print
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
' "$config"
|
||||||
|
;;
|
||||||
|
mask_host|tls_domain)
|
||||||
|
# [censorship] tls_domain = "..."
|
||||||
|
awk '
|
||||||
|
/^\[censorship\]/ { in_cens=1; next }
|
||||||
|
/^\[/ && in_cens { exit }
|
||||||
|
in_cens && $1 == "tls_domain" {
|
||||||
|
sub(/^[^=]*=[[:space:]]*"/, "")
|
||||||
|
sub(/".*$/, "")
|
||||||
|
print
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
' "$config"
|
||||||
|
;;
|
||||||
|
mask_port)
|
||||||
|
awk '
|
||||||
|
/^\[censorship\]/ { in_cens=1; next }
|
||||||
|
/^\[/ && in_cens { exit }
|
||||||
|
in_cens && $1 == "mask_port" {
|
||||||
|
sub(/^[^=]*=[[:space:]]*/, "")
|
||||||
|
gsub(/[[:space:]]/, "")
|
||||||
|
print
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
' "$config"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
grep "$key" "$config" | head -1 | sed 's/^[^=]*=[[:space:]]*//; s/^"//; s/"$//' | tr -d ' '
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
get_telemt_users_block() {
|
||||||
|
local config="${1:-$TELEMT_CONFIG}"
|
||||||
|
[ -f "$config" ] || return 1
|
||||||
|
awk '
|
||||||
|
/^\[access\.users\]/ { in_users=1; next }
|
||||||
|
/^\[/ && in_users { exit }
|
||||||
|
in_users && /^[[:space:]]*[^#[:space:]][^=]*=/ { print }
|
||||||
|
' "$config"
|
||||||
|
}
|
||||||
|
|
||||||
|
first_telemt_user_secret() {
|
||||||
|
local config="${1:-$TELEMT_CONFIG}"
|
||||||
|
get_telemt_users_block "$config" | head -1 | sed 's/^[^=]*=[[:space:]]*//; s/^"//; s/".*$//' | tr -d ' '
|
||||||
|
}
|
||||||
|
|
||||||
|
telemt_users_block_has_main() {
|
||||||
|
local users_block="$1"
|
||||||
|
printf '%s\n' "$users_block" | awk -F= '
|
||||||
|
/^[[:space:]]*#/ || ! /=/ { next }
|
||||||
|
{
|
||||||
|
key=$1
|
||||||
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
|
||||||
|
if (key ~ /^".*"$/ || key ~ /^\047.*\047$/) {
|
||||||
|
key=substr(key, 2, length(key) - 2)
|
||||||
|
}
|
||||||
|
if (key == "main") {
|
||||||
|
found=1
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
END { exit found ? 0 : 1 }
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
replace_telemt_users_block() {
|
||||||
|
local users_block="$1"
|
||||||
|
local config="${2:-$TELEMT_CONFIG}"
|
||||||
|
[ -f "$config" ] || return 1
|
||||||
|
[ -n "$users_block" ] || return 0
|
||||||
|
|
||||||
|
local tmp
|
||||||
|
tmp=$(mktemp) || return 1
|
||||||
|
awk -v users="$users_block" '
|
||||||
|
BEGIN { split(users, lines, "\n") }
|
||||||
|
/^\[access\.users\]/ {
|
||||||
|
found=1
|
||||||
|
print
|
||||||
|
for (i = 1; i in lines; i++) {
|
||||||
|
if (lines[i] != "") print lines[i]
|
||||||
|
}
|
||||||
|
in_users=1
|
||||||
|
next
|
||||||
|
}
|
||||||
|
/^\[/ && in_users { in_users=0 }
|
||||||
|
in_users { next }
|
||||||
|
{ print }
|
||||||
|
END {
|
||||||
|
if (!found) {
|
||||||
|
print ""
|
||||||
|
print "[access.users]"
|
||||||
|
for (i = 1; i in lines; i++) {
|
||||||
|
if (lines[i] != "") print lines[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' "$config" > "$tmp" && mv "$tmp" "$config"
|
||||||
|
chmod 600 "$config"
|
||||||
|
}
|
||||||
|
|
||||||
|
toml_bool_value() {
|
||||||
|
local table="$1"
|
||||||
|
local key="$2"
|
||||||
|
local config="${3:-$TELEMT_CONFIG}"
|
||||||
|
awk -v table="$table" -v key="$key" '
|
||||||
|
$0 == "[" table "]" { in_table=1; next }
|
||||||
|
/^\[/ && in_table { exit }
|
||||||
|
in_table && $1 == key {
|
||||||
|
sub(/^[^=]*=[[:space:]]*/, "")
|
||||||
|
gsub(/[[:space:]]/, "")
|
||||||
|
print
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
' "$config"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Валидация конфига ────────────────────────────────────────────────────────
|
||||||
|
validate_telemt_config() {
|
||||||
|
local config="${1:-$TELEMT_CONFIG}"
|
||||||
|
|
||||||
|
if [ ! -f "$config" ]; then
|
||||||
|
log_error "Конфиг не найден: $config"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Проверяем обязательные поля
|
||||||
|
local secret port host
|
||||||
|
secret=$(get_config_value secret "$config")
|
||||||
|
port=$(get_config_value port "$config")
|
||||||
|
host=$(get_config_value mask_host "$config")
|
||||||
|
|
||||||
|
local errors=0
|
||||||
|
|
||||||
|
if [ -z "$secret" ]; then
|
||||||
|
log_error "Не задан secret"
|
||||||
|
((errors++))
|
||||||
|
elif [ ${#secret} -lt 32 ]; then
|
||||||
|
log_warning "Secret слишком короткий (${#secret} символов, рекомендуется 32+)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$port" ]; then
|
||||||
|
log_error "Не задан порт (bind_to)"
|
||||||
|
((errors++))
|
||||||
|
elif [ "$port" -lt 1 ] || [ "$port" -gt 65535 ] 2>/dev/null; then
|
||||||
|
log_error "Порт вне диапазона: $port"
|
||||||
|
((errors++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$host" ]; then
|
||||||
|
log_error "Не задан маскировочный хост (censorship.tls_domain)"
|
||||||
|
((errors++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $errors -gt 0 ]; then
|
||||||
|
log_error "Найдено ошибок: $errors"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Конфиг валиден"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Выбор домена (интерактивный) ─────────────────────────────────────────────
|
||||||
|
select_quick_domain() {
|
||||||
|
echo "" >&2
|
||||||
|
echo -e " ${BOLD}${WHITE}🌐 Выберите домен для маскировки (Fake TLS):${NC}" >&2
|
||||||
|
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}" >&2
|
||||||
|
|
||||||
|
local i=1
|
||||||
|
local row=""
|
||||||
|
for d in "${QUICK_DOMAINS[@]}"; do
|
||||||
|
printf " ${CYAN}%2d)${NC} %-25s" "$i" "$d" >&2
|
||||||
|
if (( i % 2 == 0 )); then
|
||||||
|
echo "" >&2
|
||||||
|
fi
|
||||||
|
((i++))
|
||||||
|
done
|
||||||
|
if (( (i-1) % 2 != 0 )); then echo "" >&2; fi
|
||||||
|
|
||||||
|
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}" >&2
|
||||||
|
echo -ne " ${WHITE}Выбор (1-${#QUICK_DOMAINS[@]}):${NC} " >&2
|
||||||
|
read -r choice
|
||||||
|
|
||||||
|
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le ${#QUICK_DOMAINS[@]} ]; then
|
||||||
|
echo "${QUICK_DOMAINS[$((choice-1))]}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_error "Неверный выбор"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Выбор порта (интерактивный) ──────────────────────────────────────────────
|
||||||
|
select_port() {
|
||||||
|
echo "" >&2
|
||||||
|
echo -e " ${BOLD}${WHITE}🔌 Выберите порт:${NC}" >&2
|
||||||
|
|
||||||
|
# Проверяем стандартные порты
|
||||||
|
local busy_443 busy_8443
|
||||||
|
busy_443=$(check_port 443)
|
||||||
|
busy_8443=$(check_port 8443)
|
||||||
|
|
||||||
|
local label_443="443 (рекомендуется)"
|
||||||
|
local label_8443="8443"
|
||||||
|
[ -n "$busy_443" ] && label_443="443 ⚠️ занят"
|
||||||
|
[ -n "$busy_8443" ] && label_8443="8443 ⚠️ занят"
|
||||||
|
|
||||||
|
echo -e " ${CYAN}1)${NC} $label_443" >&2
|
||||||
|
echo -e " ${CYAN}2)${NC} $label_8443" >&2
|
||||||
|
echo -e " ${CYAN}3)${NC} Свой порт" >&2
|
||||||
|
|
||||||
|
if [ -n "$busy_443" ]; then
|
||||||
|
echo -e " ${DIM} ⚠ Порт 443 занят: $(echo "$busy_443" | head -c 60)${NC}" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -ne " ${WHITE}Выбор:${NC} " >&2
|
||||||
|
read -r choice
|
||||||
|
|
||||||
|
case "$choice" in
|
||||||
|
1) echo "443" ;;
|
||||||
|
2) echo "8443" ;;
|
||||||
|
3)
|
||||||
|
echo -ne " Введите порт (1-65535): " >&2
|
||||||
|
read -r custom_port
|
||||||
|
if [[ "$custom_port" =~ ^[0-9]+$ ]] && [ "$custom_port" -ge 1 ] && [ "$custom_port" -le 65535 ]; then
|
||||||
|
echo "$custom_port"
|
||||||
|
else
|
||||||
|
log_error "Неверный порт"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*) echo "443" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Генерация ссылки tg://proxy ──────────────────────────────────────────────
|
||||||
|
generate_proxy_link() {
|
||||||
|
local server="${1:-$(get_server_ip)}"
|
||||||
|
local port="${2:-443}"
|
||||||
|
local secret="$3"
|
||||||
|
local mask_host="${4:-}"
|
||||||
|
|
||||||
|
# Если указан mask_host (fake-TLS), формируем ee-секрет
|
||||||
|
if [ -n "$mask_host" ]; then
|
||||||
|
local domain_hex
|
||||||
|
domain_hex=$(printf '%s' "$mask_host" | xxd -p | tr -d '\n')
|
||||||
|
secret="ee${secret}${domain_hex}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "tg://proxy?server=${server}&port=${port}&secret=${secret}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Вывод информации о прокси ────────────────────────────────────────────────
|
||||||
|
show_proxy_info() {
|
||||||
|
local config="${1:-$TELEMT_CONFIG}"
|
||||||
|
local secret port mask_host ip link status
|
||||||
|
|
||||||
|
secret=$(get_config_value secret "$config")
|
||||||
|
port=$(get_config_value port "$config")
|
||||||
|
mask_host=$(get_config_value mask_host "$config")
|
||||||
|
ip=$(get_server_ip)
|
||||||
|
status=$(telemt_status)
|
||||||
|
|
||||||
|
local mode domain
|
||||||
|
mode=$(config_get mode 2>/dev/null || echo "lite")
|
||||||
|
domain=$(config_get domain 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
# Генерация ссылки: оба режима используют ee-секрет с mask_host
|
||||||
|
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
|
||||||
|
link=$(generate_proxy_link "$domain" "$port" "$secret" "$domain")
|
||||||
|
else
|
||||||
|
link=$(generate_proxy_link "$ip" "$port" "$secret" "$mask_host")
|
||||||
|
fi
|
||||||
|
|
||||||
|
local status_icon status_text
|
||||||
|
case "$status" in
|
||||||
|
running) status_icon="✅"; status_text="Работает" ;;
|
||||||
|
stopped) status_icon="⏸️"; status_text="Остановлен" ;;
|
||||||
|
*) status_icon="❌"; status_text="Не установлен" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BOLD}${WHITE}${status_icon} Статус прокси: ${status_text}${NC}"
|
||||||
|
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}"
|
||||||
|
echo -e " ${WHITE}Ядро:${NC} telemt (Rust)"
|
||||||
|
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
|
||||||
|
echo -e " ${WHITE}Домен:${NC} ${CYAN}${domain}${NC}"
|
||||||
|
else
|
||||||
|
echo -e " ${WHITE}IP:${NC} ${CYAN}${ip}${NC}"
|
||||||
|
fi
|
||||||
|
echo -e " ${WHITE}Порт:${NC} ${CYAN}${port}${NC}"
|
||||||
|
echo -e " ${WHITE}Режим:${NC} ${CYAN}${mode}${NC}"
|
||||||
|
echo -e " ${WHITE}Маскировка:${NC} ${CYAN}${mask_host}${NC}"
|
||||||
|
echo -e " ${WHITE}Secret:${NC} ${CYAN}${secret:0:16}...${NC}"
|
||||||
|
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}"
|
||||||
|
echo -e " ${WHITE}Ссылка:${NC}"
|
||||||
|
echo -e " ${GREEN}${link}${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# QR если доступен
|
||||||
|
if command -v qrencode &>/dev/null; then
|
||||||
|
qrencode -t UTF8 -m 2 "$link" 2>/dev/null
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Вывод информации о прокси (Pro-режим) ──────────────────────────────────
|
||||||
|
# В pro-режиме ссылка содержит домен (не IP) и fake-TLS секрет (ee...)
|
||||||
|
show_proxy_info_pro() {
|
||||||
|
local domain="$1"
|
||||||
|
local faketls_secret="$2"
|
||||||
|
|
||||||
|
local link="tg://proxy?server=${domain}&port=443&secret=${faketls_secret}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e " ${BOLD}${WHITE}✅ Pro-прокси настроен${NC}"
|
||||||
|
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||||||
|
echo -e " ${WHITE}Ядро:${NC} telemt (Rust)"
|
||||||
|
echo -e " ${WHITE}Домен:${NC} ${CYAN}${domain}${NC}"
|
||||||
|
echo -e " ${WHITE}Порт:${NC} ${CYAN}443${NC} (внешний, telemt)"
|
||||||
|
echo -e " ${WHITE}Режим:${NC} ${MAGENTA}Pro (fake-TLS)${NC}"
|
||||||
|
echo -e " ${WHITE}nginx:${NC} ${CYAN}127.0.0.1:8443${NC} (внутренний)"
|
||||||
|
echo -e " ${WHITE}Secret:${NC} ${CYAN}${faketls_secret:0:20}...${NC}"
|
||||||
|
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||||||
|
echo -e " ${WHITE}Ссылка для Telegram:${NC}"
|
||||||
|
echo -e " ${GREEN}${link}${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${DIM}Провайдер видит: HTTPS-трафик к ${domain}:443${NC}"
|
||||||
|
echo -e " ${DIM}Telegram-клиент маскирует соединение под TLS${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# QR если доступен
|
||||||
|
if command -v qrencode &>/dev/null; then
|
||||||
|
qrencode -t UTF8 -m 2 "$link" 2>/dev/null
|
||||||
|
fi
|
||||||
|
}
|
||||||
595
lib/templates_catalog.sh
Normal file
595
lib/templates_catalog.sh
Normal file
@@ -0,0 +1,595 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# GoTelegram v2.5.0 — website templates catalog
|
||||||
|
# Pick from ~1800 templates, preview links, git sparse-checkout downloads,
|
||||||
|
# + custom git URL templates (user-supplied public repos)
|
||||||
|
|
||||||
|
CATALOG_FILE="$(dirname "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")")/templates_catalog.json"
|
||||||
|
TEMPLATES_CACHE="/tmp/gotelegram_templates"
|
||||||
|
|
||||||
|
# Custom git template limits
|
||||||
|
CUSTOM_GIT_MAX_SIZE_MB=100
|
||||||
|
CUSTOM_GIT_CLONE_TIMEOUT=90
|
||||||
|
|
||||||
|
# ── Catalog loading ────────────────────────────────────────────────────
|
||||||
|
load_catalog() {
|
||||||
|
if [ ! -f "$CATALOG_FILE" ]; then
|
||||||
|
if type tf &>/dev/null; then
|
||||||
|
log_error "$(tf templates_catalog_not_found "$CATALOG_FILE")"
|
||||||
|
else
|
||||||
|
log_error "Templates catalog not found: $CATALOG_FILE"
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Categories ─────────────────────────────────────────────────────────
|
||||||
|
get_categories() {
|
||||||
|
jq -r '.categories[] | "\(.id)|\(.name)|\(.icon)|\(.templates | length)"' "$CATALOG_FILE" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
get_category_name() {
|
||||||
|
local cat_id="$1"
|
||||||
|
jq -r ".categories[] | select(.id == \"$cat_id\") | .name" "$CATALOG_FILE" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Templates in a category ────────────────────────────────────────────
|
||||||
|
get_templates_by_category() {
|
||||||
|
local cat_id="$1"
|
||||||
|
jq -r ".categories[] | select(.id == \"$cat_id\") | .templates[] | \"\(.id)|\(.name)|\(.source)|\(.preview_url)\"" "$CATALOG_FILE" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Template info ──────────────────────────────────────────────────────
|
||||||
|
get_template_info() {
|
||||||
|
local tpl_id="$1"
|
||||||
|
jq ".categories[].templates[] | select(.id == \"$tpl_id\")" "$CATALOG_FILE" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
get_template_field() {
|
||||||
|
local tpl_id="$1"
|
||||||
|
local field="$2"
|
||||||
|
jq -r ".categories[].templates[] | select(.id == \"$tpl_id\") | .$field" "$CATALOG_FILE" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Interactive category picker (returns category id or special __custom_git__/__random__) ──
|
||||||
|
select_category() {
|
||||||
|
load_catalog || return 1
|
||||||
|
|
||||||
|
echo "" >&2
|
||||||
|
echo -e " ${BOLD}${WHITE}$(t templates_categories)${NC}" >&2
|
||||||
|
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}" >&2
|
||||||
|
|
||||||
|
# First item: custom git URL template
|
||||||
|
printf " ${CYAN}%2d)${NC} ${GREEN}%s${NC}\n" 1 "$(t templates_custom_git)" >&2
|
||||||
|
|
||||||
|
local cats=()
|
||||||
|
local i=2
|
||||||
|
while IFS='|' read -r id name icon count; do
|
||||||
|
[ "$count" -eq 0 ] && continue
|
||||||
|
local emoji
|
||||||
|
case "$icon" in
|
||||||
|
briefcase) emoji="🏢" ;;
|
||||||
|
shopping-cart) emoji="🛒" ;;
|
||||||
|
heart) emoji="🏥" ;;
|
||||||
|
book) emoji="🎓" ;;
|
||||||
|
palette) emoji="📸" ;;
|
||||||
|
home) emoji="🏠" ;;
|
||||||
|
utensils) emoji="🍕" ;;
|
||||||
|
rocket) emoji="🎨" ;;
|
||||||
|
chart-bar) emoji="🔧" ;;
|
||||||
|
*) emoji="📄" ;;
|
||||||
|
esac
|
||||||
|
printf " ${CYAN}%2d)${NC} ${emoji} %-30s ${DIM}$(tf templates_count_fmt "$count")${NC}\n" "$i" "$name" >&2
|
||||||
|
cats+=("$id")
|
||||||
|
((i++))
|
||||||
|
done < <(get_categories)
|
||||||
|
|
||||||
|
printf " ${CYAN}%2d)${NC} %s\n" "$i" "$(t templates_random)" >&2
|
||||||
|
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}" >&2
|
||||||
|
echo -ne " ${WHITE}$(t choose):${NC} " >&2
|
||||||
|
read -r choice
|
||||||
|
|
||||||
|
if ! [[ "$choice" =~ ^[0-9]+$ ]]; then
|
||||||
|
log_error "$(t invalid_choice)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Custom git URL
|
||||||
|
if [ "$choice" -eq 1 ]; then
|
||||||
|
echo "__custom_git__"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Random
|
||||||
|
if [ "$choice" -eq "$i" ]; then
|
||||||
|
local random_cat="${cats[$((RANDOM % ${#cats[@]}))]}"
|
||||||
|
echo "$random_cat"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Regular category (offset by 1 because item 1 is custom git)
|
||||||
|
if [ "$choice" -ge 2 ] && [ "$choice" -lt "$i" ]; then
|
||||||
|
echo "${cats[$((choice-2))]}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_error "$(t invalid_choice)"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Interactive template picker ────────────────────────────────────────
|
||||||
|
select_template() {
|
||||||
|
local cat_id="$1"
|
||||||
|
local cat_name
|
||||||
|
cat_name=$(get_category_name "$cat_id")
|
||||||
|
|
||||||
|
echo "" >&2
|
||||||
|
echo -e " ${BOLD}${WHITE}$(tf templates_list "$cat_name")${NC}" >&2
|
||||||
|
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||||
|
|
||||||
|
local tpls=()
|
||||||
|
local i=1
|
||||||
|
while IFS='|' read -r id name source preview; do
|
||||||
|
printf " ${CYAN}%2d)${NC} %-30s ${DIM}[%s]${NC}\n" "$i" "$name" "$source" >&2
|
||||||
|
tpls+=("$id")
|
||||||
|
((i++))
|
||||||
|
done < <(get_templates_by_category "$cat_id")
|
||||||
|
|
||||||
|
if [ ${#tpls[@]} -eq 0 ]; then
|
||||||
|
log_info "$(t templates_cat_empty)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||||
|
echo -ne " ${WHITE}$(t choose) (1-$((i-1))):${NC} " >&2
|
||||||
|
read -r choice
|
||||||
|
|
||||||
|
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -lt "$i" ]; then
|
||||||
|
local selected_id="${tpls[$((choice-1))]}"
|
||||||
|
|
||||||
|
# Show preview
|
||||||
|
show_template_preview "$selected_id" || return 1
|
||||||
|
|
||||||
|
echo "$selected_id"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_error "$(t invalid_choice)"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Template preview ───────────────────────────────────────────────────
|
||||||
|
show_template_preview() {
|
||||||
|
local tpl_id="$1"
|
||||||
|
local info
|
||||||
|
info=$(get_template_info "$tpl_id")
|
||||||
|
|
||||||
|
local name source preview_url repo_url description
|
||||||
|
name=$(echo "$info" | jq -r '.name')
|
||||||
|
source=$(echo "$info" | jq -r '.source')
|
||||||
|
preview_url=$(echo "$info" | jq -r '.preview_url // empty')
|
||||||
|
repo_url=$(echo "$info" | jq -r '.repo_url // empty')
|
||||||
|
description=$(echo "$info" | jq -r '.description // "—"')
|
||||||
|
|
||||||
|
echo "" >&2
|
||||||
|
echo -e " ${BOLD}${WHITE}$(t templates_preview_title)${NC}" >&2
|
||||||
|
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}" >&2
|
||||||
|
echo -e " ${WHITE}$(t templates_name)${NC} $name" >&2
|
||||||
|
echo -e " ${WHITE}$(t templates_source)${NC} $source" >&2
|
||||||
|
echo -e " ${WHITE}$(t templates_description)${NC} $description" >&2
|
||||||
|
|
||||||
|
if [ -n "$preview_url" ]; then
|
||||||
|
echo "" >&2
|
||||||
|
echo -e " ${GREEN}$(t templates_preview)${NC} ${CYAN}${preview_url}${NC}" >&2
|
||||||
|
echo -e " ${DIM}$(t templates_preview_hint)${NC}" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$repo_url" ]; then
|
||||||
|
echo -e " ${DIM}$(t templates_repo) ${repo_url}${NC}" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Thanks
|
||||||
|
echo "" >&2
|
||||||
|
echo -e " ${MAGENTA}$(tf templates_thanks "$source")${NC}" >&2
|
||||||
|
|
||||||
|
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}" >&2
|
||||||
|
echo "" >&2
|
||||||
|
|
||||||
|
if ! confirm "$(t templates_install_this)"; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Template download (from catalog) ───────────────────────────────────
|
||||||
|
download_template() {
|
||||||
|
local tpl_id="$1"
|
||||||
|
local output_dir="${2:-$TEMPLATES_CACHE}"
|
||||||
|
local info
|
||||||
|
info=$(get_template_info "$tpl_id")
|
||||||
|
|
||||||
|
local repo_url sparse_path source name
|
||||||
|
repo_url=$(echo "$info" | jq -r '.repo_url')
|
||||||
|
sparse_path=$(echo "$info" | jq -r '.sparse_path')
|
||||||
|
source=$(echo "$info" | jq -r '.source')
|
||||||
|
name=$(echo "$info" | jq -r '.name')
|
||||||
|
|
||||||
|
local clone_dir="$output_dir/${tpl_id}"
|
||||||
|
rm -rf "$clone_dir"
|
||||||
|
mkdir -p "$clone_dir"
|
||||||
|
|
||||||
|
log_info "$(tf templates_downloading "$name")"
|
||||||
|
|
||||||
|
# HTML5 UP — one repo with folders
|
||||||
|
if [ "$source" = "html5up" ]; then
|
||||||
|
local tmp_clone="/tmp/html5up_clone_$$"
|
||||||
|
rm -rf "$tmp_clone"
|
||||||
|
|
||||||
|
# Sparse checkout
|
||||||
|
git clone --depth 1 --filter=blob:none --sparse "$repo_url" "$tmp_clone" 2>/dev/null
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
# Fallback: full clone
|
||||||
|
git clone --depth 1 "$repo_url" "$tmp_clone" 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d "$tmp_clone" ]; then
|
||||||
|
cd "$tmp_clone" && git sparse-checkout set "$sparse_path" 2>/dev/null
|
||||||
|
if [ -d "$tmp_clone/$sparse_path" ]; then
|
||||||
|
cp -r "$tmp_clone/$sparse_path"/* "$clone_dir/"
|
||||||
|
fi
|
||||||
|
cd - >/dev/null
|
||||||
|
fi
|
||||||
|
rm -rf "$tmp_clone"
|
||||||
|
|
||||||
|
# learning-zone — one big repo
|
||||||
|
elif [ "$source" = "learning-zone" ]; then
|
||||||
|
local tmp_clone="/tmp/lz_clone_$$"
|
||||||
|
rm -rf "$tmp_clone"
|
||||||
|
|
||||||
|
git clone --depth 1 --filter=blob:none --sparse "$repo_url" "$tmp_clone" 2>/dev/null
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
git clone --depth 1 "$repo_url" "$tmp_clone" 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d "$tmp_clone" ]; then
|
||||||
|
cd "$tmp_clone" && git sparse-checkout set "$sparse_path" 2>/dev/null
|
||||||
|
if [ -d "$tmp_clone/$sparse_path" ]; then
|
||||||
|
cp -r "$tmp_clone/$sparse_path"/* "$clone_dir/"
|
||||||
|
fi
|
||||||
|
cd - >/dev/null
|
||||||
|
fi
|
||||||
|
rm -rf "$tmp_clone"
|
||||||
|
|
||||||
|
# StartBootstrap — each template in its own repo
|
||||||
|
elif [ "$source" = "startbootstrap" ]; then
|
||||||
|
local sb_tmp="/tmp/sb_clone_$$"
|
||||||
|
rm -rf "$sb_tmp"
|
||||||
|
git clone --depth 1 "$repo_url" "$sb_tmp" 2>/dev/null
|
||||||
|
if [ -d "$sb_tmp" ]; then
|
||||||
|
rm -rf "$sb_tmp/.git"
|
||||||
|
# StartBootstrap stores production files in dist/
|
||||||
|
if [ -f "$sb_tmp/dist/index.html" ]; then
|
||||||
|
cp -r "$sb_tmp/dist/"* "$clone_dir/"
|
||||||
|
elif [ -f "$sb_tmp/index.html" ]; then
|
||||||
|
cp -r "$sb_tmp/"* "$clone_dir/"
|
||||||
|
else
|
||||||
|
local found_index
|
||||||
|
found_index=$(find "$sb_tmp" -name "index.html" -type f 2>/dev/null | head -1)
|
||||||
|
if [ -n "$found_index" ]; then
|
||||||
|
local found_dir
|
||||||
|
found_dir=$(dirname "$found_index")
|
||||||
|
cp -r "$found_dir/"* "$clone_dir/"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
rm -rf "$sb_tmp"
|
||||||
|
|
||||||
|
# ThemeWagon / ColorlibHQ — each template in its own repo
|
||||||
|
elif [ "$source" = "themewagon" ] || [ "$source" = "colorlib" ]; then
|
||||||
|
local tw_tmp="/tmp/tw_clone_$$"
|
||||||
|
rm -rf "$tw_tmp"
|
||||||
|
git clone --depth 1 "$repo_url" "$tw_tmp" 2>/dev/null
|
||||||
|
if [ -d "$tw_tmp" ]; then
|
||||||
|
rm -rf "$tw_tmp/.git"
|
||||||
|
if [ -f "$tw_tmp/dist/index.html" ]; then
|
||||||
|
cp -r "$tw_tmp/dist/"* "$clone_dir/"
|
||||||
|
elif [ -f "$tw_tmp/index.html" ]; then
|
||||||
|
cp -r "$tw_tmp/"* "$clone_dir/"
|
||||||
|
else
|
||||||
|
local found_index
|
||||||
|
found_index=$(find "$tw_tmp" -name "index.html" -type f -maxdepth 3 2>/dev/null | head -1)
|
||||||
|
if [ -n "$found_index" ]; then
|
||||||
|
local found_dir
|
||||||
|
found_dir=$(dirname "$found_index")
|
||||||
|
cp -r "$found_dir/"* "$clone_dir/"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
rm -rf "$tw_tmp"
|
||||||
|
|
||||||
|
# dawidolko — one big repo with folders (similar to learning-zone)
|
||||||
|
elif [ "$source" = "dawidolko" ]; then
|
||||||
|
local tmp_clone="/tmp/dw_clone_$$"
|
||||||
|
rm -rf "$tmp_clone"
|
||||||
|
git clone --depth 1 --filter=blob:none --sparse "$repo_url" "$tmp_clone" 2>/dev/null
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
git clone --depth 1 "$repo_url" "$tmp_clone" 2>/dev/null
|
||||||
|
fi
|
||||||
|
if [ -d "$tmp_clone" ]; then
|
||||||
|
cd "$tmp_clone" && git sparse-checkout set "$sparse_path" 2>/dev/null
|
||||||
|
if [ -d "$tmp_clone/$sparse_path" ]; then
|
||||||
|
cp -r "$tmp_clone/$sparse_path"/* "$clone_dir/"
|
||||||
|
fi
|
||||||
|
cd - >/dev/null
|
||||||
|
fi
|
||||||
|
rm -rf "$tmp_clone"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check result
|
||||||
|
if [ -f "$clone_dir/index.html" ]; then
|
||||||
|
log_success "$(tf templates_downloaded "$name")"
|
||||||
|
echo "$clone_dir"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
# fallback: find index.html in subfolders (non-standard structure)
|
||||||
|
local fallback_index
|
||||||
|
fallback_index=$(find "$clone_dir" -name "index.html" -type f 2>/dev/null | head -1)
|
||||||
|
if [ -n "$fallback_index" ]; then
|
||||||
|
local fallback_dir
|
||||||
|
fallback_dir=$(dirname "$fallback_index")
|
||||||
|
if [ "$fallback_dir" != "$clone_dir" ]; then
|
||||||
|
cp -r "$fallback_dir/"* "$clone_dir/"
|
||||||
|
log_success "$(tf templates_downloaded_subfolder "$name")"
|
||||||
|
echo "$clone_dir"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
log_error "$(t templates_no_index)"
|
||||||
|
log_dim "$(tf templates_path "$clone_dir")"
|
||||||
|
ls -la "$clone_dir" 2>/dev/null >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Custom git URL helpers ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Validate a user-supplied git URL
|
||||||
|
# Accepts: https://host/path[.git][@branch]
|
||||||
|
# Rejects: ssh://, git://, file://, absolute file paths
|
||||||
|
_validate_custom_git_url() {
|
||||||
|
local url="$1"
|
||||||
|
# Must begin with https://
|
||||||
|
[[ "$url" =~ ^https:// ]] || return 1
|
||||||
|
# Reject shell metacharacters that could be exploited
|
||||||
|
[[ "$url" =~ [[:space:]\;\`\$\(\)\<\>\|\\\&] ]] && return 1
|
||||||
|
# Reasonable length limit
|
||||||
|
[ "${#url}" -gt 512 ] && return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse URL → sets CUSTOM_GIT_CLEAN and CUSTOM_GIT_BRANCH globals
|
||||||
|
_parse_custom_git_url() {
|
||||||
|
local url="$1"
|
||||||
|
CUSTOM_GIT_CLEAN=""
|
||||||
|
CUSTOM_GIT_BRANCH=""
|
||||||
|
# Handle trailing @branch
|
||||||
|
if [[ "$url" =~ ^(https://[^@]+)@([A-Za-z0-9._/-]+)$ ]]; then
|
||||||
|
CUSTOM_GIT_CLEAN="${BASH_REMATCH[1]}"
|
||||||
|
CUSTOM_GIT_BRANCH="${BASH_REMATCH[2]}"
|
||||||
|
else
|
||||||
|
CUSTOM_GIT_CLEAN="$url"
|
||||||
|
fi
|
||||||
|
# Strip trailing slash
|
||||||
|
CUSTOM_GIT_CLEAN="${CUSTOM_GIT_CLEAN%/}"
|
||||||
|
# Append .git if missing (works better with git clone on some hosts)
|
||||||
|
if [[ ! "$CUSTOM_GIT_CLEAN" =~ \.git$ ]]; then
|
||||||
|
CUSTOM_GIT_CLEAN="${CUSTOM_GIT_CLEAN}.git"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check repo size (in MB) by inspecting cloned directory
|
||||||
|
_clone_dir_size_mb() {
|
||||||
|
local dir="$1"
|
||||||
|
du -sm "$dir" 2>/dev/null | awk '{print $1}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Show detailed help for custom git template ─────────────────────────
|
||||||
|
show_custom_git_help() {
|
||||||
|
local line
|
||||||
|
line=$(printf '─%.0s' $(seq 1 60))
|
||||||
|
echo "" >&2
|
||||||
|
echo -e " ${BOLD}${GREEN}$(t custom_git_title)${NC}" >&2
|
||||||
|
echo -e " ${DIM}${line}${NC}" >&2
|
||||||
|
echo -e " $(t custom_git_help_1)" >&2
|
||||||
|
echo -e " $(t custom_git_help_2)" >&2
|
||||||
|
echo -e " $(t custom_git_help_3)" >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo -e " ${BOLD}${WHITE}$(t custom_git_formats)${NC}" >&2
|
||||||
|
echo -e " ${CYAN}$(t custom_git_fmt_github)${NC}" >&2
|
||||||
|
echo -e " ${CYAN}$(t custom_git_fmt_gitlab)${NC}" >&2
|
||||||
|
echo -e " ${CYAN}$(t custom_git_fmt_gitext)${NC}" >&2
|
||||||
|
echo -e " ${CYAN}$(t custom_git_fmt_branch)${NC}" >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo -e " ${BOLD}${WHITE}$(t custom_git_auto_detect)${NC}" >&2
|
||||||
|
echo -e " $(t custom_git_auto_1)" >&2
|
||||||
|
echo -e " $(t custom_git_auto_2)" >&2
|
||||||
|
echo -e " $(t custom_git_auto_3)" >&2
|
||||||
|
echo -e " $(t custom_git_auto_4)" >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo -e " ${BOLD}${WHITE}$(t custom_git_requirements)${NC}" >&2
|
||||||
|
echo -e " ${YELLOW}$(t custom_git_req_1)${NC}" >&2
|
||||||
|
echo -e " ${YELLOW}$(t custom_git_req_2)${NC}" >&2
|
||||||
|
echo -e " ${YELLOW}$(t custom_git_req_3)${NC}" >&2
|
||||||
|
echo -e " ${YELLOW}$(t custom_git_req_4)${NC}" >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo -e " ${BOLD}${WHITE}$(t custom_git_examples)${NC}" >&2
|
||||||
|
echo -e " ${DIM}$(t custom_git_ex_1)${NC}" >&2
|
||||||
|
echo -e " ${DIM}$(t custom_git_ex_2)${NC}" >&2
|
||||||
|
echo -e " ${DIM}${line}${NC}" >&2
|
||||||
|
echo "" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Download a custom git template ─────────────────────────────────────
|
||||||
|
# Prompts user for a URL (unless passed), clones, detects index.html,
|
||||||
|
# copies result into $output_dir/custom_<hash>, echoes the final path.
|
||||||
|
download_custom_git_template() {
|
||||||
|
local url="${1:-}"
|
||||||
|
local output_dir="${2:-$TEMPLATES_CACHE}"
|
||||||
|
|
||||||
|
show_custom_git_help
|
||||||
|
|
||||||
|
if [ -z "$url" ]; then
|
||||||
|
echo -ne " ${WHITE}$(t custom_git_enter_url)${NC} " >&2
|
||||||
|
read -r url
|
||||||
|
url=$(echo "$url" | tr -d '\r\n[:space:]')
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$url" ]; then
|
||||||
|
log_error "$(t custom_git_empty)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! _validate_custom_git_url "$url"; then
|
||||||
|
log_error "$(t custom_git_bad_url)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
_parse_custom_git_url "$url"
|
||||||
|
local clean_url="$CUSTOM_GIT_CLEAN"
|
||||||
|
local branch="$CUSTOM_GIT_BRANCH"
|
||||||
|
|
||||||
|
# Stable-ish directory name from a hash of the original URL
|
||||||
|
local hash
|
||||||
|
hash=$(echo -n "$url" | md5sum 2>/dev/null | awk '{print $1}' | head -c 10)
|
||||||
|
[ -z "$hash" ] && hash=$(date +%s)
|
||||||
|
local tpl_id="custom_${hash}"
|
||||||
|
local clone_dir="$output_dir/${tpl_id}"
|
||||||
|
local tmp_clone="/tmp/custom_git_clone_$$"
|
||||||
|
|
||||||
|
rm -rf "$clone_dir" "$tmp_clone"
|
||||||
|
mkdir -p "$clone_dir"
|
||||||
|
|
||||||
|
log_info "$(t custom_git_cloning)"
|
||||||
|
|
||||||
|
# Clone with timeout so a hung server can't freeze the installer
|
||||||
|
local clone_status=0
|
||||||
|
local git_args=("clone" "--depth" "1")
|
||||||
|
[ -n "$branch" ] && git_args+=("--branch" "$branch")
|
||||||
|
git_args+=("$clean_url" "$tmp_clone")
|
||||||
|
|
||||||
|
if command -v timeout &>/dev/null; then
|
||||||
|
timeout "$CUSTOM_GIT_CLONE_TIMEOUT" git "${git_args[@]}" 2>/tmp/custom_git_err_$$
|
||||||
|
clone_status=$?
|
||||||
|
else
|
||||||
|
git "${git_args[@]}" 2>/tmp/custom_git_err_$$
|
||||||
|
clone_status=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $clone_status -ne 0 ] || [ ! -d "$tmp_clone" ]; then
|
||||||
|
local err_msg
|
||||||
|
err_msg=$(head -3 "/tmp/custom_git_err_$$" 2>/dev/null | tr '\n' ' ')
|
||||||
|
rm -f "/tmp/custom_git_err_$$"
|
||||||
|
rm -rf "$tmp_clone" "$clone_dir"
|
||||||
|
log_error "$(tf custom_git_clone_failed "${err_msg:-$clone_status}")"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
rm -f "/tmp/custom_git_err_$$"
|
||||||
|
|
||||||
|
# Drop .git before measuring size (we only care about payload)
|
||||||
|
rm -rf "$tmp_clone/.git"
|
||||||
|
|
||||||
|
# Size guard
|
||||||
|
local size_mb
|
||||||
|
size_mb=$(_clone_dir_size_mb "$tmp_clone")
|
||||||
|
if [ -n "$size_mb" ] && [ "$size_mb" -gt "$CUSTOM_GIT_MAX_SIZE_MB" ]; then
|
||||||
|
rm -rf "$tmp_clone" "$clone_dir"
|
||||||
|
log_error "$(tf custom_git_too_big "${size_mb}MB")"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "$(t custom_git_scanning)"
|
||||||
|
|
||||||
|
# Priority list of common static-site output folders
|
||||||
|
local candidates=("" "dist" "public" "build" "_site" "site" "docs" "out" "www")
|
||||||
|
local found_dir=""
|
||||||
|
for sub in "${candidates[@]}"; do
|
||||||
|
local try_dir="$tmp_clone"
|
||||||
|
[ -n "$sub" ] && try_dir="$tmp_clone/$sub"
|
||||||
|
if [ -f "$try_dir/index.html" ]; then
|
||||||
|
found_dir="$try_dir"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Fallback: search for any index.html in the repo (shallow depth first)
|
||||||
|
if [ -z "$found_dir" ]; then
|
||||||
|
local fallback_index
|
||||||
|
fallback_index=$(find "$tmp_clone" -maxdepth 4 -name "index.html" -type f 2>/dev/null | head -1)
|
||||||
|
if [ -n "$fallback_index" ]; then
|
||||||
|
found_dir=$(dirname "$fallback_index")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$found_dir" ] || [ ! -f "$found_dir/index.html" ]; then
|
||||||
|
rm -rf "$tmp_clone" "$clone_dir"
|
||||||
|
log_error "$(t custom_git_no_index)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Show what we found (human-friendly relative path)
|
||||||
|
local rel_path="${found_dir#$tmp_clone}"
|
||||||
|
rel_path="${rel_path#/}"
|
||||||
|
[ -z "$rel_path" ] && rel_path="(root)"
|
||||||
|
log_dim "$(tf custom_git_found_at "$rel_path")"
|
||||||
|
|
||||||
|
# Copy the detected directory as the new template
|
||||||
|
cp -r "$found_dir"/* "$clone_dir/" 2>/dev/null
|
||||||
|
cp -r "$found_dir"/.[!.]* "$clone_dir/" 2>/dev/null
|
||||||
|
|
||||||
|
rm -rf "$tmp_clone"
|
||||||
|
|
||||||
|
if [ ! -f "$clone_dir/index.html" ]; then
|
||||||
|
rm -rf "$clone_dir"
|
||||||
|
log_error "$(t custom_git_no_index)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remember the URL so users can see what template they used
|
||||||
|
echo "$url" > "$clone_dir/.custom_git_source" 2>/dev/null
|
||||||
|
|
||||||
|
log_success "$(tf custom_git_installed "$url")"
|
||||||
|
echo "$clone_dir"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Full interactive template selection ───────────────────────────────
|
||||||
|
interactive_template_selection() {
|
||||||
|
load_catalog || return 1
|
||||||
|
|
||||||
|
# Category selection
|
||||||
|
local cat_id
|
||||||
|
cat_id=$(select_category)
|
||||||
|
[ $? -ne 0 ] && return 1
|
||||||
|
|
||||||
|
# Custom git URL path
|
||||||
|
if [ "$cat_id" = "__custom_git__" ]; then
|
||||||
|
local template_dir
|
||||||
|
template_dir=$(download_custom_git_template)
|
||||||
|
[ $? -ne 0 ] && return 1
|
||||||
|
echo "$template_dir"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Template selection
|
||||||
|
local tpl_id
|
||||||
|
tpl_id=$(select_template "$cat_id")
|
||||||
|
[ $? -ne 0 ] && return 1
|
||||||
|
|
||||||
|
# Download
|
||||||
|
local template_dir
|
||||||
|
template_dir=$(download_template "$tpl_id")
|
||||||
|
[ $? -ne 0 ] && return 1
|
||||||
|
|
||||||
|
echo "$template_dir"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
347
lib/website.sh
Normal file
347
lib/website.sh
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# GoTelegram v2.5.0 — Управление сайтом (nginx + certbot + шаблоны)
|
||||||
|
|
||||||
|
# ── Установка nginx ──────────────────────────────────────────────────────────
|
||||||
|
install_nginx() {
|
||||||
|
if command -v nginx &>/dev/null; then
|
||||||
|
log_dim "nginx уже установлен"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
log_info "Установка nginx..."
|
||||||
|
case "$(get_pkg_manager)" in
|
||||||
|
apt) apt_update && apt_install nginx || return 1 ;;
|
||||||
|
dnf) dnf install -y -q nginx || return 1 ;;
|
||||||
|
yum) yum install -y -q nginx || return 1 ;;
|
||||||
|
esac
|
||||||
|
systemctl enable nginx 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Установка certbot ────────────────────────────────────────────────────────
|
||||||
|
install_certbot() {
|
||||||
|
if command -v certbot &>/dev/null; then
|
||||||
|
log_dim "certbot уже установлен"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
log_info "Установка certbot..."
|
||||||
|
case "$(get_pkg_manager)" in
|
||||||
|
apt) apt_install certbot python3-certbot-nginx || return 1 ;;
|
||||||
|
dnf) dnf install -y -q certbot python3-certbot-nginx || return 1 ;;
|
||||||
|
yum) yum install -y -q certbot python3-certbot-nginx || return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Генерация nginx конфига ──────────────────────────────────────────────────
|
||||||
|
generate_nginx_config() {
|
||||||
|
local domain="$1"
|
||||||
|
local proxy_port="${2:-443}"
|
||||||
|
local use_ssl="${3:-true}"
|
||||||
|
|
||||||
|
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
|
||||||
|
|
||||||
|
cat > "$NGINX_SITE_CONF" << 'EONGINX'
|
||||||
|
# GoTelegram v2.5.0 — nginx config
|
||||||
|
# Pro: nginx на 127.0.0.1:8443 (внутренний), telemt на 0.0.0.0:443 (внешний)
|
||||||
|
# Обычный браузер → :443 → telemt → 127.0.0.1:8443 → nginx (сайт)
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name DOMAIN_PLACEHOLDER;
|
||||||
|
|
||||||
|
# Let's Encrypt ACME challenge
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/certbot;
|
||||||
|
allow all;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Редирект на HTTPS
|
||||||
|
location / {
|
||||||
|
return 301 https://$server_name$request_uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 127.0.0.1:SSL_PORT_PLACEHOLDER ssl http2;
|
||||||
|
server_name DOMAIN_PLACEHOLDER;
|
||||||
|
|
||||||
|
# SSL сертификаты
|
||||||
|
ssl_certificate /etc/letsencrypt/live/DOMAIN_PLACEHOLDER/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/DOMAIN_PLACEHOLDER/privkey.pem;
|
||||||
|
|
||||||
|
# Современные TLS настройки
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
||||||
|
ssl_prefer_server_ciphers off;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
ssl_session_timeout 1d;
|
||||||
|
ssl_session_tickets off;
|
||||||
|
|
||||||
|
# OCSP stapling
|
||||||
|
ssl_stapling on;
|
||||||
|
ssl_stapling_verify on;
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
|
||||||
|
# Корень сайта
|
||||||
|
root /var/www/gotelegram-site;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ =404;
|
||||||
|
expires 30d;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Кеширование статики
|
||||||
|
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Скрываем служебные файлы
|
||||||
|
location ~ /\. { deny all; }
|
||||||
|
location = /robots.txt { allow all; log_not_found off; access_log off; }
|
||||||
|
location = /favicon.ico { log_not_found off; access_log off; }
|
||||||
|
}
|
||||||
|
EONGINX
|
||||||
|
|
||||||
|
# Подставляем значения (используем | как разделитель, чтобы / в домене не ломал sed)
|
||||||
|
local escaped_domain
|
||||||
|
escaped_domain=$(printf '%s\n' "$domain" | sed 's/[&/\]/\\&/g')
|
||||||
|
sed -i "s|DOMAIN_PLACEHOLDER|${escaped_domain}|g" "$NGINX_SITE_CONF"
|
||||||
|
sed -i "s|SSL_PORT_PLACEHOLDER|${proxy_port}|g" "$NGINX_SITE_CONF"
|
||||||
|
|
||||||
|
# Активируем сайт
|
||||||
|
rm -f /etc/nginx/sites-enabled/default 2>/dev/null
|
||||||
|
ln -sf "$NGINX_SITE_CONF" "$NGINX_SITE_LINK"
|
||||||
|
|
||||||
|
log_success "nginx конфиг создан для $domain"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Временный конфиг (до получения SSL) ──────────────────────────────────────
|
||||||
|
generate_nginx_temp_config() {
|
||||||
|
local domain="$1"
|
||||||
|
|
||||||
|
cat > "$NGINX_SITE_CONF" << EONGINX_TEMP
|
||||||
|
# GoTelegram — временный конфиг (до получения SSL)
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name ${domain};
|
||||||
|
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/certbot;
|
||||||
|
allow all;
|
||||||
|
}
|
||||||
|
|
||||||
|
root /var/www/gotelegram-site;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files \$uri \$uri/ =404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EONGINX_TEMP
|
||||||
|
|
||||||
|
rm -f /etc/nginx/sites-enabled/default 2>/dev/null
|
||||||
|
ln -sf "$NGINX_SITE_CONF" "$NGINX_SITE_LINK"
|
||||||
|
mkdir -p /var/www/certbot
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Получение SSL сертификата ────────────────────────────────────────────────
|
||||||
|
obtain_ssl_certificate() {
|
||||||
|
local domain="$1"
|
||||||
|
local email="${2:-}"
|
||||||
|
|
||||||
|
if [ ! -d "/etc/letsencrypt/live/$domain" ]; then
|
||||||
|
log_info "Получение SSL сертификата для $domain..."
|
||||||
|
|
||||||
|
# Временный конфиг для ACME challenge
|
||||||
|
generate_nginx_temp_config "$domain"
|
||||||
|
systemctl restart nginx 2>/dev/null
|
||||||
|
|
||||||
|
local certbot_args=(
|
||||||
|
certonly
|
||||||
|
--webroot
|
||||||
|
-w /var/www/certbot
|
||||||
|
-d "$domain"
|
||||||
|
--non-interactive
|
||||||
|
--agree-tos
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ -n "$email" ]; then
|
||||||
|
certbot_args+=(--email "$email")
|
||||||
|
else
|
||||||
|
certbot_args+=(--register-unsafely-without-email)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if certbot "${certbot_args[@]}" 2>/dev/null; then
|
||||||
|
log_success "SSL сертификат получен для $domain"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log_error "Не удалось получить SSL сертификат"
|
||||||
|
log_dim "Убедитесь что домен $domain направлен на IP этого сервера"
|
||||||
|
log_dim "и порт 80 открыт в файрволе."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_dim "SSL сертификат уже существует для $domain"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Авто-обновление сертификата ──────────────────────────────────────────────
|
||||||
|
setup_ssl_auto_renewal() {
|
||||||
|
# Certbot systemd timer (предпочтительно)
|
||||||
|
if [ -f /etc/systemd/system/certbot.timer ] || [ -f /lib/systemd/system/certbot.timer ]; then
|
||||||
|
systemctl enable certbot.timer 2>/dev/null
|
||||||
|
systemctl start certbot.timer 2>/dev/null
|
||||||
|
log_success "Авто-обновление SSL через systemd timer"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fallback: cron
|
||||||
|
if ! crontab -l 2>/dev/null | grep -q "certbot renew"; then
|
||||||
|
(crontab -l 2>/dev/null; echo "0 3 * * * certbot renew --quiet --post-hook 'systemctl reload nginx'") | crontab -
|
||||||
|
log_success "Авто-обновление SSL через cron (3:00 ежедневно)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Обновление сертификата вручную ───────────────────────────────────────────
|
||||||
|
renew_ssl_certificate() {
|
||||||
|
log_info "Обновление SSL сертификата..."
|
||||||
|
if certbot renew --quiet --post-hook "systemctl reload nginx" 2>/dev/null; then
|
||||||
|
log_success "Сертификат обновлён"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log_error "Ошибка обновления сертификата"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Дата истечения SSL ───────────────────────────────────────────────────────
|
||||||
|
get_ssl_expiry() {
|
||||||
|
local domain="$1"
|
||||||
|
local cert="/etc/letsencrypt/live/$domain/fullchain.pem"
|
||||||
|
if [ -f "$cert" ]; then
|
||||||
|
openssl x509 -enddate -noout -in "$cert" 2>/dev/null | sed 's/notAfter=//'
|
||||||
|
else
|
||||||
|
echo "N/A"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Деплой шаблона сайта ─────────────────────────────────────────────────────
|
||||||
|
deploy_template_to_nginx() {
|
||||||
|
local template_dir="$1"
|
||||||
|
local template_id="${2:-}"
|
||||||
|
local source_url=""
|
||||||
|
|
||||||
|
if [ ! -d "$template_dir" ] || [ ! -f "$template_dir/index.html" ]; then
|
||||||
|
log_error "Шаблон не содержит index.html: $template_dir"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
[ -z "$template_id" ] && template_id=$(basename "$template_dir")
|
||||||
|
[ -f "$template_dir/.custom_git_source" ] && source_url=$(head -1 "$template_dir/.custom_git_source" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
# Бекапим старый сайт
|
||||||
|
if [ -d "$WEBSITE_ROOT" ] && [ "$(ls -A "$WEBSITE_ROOT" 2>/dev/null)" ]; then
|
||||||
|
local backup_name="site_backup_$(date +%Y%m%d_%H%M%S)"
|
||||||
|
mv "$WEBSITE_ROOT" "/tmp/$backup_name" 2>/dev/null
|
||||||
|
log_dim "Старый сайт сохранён в /tmp/$backup_name"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$WEBSITE_ROOT"
|
||||||
|
cp -a "$template_dir/." "$WEBSITE_ROOT/"
|
||||||
|
rm -f "$WEBSITE_ROOT/.custom_git_source" 2>/dev/null || true
|
||||||
|
echo "$template_id" > "$WEBSITE_ROOT/.gotelegram_template_id" 2>/dev/null || true
|
||||||
|
[ -n "$source_url" ] && echo "$source_url" > "$WEBSITE_ROOT/.gotelegram_template_source" 2>/dev/null || true
|
||||||
|
chown -R www-data:www-data "$WEBSITE_ROOT" 2>/dev/null || chown -R nginx:nginx "$WEBSITE_ROOT" 2>/dev/null
|
||||||
|
chmod -R 755 "$WEBSITE_ROOT"
|
||||||
|
|
||||||
|
log_success "Шаблон развёрнут в $WEBSITE_ROOT"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Полная установка pro-режима ──────────────────────────────────────────────
|
||||||
|
setup_pro_mode() {
|
||||||
|
local domain="$1"
|
||||||
|
local template_dir="$2"
|
||||||
|
local proxy_port="${3:-443}"
|
||||||
|
local email="${4:-}"
|
||||||
|
|
||||||
|
log_step "Настройка pro-режима"
|
||||||
|
|
||||||
|
# 1. Устанавливаем nginx
|
||||||
|
run_with_spinner "Установка nginx" install_nginx || return 1
|
||||||
|
|
||||||
|
# 2. Устанавливаем certbot
|
||||||
|
run_with_spinner "Установка certbot" install_certbot || return 1
|
||||||
|
|
||||||
|
# 3. Деплоим шаблон сайта
|
||||||
|
deploy_template_to_nginx "$template_dir" || return 1
|
||||||
|
|
||||||
|
# 4. Получаем SSL
|
||||||
|
obtain_ssl_certificate "$domain" "$email" || return 1
|
||||||
|
|
||||||
|
# 5. Генерируем полный nginx конфиг с SSL
|
||||||
|
generate_nginx_config "$domain" "$proxy_port"
|
||||||
|
|
||||||
|
# 6. Тестируем и перезапускаем nginx
|
||||||
|
if nginx -t 2>/dev/null; then
|
||||||
|
systemctl restart nginx
|
||||||
|
log_success "nginx запущен с SSL"
|
||||||
|
else
|
||||||
|
log_error "Ошибка в конфигурации nginx"
|
||||||
|
nginx -t
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 7. Настраиваем авто-обновление SSL
|
||||||
|
setup_ssl_auto_renewal
|
||||||
|
|
||||||
|
# 8. Показываем благодарности авторам шаблонов
|
||||||
|
show_credits
|
||||||
|
|
||||||
|
log_success "Pro-режим настроен: https://${domain}"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Управление nginx ────────────────────────────────────────────────────────
|
||||||
|
nginx_status() {
|
||||||
|
if systemctl is-active --quiet nginx 2>/dev/null; then
|
||||||
|
echo "running"
|
||||||
|
else
|
||||||
|
echo "stopped"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
restart_nginx() {
|
||||||
|
if nginx -t 2>/dev/null; then
|
||||||
|
systemctl restart nginx 2>/dev/null
|
||||||
|
log_success "nginx перезапущен"
|
||||||
|
else
|
||||||
|
log_error "Ошибка конфигурации nginx"
|
||||||
|
nginx -t
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Удаление pro-режима ──────────────────────────────────────────────────────
|
||||||
|
remove_pro_mode() {
|
||||||
|
log_info "Удаление pro-режима..."
|
||||||
|
rm -f "$NGINX_SITE_CONF" "$NGINX_SITE_LINK"
|
||||||
|
rm -rf "$WEBSITE_ROOT"
|
||||||
|
systemctl restart nginx 2>/dev/null
|
||||||
|
log_success "Pro-режим удалён (nginx оставлен)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Смена шаблона ────────────────────────────────────────────────────────────
|
||||||
|
switch_template() {
|
||||||
|
local new_template_dir="$1"
|
||||||
|
deploy_template_to_nginx "$new_template_dir" "$(basename "$new_template_dir")"
|
||||||
|
# nginx не требует перезапуска — статика обновилась на месте
|
||||||
|
log_success "Шаблон сайта обновлён"
|
||||||
|
}
|
||||||
16339
templates_catalog.json
Normal file
16339
templates_catalog.json
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user