mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-05-19 14:36:05 +00:00
v2.3.1: admin management (auto-register first admin, /addadmin, /deladmin, menu button)
This commit is contained in:
@@ -64,15 +64,67 @@ TIP_LINK = "https://pay.cloudtips.ru/p/7410814f"
|
|||||||
PROMO_STAMP_FILE = "/opt/gotelegram/.promo_bot_last_shown"
|
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",
|
||||||
@@ -230,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
|
||||||
|
|
||||||
@@ -286,7 +358,10 @@ def get_main_menu() -> InlineKeyboardMarkup:
|
|||||||
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"),
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
@@ -299,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, promo once per day."""
|
"""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 = (
|
||||||
@@ -327,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")
|
||||||
|
|
||||||
@@ -1322,6 +1428,114 @@ 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
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -1463,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
|
||||||
@@ -1500,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,
|
||||||
@@ -1560,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))
|
||||||
|
|||||||
17
install.sh
17
install.sh
@@ -799,14 +799,20 @@ bot_install() {
|
|||||||
[ -z "$token" ] && log_error "Токен не может быть пустым"
|
[ -z "$token" ] && log_error "Токен не может быть пустым"
|
||||||
done
|
done
|
||||||
|
|
||||||
echo -ne " ${WHITE}ID администратора (Enter = доступ для всех):${NC} "
|
echo -ne " ${WHITE}ID администраторов (через пробел/запятую, Enter = авто):${NC} "
|
||||||
read -r admin_id
|
read -r admin_ids
|
||||||
|
# Нормализуем: пробелы и запятые → запятые
|
||||||
|
admin_ids=$(echo "$admin_ids" | tr ' ' ',' | sed 's/,,*/,/g; s/^,//; s/,$//')
|
||||||
|
|
||||||
{
|
{
|
||||||
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"
|
||||||
|
|
||||||
|
if [ -z "$admin_ids" ]; then
|
||||||
|
echo -e " ${YELLOW}Авто-режим: первый кто напишет /start станет админом${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
chmod 600 "$BOT_DIR/.env"
|
chmod 600 "$BOT_DIR/.env"
|
||||||
log_success ".env создан"
|
log_success ".env создан"
|
||||||
else
|
else
|
||||||
@@ -908,9 +914,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"
|
||||||
|
|||||||
Reference in New Issue
Block a user