/
TimTim
/
PrinterBOTAI
Обзор
Документация
Войти
/
TimTim
/
PrinterBOTAI
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
bot.py
860 строк
39 KB
TimTim
Create: BEST3.py, bot.py
21 июл 2026, 05:52
Верифицирован
21 июл 2026, 05:52
f600f02
Код
Авторство
О чём код?
import os import sys import asyncio import csv import logging import json from datetime import datetime from pathlib import Path from PIL import Image, ImageDraw, ImageFont, ImageOps from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, ReplyKeyboardMarkup from telegram.ext import ( Application, CommandHandler, MessageHandler, CallbackQueryHandler, ConversationHandler, ContextTypes, filters, ) # ---------- Настройки ---------- BOT_TOKEN = "8716052149:AAEDSWfrnNWMdGVE4sxI_EE7GKVjYaTAWE8" ADMIN_IDS = [238285817] PHOTOS_DIR = "photos" TEMP_DIR = "temp" STATS_FILE = "stats.csv" CONFIG_FILE = "config.json" LOCK_FILE = "bot.lock" os.makedirs(PHOTOS_DIR, exist_ok=True) os.makedirs(TEMP_DIR, exist_ok=True) COLLECT_PHOTOS, CONFIRM, PREVIEW = range(3) print_queue = asyncio.Queue() MAX_PHOTOS = 10 # ---------- Блокировка единственного экземпляра ---------- def check_single_instance(): if os.name == 'nt': try: if os.path.exists(LOCK_FILE): with open(LOCK_FILE, 'r') as f: pid = int(f.read().strip()) try: import psutil if psutil.pid_exists(pid): print(f"⚠️ Бот уже запущен (PID: {pid})") return False except: pass os.remove(LOCK_FILE) with open(LOCK_FILE, 'w') as f: f.write(str(os.getpid())) return True except Exception as e: print(f"⚠️ Ошибка блокировки: {e}") return True else: import fcntl try: fd = open(LOCK_FILE, 'w') fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) fd.write(str(os.getpid())) fd.flush() return fd except: print("⚠️ Бот уже запущен!") return False def release_lock(lock_fd=None): if os.name == 'nt': try: if os.path.exists(LOCK_FILE): os.remove(LOCK_FILE) except: pass else: if lock_fd: try: import fcntl fcntl.flock(lock_fd, fcntl.LOCK_UN) lock_fd.close() if os.path.exists(LOCK_FILE): os.remove(LOCK_FILE) except: pass # ---------- Конфигурация ---------- def load_config(): if os.path.isfile(CONFIG_FILE): try: with open(CONFIG_FILE, "r", encoding="utf-8") as f: return json.load(f) except: pass return { "welcome_message": "👋 Добро пожаловать в Photo Print Bot!\n\nОтправьте фото или нажмите кнопку, чтобы начать.", "frame_enabled": False, "frame_color": "#000000", "frame_width": 10, "overlay_text": "", "overlay_position": "bottom", "overlay_font_size": 24, "printer_name": "", "button_new_order": "📸 Новый заказ", "button_reset": "🔄 Сбросить", "button_help": "ℹ️ Помощь", "button_cancel": "❌ Отменить заказ", "button_reset_all": "🔄 Сбросить все", "button_preview": "👁 Предпросмотр", "button_confirm": "✅ Подтвердить печать", "text_start": "👋 Добро пожаловать в Photo Print Bot!\n\n📸 Отправьте фото или нажмите кнопку 'Новый заказ', чтобы начать.", "text_order_start": "📸 Отправьте фото\n\nМожете прислать несколько фото.\nПосле отправки всех фото введите количество копий.\n\n📊 Получено фото: 0\n\n💡 Подсказка: отправьте фото, затем введите число (например, 2) для количества копий.", "text_photo_added": "✅ Фото добавлено ({} из {})\n\n📊 Получено фото: {}\n\n📝 Что дальше:\n• Отправьте ещё фото\n• Или введите число - количество копий (например, 2)\n\n💡 Подсказка: количество копий указывается для ВСЕХ фото сразу", "text_enter_count": "❌ Пожалуйста, введите положительное число от 1 до 100.\n\n📝 Пример: 2 - значит каждое фото будет распечатано 2 раза.", "text_order_summary": "📋 ПРОВЕРЬТЕ ЗАКАЗ\n\n📸 Количество фото: {}\n📎 Копий каждого: {}\n\n✅ Всё верно? Нажмите 'Подтвердить печать'\n👁 Хотите посмотреть как будет выглядеть фото? Нажмите 'Предпросмотр'\n❌ Передумали? Нажмите 'Отменить'", "text_preview_generating": "⏳ Генерирую предпросмотр...\n\nПожалуйста, подождите...", "text_preview_error": "❌ Ошибка при создании предпросмотра.\n\nПопробуйте снова или нажмите 'Подтвердить печать' без предпросмотра.", "text_loading_photos": "⏳ Загружаю фото и ставлю в очередь печати...\n\nПожалуйста, подождите...", "text_loading_progress": "⏳ Загружаю фото... {}/{}", "text_order_success": "✅ ЗАКАЗ ПРИНЯТ!\n\n📸 Все {} фото добавлены в очередь печати!\n📎 Копий: {}\n\n🖨 Печать начнётся автоматически.\n\n📌 Хотите сделать ещё один заказ? Нажмите 'Новый заказ'", "text_order_cancelled": "❌ Заказ отменён.\n\nНажмите 'Новый заказ' чтобы начать снова.", "text_help": "📖 ПОМОЩЬ ПО ИСПОЛЬЗОВАНИЮ БОТА\n\n1️⃣ Отправьте фото или нажмите 'Новый заказ'\n2️⃣ Можно отправить несколько фото (до 10)\n3️⃣ Введите количество копий (число от 1 до 100)\n4️⃣ Подтвердите заказ\n\n🔹 КНОПКИ УПРАВЛЕНИЯ:\n• 'Предпросмотр' - посмотреть как будет выглядеть фото с эффектами\n• 'Подтвердить печать' - отправить в печать\n• 'Отменить' - отменить текущий заказ\n• 'Сбросить' - сбросить все и начать заново\n• 'Новый заказ' - начать новый заказ\n\n⚠️ ВАЖНО:\n• Количество копий указывается для ВСЕХ фото сразу\n• Можно отправить до 10 фото за один заказ\n• Печать начинается автоматически после подтверждения\n\n❓ Вопросы: @your_admin", "text_reset": "🔄 Состояние сброшено.\n\n📌 Нажмите 'Новый заказ' чтобы начать.\n\n💡 Подсказка: вы можете отправить несколько фото за один заказ." } config = load_config() # ---------- Статистика ---------- def write_stats(user_id: int, username: str, copies: int): file_exists = os.path.isfile(STATS_FILE) with open(STATS_FILE, "a", newline="", encoding="utf-8") as f: writer = csv.writer(f) if not file_exists: writer.writerow(["timestamp", "user_id", "username", "copies"]) writer.writerow([datetime.now().isoformat(), user_id, username, copies]) # ---------- Обработка изображений ---------- def add_frame_and_overlay(image_path: str, output_path: str): try: image = Image.open(image_path) if image.mode == 'RGBA': image = image.convert('RGB') if config.get("frame_enabled", False): frame_color = config.get("frame_color", "#000000") frame_width = config.get("frame_width", 10) frame_color = tuple(int(frame_color.lstrip('#')[i:i+2], 16) for i in (0, 2, 4)) image = ImageOps.expand(image, border=frame_width, fill=frame_color) overlay_text = config.get("overlay_text", "") if overlay_text: draw = ImageDraw.Draw(image) try: font = ImageFont.truetype("arial.ttf", config.get("overlay_font_size", 24)) except: font = ImageFont.load_default() position = config.get("overlay_position", "bottom") text_bbox = draw.textbbox((0, 0), overlay_text, font=font) text_width = text_bbox[2] - text_bbox[0] text_height = text_bbox[3] - text_bbox[1] x = (image.width - text_width) // 2 if position == "top": y = 20 else: y = image.height - text_height - 20 overlay = Image.new('RGBA', image.size, (0, 0, 0, 0)) overlay_draw = ImageDraw.Draw(overlay) overlay_draw.rectangle( [x - 10, y - 10, x + text_width + 10, y + text_height + 10], fill=(0, 0, 0, 128) ) image = Image.alpha_composite(image.convert('RGBA'), overlay).convert('RGB') draw = ImageDraw.Draw(image) draw.text((x, y), overlay_text, fill="white", font=font) image.save(output_path, quality=95) return True except Exception as e: logging.error(f"Ошибка обработки изображения: {e}") return False # ---------- Печать ---------- def print_file_blocking(file_path: str, copies: int): try: temp_processed = file_path + "_processed.jpg" if not add_frame_and_overlay(file_path, temp_processed): temp_processed = file_path printer_name = config.get("printer_name", "") if os.name == 'nt': import win32print import win32ui from PIL import ImageWin if printer_name: try: printers = [p[2] for p in win32print.EnumPrinters(win32print.PRINTER_ENUM_LOCAL | win32print.PRINTER_ENUM_CONNECTIONS)] if printer_name not in printers: logging.warning(f"Принтер '{printer_name}' не найден, используем принтер по умолчанию") printer_name = win32print.GetDefaultPrinter() except: printer_name = win32print.GetDefaultPrinter() else: printer_name = win32print.GetDefaultPrinter() hprinter = win32print.OpenPrinter(printer_name) try: for _ in range(copies): image = Image.open(temp_processed) if image.mode not in ("RGB", "RGBA"): image = image.convert("RGB") hdc = win32ui.CreateDC() hdc.CreatePrinterDC(printer_name) page_w = hdc.GetDeviceCaps(110) page_h = hdc.GetDeviceCaps(111) hdc.DeleteDC() img_w, img_h = image.size ratio = img_w / img_h if ratio > page_w / page_h: new_w = page_w new_h = int(page_w / ratio) else: new_h = page_h new_w = int(page_h * ratio) x = (page_w - new_w) // 2 y = (page_h - new_h) // 2 hdc = win32ui.CreateDC() hdc.CreatePrinterDC(printer_name) hdc.StartDoc(file_path) hdc.StartPage() dib = ImageWin.Dib(image) dib.draw(hdc.GetHandleOutput(), (x, y, x + new_w, y + new_h)) hdc.EndPage() hdc.EndDoc() hdc.DeleteDC() finally: win32print.ClosePrinter(hprinter) else: dest = f'-d "{printer_name}"' if printer_name else '' for _ in range(copies): os.system(f'lp {dest} -n 1 "{temp_processed}"') if temp_processed != file_path and os.path.exists(temp_processed): os.remove(temp_processed) return True except Exception as e: logging.error(f"Ошибка печати: {e}") return False # ---------- Фоновый worker ---------- async def print_worker(bot): while True: try: file_path, copies, user_id, username = await print_queue.get() logging.info(f"Печать {file_path} x{copies} для {username}") success = await asyncio.to_thread(print_file_blocking, file_path, copies) if success: write_stats(user_id, username, copies) for admin_id in ADMIN_IDS: try: await bot.send_message( admin_id, f"🖨 Печать завершена\n" f"👤 Пользователь: {username} (ID: {user_id})\n" f"📄 Файл: {os.path.basename(file_path)}\n" f"📎 Копий: {copies}" ) except Exception as e: logging.error(f"Ошибка уведомления админу {admin_id}: {e}") else: for admin_id in ADMIN_IDS: try: await bot.send_message( admin_id, f"❌ Ошибка печати\n" f"👤 Пользователь: {username} (ID: {user_id})\n" f"📄 Файл: {os.path.basename(file_path)}\n" f"📎 Копий: {copies}" ) except: pass print_queue.task_done() except Exception as e: logging.error(f"Ошибка в print_worker: {e}") # ---------- Клавиатуры ---------- def get_main_keyboard(): keyboard = [ [KeyboardButton("📸 Новый заказ")], [KeyboardButton("🔄 Сбросить")], [KeyboardButton("❓ Помощь")], ] return ReplyKeyboardMarkup(keyboard, resize_keyboard=True, one_time_keyboard=False) def get_order_keyboard(): keyboard = [ [KeyboardButton("📸 Добавить ещё фото")], [KeyboardButton("✅ Готово, ввести количество")], [KeyboardButton("❌ Отменить заказ")], ] return ReplyKeyboardMarkup(keyboard, resize_keyboard=True, one_time_keyboard=False) def get_confirm_keyboard(photo_count): keyboard = [ [KeyboardButton(f"👁 Предпросмотр ({photo_count} фото)")], [KeyboardButton("✅ Подтвердить печать")], [KeyboardButton("❌ Отменить заказ")], ] return ReplyKeyboardMarkup(keyboard, resize_keyboard=True, one_time_keyboard=False) def get_cancel_keyboard(): keyboard = [ [KeyboardButton("❌ Отменить заказ")], [KeyboardButton("🔄 Сбросить всё")], ] return ReplyKeyboardMarkup(keyboard, resize_keyboard=True, one_time_keyboard=False) def get_inline_confirm_keyboard(photo_count): return InlineKeyboardMarkup([ [InlineKeyboardButton(f"👁 Предпросмотр ({photo_count} фото)", callback_data="preview")], [InlineKeyboardButton("✅ Подтвердить печать", callback_data="confirm")], [InlineKeyboardButton("❌ Отменить", callback_data="cancel")], ]) # ---------- Обработчики ---------- async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): welcome_text = ( "👋 Добро пожаловать в Photo Print Bot!\n\n" "🖨 Я помогу вам распечатать фото.\n\n" "📌 Чтобы начать, отправьте фото или нажмите кнопку '📸 Новый заказ'." ) await update.message.reply_text( welcome_text, reply_markup=get_main_keyboard() ) async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE): help_text = ( "📖 КАК РАБОТАЕТ БОТ\n\n" "1️⃣ Отправьте фото или нажмите '📸 Новый заказ'\n" "2️⃣ Можно отправить до 10 фото\n" "3️⃣ Нажмите '✅ Готово, ввести количество'\n" "4️⃣ Введите число от 1 до 100 (количество копий)\n" "5️⃣ Подтвердите заказ\n\n" "🔹 КНОПКИ:\n" "• '📸 Новый заказ' - начать заказ\n" "• '🔄 Сбросить' - очистить всё\n" "• '❓ Помощь' - эта справка\n\n" "⚠️ ВАЖНО:\n" "• Количество копий для ВСЕХ фото сразу\n" "• Можно отправить до 10 фото за раз\n" "• Печать начинается автоматически\n\n" "❓ Вопросы: @your_admin" ) await update.message.reply_text( help_text, reply_markup=get_main_keyboard() ) async def new_order(update: Update, context: ContextTypes.DEFAULT_TYPE): context.user_data.clear() context.user_data['pending_photos'] = [] context.user_data['photo_counter'] = 0 text = ( "📸 НАЧИНАЕМ НОВЫЙ ЗАКАЗ\n\n" "Отправьте фото (можно несколько).\n" "После отправки всех фото нажмите '✅ Готово, ввести количество'.\n\n" f"📊 Получено фото: 0/{MAX_PHOTOS}\n" "💡 Подсказка: можно отправить до 10 фото" ) await update.message.reply_text( text, reply_markup=get_order_keyboard() ) return COLLECT_PHOTOS async def handle_photo(update: Update, context: ContextTypes.DEFAULT_TYPE): # Проверяем, есть ли активный заказ if 'pending_photos' not in context.user_data: context.user_data['pending_photos'] = [] context.user_data['photo_counter'] = 0 pending = context.user_data['pending_photos'] if len(pending) >= MAX_PHOTOS: await update.message.reply_text( f"⚠️ Достигнут лимит - {MAX_PHOTOS} фото.\n" "Нажмите '✅ Готово, ввести количество' чтобы продолжить.", reply_markup=get_order_keyboard() ) return COLLECT_PHOTOS file_id = update.message.photo[-1].file_id pending.append(file_id) context.user_data['photo_counter'] = len(pending) remaining = MAX_PHOTOS - len(pending) text = ( f"✅ Фото добавлено! ({len(pending)}/{MAX_PHOTOS})\n\n" f"📊 Получено фото: {len(pending)}\n" f"📎 Осталось места: {remaining}\n\n" "📝 Что дальше:\n" "• Отправьте ещё фото\n" "• Или нажмите '✅ Готово, ввести количество'" ) await update.message.reply_text( text, reply_markup=get_order_keyboard() ) return COLLECT_PHOTOS async def ready_to_count(update: Update, context: ContextTypes.DEFAULT_TYPE): pending = context.user_data.get('pending_photos', []) if not pending: await update.message.reply_text( "❌ Вы не отправили ни одного фото.\n\n" "Сначала отправьте фото, а затем нажмите '✅ Готово'.", reply_markup=get_order_keyboard() ) return COLLECT_PHOTOS text = ( f"📸 У вас {len(pending)} фото.\n\n" "📝 Введите количество копий (число от 1 до 100).\n\n" "💡 Пример: 2 - значит каждое фото будет распечатано 2 раза." ) await update.message.reply_text( text, reply_markup=get_cancel_keyboard() ) return COLLECT_PHOTOS async def handle_count(update: Update, context: ContextTypes.DEFAULT_TYPE): pending = context.user_data.get('pending_photos', []) if not pending: await update.message.reply_text( "❌ Сначала отправьте фото.\n\n" "Нажмите '📸 Новый заказ' чтобы начать.", reply_markup=get_main_keyboard() ) return ConversationHandler.END try: count = int(update.message.text.strip()) if count <= 0 or count > 100: raise ValueError except ValueError: await update.message.reply_text( "❌ Введите число от 1 до 100.\n\n" "💡 Например: 2 - для двух копий каждого фото.", reply_markup=get_cancel_keyboard() ) return COLLECT_PHOTOS context.user_data['copies'] = count text = ( f"📋 ПРОВЕРЬТЕ ЗАКАЗ\n\n" f"📸 Фото: {len(pending)}\n" f"📎 Копий каждого: {count}\n" f"📊 Всего отпечатков: {len(pending) * count}\n\n" "✅ Всё верно? Нажмите '✅ Подтвердить печать'\n" "👁 Хотите посмотреть как будет выглядеть фото? Нажмите 'Предпросмотр'\n" "❌ Передумали? Нажмите '❌ Отменить'" ) await update.message.reply_text( text, reply_markup=get_confirm_keyboard(len(pending)) ) return CONFIRM async def preview_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() pending = context.user_data.get('pending_photos', []) copies = context.user_data.get('copies', 1) if not pending: await query.edit_message_text( "❌ Нет фото для предпросмотра.", reply_markup=get_main_keyboard() ) return CONFIRM await query.edit_message_text("⏳ Генерирую предпросмотр... Пожалуйста, подождите...") try: tg_file = await context.bot.get_file(pending[0]) ext = tg_file.file_path.split('.')[-1] if '.' in tg_file.file_path else 'jpg' preview_path = os.path.join(TEMP_DIR, f"preview_{datetime.now().strftime('%Y%m%d%H%M%S%f')}.{ext}") await tg_file.download_to_drive(preview_path) preview_processed = preview_path + "_preview.jpg" if add_frame_and_overlay(preview_path, preview_processed): with open(preview_processed, 'rb') as f: await query.message.reply_photo( photo=f, caption=( f"👁 ПРЕДПРОСМОТР\n\n" f"📎 Всего фото: {len(pending)}\n" f"📋 Копий каждого: {copies}\n" f"📊 Всего отпечатков: {len(pending) * copies}\n" f"🎨 Рамка: {'✅ Включена' if config['frame_enabled'] else '❌ Выключена'}\n" f"✏️ Текст: {config['overlay_text'] or 'Нет'}\n\n" f"✅ Всё устраивает? Нажмите 'Подтвердить печать'" ) ) else: await query.message.reply_text("❌ Ошибка при создании предпросмотра.") for path in [preview_path, preview_processed]: if os.path.exists(path): try: os.remove(path) except: pass text = ( f"📋 ПРОВЕРЬТЕ ЗАКАЗ\n\n" f"📸 Фото: {len(pending)}\n" f"📎 Копий каждого: {copies}\n" f"📊 Всего отпечатков: {len(pending) * copies}\n\n" f"✅ Всё верно? Нажмите 'Подтвердить печать'" ) await query.message.reply_text( text, reply_markup=get_confirm_keyboard(len(pending)) ) except Exception as e: logging.error(f"Ошибка предпросмотра: {e}") await query.message.reply_text( "❌ Ошибка при создании предпросмотра.\n\n" "Попробуйте снова или нажмите 'Подтвердить печать'.", reply_markup=get_confirm_keyboard(len(pending)) ) return CONFIRM async def confirm_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() if query.data == "cancel": context.user_data.clear() await query.edit_message_text( "❌ Заказ отменён.\n\n" "Нажмите '📸 Новый заказ' чтобы начать снова.", reply_markup=get_main_keyboard() ) return ConversationHandler.END user = update.effective_user copies = context.user_data.get('copies', 1) pending = context.user_data.get('pending_photos', []) if not pending: await query.edit_message_text( "❌ Нет фото для печати.\n\n" "Нажмите '📸 Новый заказ' чтобы начать.", reply_markup=get_main_keyboard() ) return ConversationHandler.END await query.edit_message_text("⏳ Загружаю фото и ставлю в очередь печати...\n\nПожалуйста, подождите...") total_photos = len(pending) username = user.username or user.full_name or f"User_{user.id}" for idx, file_id in enumerate(pending, 1): try: tg_file = await context.bot.get_file(file_id) ext = tg_file.file_path.split('.')[-1] if '.' in tg_file.file_path else 'jpg' local_name = f"{user.id}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}.{ext}" temp_path = os.path.join(TEMP_DIR, local_name) await tg_file.download_to_drive(temp_path) archive_path = os.path.join(PHOTOS_DIR, local_name) os.replace(temp_path, archive_path) await print_queue.put((archive_path, copies, user.id, username)) if idx % 3 == 0 or idx == total_photos: await query.message.edit_text(f"⏳ Загружаю фото... {idx}/{total_photos}") except Exception as e: logging.error(f"Ошибка загрузки фото {idx}: {e}") await query.message.reply_text(f"⚠️ Ошибка при загрузке фото {idx}") context.user_data.clear() success_text = ( f"✅ ЗАКАЗ ПРИНЯТ!\n\n" f"📸 Все {total_photos} фото добавлены в очередь печати!\n" f"📎 Копий каждого: {copies}\n" f"📊 Всего отпечатков: {total_photos * copies}\n\n" f"🖨 Печать начнётся автоматически.\n\n" f"📌 Хотите сделать ещё один заказ? Нажмите '📸 Новый заказ'" ) await query.message.reply_text( success_text, reply_markup=get_main_keyboard() ) return ConversationHandler.END async def cancel_order(update: Update, context: ContextTypes.DEFAULT_TYPE): context.user_data.clear() await update.message.reply_text( "❌ Заказ отменён.\n\n" "Нажмите '📸 Новый заказ' чтобы начать снова.", reply_markup=get_main_keyboard() ) return ConversationHandler.END async def reset_all(update: Update, context: ContextTypes.DEFAULT_TYPE): context.user_data.clear() await update.message.reply_text( "🔄 Всё сброшено.\n\n" "Нажмите '📸 Новый заказ' чтобы начать.", reply_markup=get_main_keyboard() ) return ConversationHandler.END async def handle_text(update: Update, context: ContextTypes.DEFAULT_TYPE): text = update.message.text # Обработка кнопок if text == "📸 Новый заказ": return await new_order(update, context) elif text == "🔄 Сбросить": context.user_data.clear() await update.message.reply_text( "🔄 Состояние сброшено.\n\n" "Нажмите '📸 Новый заказ' чтобы начать.", reply_markup=get_main_keyboard() ) return ConversationHandler.END elif text == "❓ Помощь": return await help_command(update, context) elif text == "📸 Добавить ещё фото": pending = context.user_data.get('pending_photos', []) if len(pending) >= MAX_PHOTOS: await update.message.reply_text( f"⚠️ Достигнут лимит - {MAX_PHOTOS} фото.\n" "Нажмите '✅ Готово, ввести количество'.", reply_markup=get_order_keyboard() ) return COLLECT_PHOTOS await update.message.reply_text( f"📸 Отправьте ещё фото ({len(pending)}/{MAX_PHOTOS})\n\n" "💡 Нажмите '✅ Готово' когда закончите.", reply_markup=get_order_keyboard() ) return COLLECT_PHOTOS elif text == "✅ Готово, ввести количество": return await ready_to_count(update, context) elif text == "❌ Отменить заказ": return await cancel_order(update, context) elif text == "🔄 Сбросить всё": return await reset_all(update, context) elif text.startswith("👁 Предпросмотр"): # Извлекаем количество фото из текста pending = context.user_data.get('pending_photos', []) if not pending: await update.message.reply_text( "❌ Нет фото для предпросмотра.", reply_markup=get_main_keyboard() ) return CONFIRM copies = context.user_data.get('copies', 1) # Отправляем инлайн-кнопки для предпросмотра await update.message.reply_text( f"👁 Нажмите кнопку для предпросмотра:\n\n" f"📸 Фото: {len(pending)}\n" f"📎 Копий: {copies}", reply_markup=get_inline_confirm_keyboard(len(pending)) ) return CONFIRM elif text == "✅ Подтвердить печать": pending = context.user_data.get('pending_photos', []) if not pending: await update.message.reply_text( "❌ Нет фото для печати.", reply_markup=get_main_keyboard() ) return CONFIRM # Запускаем печать напрямую user = update.effective_user copies = context.user_data.get('copies', 1) total_photos = len(pending) username = user.username or user.full_name or f"User_{user.id}" await update.message.reply_text("⏳ Загружаю фото и ставлю в очередь печати...") for idx, file_id in enumerate(pending, 1): try: tg_file = await context.bot.get_file(file_id) ext = tg_file.file_path.split('.')[-1] if '.' in tg_file.file_path else 'jpg' local_name = f"{user.id}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}.{ext}" temp_path = os.path.join(TEMP_DIR, local_name) await tg_file.download_to_drive(temp_path) archive_path = os.path.join(PHOTOS_DIR, local_name) os.replace(temp_path, archive_path) await print_queue.put((archive_path, copies, user.id, username)) if idx % 3 == 0 or idx == total_photos: await update.message.reply_text(f"⏳ Загружаю фото... {idx}/{total_photos}") except Exception as e: logging.error(f"Ошибка загрузки фото {idx}: {e}") await update.message.reply_text(f"⚠️ Ошибка при загрузке фото {idx}") context.user_data.clear() success_text = ( f"✅ ЗАКАЗ ПРИНЯТ!\n\n" f"📸 Все {total_photos} фото добавлены в очередь печати!\n" f"📎 Копий каждого: {copies}\n" f"📊 Всего отпечатков: {total_photos * copies}\n\n" f"🖨 Печать начнётся автоматически." ) await update.message.reply_text( success_text, reply_markup=get_main_keyboard() ) return ConversationHandler.END else: # Проверяем, может быть это число? pending = context.user_data.get('pending_photos', []) if pending: try: count = int(text) if 0 < count <= 100: context.user_data['copies'] = count text_summary = ( f"📋 ПРОВЕРЬТЕ ЗАКАЗ\n\n" f"📸 Фото: {len(pending)}\n" f"📎 Копий каждого: {count}\n" f"📊 Всего отпечатков: {len(pending) * count}\n\n" f"✅ Всё верно? Нажмите 'Подтвердить печать'" ) await update.message.reply_text( text_summary, reply_markup=get_confirm_keyboard(len(pending)) ) return CONFIRM except: pass await update.message.reply_text( "❓ Я не понимаю эту команду.\n\n" "🔹 Используйте кнопки для управления:\n" "• '📸 Новый заказ' - начать заказ\n" "• '🔄 Сбросить' - очистить всё\n" "• '❓ Помощь' - справка\n\n" "📸 Или просто отправьте фото!", reply_markup=get_main_keyboard() ) return ConversationHandler.END # ---------- Запуск бота ---------- async def post_init(app: Application): app.create_task(print_worker(app.bot)) for admin_id in ADMIN_IDS: try: await app.bot.send_message(admin_id, "✅ Бот запущен и готов к работе.") except: pass def main(): lock_fd = check_single_instance() if not lock_fd: print("❌ Бот уже запущен. Завершение...") sys.exit(1) logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" ) try: app = Application.builder().token(BOT_TOKEN).post_init(post_init).build() # Основной диалог conv_handler = ConversationHandler( entry_points=[ MessageHandler(filters.Regex(r'^📸 Новый заказ$'), new_order), MessageHandler(filters.PHOTO, handle_photo), CommandHandler("start", start), ], states={ COLLECT_PHOTOS: [ MessageHandler(filters.PHOTO, handle_photo), MessageHandler(filters.Regex(r'^📸 Добавить ещё фото$'), handle_text), MessageHandler(filters.Regex(r'^✅ Готово, ввести количество$'), ready_to_count), MessageHandler(filters.Regex(r'^❌ Отменить заказ$'), cancel_order), MessageHandler(filters.Regex(r'^🔄 Сбросить всё$'), reset_all), MessageHandler(filters.TEXT & ~filters.COMMAND, handle_count), ], CONFIRM: [ CallbackQueryHandler(preview_callback, pattern="^preview$"), CallbackQueryHandler(confirm_callback, pattern="^(confirm|cancel)$"), MessageHandler(filters.Regex(r'^👁 Предпросмотр'), handle_text), MessageHandler(filters.Regex(r'^✅ Подтвердить печать$'), handle_text), MessageHandler(filters.Regex(r'^❌ Отменить заказ$'), cancel_order), MessageHandler(filters.Regex(r'^🔄 Сбросить всё$'), reset_all), ], }, fallbacks=[ CommandHandler("start", start), MessageHandler(filters.Regex(r'^🔄 Сбросить$'), reset_all), MessageHandler(filters.Regex(r'^📸 Новый заказ$'), new_order), MessageHandler(filters.Regex(r'^❓ Помощь$'), help_command), ], ) app.add_handler(conv_handler) # Обработчики для кнопок app.add_handler(MessageHandler(filters.Regex(r'^📸 Новый заказ$'), new_order)) app.add_handler(MessageHandler(filters.Regex(r'^🔄 Сбросить$'), reset_all)) app.add_handler(MessageHandler(filters.Regex(r'^❓ Помощь$'), help_command)) # Запуск logging.info("✅ Бот запущен. Ожидание сообщений...") app.run_polling() except Exception as e: logging.error(f"Ошибка: {e}") finally: release_lock(lock_fd) if __name__ == "__main__": main()