From 2a89917c446041f4882dcf7101f76e5158fc1e6b Mon Sep 17 00:00:00 2001 From: apuc Date: Sun, 3 May 2026 15:20:59 +0300 Subject: [PATCH] Some fix --- bot.py | 465 ++++++++++++++++++++++++++++------------------- config.py | 5 +- requirements.txt | 5 +- 3 files changed, 285 insertions(+), 190 deletions(-) diff --git a/bot.py b/bot.py index 6d947e5..ab92075 100644 --- a/bot.py +++ b/bot.py @@ -5,18 +5,18 @@ import sys import logging import base64 from datetime import datetime -from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram import Update from telegram.ext import ( Application, CommandHandler, MessageHandler, - CallbackQueryHandler, ConversationHandler, filters, ContextTypes ) from telegram.request import HTTPXRequest import httpx +from httpx_socks import AsyncProxyTransport from config import TELEGRAM_BOT_TOKEN, PIApi_API_KEY, PIApi_BASE_URL, PROXY_URL, PROXY_TYPE from database import init_db, save_user, save_image_record, update_image_record, get_user_images @@ -24,75 +24,116 @@ import aiosqlite # Настройка логирования logging.basicConfig( - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + format='%(asctime)s - %(levelname)s - %(message)s', level=logging.INFO ) logger = logging.getLogger(__name__) -# Состояния для ConversationHandler +# Состояния AWAITING_PROMPT = 1 -# Создаем директории +# Директории UPLOAD_DIR = "uploads" PROCESSED_DIR = "processed" os.makedirs(UPLOAD_DIR, exist_ok=True) os.makedirs(PROCESSED_DIR, exist_ok=True) -# Глобальная сессия -session = None - - -async def get_session(): - """Создание aiohttp сессии""" - global session - if session is None: - connector = None - if PROXY_URL: - # Создаем коннектор с прокси для aiohttp - from aiohttp_socks import ProxyConnector - try: - connector = ProxyConnector.from_url(PROXY_URL) - logger.info(f"🔌 Создан SOCKS коннектор для aiohttp") - except: - connector = aiohttp.TCPConnector() - else: - connector = aiohttp.TCPConnector() - - session = aiohttp.ClientSession(connector=connector) - return session - - -async def close_session(): - """Закрытие aiohttp сессии""" - global session - if session: - await session.close() - session = None - def get_telegram_request(): - """Правильная настройка прокси для Telegram""" - if PROXY_URL: - logger.info(f"🔌 Настройка прокси для Telegram: {PROXY_URL}") - try: - # Используем монтированный транспорт - transport = httpx.AsyncHTTPTransport(proxy=PROXY_URL) + """Настройка SOCKS5 прокси для Telegram с использованием httpx_socks""" + if not PROXY_URL: + return None + + logger.info(f"🔌 Настройка SOCKS5 прокси для Telegram") + + try: + # Парсим прокси URL + import re + match = re.search(r'socks5://([^:]+):([^@]+)@([^:]+):(\d+)', PROXY_URL) + + if match: + username = match.group(1) + password = match.group(2) + host = match.group(3) + port = int(match.group(4)) + + logger.info(f"🔌 Прокси: {host}:{port}") + + # Создаем транспорт с SOCKS5 прокси + transport = AsyncProxyTransport.from_url( + f"socks5://{username}:{password}@{host}:{port}" + ) request = HTTPXRequest(transport=transport) - logger.info("✅ Прокси настроен успешно") + logger.info("✅ SOCKS5 прокси настроен успешно") return request - except Exception as e: - logger.error(f"Ошибка настройки прокси: {e}") - # Возвращаем обычный запрос без прокси - return HTTPXRequest() - else: - logger.info("🔌 Прокси не используется") - return HTTPXRequest() + else: + # Пробуем без авторизации + from urllib.parse import urlparse + parsed = urlparse(PROXY_URL) + host = parsed.hostname + port = parsed.port + + transport = AsyncProxyTransport.from_url(f"socks5://{host}:{port}") + request = HTTPXRequest(transport=transport) + logger.info("✅ SOCKS5 прокси настроен (без авторизации)") + return request + + except Exception as e: + logger.error(f"Ошибка настройки: {e}") + return None + + +async def test_telegram_connection(): + """Тест подключения к Telegram через прокси""" + if not PROXY_URL: + logger.error("Прокси не настроен") + return False + + logger.info("Тестируем подключение к Telegram через SOCKS5 прокси...") + + try: + # Используем aiohttp с socks прокси + from aiohttp_socks import ProxyConnector + + # Парсим прокси + import re + match = re.search(r'socks5://([^:]+):([^@]+)@([^:]+):(\d+)', PROXY_URL) + + if match: + username = match.group(1) + password = match.group(2) + host = match.group(3) + port = int(match.group(4)) + + connector = ProxyConnector.from_url(f"socks5://{username}:{password}@{host}:{port}") + else: + from urllib.parse import urlparse + parsed = urlparse(PROXY_URL) + connector = ProxyConnector.from_url(f"socks5://{parsed.hostname}:{parsed.port}") + + async with aiohttp.ClientSession(connector=connector) as session: + url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/getMe" + async with session.get(url, timeout=15) as response: + if response.status == 200: + data = await response.json() + if data.get('ok'): + logger.info(f"✅ Подключение работает! Бот: @{data['result']['username']}") + return True + else: + logger.error(f"Ошибка API: {data}") + return False + else: + logger.error(f"HTTP {response.status}") + return False + except Exception as e: + logger.error(f"Ошибка: {e}") + return False async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Обработчик команды /start""" - logger.info(f"✅ Получена команда /start от {update.effective_user.id}") + """Обработчик /start""" user = update.effective_user + logger.info(f"Start от {user.id}") await save_user( user_id=user.id, @@ -101,169 +142,179 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): last_name=user.last_name ) - welcome_text = ( + await update.message.reply_text( f"👋 Привет, {user.first_name}!\n\n" f"Я бот для обработки фотографий через нейросеть.\n\n" - f"📸 Просто отправь мне фото и напиши, как его изменить!\n\n" - f"🔧 Команды:\n" + f"📸 Отправь мне фото и напиши, как его изменить!\n\n" + f"Команды:\n" f"/start - Приветствие\n" f"/test - Проверка работы\n" - f"/help - Помощь\n" f"/history - История обработок" ) - await update.message.reply_text(welcome_text) async def test_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """Тестовая команда""" - logger.info(f"✅ Получена команда /test от {update.effective_user.id}") - await update.message.reply_text("✅ Бот работает! Команды обрабатываются нормально.") + logger.info(f"Test от {update.effective_user.id}") + await update.message.reply_text("✅ Бот работает через SOCKS5 прокси!") async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Обработчик команды /help""" - logger.info(f"✅ Получена команда /help от {update.effective_user.id}") - help_text = ( - "📖 **Инструкция:**\n\n" - "1️⃣ Отправь фото\n" - "2️⃣ Напиши текстовое описание изменений\n" - "3️⃣ Получи результат!\n\n" - "**Примеры промптов:**\n" - "- 'сделай фон пляжем'\n" - "- 'добавь солнечные очки'\n" - "- 'преврати в рисунок акварелью'\n\n" - "📊 /history - История\n" - "🔧 /test - Проверка" + """Помощь""" + await update.message.reply_text( + "📖 Инструкция:\n\n" + "1. Отправь фото\n" + "2. Напиши промпт (например: 'сделай фон пляжем')\n" + "3. Получи результат!\n\n" + "Команды: /start, /test, /history" ) - await update.message.reply_text(help_text, parse_mode='Markdown') async def handle_photo(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Обработчик получения фото""" - logger.info(f"✅ Получено фото от {update.effective_user.id}") + """Обработка фото""" user = update.effective_user + logger.info(f"Фото от {user.id}") photo_file = await update.message.photo[-1].get_file() file_id = photo_file.file_id + # Сохраняем фото timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') - local_filename = f"user_{user.id}_{timestamp}.jpg" - local_path = os.path.join(UPLOAD_DIR, local_filename) - + local_path = os.path.join(UPLOAD_DIR, f"user_{user.id}_{timestamp}.jpg") await photo_file.download_to_drive(local_path) + # Запись в БД image_id = await save_image_record( user_id=user.id, original_file_id=file_id, original_url=local_path ) - context.user_data['current_image_id'] = image_id - context.user_data['current_file_path'] = local_path + context.user_data['image_id'] = image_id + context.user_data['image_path'] = local_path await update.message.reply_text( "📸 Фото получено!\n\n" - "Теперь напиши, как изменить это фото.\n" - "❌ Для отмены отправь /cancel" + "Напиши, как изменить это фото.\n" + "Например: 'сделай фон пляжем' или 'преврати в рисунок'\n\n" + "/cancel - отмена" ) return AWAITING_PROMPT async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Отмена операции""" - logger.info(f"Отмена операции от {update.effective_user.id}") - if 'current_file_path' in context.user_data: - file_path = context.user_data['current_file_path'] - if os.path.exists(file_path): - os.remove(file_path) + """Отмена""" + if 'image_path' in context.user_data: + path = context.user_data['image_path'] + if os.path.exists(path): + os.remove(path) await update.message.reply_text("Операция отменена.") context.user_data.clear() return ConversationHandler.END -async def process_image_with_prompt(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Обработка изображения через PiAPI""" +async def process_image(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработка через PiAPI""" prompt = update.message.text user = update.effective_user - logger.info(f"Получен промпт от {user.id}: {prompt}") + logger.info(f"Промпт от {user.id}: {prompt[:50]}") - image_id = context.user_data.get('current_image_id') - file_path = context.user_data.get('current_file_path') + image_id = context.user_data.get('image_id') + image_path = context.user_data.get('image_path') - if not image_id or not file_path: - await update.message.reply_text("❌ Ошибка: изображение не найдено") + if not image_id or not image_path: + await update.message.reply_text("❌ Ошибка: фото не найдено") return ConversationHandler.END status_msg = await update.message.reply_text( - f"🎨 Обрабатываю...\n\nПромпт: {prompt}\n\n⏳ Обычно это занимает 10-30 секунд") + f"🎨 Обрабатываю...\n\n" + f"📝 Промпт: {prompt}\n\n" + f"⏳ Это займет 10-30 секунд" + ) try: async with aiosqlite.connect("bot_database.db") as db: await db.execute("UPDATE images SET prompt = ? WHERE id = ?", (prompt, image_id)) await db.commit() - result_url = await call_piapi_image_edit(file_path, prompt) + result_url = await call_piapi_with_proxy(image_path, prompt) if result_url: - result_filename = f"user_{user.id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}_result.jpg" - result_path = os.path.join(PROCESSED_DIR, result_filename) + result_path = os.path.join( + PROCESSED_DIR, + f"user_{user.id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}_result.jpg" + ) - current_session = await get_session() - async with current_session.get(result_url) as resp: - if resp.status == 200: - with open(result_path, "wb") as f: - f.write(await resp.read()) + # Скачиваем результат через SOCKS5 + from aiohttp_socks import ProxyConnector + import re - # Удаляем сообщение о статусе - await status_msg.delete() + match = re.search(r'socks5://([^:]+):([^@]+)@([^:]+):(\d+)', PROXY_URL) + if match: + username = match.group(1) + password = match.group(2) + host = match.group(3) + port = int(match.group(4)) + connector = ProxyConnector.from_url(f"socks5://{username}:{password}@{host}:{port}") + else: + from urllib.parse import urlparse + parsed = urlparse(PROXY_URL) + connector = ProxyConnector.from_url(f"socks5://{parsed.hostname}:{parsed.port}") - # Отправляем результат - with open(result_path, "rb") as photo: - await update.message.reply_photo( - photo=photo, - caption=f"✅ Готово!\n\n✨ Промпт: {prompt}" + async with aiohttp.ClientSession(connector=connector) as session: + async with session.get(result_url) as resp: + if resp.status == 200: + with open(result_path, "wb") as f: + f.write(await resp.read()) + + await status_msg.delete() + with open(result_path, "rb") as photo: + await update.message.reply_photo( + photo=photo, + caption=f"✅ Готово!\n\n✨ {prompt}" + ) + + msg = await update.message.reply_photo(photo=open(result_path, "rb")) + await update_image_record( + image_id=image_id, + processed_file_id=msg.photo[-1].file_id, + processed_url=result_path, + status='completed' ) - # Сохраняем file_id - msg = await update.message.reply_photo(photo=open(result_path, "rb")) - processed_file_id = msg.photo[-1].file_id - - await update_image_record( - image_id=image_id, - processed_file_id=processed_file_id, - processed_url=result_path, - status='completed' - ) - else: - raise Exception("Ошибка загрузки результата") + logger.info(f"Обработка успешна для {user.id}") + else: + raise Exception(f"HTTP {resp.status}") else: raise Exception("Не получен URL результата") except Exception as e: - error_msg = str(e) - logger.error(f"Ошибка: {error_msg}") - await status_msg.edit_text(f"❌ Ошибка: {error_msg}\n\nПопробуйте еще раз или отправьте другое фото.") + logger.error(f"Ошибка: {e}") + await status_msg.edit_text(f"❌ Ошибка: {str(e)}\n\nПопробуйте еще раз.") await update_image_record(image_id=image_id, status='failed') + if os.path.exists(image_path): + os.remove(image_path) + context.user_data.clear() return ConversationHandler.END -async def call_piapi_image_edit(image_path: str, prompt: str) -> str: - """Вызов PiAPI""" +async def call_piapi_with_proxy(image_path: str, prompt: str) -> str: + """Вызов PiAPI через SOCKS5 прокси""" if not PIApi_API_KEY: raise Exception("PIApi_API_KEY не настроен") + with open(image_path, "rb") as f: + image_base64 = base64.b64encode(f.read()).decode('utf-8') + headers = { "x-api-key": PIApi_API_KEY, "Content-Type": "application/json" } - with open(image_path, "rb") as image_file: - image_base64 = base64.b64encode(image_file.read()).decode('utf-8') - payload = { "model": "black-forest-labs/FLUX.1-dev", "task_type": "image-to-image", @@ -276,50 +327,87 @@ async def call_piapi_image_edit(image_path: str, prompt: str) -> str: } } - current_session = await get_session() + # Создаем SOCKS5 коннектор + from aiohttp_socks import ProxyConnector + import re - async with current_session.post(PIApi_BASE_URL, headers=headers, json=payload, timeout=30) as response: - if response.status != 200: - raise Exception(f"HTTP {response.status}") + match = re.search(r'socks5://([^:]+):([^@]+)@([^:]+):(\d+)', PROXY_URL) + if match: + username = match.group(1) + password = match.group(2) + host = match.group(3) + port = int(match.group(4)) + connector = ProxyConnector.from_url(f"socks5://{username}:{password}@{host}:{port}") + else: + from urllib.parse import urlparse + parsed = urlparse(PROXY_URL) + connector = ProxyConnector.from_url(f"socks5://{parsed.hostname}:{parsed.port}") - data = await response.json() - if data.get('code') != 200: - raise Exception(f"API Error: {data.get('message')}") + async with aiohttp.ClientSession(connector=connector) as session: + async with session.post( + PIApi_BASE_URL, + headers=headers, + json=payload, + timeout=30 + ) as response: + if response.status != 200: + text = await response.text() + raise Exception(f"HTTP {response.status}: {text[:100]}") - task_id = data['data']['task_id'] + data = await response.json() + if data.get('code') != 200: + raise Exception(f"API Error: {data.get('message')}") - result_url = await poll_task_status(task_id, headers) - return result_url + task_id = data['data']['task_id'] + logger.info(f"Создана задача: {task_id}") + + return await poll_task_with_proxy(task_id) -async def poll_task_status(task_id: str, headers: dict, max_attempts: int = 60) -> str: - """Ожидание завершения""" +async def poll_task_with_proxy(task_id: str, max_attempts: int = 60) -> str: + """Ожидание завершения задачи через SOCKS5 прокси""" get_url = f"https://api.piapi.ai/api/v1/task/{task_id}" - current_session = await get_session() + headers = {"x-api-key": PIApi_API_KEY} + + from aiohttp_socks import ProxyConnector + import re + + match = re.search(r'socks5://([^:]+):([^@]+)@([^:]+):(\d+)', PROXY_URL) + if match: + username = match.group(1) + password = match.group(2) + host = match.group(3) + port = int(match.group(4)) + connector = ProxyConnector.from_url(f"socks5://{username}:{password}@{host}:{port}") + else: + from urllib.parse import urlparse + parsed = urlparse(PROXY_URL) + connector = ProxyConnector.from_url(f"socks5://{parsed.hostname}:{parsed.port}") + + async with aiohttp.ClientSession(connector=connector) as session: + for attempt in range(max_attempts): + try: + async with session.get(get_url, headers=headers) as response: + if response.status != 200: + await asyncio.sleep(2) + continue + + data = await response.json() + status = data.get('data', {}).get('status') + + if status == 'completed': + output = data.get('data', {}).get('output', {}) + result_url = output.get('url') or output.get('image') + if result_url: + logger.info(f"Задача {task_id} выполнена") + return result_url + elif status == 'failed': + raise Exception("Задача не выполнена") - for attempt in range(max_attempts): - try: - async with current_session.get(get_url, headers=headers) as response: - if response.status != 200: await asyncio.sleep(2) - continue - - data = await response.json() - status = data.get('data', {}).get('status') - logger.info(f"Статус задачи {task_id}: {status} (попытка {attempt + 1})") - - if status == 'completed': - output = data.get('data', {}).get('output', {}) - result_url = output.get('url') or output.get('image') - if result_url: - return result_url - elif status == 'failed': - raise Exception("Задача не выполнена") - + except Exception as e: + logger.error(f"Ошибка опроса: {e}") await asyncio.sleep(2) - except Exception as e: - logger.error(f"Ошибка: {e}") - await asyncio.sleep(2) raise Exception("Превышено время ожидания") @@ -330,19 +418,17 @@ async def history_command(update: Update, context: ContextTypes.DEFAULT_TYPE): images = await get_user_images(user.id, limit=5) if not images: - await update.message.reply_text( - "📭 У вас пока нет обработанных изображений.\n\nОтправьте фото для начала работы!") + await update.message.reply_text("📭 Нет обработанных изображений\n\nОтправьте фото для начала работы!") return - text = "🖼 **Ваши последние обработки:**\n\n" + text = "🖼 **История обработок:**\n\n" for i, img in enumerate(images, 1): - status_emoji = "✅" if img['status'] == 'completed' else "❌" if img['status'] == 'failed' else "⏳" + status = "✅" if img['status'] == 'completed' else "❌" date = img['created_at'][:16].replace('T', ' ') - prompt_preview = img['prompt'][:40] + "..." if img['prompt'] and len(img['prompt']) > 40 else img[ - 'prompt'] or "без промпта" - text += f"{status_emoji} **{i}.** {date}\n 📝 {prompt_preview}\n\n" + prompt = img['prompt'][:35] + "..." if img['prompt'] and len(img['prompt']) > 35 else img[ + 'prompt'] or "без промпта" + text += f"{status} **{i}.** {date}\n 📝 {prompt}\n\n" - text += "💡 Отправьте новое фото для обработки!" await update.message.reply_text(text, parse_mode='Markdown') @@ -356,37 +442,48 @@ async def error_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): async def main(): """Запуск бота""" print("\n" + "=" * 50) - print("🤖 ЗАПУСК TELEGRAM БОТА") + print("🤖 ЗАПУСК TELEGRAM БОТА ЧЕРЕЗ SOCKS5 ПРОКСИ") print("=" * 50) - if PROXY_URL: - print(f"🔌 Прокси: {PROXY_URL.split('@')[-1] if '@' in PROXY_URL else PROXY_URL}") - else: - print("🔌 Прокси: Не используется") + if not PROXY_URL: + print("❌ Прокси не настроен в .env файле!") + return + print(f"🔌 Тип: {PROXY_TYPE.upper()}") print("=" * 50) + # Проверяем подключение + if not await test_telegram_connection(): + print("\n❌ НЕТ ПОДКЛЮЧЕНИЯ К TELEGRAM ЧЕРЕЗ ПРОКСИ!") + print("Проверьте настройки прокси в файле .env") + return + + # Инициализация await init_db() # Создаем приложение request = get_telegram_request() + if request is None: + print("\n❌ Не удалось настроить прокси для Telegram!") + return + application = Application.builder().token(TELEGRAM_BOT_TOKEN).request(request).build() # Регистрируем обработчики application.add_handler(CommandHandler("start", start)) + application.add_handler(CommandHandler("test", test_command)) application.add_handler(CommandHandler("help", help_command)) application.add_handler(CommandHandler("history", history_command)) - application.add_handler(CommandHandler("test", test_command)) conv_handler = ConversationHandler( entry_points=[MessageHandler(filters.PHOTO, handle_photo)], - states={AWAITING_PROMPT: [MessageHandler(filters.TEXT & ~filters.COMMAND, process_image_with_prompt)]}, + states={AWAITING_PROMPT: [MessageHandler(filters.TEXT & ~filters.COMMAND, process_image)]}, fallbacks=[CommandHandler("cancel", cancel)], ) application.add_handler(conv_handler) application.add_error_handler(error_handler) - print("\n✅ Бот запускается...") + print("\n✅ Бот запускается через SOCKS5 прокси...") try: await application.initialize() @@ -394,11 +491,10 @@ async def main(): await application.updater.start_polling() print("\n" + "=" * 50) - print("🤖 БОТ УСПЕШНО ЗАПУЩЕН!") + print("🤖 БОТ УСПЕШНО ЗАПУЩЕН ЧЕРЕЗ SOCKS5 ПРОКСИ!") print("=" * 50) - print("\n📱 Отправь команду /test в Telegram для проверки\n") + print("\n📱 Отправь команду /test в Telegram\n") - # Бесконечное ожидание await asyncio.Event().wait() except KeyboardInterrupt: @@ -407,7 +503,6 @@ async def main(): logger.error(f"Ошибка: {e}") print(f"\n❌ Ошибка: {e}") finally: - await close_session() await application.updater.stop() await application.stop() diff --git a/config.py b/config.py index 97bb203..60aaa57 100644 --- a/config.py +++ b/config.py @@ -7,12 +7,11 @@ TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN") PIApi_API_KEY = os.getenv("PIApi_API_KEY") PIApi_BASE_URL = "https://api.piapi.ai/api/v1/task" -# Настройки прокси +# Прокси для всего PROXY_URL = os.getenv("PROXY_URL") PROXY_TYPE = os.getenv("PROXY_TYPE", "http") -# Проверка наличия прокси if PROXY_URL: - print(f"🔧 Обнаружен прокси: {PROXY_URL.split('@')[-1] if '@' in PROXY_URL else PROXY_URL}") + print(f"🔧 Прокси: {PROXY_URL.split('@')[-1] if '@' in PROXY_URL else PROXY_URL}") else: print("ℹ️ Прокси не настроен") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 9bbe305..e306e59 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,8 @@ -python-telegram-bot==20.7 +python-telegram-bot[socks]==20.7 aiohttp==3.9.1 aiosqlite==0.19.0 python-dotenv==1.0.0 Pillow==10.1.0 httpx==0.25.2 -aiohttp-socks==0.8.4 \ No newline at end of file +aiohttp-socks==0.8.4 +httpx-socks==0.8.0 \ No newline at end of file