mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-06-11 09:52:47 +00:00
Compare commits
8 Commits
v2.3.0
...
d70e046035
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d70e046035 | ||
|
|
c70cb36a2b | ||
|
|
b36fb5cf10 | ||
|
|
8b4b4892a4 | ||
|
|
046a08fdb6 | ||
|
|
a21d2ebea2 | ||
|
|
96cbd243d9 | ||
|
|
3f136ec8a0 |
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
*.env.local
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# Backups (contain secrets)
|
||||||
|
backups/
|
||||||
|
*.tar.gz
|
||||||
|
*.tar.gz.enc
|
||||||
|
*.sha256
|
||||||
|
|
||||||
|
# Temp
|
||||||
|
/tmp/
|
||||||
|
*.tmp
|
||||||
|
*.swp
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
115
bootstrap.sh
Executable file
115
bootstrap.sh
Executable file
@@ -0,0 +1,115 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# GoTelegram Pro — Bootstrap installer for private repo
|
||||||
|
# Downloads all files and launches install.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO="anten-ka/gotelegram_pro"
|
||||||
|
BRANCH="test"
|
||||||
|
PAT="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"
|
||||||
|
INSTALL_DIR="/opt/gotelegram"
|
||||||
|
API="https://api.github.com/repos/${REPO}/contents"
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e " ${YELLOW}╔══════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${BOLD}GoTelegram Pro — Установка${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} MTProxy менеджер с Telegram-ботом ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} Stealth-режим, 1800+ шаблонов сайтов ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}╚══════════════════════════════════════════════════════╝${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check root
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
echo -e " ${RED}✗${NC} Запустите от root: ${CYAN}sudo bash bootstrap.sh${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check dependencies
|
||||||
|
for cmd in curl jq; do
|
||||||
|
if ! command -v "$cmd" &>/dev/null; then
|
||||||
|
echo -e " ${CYAN}↻${NC} Установка $cmd..."
|
||||||
|
apt-get update -qq && apt-get install -y -qq "$cmd" >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
download_file() {
|
||||||
|
local remote_path="$1"
|
||||||
|
local local_path="$2"
|
||||||
|
local dir
|
||||||
|
dir=$(dirname "$local_path")
|
||||||
|
mkdir -p "$dir"
|
||||||
|
|
||||||
|
local http_code
|
||||||
|
http_code=$(curl -sL -w "%{http_code}" -o "$local_path" \
|
||||||
|
-H "Authorization: token ${PAT}" \
|
||||||
|
-H "Accept: application/vnd.github.raw" \
|
||||||
|
"${API}/${remote_path}?ref=${BRANCH}")
|
||||||
|
|
||||||
|
if [ "$http_code" != "200" ]; then
|
||||||
|
echo -e " ${RED}✗${NC} Ошибка загрузки ${remote_path} (HTTP ${http_code})"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# File list
|
||||||
|
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"
|
||||||
|
"gotelegram-bot/bot.py"
|
||||||
|
"gotelegram-bot/config.example.env"
|
||||||
|
"gotelegram-bot/requirements.txt"
|
||||||
|
"gotelegram-bot/README.md"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo -e " ${CYAN}↻${NC} Загрузка файлов в ${INSTALL_DIR}..."
|
||||||
|
mkdir -p "${INSTALL_DIR}/lib" "${INSTALL_DIR}/gotelegram-bot"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if [ "$failed" -gt 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo -e " ${RED}✗${NC} Не удалось загрузить ${failed} файл(ов)"
|
||||||
|
echo -e " ${YELLOW}Проверьте токен доступа и подключение к сети${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fix permissions and line endings
|
||||||
|
echo -e " ${CYAN}↻${NC} Настройка прав..."
|
||||||
|
chmod +x "${INSTALL_DIR}/install.sh" "${INSTALL_DIR}/install_gotelegram_bot.sh"
|
||||||
|
chmod +x "${INSTALL_DIR}"/lib/*.sh
|
||||||
|
sed -i 's/\r$//' "${INSTALL_DIR}/install.sh" "${INSTALL_DIR}/install_gotelegram_bot.sh" "${INSTALL_DIR}"/lib/*.sh 2>/dev/null
|
||||||
|
|
||||||
|
# Create symlink
|
||||||
|
ln -sf "${INSTALL_DIR}/install.sh" /usr/local/bin/gotelegram
|
||||||
|
echo -e " ${GREEN}✓${NC} Команда ${CYAN}gotelegram${NC} доступна"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e " ${GREEN}✓${NC} Установка завершена! Запуск..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Launch
|
||||||
|
exec bash "${INSTALL_DIR}/install.sh"
|
||||||
@@ -50,7 +50,7 @@ logger = logging.getLogger(__name__)
|
|||||||
# CONFIGURATION
|
# CONFIGURATION
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
GOTELEGRAM_VERSION = "2.3.0"
|
GOTELEGRAM_VERSION = "2.3.1"
|
||||||
GOTELEGRAM_CONFIG = "/opt/gotelegram/config.json"
|
GOTELEGRAM_CONFIG = "/opt/gotelegram/config.json"
|
||||||
TELEMT_CONFIG = "/etc/telemt/config.toml"
|
TELEMT_CONFIG = "/etc/telemt/config.toml"
|
||||||
TELEMT_SERVICE = "telemt"
|
TELEMT_SERVICE = "telemt"
|
||||||
@@ -58,19 +58,73 @@ WEBSITE_ROOT = "/var/www/gotelegram-site"
|
|||||||
BACKUP_DIR = "/opt/gotelegram/backups"
|
BACKUP_DIR = "/opt/gotelegram/backups"
|
||||||
TEMPLATES_CATALOG = "/opt/gotelegram/templates_catalog.json"
|
TEMPLATES_CATALOG = "/opt/gotelegram/templates_catalog.json"
|
||||||
|
|
||||||
PROMO_LINK = "https://vk.cc/ct29NQ"
|
PROMO_LINK_1 = "https://vk.cc/ct29NQ"
|
||||||
|
PROMO_LINK_2 = "https://vk.cc/cUxAhj"
|
||||||
TIP_LINK = "https://pay.cloudtips.ru/p/7410814f"
|
TIP_LINK = "https://pay.cloudtips.ru/p/7410814f"
|
||||||
|
PROMO_STAMP_FILE = "/opt/gotelegram/.promo_bot_last_shown"
|
||||||
|
|
||||||
BOT_TOKEN = os.getenv("BOT_TOKEN")
|
BOT_TOKEN = os.getenv("BOT_TOKEN")
|
||||||
ALLOWED_IDS_STR = os.getenv("ALLOWED_IDS", "")
|
ENV_FILE = "/opt/gotelegram-bot/.env"
|
||||||
|
|
||||||
|
# ── Загрузка ALLOWED_IDS ────────────────────────────────────────────────────
|
||||||
|
# Поддерживает запятую, пробел, или их комбинацию как разделитель
|
||||||
ALLOWED_IDS: set = set()
|
ALLOWED_IDS: set = set()
|
||||||
for _id_str in ALLOWED_IDS_STR.split(","):
|
_WAITING_FOR_ADMIN = False # True если список пуст → ждём первого админа
|
||||||
_id_str = _id_str.strip()
|
|
||||||
if _id_str:
|
|
||||||
try:
|
def _load_allowed_ids() -> None:
|
||||||
ALLOWED_IDS.add(int(_id_str))
|
"""Загрузить ALLOWED_IDS из переменной окружения."""
|
||||||
except ValueError:
|
global ALLOWED_IDS, _WAITING_FOR_ADMIN
|
||||||
logging.warning(f"Invalid ALLOWED_IDS entry: {_id_str}")
|
raw = os.getenv("ALLOWED_IDS", "")
|
||||||
|
ALLOWED_IDS = set()
|
||||||
|
# Разделители: запятая, пробел, или оба
|
||||||
|
for part in re.split(r'[,\s]+', raw):
|
||||||
|
part = part.strip()
|
||||||
|
if part:
|
||||||
|
try:
|
||||||
|
ALLOWED_IDS.add(int(part))
|
||||||
|
except ValueError:
|
||||||
|
logging.warning(f"Invalid ALLOWED_IDS entry: {part}")
|
||||||
|
_WAITING_FOR_ADMIN = len(ALLOWED_IDS) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def _save_allowed_ids() -> None:
|
||||||
|
"""Сохранить ALLOWED_IDS в .env файл и обновить os.environ."""
|
||||||
|
global _WAITING_FOR_ADMIN
|
||||||
|
ids_str = ",".join(str(i) for i in sorted(ALLOWED_IDS))
|
||||||
|
os.environ["ALLOWED_IDS"] = ids_str
|
||||||
|
_WAITING_FOR_ADMIN = len(ALLOWED_IDS) == 0
|
||||||
|
|
||||||
|
if not os.path.exists(ENV_FILE):
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(ENV_FILE, "r") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
found = False
|
||||||
|
new_lines = []
|
||||||
|
for line in lines:
|
||||||
|
if line.strip().startswith("ALLOWED_IDS="):
|
||||||
|
if ids_str:
|
||||||
|
new_lines.append(f"ALLOWED_IDS={ids_str}\n")
|
||||||
|
# Если пусто — удаляем строку
|
||||||
|
found = True
|
||||||
|
else:
|
||||||
|
new_lines.append(line)
|
||||||
|
|
||||||
|
if not found and ids_str:
|
||||||
|
new_lines.append(f"ALLOWED_IDS={ids_str}\n")
|
||||||
|
|
||||||
|
with open(ENV_FILE, "w") as f:
|
||||||
|
f.writelines(new_lines)
|
||||||
|
|
||||||
|
logger.info(f"ALLOWED_IDS updated in .env: {ids_str or '(empty)'}")
|
||||||
|
except OSError as e:
|
||||||
|
logger.error(f"Failed to update .env: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
_load_allowed_ids()
|
||||||
|
|
||||||
LITE_DOMAINS = [
|
LITE_DOMAINS = [
|
||||||
"google.com",
|
"google.com",
|
||||||
@@ -228,21 +282,41 @@ async def check_old_container() -> Optional[str]:
|
|||||||
|
|
||||||
|
|
||||||
def is_user_allowed(user_id: int) -> bool:
|
def is_user_allowed(user_id: int) -> bool:
|
||||||
"""Check if user ID is in ALLOWED_IDS."""
|
"""Check if user ID is in ALLOWED_IDS. If list is empty — waiting for admin."""
|
||||||
if not ALLOWED_IDS:
|
if _WAITING_FOR_ADMIN:
|
||||||
return True
|
return False # Никому не даём доступ пока не назначен админ
|
||||||
return user_id in ALLOWED_IDS
|
return user_id in ALLOWED_IDS
|
||||||
|
|
||||||
|
|
||||||
|
def add_admin(user_id: int) -> None:
|
||||||
|
"""Добавить администратора и сохранить в .env."""
|
||||||
|
ALLOWED_IDS.add(user_id)
|
||||||
|
_save_allowed_ids()
|
||||||
|
logger.info(f"Admin added: {user_id}")
|
||||||
|
|
||||||
|
|
||||||
|
def remove_admin(user_id: int) -> None:
|
||||||
|
"""Убрать администратора и сохранить в .env."""
|
||||||
|
ALLOWED_IDS.discard(user_id)
|
||||||
|
_save_allowed_ids()
|
||||||
|
logger.info(f"Admin removed: {user_id}")
|
||||||
|
|
||||||
|
|
||||||
async def require_auth(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
|
async def require_auth(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
|
||||||
"""Check authorization and send error if not allowed."""
|
"""Check authorization and send error if not allowed."""
|
||||||
if not is_user_allowed(update.effective_user.id):
|
user_id = update.effective_user.id
|
||||||
await update.message.reply_text(
|
|
||||||
f"Access denied. Your ID: {update.effective_user.id}"
|
# Режим ожидания первого админа — обрабатывается в cmd_start
|
||||||
)
|
if _WAITING_FOR_ADMIN:
|
||||||
logger.warning(
|
return False
|
||||||
f"Unauthorized access attempt from user {update.effective_user.id}"
|
|
||||||
)
|
if not is_user_allowed(user_id):
|
||||||
|
if update.message:
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"⛔ Доступ запрещён.\nВаш ID: <code>{user_id}</code>",
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
|
logger.warning(f"Unauthorized access attempt from user {user_id}")
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -277,14 +351,17 @@ def get_main_menu() -> InlineKeyboardMarkup:
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
InlineKeyboardButton("🌐 Website/SSL", callback_data="menu_website"),
|
InlineKeyboardButton("🌐 Website/SSL", callback_data="menu_website"),
|
||||||
InlineKeyboardButton("🎁 Promo", callback_data="menu_promo"),
|
InlineKeyboardButton("🎁 Промо", callback_data="menu_promo"),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
InlineKeyboardButton("📊 Traffic Stats", callback_data="menu_stats"),
|
InlineKeyboardButton("📊 Traffic Stats", callback_data="menu_stats"),
|
||||||
InlineKeyboardButton("🗑️ Remove", callback_data="menu_remove"),
|
InlineKeyboardButton("🗑️ Remove", callback_data="menu_remove"),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
InlineKeyboardButton("👤 Админы", callback_data="menu_admins"),
|
||||||
InlineKeyboardButton("ℹ️ Credits", callback_data="menu_credits"),
|
InlineKeyboardButton("ℹ️ Credits", callback_data="menu_credits"),
|
||||||
|
],
|
||||||
|
[
|
||||||
InlineKeyboardButton("❌ Close", callback_data="close_menu"),
|
InlineKeyboardButton("❌ Close", callback_data="close_menu"),
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
@@ -297,8 +374,37 @@ def get_main_menu() -> InlineKeyboardMarkup:
|
|||||||
|
|
||||||
|
|
||||||
async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
"""Start command - show main menu."""
|
"""Start command - show main menu, promo once per day.
|
||||||
if not await require_auth(update, context):
|
|
||||||
|
Если ALLOWED_IDS пуст — режим авто-регистрации первого админа.
|
||||||
|
"""
|
||||||
|
user = update.effective_user
|
||||||
|
user_id = user.id
|
||||||
|
|
||||||
|
# ── Режим ожидания первого админа ──
|
||||||
|
if _WAITING_FOR_ADMIN:
|
||||||
|
name = user.full_name or user.username or str(user_id)
|
||||||
|
text = (
|
||||||
|
f"<b>👋 Привет, {html.escape(name)}!</b>\n\n"
|
||||||
|
f"Бот ещё не настроен.\n"
|
||||||
|
f"Ваш Telegram ID: <code>{user_id}</code>\n\n"
|
||||||
|
f"Назначить вас администратором?"
|
||||||
|
)
|
||||||
|
keyboard = InlineKeyboardMarkup([
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("✅ Да", callback_data=f"admin_confirm_{user_id}"),
|
||||||
|
InlineKeyboardButton("❌ Нет", callback_data="admin_cancel"),
|
||||||
|
]
|
||||||
|
])
|
||||||
|
await update.message.reply_text(text, reply_markup=keyboard, parse_mode="HTML")
|
||||||
|
return
|
||||||
|
|
||||||
|
# ── Проверка доступа ──
|
||||||
|
if not is_user_allowed(user_id):
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"⛔ Доступ запрещён.\nВаш ID: <code>{user_id}</code>",
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
welcome = (
|
welcome = (
|
||||||
@@ -311,6 +417,13 @@ async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|||||||
welcome, reply_markup=get_main_menu(), parse_mode="HTML"
|
welcome, reply_markup=get_main_menu(), parse_mode="HTML"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Промо раз в сутки
|
||||||
|
if should_show_promo_bot():
|
||||||
|
mark_promo_shown_bot()
|
||||||
|
await update.message.reply_text(
|
||||||
|
get_promo_text(), parse_mode="HTML"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
"""Help command - show available commands."""
|
"""Help command - show available commands."""
|
||||||
@@ -318,12 +431,14 @@ async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
help_text = (
|
help_text = (
|
||||||
"<b>GoTelegram Bot Commands</b>\n\n"
|
"<b>GoTelegram Bot — Команды</b>\n\n"
|
||||||
"/start - Show main menu\n"
|
"/start — Главное меню\n"
|
||||||
"/help - Show this help message\n"
|
"/help — Эта справка\n"
|
||||||
"/status - Quick status check\n"
|
"/status — Быстрый статус\n"
|
||||||
"/logs - Show recent logs\n\n"
|
"/logs — Последние логи\n"
|
||||||
"Use the inline menu for all other operations."
|
"/addadmin ID — Добавить админа\n"
|
||||||
|
"/deladmin ID — Удалить админа\n\n"
|
||||||
|
"Используйте кнопки меню для остальных операций."
|
||||||
)
|
)
|
||||||
await update.message.reply_text(help_text, parse_mode="HTML")
|
await update.message.reply_text(help_text, parse_mode="HTML")
|
||||||
|
|
||||||
@@ -826,7 +941,7 @@ async def cb_pro_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
|
|
||||||
|
|
||||||
async def get_proxy_link() -> Optional[str]:
|
async def get_proxy_link() -> Optional[str]:
|
||||||
"""Generate proxy link from config."""
|
"""Generate proxy link from config. Pro-mode uses domain + fake-TLS secret."""
|
||||||
config = load_json(GOTELEGRAM_CONFIG)
|
config = load_json(GOTELEGRAM_CONFIG)
|
||||||
if not config:
|
if not config:
|
||||||
return None
|
return None
|
||||||
@@ -842,12 +957,20 @@ async def get_proxy_link() -> Optional[str]:
|
|||||||
if not secret:
|
if not secret:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Get server IP
|
mode = config.get("mode", "lite")
|
||||||
|
domain = config.get("domain", "")
|
||||||
|
port = config.get("port", 443)
|
||||||
|
|
||||||
|
# Pro-режим: ссылка с доменом и fake-TLS секретом (ee + secret + hex domain)
|
||||||
|
if mode == "pro" and domain:
|
||||||
|
domain_hex = domain.encode().hex()
|
||||||
|
faketls_secret = f"ee{secret}{domain_hex}"
|
||||||
|
return f"tg://proxy?server={domain}&port={port}&secret={faketls_secret}"
|
||||||
|
|
||||||
|
# Lite-режим: IP
|
||||||
code, stdout, _ = await sh("curl", "-s", "-4", "--max-time", "5", "https://api.ipify.org")
|
code, stdout, _ = await sh("curl", "-s", "-4", "--max-time", "5", "https://api.ipify.org")
|
||||||
server = stdout.strip() if code == 0 and stdout.strip() else "0.0.0.0"
|
server = stdout.strip() if code == 0 and stdout.strip() else "0.0.0.0"
|
||||||
|
|
||||||
port = config.get("port", 443)
|
|
||||||
|
|
||||||
return f"tg://proxy?server={server}&port={port}&secret={secret}"
|
return f"tg://proxy?server={server}&port={port}&secret={secret}"
|
||||||
|
|
||||||
|
|
||||||
@@ -1305,28 +1428,170 @@ async def cb_ssl_status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
|
|||||||
await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML")
|
await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ADMIN MANAGEMENT
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
async def cb_menu_admins(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Показать список админов и кнопки управления."""
|
||||||
|
query = update.callback_query
|
||||||
|
await query.answer()
|
||||||
|
|
||||||
|
if ALLOWED_IDS:
|
||||||
|
ids_list = "\n".join(f" • <code>{uid}</code>" for uid in sorted(ALLOWED_IDS))
|
||||||
|
text = f"<b>👤 Администраторы</b>\n\n{ids_list}\n"
|
||||||
|
else:
|
||||||
|
text = "<b>👤 Администраторы</b>\n\n<i>Список пуст — доступ для всех</i>\n"
|
||||||
|
|
||||||
|
text += (
|
||||||
|
f"\nВсего: {len(ALLOWED_IDS)}\n\n"
|
||||||
|
"Чтобы <b>добавить</b> — перешлите любое сообщение от нового админа, "
|
||||||
|
"или отправьте команду:\n"
|
||||||
|
"<code>/addadmin 123456789</code>\n\n"
|
||||||
|
"Чтобы <b>удалить</b>:\n"
|
||||||
|
"<code>/deladmin 123456789</code>"
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard = InlineKeyboardMarkup([
|
||||||
|
[InlineKeyboardButton("« Назад", callback_data="menu_main")],
|
||||||
|
])
|
||||||
|
await safe_edit_message(query, text, reply_markup=keyboard, parse_mode="HTML")
|
||||||
|
|
||||||
|
|
||||||
|
async def cmd_addadmin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""/addadmin ID [ID2 ID3 ...] — добавить админа вручную."""
|
||||||
|
if not is_user_allowed(update.effective_user.id):
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"⛔ Доступ запрещён.\nВаш ID: <code>{update.effective_user.id}</code>",
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
args = context.args or []
|
||||||
|
if not args:
|
||||||
|
await update.message.reply_text(
|
||||||
|
"Использование: <code>/addadmin ID [ID2 ID3 ...]</code>\n"
|
||||||
|
"Пример: <code>/addadmin 123456789 987654321</code>",
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
added = []
|
||||||
|
errors = []
|
||||||
|
for a in args:
|
||||||
|
a = a.strip().replace(",", "")
|
||||||
|
if not a:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
uid = int(a)
|
||||||
|
add_admin(uid)
|
||||||
|
added.append(str(uid))
|
||||||
|
except ValueError:
|
||||||
|
errors.append(a)
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
if added:
|
||||||
|
parts.append(f"✅ Добавлены: {', '.join(added)}")
|
||||||
|
if errors:
|
||||||
|
parts.append(f"❌ Ошибки: {', '.join(errors)}")
|
||||||
|
|
||||||
|
await update.message.reply_text("\n".join(parts), parse_mode="HTML")
|
||||||
|
|
||||||
|
|
||||||
|
async def cmd_deladmin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""/deladmin ID — удалить админа."""
|
||||||
|
if not is_user_allowed(update.effective_user.id):
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"⛔ Доступ запрещён.\nВаш ID: <code>{update.effective_user.id}</code>",
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
args = context.args or []
|
||||||
|
if not args:
|
||||||
|
await update.message.reply_text(
|
||||||
|
"Использование: <code>/deladmin ID</code>",
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
removed = []
|
||||||
|
for a in args:
|
||||||
|
a = a.strip().replace(",", "")
|
||||||
|
try:
|
||||||
|
uid = int(a)
|
||||||
|
if uid == update.effective_user.id:
|
||||||
|
await update.message.reply_text("⚠️ Нельзя удалить себя!")
|
||||||
|
continue
|
||||||
|
if uid in ALLOWED_IDS:
|
||||||
|
remove_admin(uid)
|
||||||
|
removed.append(str(uid))
|
||||||
|
else:
|
||||||
|
await update.message.reply_text(f"ID {uid} не найден в списке")
|
||||||
|
except ValueError:
|
||||||
|
await update.message.reply_text(f"❌ Некорректный ID: {html.escape(a)}")
|
||||||
|
|
||||||
|
if removed:
|
||||||
|
await update.message.reply_text(f"✅ Удалены: {', '.join(removed)}")
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# PROMO & CREDITS
|
# PROMO & CREDITS
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def get_promo_text() -> str:
|
||||||
|
"""Return promo text with 2 hosters + donate."""
|
||||||
|
return (
|
||||||
|
"<b>💰 Хостинг #1 — скидка до 60%</b>\n"
|
||||||
|
f"<a href='{PROMO_LINK_1}'>{PROMO_LINK_1}</a>\n\n"
|
||||||
|
"<b>Промокоды:</b>\n"
|
||||||
|
" <code>OFF60</code> — 60% на первый месяц\n"
|
||||||
|
" <code>antenka20</code> — 20% + 3% за 3 мес\n"
|
||||||
|
" <code>antenka6</code> — 15% + 5% за 6 мес\n\n"
|
||||||
|
"━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
|
||||||
|
"<b>💰 Хостинг #2 — скидка до 60%</b>\n"
|
||||||
|
f"<a href='{PROMO_LINK_2}'>{PROMO_LINK_2}</a>\n\n"
|
||||||
|
"<b>Промокод:</b>\n"
|
||||||
|
" <code>OFF60</code> — 60% на первый месяц\n\n"
|
||||||
|
"━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
|
||||||
|
"<b>☕ Донат / Чаевые</b>\n"
|
||||||
|
f"<a href='{TIP_LINK}'>{TIP_LINK}</a>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def should_show_promo_bot() -> bool:
|
||||||
|
"""Check if promo should be shown (once per 24h)."""
|
||||||
|
try:
|
||||||
|
if not os.path.exists(PROMO_STAMP_FILE):
|
||||||
|
return True
|
||||||
|
with open(PROMO_STAMP_FILE, "r") as f:
|
||||||
|
last_ts = int(f.read().strip())
|
||||||
|
return (int(time.time()) - last_ts) >= 86400
|
||||||
|
except (ValueError, OSError):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def mark_promo_shown_bot() -> None:
|
||||||
|
"""Mark promo as shown."""
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(PROMO_STAMP_FILE), exist_ok=True)
|
||||||
|
with open(PROMO_STAMP_FILE, "w") as f:
|
||||||
|
f.write(str(int(time.time())))
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def cb_menu_promo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cb_menu_promo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
"""Promo information."""
|
"""Promo information — always shown from menu."""
|
||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
await query.answer()
|
await query.answer()
|
||||||
|
|
||||||
text = (
|
|
||||||
f"<b>🎁 GoTelegram Promo</b>\n\n"
|
|
||||||
f"Share the love! Invite friends to use GoTelegram.\n\n"
|
|
||||||
f"<a href='{PROMO_LINK}'>Promo Link</a>\n\n"
|
|
||||||
f"Support development:\n"
|
|
||||||
f"<a href='{TIP_LINK}'>Send a Tip</a>"
|
|
||||||
)
|
|
||||||
|
|
||||||
keyboard = InlineKeyboardMarkup(
|
keyboard = InlineKeyboardMarkup(
|
||||||
[[InlineKeyboardButton("« Back", callback_data="menu_main")]]
|
[[InlineKeyboardButton("« Назад", callback_data="menu_main")]]
|
||||||
)
|
)
|
||||||
await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML")
|
await safe_edit_message(query, get_promo_text(), reply_markup=keyboard, parse_mode="HTML")
|
||||||
|
|
||||||
|
|
||||||
async def cb_menu_credits(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def cb_menu_credits(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
@@ -1412,9 +1677,43 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
data = query.data
|
data = query.data
|
||||||
|
|
||||||
|
# ── Авто-регистрация админа (до проверки доступа!) ──
|
||||||
|
if data.startswith("admin_confirm_"):
|
||||||
|
await query.answer()
|
||||||
|
try:
|
||||||
|
new_admin_id = int(data.split("_")[-1])
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
await safe_edit_message(query, "❌ Ошибка: некорректный ID")
|
||||||
|
return
|
||||||
|
# Безопасность: только тот кто нажал кнопку может стать админом
|
||||||
|
if update.effective_user.id != new_admin_id:
|
||||||
|
await query.answer("Эта кнопка не для вас", show_alert=True)
|
||||||
|
return
|
||||||
|
# Race condition: если кто-то уже стал админом
|
||||||
|
if not _WAITING_FOR_ADMIN:
|
||||||
|
await safe_edit_message(query, "ℹ️ Администратор уже назначен.")
|
||||||
|
return
|
||||||
|
add_admin(new_admin_id)
|
||||||
|
await safe_edit_message(
|
||||||
|
query,
|
||||||
|
f"✅ <b>Вы назначены администратором!</b>\n\n"
|
||||||
|
f"ID: <code>{new_admin_id}</code>\n\n"
|
||||||
|
f"Нажмите /start чтобы открыть меню.",
|
||||||
|
parse_mode="HTML",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if data == "admin_cancel":
|
||||||
|
await query.answer()
|
||||||
|
await safe_edit_message(
|
||||||
|
query,
|
||||||
|
"👋 Ок. Напишите /start когда будете готовы.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# Access control
|
# Access control
|
||||||
if not is_user_allowed(update.effective_user.id):
|
if not is_user_allowed(update.effective_user.id):
|
||||||
await query.answer("Access denied")
|
await query.answer("Доступ запрещён")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Main menu
|
# Main menu
|
||||||
@@ -1449,6 +1748,7 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
"menu_website": cb_menu_website,
|
"menu_website": cb_menu_website,
|
||||||
"menu_promo": cb_menu_promo,
|
"menu_promo": cb_menu_promo,
|
||||||
"menu_credits": cb_menu_credits,
|
"menu_credits": cb_menu_credits,
|
||||||
|
"menu_admins": cb_menu_admins,
|
||||||
"menu_remove": cb_menu_remove,
|
"menu_remove": cb_menu_remove,
|
||||||
"install_mode_lite": cb_install_mode_lite,
|
"install_mode_lite": cb_install_mode_lite,
|
||||||
"install_mode_pro": cb_install_mode_pro,
|
"install_mode_pro": cb_install_mode_pro,
|
||||||
@@ -1509,6 +1809,8 @@ def main() -> None:
|
|||||||
application.add_handler(CommandHandler("help", cmd_help))
|
application.add_handler(CommandHandler("help", cmd_help))
|
||||||
application.add_handler(CommandHandler("status", cmd_status))
|
application.add_handler(CommandHandler("status", cmd_status))
|
||||||
application.add_handler(CommandHandler("logs", cmd_logs))
|
application.add_handler(CommandHandler("logs", cmd_logs))
|
||||||
|
application.add_handler(CommandHandler("addadmin", cmd_addadmin))
|
||||||
|
application.add_handler(CommandHandler("deladmin", cmd_deladmin))
|
||||||
|
|
||||||
# Callback query handler (buttons)
|
# Callback query handler (buttons)
|
||||||
application.add_handler(CallbackQueryHandler(handle_callback))
|
application.add_handler(CallbackQueryHandler(handle_callback))
|
||||||
|
|||||||
198
install.sh
198
install.sh
@@ -112,8 +112,7 @@ show_main_menu() {
|
|||||||
|
|
||||||
if command -v qrencode &>/dev/null; then
|
if command -v qrencode &>/dev/null; then
|
||||||
echo ""
|
echo ""
|
||||||
echo ""
|
qrencode -t UTF8 -m 2 "$link" 2>/dev/null | while IFS= read -r qr_line; do
|
||||||
qrencode -t UTF8 -m 1 "$link" 2>/dev/null | while IFS= read -r qr_line; do
|
|
||||||
echo " ${qr_line}"
|
echo " ${qr_line}"
|
||||||
done
|
done
|
||||||
echo ""
|
echo ""
|
||||||
@@ -455,11 +454,21 @@ menu_status() {
|
|||||||
|
|
||||||
# ── Ссылка ───────────────────────────────────────────────────────────────────
|
# ── Ссылка ───────────────────────────────────────────────────────────────────
|
||||||
menu_link() {
|
menu_link() {
|
||||||
local secret port ip link
|
local secret port ip link mode domain
|
||||||
secret=$(get_config_value secret)
|
secret=$(get_config_value secret)
|
||||||
port=$(get_config_value port)
|
port=$(get_config_value port)
|
||||||
ip=$(get_server_ip)
|
ip=$(get_server_ip)
|
||||||
link=$(generate_proxy_link "$ip" "$port" "$secret")
|
mode=$(config_get mode 2>/dev/null || echo "lite")
|
||||||
|
domain=$(config_get domain 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
|
||||||
|
local domain_hex faketls_secret
|
||||||
|
domain_hex=$(printf '%s' "$domain" | xxd -p | tr -d '\n')
|
||||||
|
faketls_secret="ee${secret}${domain_hex}"
|
||||||
|
link="tg://proxy?server=${domain}&port=${port}&secret=${faketls_secret}"
|
||||||
|
else
|
||||||
|
link=$(generate_proxy_link "$ip" "$port" "$secret")
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${BOLD}${WHITE}🔗 Ссылка для подключения:${NC}"
|
echo -e " ${BOLD}${WHITE}🔗 Ссылка для подключения:${NC}"
|
||||||
@@ -474,18 +483,30 @@ menu_link() {
|
|||||||
|
|
||||||
# ── Поделиться ───────────────────────────────────────────────────────────────
|
# ── Поделиться ───────────────────────────────────────────────────────────────
|
||||||
menu_share() {
|
menu_share() {
|
||||||
local secret port ip link
|
local secret port ip link mode domain server_display
|
||||||
secret=$(get_config_value secret)
|
secret=$(get_config_value secret)
|
||||||
port=$(get_config_value port)
|
port=$(get_config_value port)
|
||||||
ip=$(get_server_ip)
|
ip=$(get_server_ip)
|
||||||
link=$(generate_proxy_link "$ip" "$port" "$secret")
|
mode=$(config_get mode 2>/dev/null || echo "lite")
|
||||||
|
domain=$(config_get domain 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
|
||||||
|
local domain_hex faketls_secret
|
||||||
|
domain_hex=$(printf '%s' "$domain" | xxd -p | tr -d '\n')
|
||||||
|
faketls_secret="ee${secret}${domain_hex}"
|
||||||
|
link="tg://proxy?server=${domain}&port=${port}&secret=${faketls_secret}"
|
||||||
|
server_display="$domain"
|
||||||
|
else
|
||||||
|
link=$(generate_proxy_link "$ip" "$port" "$secret")
|
||||||
|
server_display="$ip"
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${BOLD}📤 Перешлите это сообщение:${NC}"
|
echo -e " ${BOLD}📤 Перешлите это сообщение:${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo "🔐 MTProxy для Telegram (GoTelegram v${GOTELEGRAM_VERSION})"
|
echo "🔐 MTProxy для Telegram (GoTelegram v${GOTELEGRAM_VERSION})"
|
||||||
echo ""
|
echo ""
|
||||||
echo "🌍 Сервер: $ip"
|
echo "🌍 Сервер: $server_display"
|
||||||
echo "🔌 Порт: $port"
|
echo "🔌 Порт: $port"
|
||||||
echo ""
|
echo ""
|
||||||
echo "👉 Подключиться одним нажатием:"
|
echo "👉 Подключиться одним нажатием:"
|
||||||
@@ -778,14 +799,25 @@ bot_install() {
|
|||||||
[ -z "$token" ] && log_error "Токен не может быть пустым"
|
[ -z "$token" ] && log_error "Токен не может быть пустым"
|
||||||
done
|
done
|
||||||
|
|
||||||
echo -ne " ${WHITE}ID администратора (Enter = доступ для всех):${NC} "
|
echo ""
|
||||||
read -r admin_id
|
echo -e " ${WHITE}Как добавить администратора?${NC}"
|
||||||
|
echo -e " ${CYAN}1${NC}) Автоматически — бот определит ID при первом /start"
|
||||||
|
echo -e " ${CYAN}2${NC}) Вручную — ввести ID сейчас"
|
||||||
|
echo -ne " ${WHITE}Выбор [1]:${NC} "
|
||||||
|
read -r admin_mode
|
||||||
|
admin_mode="${admin_mode:-1}"
|
||||||
|
|
||||||
|
local admin_ids=""
|
||||||
|
if [ "$admin_mode" = "2" ]; then
|
||||||
|
echo -ne " ${WHITE}ID администраторов (через пробел/запятую):${NC} "
|
||||||
|
read -r admin_ids
|
||||||
|
admin_ids=$(echo "$admin_ids" | tr ' ' ',' | sed 's/,,*/,/g; s/^,//; s/,$//')
|
||||||
|
fi
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "BOT_TOKEN=$token"
|
echo "BOT_TOKEN=$token"
|
||||||
[ -n "$admin_id" ] && echo "ALLOWED_IDS=$admin_id"
|
[ -n "$admin_ids" ] && echo "ALLOWED_IDS=$admin_ids"
|
||||||
} > "$BOT_DIR/.env"
|
} > "$BOT_DIR/.env"
|
||||||
|
|
||||||
chmod 600 "$BOT_DIR/.env"
|
chmod 600 "$BOT_DIR/.env"
|
||||||
log_success ".env создан"
|
log_success ".env создан"
|
||||||
else
|
else
|
||||||
@@ -814,6 +846,59 @@ SVCEOF
|
|||||||
systemctl enable "$BOT_SERVICE" &>/dev/null
|
systemctl enable "$BOT_SERVICE" &>/dev/null
|
||||||
systemctl restart "$BOT_SERVICE" 2>/dev/null || systemctl start "$BOT_SERVICE"
|
systemctl restart "$BOT_SERVICE" 2>/dev/null || systemctl start "$BOT_SERVICE"
|
||||||
|
|
||||||
|
# Если авто-режим — ждём пока бот словит первого админа
|
||||||
|
local has_ids
|
||||||
|
has_ids=$(grep "^ALLOWED_IDS=" "$BOT_DIR/.env" 2>/dev/null | cut -d= -f2)
|
||||||
|
if [ -z "$has_ids" ]; then
|
||||||
|
echo ""
|
||||||
|
echo -e " ${YELLOW}╔══════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${BOLD}Ожидание администратора${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} Откройте бота в Telegram и отправьте ${CYAN}/start${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} Бот автоматически назначит вас администратором ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${DIM}Нажмите Ctrl+C чтобы пропустить${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}╚══════════════════════════════════════════════════════╝${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
|
||||||
|
local i=0
|
||||||
|
local waited=0
|
||||||
|
local max_wait=300 # 5 минут максимум
|
||||||
|
|
||||||
|
# Ловим Ctrl+C чтобы выйти из ожидания без убийства скрипта
|
||||||
|
local interrupted=0
|
||||||
|
trap 'interrupted=1' INT
|
||||||
|
|
||||||
|
while [ $waited -lt $max_wait ] && [ $interrupted -eq 0 ]; do
|
||||||
|
printf "\r ${CYAN}${frames[$i]}${NC} Ожидание... напишите /start боту (%d сек) " "$waited" >&2
|
||||||
|
i=$(( (i+1) % ${#frames[@]} ))
|
||||||
|
sleep 1
|
||||||
|
waited=$((waited + 1))
|
||||||
|
|
||||||
|
# Проверяем появился ли ALLOWED_IDS
|
||||||
|
has_ids=$(grep "^ALLOWED_IDS=" "$BOT_DIR/.env" 2>/dev/null | cut -d= -f2)
|
||||||
|
if [ -n "$has_ids" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
trap - INT
|
||||||
|
printf "\r\033[K" >&2 # очистить строку со спиннером
|
||||||
|
|
||||||
|
if [ -n "$has_ids" ]; then
|
||||||
|
echo ""
|
||||||
|
log_success "Администратор назначен!"
|
||||||
|
echo -e " ${WHITE}ID:${NC} ${GREEN}${has_ids}${NC}"
|
||||||
|
elif [ $interrupted -eq 1 ]; then
|
||||||
|
echo ""
|
||||||
|
log_warning "Пропущено. Добавить админа позже: меню → Telegram-бот → Настройки"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
log_warning "Таймаут (5 мин). Добавить админа: меню → Telegram-бот → Настройки"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
log_success "Бот установлен и запущен!"
|
log_success "Бот установлен и запущен!"
|
||||||
echo -e " ${DIM}Проверка: systemctl status $BOT_SERVICE${NC}"
|
echo -e " ${DIM}Проверка: systemctl status $BOT_SERVICE${NC}"
|
||||||
@@ -887,9 +972,10 @@ bot_edit_config() {
|
|||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
2)
|
2)
|
||||||
echo -ne " ${WHITE}ALLOWED_IDS (через запятую, пусто = все):${NC} "
|
echo -ne " ${WHITE}ALLOWED_IDS (через пробел/запятую, пусто = авто):${NC} "
|
||||||
read -r new_ids
|
read -r new_ids
|
||||||
new_ids=$(echo "$new_ids" | tr -d '[:space:]')
|
# Нормализуем: пробелы и запятые → запятые, убираем лишнее
|
||||||
|
new_ids=$(echo "$new_ids" | tr ' ' ',' | sed 's/,,*/,/g; s/^,//; s/,$//')
|
||||||
if grep -q "^ALLOWED_IDS=" "$BOT_DIR/.env" 2>/dev/null; then
|
if grep -q "^ALLOWED_IDS=" "$BOT_DIR/.env" 2>/dev/null; then
|
||||||
if [ -n "$new_ids" ]; then
|
if [ -n "$new_ids" ]; then
|
||||||
sed -i "s|^ALLOWED_IDS=.*|ALLOWED_IDS=$new_ids|" "$BOT_DIR/.env"
|
sed -i "s|^ALLOWED_IDS=.*|ALLOWED_IDS=$new_ids|" "$BOT_DIR/.env"
|
||||||
@@ -924,16 +1010,91 @@ bot_remove() {
|
|||||||
menu_promo() {
|
menu_promo() {
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${YELLOW}╔══════════════════════════════════════════════════════╗${NC}"
|
echo -e " ${YELLOW}╔══════════════════════════════════════════════════════╗${NC}"
|
||||||
echo -e " ${YELLOW}║${NC} ${BOLD}💰 ХОСТИНГ СО СКИДКОЙ ДО -60%${NC} ${YELLOW}║${NC}"
|
echo -e " ${YELLOW}║${NC} ${BOLD}💰 ХОСТИНГ #1 — СКИДКА ДО 60%${NC} ${YELLOW}║${NC}"
|
||||||
echo -e " ${YELLOW}║${NC} Ссылка: ${CYAN}https://vk.cc/ct29NQ${NC} ${YELLOW}║${NC}"
|
echo -e " ${YELLOW}║${NC} Ссылка: ${CYAN}https://vk.cc/ct29NQ${NC} ${YELLOW}║${NC}"
|
||||||
echo -e " ${YELLOW}║${NC} ${YELLOW}║${NC}"
|
echo -e " ${YELLOW}║${NC} ${YELLOW}║${NC}"
|
||||||
echo -e " ${YELLOW}║${NC} Промокоды: OFF60, antenka20, antenka6, antenka12 ${YELLOW}║${NC}"
|
echo -e " ${YELLOW}║${NC} ${WHITE}OFF60${NC} — 60% скидки на первый месяц ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${WHITE}antenka20${NC} — 20% + 3% при оплате за 3 месяца ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${WHITE}antenka6${NC} — 15% + 5% при оплате за 6 месяцев ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}╟──────────────────────────────────────────────────────╢${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${BOLD}💰 ХОСТИНГ #2 — СКИДКА ДО 60%${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} Ссылка: ${CYAN}https://vk.cc/cUxAhj${NC} ${YELLOW}║${NC}"
|
||||||
echo -e " ${YELLOW}║${NC} ${YELLOW}║${NC}"
|
echo -e " ${YELLOW}║${NC} ${YELLOW}║${NC}"
|
||||||
echo -e " ${YELLOW}║${NC} Донат: ${CYAN}https://pay.cloudtips.ru/p/7410814f${NC} ${YELLOW}║${NC}"
|
echo -e " ${YELLOW}║${NC} ${WHITE}OFF60${NC} — 60% скидки на первый месяц ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}╟──────────────────────────────────────────────────────╢${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${BOLD}☕ Донат / Чаевые${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${CYAN}https://pay.cloudtips.ru/p/7410814f${NC} ${YELLOW}║${NC}"
|
||||||
echo -e " ${YELLOW}╚══════════════════════════════════════════════════════╝${NC}"
|
echo -e " ${YELLOW}╚══════════════════════════════════════════════════════╝${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Проверка: показывать ли промо (раз в сутки) ────────────────────────────
|
||||||
|
should_show_promo() {
|
||||||
|
local stamp_file="$GOTELEGRAM_DIR/.promo_last_shown"
|
||||||
|
if [ ! -f "$stamp_file" ]; then
|
||||||
|
return 0 # никогда не показывали
|
||||||
|
fi
|
||||||
|
local last_shown now diff
|
||||||
|
last_shown=$(cat "$stamp_file" 2>/dev/null || echo "0")
|
||||||
|
last_shown="${last_shown//[^0-9]/}"
|
||||||
|
last_shown="${last_shown:-0}"
|
||||||
|
now=$(date +%s)
|
||||||
|
diff=$(( now - last_shown ))
|
||||||
|
# 86400 = 24 часа
|
||||||
|
[ "$diff" -ge 86400 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
mark_promo_shown() {
|
||||||
|
mkdir -p "$GOTELEGRAM_DIR"
|
||||||
|
date +%s > "$GOTELEGRAM_DIR/.promo_last_shown"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Промо с QR и задержкой (при установке + раз в сутки) ───────────────────
|
||||||
|
show_promo_with_qr() {
|
||||||
|
echo ""
|
||||||
|
echo -e " ${YELLOW}╔══════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${BOLD}💰 ХОСТИНГ #1 — СКИДКА ДО 60%${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} Ссылка: ${CYAN}https://vk.cc/ct29NQ${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${WHITE}OFF60${NC} — 60% скидки на первый месяц ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${WHITE}antenka20${NC} — 20% + 3% при оплате за 3 месяца ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${WHITE}antenka6${NC} — 15% + 5% при оплате за 6 месяцев ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}╟──────────────────────────────────────────────────────╢${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${BOLD}💰 ХОСТИНГ #2 — СКИДКА ДО 60%${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} Ссылка: ${CYAN}https://vk.cc/cUxAhj${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}║${NC} ${WHITE}OFF60${NC} — 60% скидки на первый месяц ${YELLOW}║${NC}"
|
||||||
|
echo -e " ${YELLOW}╚══════════════════════════════════════════════════════╝${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# QR-коды
|
||||||
|
if command -v qrencode &>/dev/null; then
|
||||||
|
echo -e " ${DIM}── QR: Хостинг #1 ──${NC}"
|
||||||
|
qrencode -t UTF8 -m 1 "https://vk.cc/ct29NQ" 2>/dev/null | while IFS= read -r qr_line; do
|
||||||
|
echo " $qr_line"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
echo -e " ${DIM}── QR: Хостинг #2 ──${NC}"
|
||||||
|
qrencode -t UTF8 -m 1 "https://vk.cc/cUxAhj" 2>/dev/null | while IFS= read -r qr_line; do
|
||||||
|
echo " $qr_line"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
echo -e " ${DIM}── QR: Чаевые / Донат ──${NC}"
|
||||||
|
qrencode -t UTF8 -m 1 "https://pay.cloudtips.ru/p/7410814f" 2>/dev/null | while IFS= read -r qr_line; do
|
||||||
|
echo " $qr_line"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
mark_promo_shown
|
||||||
|
|
||||||
|
# 5-секундная задержка с обратным отсчётом
|
||||||
|
for i in 5 4 3 2 1; do
|
||||||
|
echo -ne "\r ${DIM}Меню через ${i} сек...${NC} "
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo -ne "\r \r"
|
||||||
|
}
|
||||||
|
|
||||||
# ── Точка входа ──────────────────────────────────────────────────────────────
|
# ── Точка входа ──────────────────────────────────────────────────────────────
|
||||||
main() {
|
main() {
|
||||||
check_root
|
check_root
|
||||||
@@ -944,6 +1105,11 @@ main() {
|
|||||||
check_os
|
check_os
|
||||||
check_disk_space 500
|
check_disk_space 500
|
||||||
|
|
||||||
|
# Промо раз в сутки
|
||||||
|
if should_show_promo; then
|
||||||
|
show_promo_with_qr
|
||||||
|
fi
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
clear
|
clear
|
||||||
show_main_menu
|
show_main_menu
|
||||||
|
|||||||
132
install_gotelegram_bot.sh
Executable file
132
install_gotelegram_bot.sh
Executable file
@@ -0,0 +1,132 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# GoTelegram v2.2.1 — Установка 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"
|
||||||
|
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
echo -e "${RED}Запустите с sudo.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${CYAN}╔═══════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${CYAN}║${NC} ${GREEN}GoTelegram v2.2.1 — Установка бота${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)"
|
||||||
|
|
||||||
|
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.2.1 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"
|
||||||
|
|
||||||
|
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}"
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
# Цвета, логирование, спиннер, системные функции, совместимость с v1
|
# Цвета, логирование, спиннер, системные функции, совместимость с v1
|
||||||
|
|
||||||
# ── Версия ────────────────────────────────────────────────────────────────────
|
# ── Версия ────────────────────────────────────────────────────────────────────
|
||||||
GOTELEGRAM_VERSION="2.3.0"
|
GOTELEGRAM_VERSION="2.3.1"
|
||||||
GOTELEGRAM_NAME="GoTelegram"
|
GOTELEGRAM_NAME="GoTelegram"
|
||||||
|
|
||||||
# ── Пути ──────────────────────────────────────────────────────────────────────
|
# ── Пути ──────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -257,11 +257,21 @@ show_proxy_info() {
|
|||||||
port=$(get_config_value port "$config")
|
port=$(get_config_value port "$config")
|
||||||
mask_host=$(get_config_value mask_host "$config")
|
mask_host=$(get_config_value mask_host "$config")
|
||||||
ip=$(get_server_ip)
|
ip=$(get_server_ip)
|
||||||
link=$(generate_proxy_link "$ip" "$port" "$secret")
|
|
||||||
status=$(telemt_status)
|
status=$(telemt_status)
|
||||||
|
|
||||||
local mode
|
local mode domain
|
||||||
mode=$(config_get mode 2>/dev/null || echo "lite")
|
mode=$(config_get mode 2>/dev/null || echo "lite")
|
||||||
|
domain=$(config_get domain 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
# Pro-режим: ссылка с доменом и fake-TLS секретом
|
||||||
|
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
|
||||||
|
local domain_hex faketls_secret
|
||||||
|
domain_hex=$(printf '%s' "$domain" | xxd -p | tr -d '\n')
|
||||||
|
faketls_secret="ee${secret}${domain_hex}"
|
||||||
|
link="tg://proxy?server=${domain}&port=${port}&secret=${faketls_secret}"
|
||||||
|
else
|
||||||
|
link=$(generate_proxy_link "$ip" "$port" "$secret")
|
||||||
|
fi
|
||||||
|
|
||||||
local status_icon status_text
|
local status_icon status_text
|
||||||
case "$status" in
|
case "$status" in
|
||||||
@@ -274,7 +284,11 @@ show_proxy_info() {
|
|||||||
echo -e " ${BOLD}${WHITE}${status_icon} Статус прокси: ${status_text}${NC}"
|
echo -e " ${BOLD}${WHITE}${status_icon} Статус прокси: ${status_text}${NC}"
|
||||||
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}"
|
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}"
|
||||||
echo -e " ${WHITE}Ядро:${NC} telemt (Rust)"
|
echo -e " ${WHITE}Ядро:${NC} telemt (Rust)"
|
||||||
echo -e " ${WHITE}IP:${NC} ${CYAN}${ip}${NC}"
|
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}${port}${NC}"
|
||||||
echo -e " ${WHITE}Режим:${NC} ${CYAN}${mode}${NC}"
|
echo -e " ${WHITE}Режим:${NC} ${CYAN}${mode}${NC}"
|
||||||
echo -e " ${WHITE}Маскировка:${NC} ${CYAN}${mask_host}${NC}"
|
echo -e " ${WHITE}Маскировка:${NC} ${CYAN}${mask_host}${NC}"
|
||||||
|
|||||||
Reference in New Issue
Block a user