import os import json import logging import requests import random from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup from telegram.ext import ( Application, CommandHandler, CallbackQueryHandler, ContextTypes, MessageHandler, filters, JobQueue ) # Настройка логирования logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) # ====================================================================== # --- ⚠️ КОНФИГУРАЦИЯ --- # В коде bot.py: TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN") WORKER_API_URL = "https://sk0lovek-aternosbot.hf.space" # ====================================================================== # Проверка конфигурации if not all([TELEGRAM_TOKEN, WORKER_API_URL]): logger.error("ОШИБКА: Не заданы все необходимые переменные конфигурации.") exit(1) # --- Глобальное состояние --- USER_DATA = {} DATA_FILE = "data.json" tg_app = None # ---------------------------------------------------------------------- # ФУНКЦИИ УПРАВЛЕНИЯ ДАННЫМИ # ---------------------------------------------------------------------- def load_data(): """Загружает данные пользователей из файла.""" global USER_DATA if os.path.exists(DATA_FILE): try: with open(DATA_FILE, 'r') as f: USER_DATA = json.load(f) # Инициализация недостающих полей для старых пользователей for chat_id in list(USER_DATA.keys()): if 'version' not in USER_DATA[chat_id]: USER_DATA[chat_id]['version'] = "1.20.1" if 'server_type' not in USER_DATA[chat_id]: USER_DATA[chat_id]['server_type'] = "vanilla" if 'state' not in USER_DATA[chat_id]: USER_DATA[chat_id]['state'] = "menu" except json.JSONDecodeError: logger.error("Ошибка декодирования JSON. Файл данных поврежден.") USER_DATA = {} else: logger.warning("Файл данных не найден. Создаю новое пустое состояние.") USER_DATA = {} def save_data(): """Сохраняет данные пользователей в файл.""" try: with open(DATA_FILE, 'w') as f: json.dump(USER_DATA, f, indent=4) except IOError as e: logger.error(f"Ошибка сохранения данных: {e}") # ---------------------------------------------------------------------- # ФУНКЦИИ API WORKER # ---------------------------------------------------------------------- async def start_bot_in_worker(chat_id: str, host: str, port: str, username: str, version: str, server_type: str) -> bool: """Отправляет запрос на запуск бота в Worker Service.""" url = f"{WORKER_API_URL}/api/start" payload = { "chatId": chat_id, "host": host, "port": port, "username": username, "version": version, "serverType": server_type } try: response = requests.post(url, json=payload, timeout=10) response.raise_for_status() logger.info(f"Worker API: Бот запущен для {chat_id}. Статус: {response.status_code}") return True except requests.exceptions.RequestException as e: logger.error(f"Ошибка при запросе к Worker API (START): {e}") return False async def stop_bot_in_worker(chat_id: str) -> bool: """Отправляет запрос на остановку бота в Worker Service.""" url = f"{WORKER_API_URL}/api/stop" payload = {"chatId": chat_id} try: response = requests.post(url, json=payload, timeout=5) response.raise_for_status() logger.info(f"Worker API: Бот остановлен для {chat_id}. Статус: {response.status_code}") return True except requests.exceptions.RequestException as e: logger.error(f"Ошибка при запросе к Worker API (STOP): {e}") return False async def get_bot_status(chat_id: str) -> bool: """Получает статус бота из Worker Service.""" url = f"{WORKER_API_URL}/api/status/{chat_id}" try: response = requests.get(url, timeout=5) response.raise_for_status() return response.json().get('isRunning', False) except requests.exceptions.RequestException as e: logger.error(f"Ошибка при запросе к Worker API (STATUS): {e}") return False async def send_command_to_bot(chat_id: str, command: str) -> bool: """Отправляет команду чата Mineflayer-боту.""" url = f"{WORKER_API_URL}/api/command" payload = {"chatId": chat_id, "command": command} try: response = requests.post(url, json=payload, timeout=5) response.raise_for_status() return True except requests.exceptions.RequestException as e: logger.error(f"Ошибка при запросе к Worker API (COMMAND): {e}") return False # ---------------------------------------------------------------------- # 🕵️ ANTI-KICK (ANTI-AFK) JOB # ---------------------------------------------------------------------- async def anti_afk_job(context: ContextTypes.DEFAULT_TYPE): """ Эта функция запускается каждые 45 секунд. Она проверяет всех активных ботов и отправляет сообщение в чат, чтобы сервер не кикнул за AFK. """ global USER_DATA # Фразы для спама, чтобы админы не сразу спалили, что это бот (или наоборот) afk_phrases = [ "Anti-AFK", "Stay alive", "Working...", "Not AFK", "Server check", "Bot active" ] # Проходимся по всем пользователям for chat_id in list(USER_DATA.keys()): try: # Спрашиваем у воркера: "Эй, бот для этого чела жив?" is_running = await get_bot_status(chat_id) if is_running: # Если бот жив, кидаем рандомную фразу в чат майнкрафта msg = random.choice(afk_phrases) # Можно также отправлять команду прыжка, если воркер поддерживает, # но чат - самый надежный способ. await send_command_to_bot(chat_id, msg) logger.info(f"[Anti-AFK] Сообщение '{msg}' отправлено для {chat_id}") except Exception as e: logger.error(f"[Anti-AFK] Ошибка для {chat_id}: {e}") # ---------------------------------------------------------------------- # ФУНКЦИИ TELEGRAM # ---------------------------------------------------------------------- def escape_markdown_v2(text: str, ignore_in_code_block: bool = False) -> str: """Экранирует специальные символы для MarkdownV2.""" specials = ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!'] escaped_text = "" in_code_block = False i = 0 while i < len(text): char = text[i] if char == '`': if text[i:i + 3] == '```': escaped_text += '```' i += 3 in_code_block = not in_code_block continue elif text[i:i + 1] == '`': escaped_text += '`' i += 1 in_code_block = not in_code_block continue if in_code_block and ignore_in_code_block: escaped_text += char elif char in specials: escaped_text += '\\' + char else: escaped_text += char i += 1 return escaped_text.replace(r'\\`', '`') def get_main_menu_keyboard(chat_id_str: str, is_running: bool, is_setting_server: bool = False, is_setting_version: bool = False, is_setting_type: bool = False) -> InlineKeyboardMarkup: """Генерирует основное меню.""" data = USER_DATA.get(chat_id_str, {}) host = data.get('host', 'Нет') port = data.get('port', '25565') host_port = f"{host}:{port}" username = data.get('username', 'MineflayerBot') version = data.get('version', '1.20.1') server_type = data.get('server_type', 'vanilla') status_text = "🟢 Бот Активен" if is_running else "🔴 Бот Неактивен" start_stop_text = "🛑 Остановить Бота" if is_running else "▶️ Запустить Бота" keyboard = [] keyboard.append([InlineKeyboardButton(status_text, callback_data='status')]) settings_buttons = [ InlineKeyboardButton(f"🔗 Сервер: {host_port}", callback_data='set_server' if not is_setting_server else 'back_to_menu'), InlineKeyboardButton(f"👤 Имя: {username}", callback_data='set_username'), ] keyboard.append(settings_buttons) core_buttons = [ InlineKeyboardButton(f"📦 Ядро: {server_type.upper()}", callback_data='set_server_type' if not is_setting_type else 'back_to_menu'), InlineKeyboardButton(f"🕹️ Версия: {version}", callback_data='set_version' if not is_setting_version else 'back_to_menu'), ] keyboard.append(core_buttons) keyboard.append([InlineKeyboardButton(start_stop_text, callback_data='toggle_bot')]) ai_buttons = [ InlineKeyboardButton("🤖 AI: !start", callback_data='ai_start'), InlineKeyboardButton("⚔️ AI: !stop", callback_data='ai_stop') ] keyboard.append(ai_buttons) if is_setting_server or is_setting_version or is_setting_type: keyboard.append([InlineKeyboardButton("🔙 Назад в меню", callback_data='back_to_menu')]) return InlineKeyboardMarkup(keyboard) async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: chat_id = str(update.effective_chat.id) if chat_id not in USER_DATA: USER_DATA[chat_id] = { "host": "localhost", "port": "25565", "username": f"MineflayerBot_{chat_id[:4]}", "version": "1.20.1", "server_type": "vanilla", "state": "menu" } save_data() is_running = await get_bot_status(chat_id) current_state = USER_DATA[chat_id]['state'] is_setting_server = current_state == 'setting_host_port' is_setting_version = current_state == 'setting_version' is_setting_type = current_state == 'setting_server_type' reply_markup = get_main_menu_keyboard(chat_id, is_running, is_setting_server, is_setting_version, is_setting_type) data = USER_DATA[chat_id] host_esc = escape_markdown_v2(data['host'], ignore_in_code_block=True) port_esc = escape_markdown_v2(data['port'], ignore_in_code_block=True) username_esc = escape_markdown_v2(data['username'], ignore_in_code_block=True) version_esc = escape_markdown_v2(data['version'], ignore_in_code_block=True) server_type_esc = escape_markdown_v2(data['server_type'].upper(), ignore_in_code_block=True) message = ( "⚙️ **Главное меню управления Mineflayer Bot**\n\n" f"🔗 **Сервер:** `{host_esc}:{port_esc}`\n" f"👤 **Имя:** `{username_esc}`\n" f"📦 **Ядро:** `{server_type_esc}`\n" f"🕹️ **Версия:** `{version_esc}`\n" f"🚦 **Статус:** {'🟢 Активен' if is_running else '🔴 Неактивен'}\n\n" f"_Автоматический Anti\\-Kick \\(спам в чат\\) включен\\._" ) if update.callback_query is None: await update.message.reply_text(message, reply_markup=reply_markup, parse_mode='MarkdownV2') else: try: await update.callback_query.edit_message_text(message, reply_markup=reply_markup, parse_mode='MarkdownV2') except Exception: pass async def set_server_type_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: chat_id = str(update.effective_chat.id) USER_DATA[chat_id]['state'] = 'setting_server_type' save_data() keyboard = [ [InlineKeyboardButton("Ванильный (Vanilla)", callback_data='type_vanilla')], [InlineKeyboardButton("Paper/Spigot (Плагины)", callback_data='type_spigot')], [InlineKeyboardButton("Forge (Моды)", callback_data='type_forge')], [InlineKeyboardButton("Fabric (Моды)", callback_data='type_fabric')], [InlineKeyboardButton("🔙 Назад в меню", callback_data='back_to_menu')] ] reply_markup = InlineKeyboardMarkup(keyboard) await update.callback_query.edit_message_text( "📦 **Выберите тип ядра сервера:**\n\n" "*\\(Это важно для правильного подключения к серверам с модами или плагинами\\.\\)*", reply_markup=reply_markup, parse_mode='MarkdownV2' ) async def setserver_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: chat_id = str(update.effective_chat.id) USER_DATA[chat_id]['state'] = 'setting_host_port' save_data() message_text = ( "🔗 **Введите адрес сервера \\(host:port\\)**:\n\n" "Пример: `my_server\\.aternos\\.me:25565`\n\n" "*\\(Вы можете просто отправить новый адрес в следующем сообщении\\.\\)*" ) if update.callback_query: await update.callback_query.edit_message_text(message_text, parse_mode='MarkdownV2') else: await update.message.reply_text(message_text, parse_mode='MarkdownV2') async def setversion_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: chat_id = str(update.effective_chat.id) USER_DATA[chat_id]['state'] = 'setting_version' save_data() keyboard = [ [InlineKeyboardButton("1.20.1", callback_data='v_1.20.1'), InlineKeyboardButton("1.19.4", callback_data='v_1.19.4')], [InlineKeyboardButton("1.18.2", callback_data='v_1.18.2'), InlineKeyboardButton("1.17.1", callback_data='v_1.17.1')], [InlineKeyboardButton("1.16.5", callback_data='v_1.16.5'), InlineKeyboardButton("1.12.2", callback_data='v_1.12.2')], [InlineKeyboardButton("🔙 Назад в меню", callback_data='back_to_menu')] ] reply_markup = InlineKeyboardMarkup(keyboard) await update.callback_query.edit_message_text( "🕹️ **Выберите версию Minecraft**:\n\n*\\(Это обязательно должно соответствовать версии сервера\\.\\)*", reply_markup=reply_markup, parse_mode='MarkdownV2' ) async def set_username_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: chat_id = str(update.effective_chat.id) USER_DATA[chat_id]['state'] = 'setting_username' save_data() await update.callback_query.edit_message_text( "👤 **Введите новое имя для бота:**\n\nПример: `MyFarmingBot`\n\n*\\(Имя должно соответствовать правилам Minecraft\\.\\)*", parse_mode='MarkdownV2' ) async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: query = update.callback_query await query.answer() chat_id = str(query.message.chat.id) data = USER_DATA.get(chat_id, {}) callback_data = query.data if callback_data == 'back_to_menu': USER_DATA[chat_id]['state'] = 'menu' save_data() await start_command(update, context) return if callback_data == 'set_server': await setserver_command(update, context) return if callback_data == 'set_version': await setversion_command(update, context) return if callback_data == 'set_username': await set_username_command(update, context) return if callback_data == 'set_server_type': await set_server_type_command(update, context) return if callback_data.startswith('type_'): server_type = callback_data.split('_')[1] if server_type == 'spigot': server_type = 'vanilla' USER_DATA[chat_id]['server_type'] = server_type USER_DATA[chat_id]['state'] = 'menu' save_data() await start_command(update, context) return if callback_data.startswith('v_'): version = callback_data.split('_')[1] USER_DATA[chat_id]['version'] = version USER_DATA[chat_id]['state'] = 'menu' save_data() await start_command(update, context) return if callback_data == 'ai_start': if await send_command_to_bot(chat_id, "!start"): await query.edit_message_text( "🤖 **AI Gunner запущен\\!** Он начнет поиск оружия и мобов\\. *\\(Через несколько секунд бот ответит в чате сервера\\)*", parse_mode='MarkdownV2') else: await query.edit_message_text( "❌ **Ошибка:** Не удалось отправить команду \\!start\\. Убедитесь, что бот запущен\\.", parse_mode='MarkdownV2') return if callback_data == 'ai_stop': if await send_command_to_bot(chat_id, "!stop"): await query.edit_message_text("⚔️ **AI Gunner остановлен\\!** Бот переходит в режим ожидания\\.", parse_mode='MarkdownV2') else: await query.edit_message_text( "❌ **Ошибка:** Не удалось отправить команду \\!stop\\. Убедитесь, что бот запущен\\.", parse_mode='MarkdownV2') return if callback_data == 'toggle_bot': is_running = await get_bot_status(chat_id) if is_running: if await stop_bot_in_worker(chat_id): message = "🛑 **Бот остановлен\\!**" else: message = "❌ **Ошибка:** Не удалось отправить команду остановки Worker Service\\." else: host = data.get('host') port = data.get('port') version = data.get('version') server_type = data.get('server_type') if not host or host == 'localhost': await query.edit_message_text("❌ **Ошибка:** Сначала укажите адрес сервера через '🔗 Сервер'\\.", parse_mode='MarkdownV2') return if await start_bot_in_worker(chat_id, host, port, data.get('username'), version, server_type): host_esc = escape_markdown_v2(host, ignore_in_code_block=True) port_esc = escape_markdown_v2(port, ignore_in_code_block=True) version_esc = escape_markdown_v2(version, ignore_in_code_block=True) server_type_esc = escape_markdown_v2(server_type.upper(), ignore_in_code_block=True) message = f"▶️ **Бот запущен\\!** Попытка подключения к `{host_esc}:{port_esc}` \\(Версия: `{version_esc}`, Ядро: `{server_type_esc}`\\)\\. Ожидайте уведомления о статусе\\." else: message = "❌ **Ошибка:** Не удалось отправить команду запуска Worker Service\\. Проверьте логи или статус Worker'а\\." USER_DATA[chat_id]['state'] = 'menu' save_data() await query.message.reply_text(message, parse_mode='MarkdownV2') await start_command(update, context) return if callback_data == 'status': await start_command(update, context) return async def text_message_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: chat_id = str(update.effective_chat.id) user_input = update.message.text.strip() data = USER_DATA.get(chat_id) if not data: await start_command(update, context) return state = data.get('state', 'menu') if state == 'setting_host_port': if ':' in user_input: parts = user_input.split(':') host = parts[0] port = parts[1] if len(parts) > 1 else "25565" if not port.isdigit(): await update.message.reply_text("❌ **Ошибка:** Порт должен быть числом\\.", parse_mode='MarkdownV2') return USER_DATA[chat_id]['host'] = host USER_DATA[chat_id]['port'] = port USER_DATA[chat_id]['state'] = 'menu' save_data() host_esc = escape_markdown_v2(host, ignore_in_code_block=True) port_esc = escape_markdown_v2(port, ignore_in_code_block=True) await update.message.reply_text(f"✅ Сервер установлен: `{host_esc}:{port_esc}`", parse_mode='MarkdownV2') await start_command(update, context) else: await update.message.reply_text("❌ **Ошибка:** Введите в формате `host:port`\\.", parse_mode='MarkdownV2') elif state == 'setting_username': if 3 <= len(user_input) <= 16 and all(c.isalnum() or c in '_-' for c in user_input): USER_DATA[chat_id]['username'] = user_input USER_DATA[chat_id]['state'] = 'menu' save_data() username_esc = escape_markdown_v2(user_input, ignore_in_code_block=True) await update.message.reply_text(f"✅ Имя установлено: `{username_esc}`", parse_mode='MarkdownV2') await start_command(update, context) else: await update.message.reply_text( "❌ **Ошибка:** Имя должно быть от 3 до 16 символов и содержать только латинские буквы, цифры, \\_ или \\-\\.", parse_mode='MarkdownV2') elif state == 'setting_version': await update.message.reply_text( "⚠️ **Используйте кнопки для выбора версии\\.** Нажмите /menu и выберите '🕹️ Версия'\\.", parse_mode='MarkdownV2') elif state == 'setting_server_type': await update.message.reply_text( "⚠️ **Используйте кнопки для выбора типа ядра\\.** Нажмите /menu и выберите '📦 Ядро'\\.", parse_mode='MarkdownV2') elif state == 'menu': is_running = await get_bot_status(chat_id) if is_running: if await send_command_to_bot(chat_id, user_input): user_input_esc = escape_markdown_v2(user_input, ignore_in_code_block=True) await update.message.reply_text(f"💬 Команда отправлена боту: `{user_input_esc}`", parse_mode='MarkdownV2') else: await update.message.reply_text( "❌ **Ошибка:** Не удалось отправить команду Worker Service\\. Проверьте логи Worker'а\\.", parse_mode='MarkdownV2') else: await update.message.reply_text("🤖 Бот не запущен\\. Запустите его через /menu\\.", parse_mode='MarkdownV2') # ---------------------------------------------------------------------- # ТОЧКА ВХОДА (POLLING) # ---------------------------------------------------------------------- def main(): global tg_app load_data() tg_app = Application.builder().token(TELEGRAM_TOKEN).build() # Регистрация команд и обработчиков tg_app.add_handler(CommandHandler(["start", "menu"], start_command)) tg_app.add_handler(CallbackQueryHandler(button_callback)) tg_app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, text_message_handler)) # --- ⚠️ РЕГИСТРАЦИЯ ANTI-AFK ЗАДАЧИ --- # Запускается каждые 45 секунд, первый запуск через 10 секунд tg_app.job_queue.run_repeating(anti_afk_job, interval=45, first=10) # -------------------------------------- logger.info("Бот запущен...") tg_app.run_polling(drop_pending_updates=True) if __name__ == '__main__': main()