commit ca4effea8e06cd2cd6416d17803d7963317dbb21 Author: apuc Date: Sat May 2 17:08:05 2026 +0300 first diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d7986e4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,70 @@ +nano .gitignore +# или +cat > .gitignore << 'EOF' +# Python +__pycache__/ +*.py[cod] +*.so +.Python +.venv/ +venv/ +env/ +ENV/ + +# Database +*.db +*.sqlite +*.db-journal +*.db-wal +*.db-shm +bot_database.db* + +# Project directories +uploads/ +processed/ +images/ +temp/ +tmp/ + +# Environment +.env +.env.local +.env.*.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Logs +*.log +logs/ + +# OS files +Thumbs.db +desktop.ini + +# Backup files +*.bak +*.backup +*.old + +# Secrets +secrets/ +keys/ +*.key +*.pem + +# Archives +*.zip +*.tar.gz +*.rar + +# Temporary +*.tmp +*.temp +*.cache +EOF \ No newline at end of file diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..6d947e5 --- /dev/null +++ b/bot.py @@ -0,0 +1,419 @@ +import asyncio +import aiohttp +import os +import sys +import logging +import base64 +from datetime import datetime +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ( + Application, + CommandHandler, + MessageHandler, + CallbackQueryHandler, + ConversationHandler, + filters, + ContextTypes +) +from telegram.request import HTTPXRequest +import httpx + +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 +import aiosqlite + +# Настройка логирования +logging.basicConfig( + format='%(asctime)s - %(name)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) + request = HTTPXRequest(transport=transport) + logger.info("✅ Прокси настроен успешно") + return request + except Exception as e: + logger.error(f"Ошибка настройки прокси: {e}") + # Возвращаем обычный запрос без прокси + return HTTPXRequest() + else: + logger.info("🔌 Прокси не используется") + return HTTPXRequest() + + +async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработчик команды /start""" + logger.info(f"✅ Получена команда /start от {update.effective_user.id}") + user = update.effective_user + + await save_user( + user_id=user.id, + username=user.username, + first_name=user.first_name, + last_name=user.last_name + ) + + welcome_text = ( + f"👋 Привет, {user.first_name}!\n\n" + f"Я бот для обработки фотографий через нейросеть.\n\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("✅ Бот работает! Команды обрабатываются нормально.") + + +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(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 + + 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) + + 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 + + await update.message.reply_text( + "📸 Фото получено!\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) + + 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""" + prompt = update.message.text + user = update.effective_user + logger.info(f"Получен промпт от {user.id}: {prompt}") + + image_id = context.user_data.get('current_image_id') + file_path = context.user_data.get('current_file_path') + + if not image_id or not file_path: + await update.message.reply_text("❌ Ошибка: изображение не найдено") + return ConversationHandler.END + + status_msg = await update.message.reply_text( + f"🎨 Обрабатываю...\n\nПромпт: {prompt}\n\n⏳ Обычно это занимает 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) + + 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) + + 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()) + + # Удаляем сообщение о статусе + await status_msg.delete() + + # Отправляем результат + with open(result_path, "rb") as photo: + await update.message.reply_photo( + photo=photo, + caption=f"✅ Готово!\n\n✨ Промпт: {prompt}" + ) + + # Сохраняем 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("Ошибка загрузки результата") + 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Попробуйте еще раз или отправьте другое фото.") + await update_image_record(image_id=image_id, status='failed') + + context.user_data.clear() + return ConversationHandler.END + + +async def call_piapi_image_edit(image_path: str, prompt: str) -> str: + """Вызов PiAPI""" + if not PIApi_API_KEY: + raise Exception("PIApi_API_KEY не настроен") + + 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", + "input": { + "image": f"data:image/jpeg;base64,{image_base64}", + "prompt": prompt, + "num_inference_steps": 28, + "guidance_scale": 7.5, + "strength": 0.8 + } + } + + current_session = await get_session() + + 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}") + + data = await response.json() + if data.get('code') != 200: + raise Exception(f"API Error: {data.get('message')}") + + task_id = data['data']['task_id'] + + result_url = await poll_task_status(task_id, headers) + return result_url + + +async def poll_task_status(task_id: str, headers: dict, max_attempts: int = 60) -> str: + """Ожидание завершения""" + get_url = f"https://api.piapi.ai/api/v1/task/{task_id}" + current_session = await get_session() + + 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("Задача не выполнена") + + await asyncio.sleep(2) + except Exception as e: + logger.error(f"Ошибка: {e}") + await asyncio.sleep(2) + + raise Exception("Превышено время ожидания") + + +async def history_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + """История обработок""" + user = update.effective_user + images = await get_user_images(user.id, limit=5) + + if not images: + await update.message.reply_text( + "📭 У вас пока нет обработанных изображений.\n\nОтправьте фото для начала работы!") + return + + text = "🖼 **Ваши последние обработки:**\n\n" + for i, img in enumerate(images, 1): + status_emoji = "✅" if img['status'] == 'completed' else "❌" if img['status'] == 'failed' 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" + + text += "💡 Отправьте новое фото для обработки!" + await update.message.reply_text(text, parse_mode='Markdown') + + +async def error_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработчик ошибок""" + logger.error(f"Ошибка: {context.error}") + if update and update.effective_message: + await update.effective_message.reply_text("😵 Произошла ошибка. Попробуйте позже.") + + +async def main(): + """Запуск бота""" + print("\n" + "=" * 50) + print("🤖 ЗАПУСК TELEGRAM БОТА") + print("=" * 50) + + if PROXY_URL: + print(f"🔌 Прокси: {PROXY_URL.split('@')[-1] if '@' in PROXY_URL else PROXY_URL}") + else: + print("🔌 Прокси: Не используется") + + print("=" * 50) + + await init_db() + + # Создаем приложение + request = get_telegram_request() + application = Application.builder().token(TELEGRAM_BOT_TOKEN).request(request).build() + + # Регистрируем обработчики + application.add_handler(CommandHandler("start", start)) + 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)]}, + fallbacks=[CommandHandler("cancel", cancel)], + ) + application.add_handler(conv_handler) + application.add_error_handler(error_handler) + + print("\n✅ Бот запускается...") + + try: + await application.initialize() + await application.start() + await application.updater.start_polling() + + print("\n" + "=" * 50) + print("🤖 БОТ УСПЕШНО ЗАПУЩЕН!") + print("=" * 50) + print("\n📱 Отправь команду /test в Telegram для проверки\n") + + # Бесконечное ожидание + await asyncio.Event().wait() + + except KeyboardInterrupt: + print("\n🛑 Бот остановлен") + except Exception as e: + logger.error(f"Ошибка: {e}") + print(f"\n❌ Ошибка: {e}") + finally: + await close_session() + await application.updater.stop() + await application.stop() + + +if __name__ == "__main__": + if sys.platform == 'win32': + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + + asyncio.run(main()) \ No newline at end of file diff --git a/check_proxy.py b/check_proxy.py new file mode 100644 index 0000000..f40b7d9 --- /dev/null +++ b/check_proxy.py @@ -0,0 +1,82 @@ +import asyncio +import aiohttp +import sys +from config import PROXY_URL, PROXY_TYPE + + +async def check_proxy(): + """Проверка работоспособности прокси""" + + if not PROXY_URL: + print("❌ Прокси не настроен в файле .env") + return False + + print(f"\n🔍 Проверяем прокси: {PROXY_URL}") + print(f"📡 Тип прокси: {PROXY_TYPE.upper()}") + print("-" * 50) + + success_count = 0 + total_tests = 0 + + # Тест 1: Проверка подключения к Telegram API + print("\n📡 Тест: Подключение к API...") + test_urls = [ + ("Telegram API", "https://api.telegram.org"), + ("Google", "https://google.com"), + ("IP Check", "https://api.ipify.org?format=json") + ] + + for name, url in test_urls: + total_tests += 1 + print(f" → Проверяю {name}...") + try: + async with aiohttp.ClientSession() as session: + async with session.get(url, proxy=PROXY_URL, timeout=10) as response: + if response.status == 200: + print(f" ✅ Успешно (статус: {response.status})") + success_count += 1 + if "ipify" in url: + data = await response.json() + print(f" 🌐 Ваш IP через прокси: {data.get('ip')}") + else: + print(f" ❌ Ошибка: статус {response.status}") + except asyncio.TimeoutError: + print(f" ❌ Таймаут подключения") + except Exception as e: + print(f" ❌ Ошибка: {type(e).__name__}") + + # Результат + print("\n" + "=" * 50) + if success_count == total_tests: + print("✅ ПРОКСИ РАБОТАЕТ!") + print("=" * 50) + return True + else: + print(f"⚠️ ПРОКСИ РАБОТАЕТ ЧАСТИЧНО ({success_count}/{total_tests} тестов)") + print("=" * 50) + return True if success_count > 0 else False + + +async def main(): + print("\n" + "=" * 50) + print("🔍 ПРОВЕРКА ПРОКСИ ДЛЯ TELEGRAM БОТА") + print("=" * 50) + + result = await check_proxy() + + if result: + print("\n✅ ПРОКСИ РАБОТАЕТ! Можно запускать бота.") + print("\nЗапустите бота командой: python bot.py") + else: + print("\n❌ ПРОКСИ НЕ РАБОТАЕТ! Бот не сможет подключиться.") + print("\nРекомендации:") + print("1. Проверьте правильность данных в файле .env") + print("2. Убедитесь, что прокси-сервер активен") + print("3. Попробуйте другой прокси") + + return 0 if result else 1 + + +if __name__ == "__main__": + exit_code = asyncio.run(main()) + sys.exit(exit_code) \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..97bb203 --- /dev/null +++ b/config.py @@ -0,0 +1,18 @@ +import os +from dotenv import load_dotenv + +load_dotenv() + +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}") +else: + print("ℹ️ Прокси не настроен") \ No newline at end of file diff --git a/database.py b/database.py new file mode 100644 index 0000000..f321cd7 --- /dev/null +++ b/database.py @@ -0,0 +1,113 @@ +import aiosqlite +from datetime import datetime +from typing import Optional, List, Dict, Any + +DATABASE_PATH = "bot_database.db" + + +async def init_db(): + """Инициализация базы данных и создание таблиц""" + async with aiosqlite.connect(DATABASE_PATH) as db: + # Включаем WAL режим для лучшей производительности + await db.execute("PRAGMA journal_mode=WAL;") + + # Таблица пользователей + await db.execute(""" + CREATE TABLE IF NOT EXISTS users ( + user_id INTEGER PRIMARY KEY, + username TEXT, + first_name TEXT, + last_name TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ) + """) + + # Таблица обработанных изображений + await db.execute(""" + CREATE TABLE IF NOT EXISTS images ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + original_file_id TEXT NOT NULL, + original_url TEXT, + processed_file_id TEXT, + processed_url TEXT, + prompt TEXT, + status TEXT DEFAULT 'processing', + created_at TEXT NOT NULL, + completed_at TEXT, + FOREIGN KEY(user_id) REFERENCES users(user_id) ON DELETE CASCADE + ) + """) + + await db.commit() + + +async def save_user(user_id: int, username: str = None, first_name: str = None, last_name: str = None): + """Сохранение или обновление информации о пользователе""" + now = datetime.now().isoformat() + async with aiosqlite.connect(DATABASE_PATH) as db: + await db.execute(""" + INSERT INTO users (user_id, username, first_name, last_name, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(user_id) DO UPDATE SET + username = excluded.username, + first_name = excluded.first_name, + last_name = excluded.last_name, + updated_at = excluded.updated_at + """, (user_id, username, first_name, last_name, now, now)) + await db.commit() + + +async def save_image_record( + user_id: int, + original_file_id: str, + prompt: str = None, + original_url: str = None +) -> int: + """Сохранение записи об изображении""" + now = datetime.now().isoformat() + async with aiosqlite.connect(DATABASE_PATH) as db: + cursor = await db.execute(""" + INSERT INTO images (user_id, original_file_id, original_url, prompt, created_at, status) + VALUES (?, ?, ?, ?, ?, 'processing') + RETURNING id + """, (user_id, original_file_id, original_url, prompt, now)) + await db.commit() + row = await cursor.fetchone() + return row[0] if row else None + + +async def update_image_record( + image_id: int, + processed_file_id: str = None, + processed_url: str = None, + status: str = 'completed' +): + """Обновление записи об изображении после обработки""" + now = datetime.now().isoformat() + async with aiosqlite.connect(DATABASE_PATH) as db: + await db.execute(""" + UPDATE images + SET processed_file_id = COALESCE(?, processed_file_id), + processed_url = COALESCE(?, processed_url), + status = ?, + completed_at = ? + WHERE id = ? + """, (processed_file_id, processed_url, status, now, image_id)) + await db.commit() + + +async def get_user_images(user_id: int, limit: int = 10) -> List[Dict[str, Any]]: + """Получение истории обработанных изображений пользователя""" + async with aiosqlite.connect(DATABASE_PATH) as db: + db.row_factory = aiosqlite.Row + cursor = await db.execute(""" + SELECT id, original_file_id, processed_file_id, prompt, status, created_at, completed_at + FROM images + WHERE user_id = ? + ORDER BY created_at DESC + LIMIT ? + """, (user_id, limit)) + rows = await cursor.fetchall() + return [dict(row) for row in rows] \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..5596b44 --- /dev/null +++ b/main.py @@ -0,0 +1,16 @@ +# This is a sample Python script. + +# Press Shift+F10 to execute it or replace it with your code. +# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings. + + +def print_hi(name): + # Use a breakpoint in the code line below to debug your script. + print(f'Hi, {name}') # Press Ctrl+F8 to toggle the breakpoint. + + +# Press the green button in the gutter to run the script. +if __name__ == '__main__': + print_hi('PyCharm') + +# See PyCharm help at https://www.jetbrains.com/help/pycharm/ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9bbe305 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +python-telegram-bot==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 diff --git a/test_proxy.py b/test_proxy.py new file mode 100644 index 0000000..3bb8a38 --- /dev/null +++ b/test_proxy.py @@ -0,0 +1,17 @@ +import aiohttp +import asyncio + + +async def test_proxy(): + proxy_url = "http://FYqj9n:n4br4L@185.168.251.163:8000" + + try: + async with aiohttp.ClientSession() as session: + async with session.get("https://api.ipify.org?format=json", proxy=proxy_url) as resp: + data = await resp.json() + print(f"✅ Прокси работает! Ваш IP: {data['ip']}") + except Exception as e: + print(f"❌ Ошибка прокси: {e}") + + +asyncio.run(test_proxy()) \ No newline at end of file