import asyncio import json import os import urllib.parse from datetime import datetime from aiogram import Bot, Dispatcher, types, F from aiogram.filters import Command from aiogram.utils.keyboard import ReplyKeyboardBuilder, InlineKeyboardBuilder from aiogram.types import InputMediaPhoto, InputFile from flask import Flask, request, jsonify, render_template_string, redirect, Response import logging import threading from huggingface_hub import HfApi, hf_hub_download from huggingface_hub.utils import RepositoryNotFoundError from werkzeug.utils import secure_filename import time import uuid logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) BOT_TOKEN = '7835463659:AAGNePbelZIAOeaglyQi1qulOqnjs4BGQn4' bot = Bot(token=BOT_TOKEN) dp = Dispatcher() app = Flask(__name__) DATA_FILE = 'datatestoboto.json' REPO_ID = "Kgshop/bottest" # Changed repository ID HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") asyncio_loop = None bot_is_ready = threading.Event() def load_data(): try: download_db_from_hf() with open(DATA_FILE, 'r', encoding='utf-8') as f: loaded_data = json.load(f) if not (isinstance(loaded_data, dict) and 'products' in loaded_data and 'orders' in loaded_data): logger.error("Неверная структура JSON файла") loaded_data = {'products': [], 'orders': [], 'categories': [], 'users': []} if "categories" not in loaded_data: loaded_data["categories"] = [] if "users" not in loaded_data: loaded_data["users"] = [] for order in loaded_data['orders']: if 'completed' not in order: order['completed'] = False return loaded_data except Exception as e: logger.error(f"Ошибка при загрузке данных: {e}") # If download fails AND local file doesn't exist or is corrupted, return empty structure if not os.path.exists(DATA_FILE) or not isinstance(load_data_from_local(), dict): return {'products': [], 'orders': [], 'categories': [], 'users': []} else: # If download fails but local data is usable, load local data logger.warning("Загрузка с HF не удалась, использую локальные данные.") return load_data_from_local() def load_data_from_local(): try: with open(DATA_FILE, 'r', encoding='utf-8') as f: loaded_data = json.load(f) if not (isinstance(loaded_data, dict) and 'products' in loaded_data and 'orders' in loaded_data): return {'products': [], 'orders': [], 'categories': [], 'users': []} if "categories" not in loaded_data: loaded_data["categories"] = [] if "users" not in loaded_data: loaded_data["users"] = [] for order in loaded_data['orders']: if 'completed' not in order: order['completed'] = False return loaded_data except Exception as e: logger.error(f"Ошибка при загрузке локальных данных: {e}") return {'products': [], 'orders': [], 'categories': [], 'users': []} def save_data(data): try: with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=4) except Exception as e: logger.error(f"Ошибка при сохранении данных: {e}") def upload_db_to_hf(): try: if not HF_TOKEN_WRITE: logger.warning("HF_TOKEN_WRITE не установлен. Пропуск загрузки на Hugging Face.") return if not os.path.exists(DATA_FILE): logger.warning(f"Файл данных {DATA_FILE} не найден. Пропуск загрузки на Hugging Face.") return api = HfApi() api.upload_file( path_or_fileobj=DATA_FILE, path_in_repo=DATA_FILE, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"Автоматическое резервное копирование {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", create_pr=True ) logger.info("База загружена на Hugging Face") except Exception as e: logger.error(f"Ошибка при загрузке резервной копии: {e}") def download_db_from_hf(): try: if not HF_TOKEN_READ: logger.warning("HF_TOKEN_READ не установлен. Пропуск скачивания с Hugging Face.") return hf_hub_download( repo_id=REPO_ID, filename=DATA_FILE, repo_type="dataset", token=HF_TOKEN_READ, local_dir=".", local_dir_use_symlinks=False ) logger.info("База скачана из Hugging Face") except Exception as e: logger.error(f"Ошибка при скачивании: {e}") raise def start_periodic_backup(): def backup_loop(): upload_db_to_hf() threading.Timer(30, backup_loop).start() threading.Timer(30, backup_loop).start() logger.info("Периодическое копирование каждые 30 секунд запущено") data = load_data() def get_main_keyboard(): builder = ReplyKeyboardBuilder() builder.button(text="📋 Каталог") builder.button(text="🛒 Корзина") builder.button(text="📦 Заказы") builder.adjust(2) return builder.as_markup(resize_keyboard=True) def get_category_keyboard(): builder = InlineKeyboardBuilder() for category in data['categories']: builder.button(text=category['name'], callback_data=f"cat_{category['id']}") builder.adjust(2) return builder.as_markup() def get_product_keyboard(product_id): builder = InlineKeyboardBuilder() builder.button(text="Добавить в корзину", callback_data=f"add_{product_id}") return builder.as_markup() @dp.message(Command("start")) async def cmd_start(message: types.Message): user_id = message.from_user.id if user_id not in data.get('users', []): data['users'].append(user_id) save_data(data) upload_db_to_hf() # Upload after adding a new user logger.info(f"Новый пользователь добавлен: {user_id}") await message.answer("Здравствуйте ! это магазин Routine!. Выберите действие:", reply_markup=get_main_keyboard()) @dp.message(F.text == "📋 Каталог") async def show_categories(message: types.Message): if not data['categories']: await message.answer("Нет доступных категорий.") return await message.answer("Выберите категорию:", reply_markup=get_category_keyboard()) @dp.callback_query(F.data.startswith("cat_")) async def show_products_in_category(callback_query: types.CallbackQuery): try: cat_id = int(callback_query.data.split('_')[1]) products_in_cat = [p for p in data['products'] if p.get('category_id') == cat_id] await bot.answer_callback_query(callback_query.id) # Answer immediately if not products_in_cat: await bot.send_message(callback_query.from_user.id, "В этой категории нет товаров.") return async def send_product_batch(products_batch): for product in products_batch: caption = f"🏷 {product['name']} - {product['price']} сом\nОписание: {product['description']}\n/id: {product['id']}" photos = product.get('photos', []) try: if photos: media_group = [] for idx, photo in enumerate(photos): photo_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{photo}" if idx == 0: media_group.append(InputMediaPhoto(media=photo_url, caption=caption)) else: media_group.append(InputMediaPhoto(media=photo_url)) if media_group: # Send media group only if there are photos await bot.send_media_group(chat_id=callback_query.from_user.id, media=media_group) # Always send the keyboard separately for reliability, especially with media groups await bot.send_message(callback_query.from_user.id, "Выберите действие:", reply_markup=get_product_keyboard(product['id'])) else: await bot.send_message(callback_query.from_user.id, caption, reply_markup=get_product_keyboard(product['id'])) except Exception as e: logger.error(f"Ошибка при отправке медиа или сообщения для товара {product.get('id')}: {e}") # Fallback to sending text if media fails await bot.send_message(callback_query.from_user.id, caption, reply_markup=get_product_keyboard(product['id'])) batch_size = 3 # Adjusted batch size for potentially faster sending for i in range(0, len(products_in_cat), batch_size): batch = products_in_cat[i:i + batch_size] await send_product_batch(batch) await asyncio.sleep(0.5) # Increase sleep slightly except Exception as e: logger.error(f"Ошибка в show_products_in_category: {e}") await bot.send_message(callback_query.from_user.id, "Произошла ошибка при загрузке товаров.") @dp.message(F.text == "🛒 Корзина") async def show_cart(message: types.Message): user_id = message.from_user.id cart = next((o for o in data.get('orders', []) if o['user_id'] == user_id and not o.get('completed')), None) if not cart or not cart['items']: await message.answer("Ваша корзина пуста.") return total = 0 response = "Ваша корзина:\n" for item in cart['items']: product = next((p for p in data.get('products', []) if p['id'] == item['product_id']), None) if product: response += f"🏷 {product['name']} - {product['price']} сом x {item['quantity']}\n" total += product['price'] * item['quantity'] else: response += f"Неизвестный товар (ID: {item['product_id']}) x {item['quantity']}\n" response += f"\nИтого: {total} сом" builder = InlineKeyboardBuilder() builder.button(text="Оформить заказ", callback_data=f"complete_{user_id}") await message.answer(response, reply_markup=builder.as_markup()) @dp.callback_query(F.data.startswith("add_")) async def choose_quantity(callback_query: types.CallbackQuery): try: product_id = int(callback_query.data.split('_')[1]) builder = InlineKeyboardBuilder() for i in range(1, 11): builder.button(text=str(i), callback_data=f"confirm_{product_id}_{i}") builder.adjust(5) await bot.send_message(callback_query.from_user.id, "Выберите количество:", reply_markup=builder.as_markup()) await bot.answer_callback_query(callback_query.id) except Exception as e: logger.error(f"Ошибка при выборе количества: {e}") await bot.answer_callback_query(callback_query.id, "Ошибка") @dp.callback_query(F.data.startswith("confirm_")) async def confirm_add_to_cart(callback_query: types.CallbackQuery): try: parts = callback_query.data.split('_') product_id = int(parts[1]) quantity = int(parts[2]) product = next((p for p in data.get('products', []) if p['id'] == product_id), None) if product: user_id = callback_query.from_user.id cart = next((o for o in data.get('orders', []) if o['user_id'] == user_id and not o.get('completed')), None) if not cart: cart = {'user_id': user_id, 'items': [], 'date': datetime.now().isoformat(), 'completed': False} data['orders'].append(cart) cart['items'].append({'product_id': product_id, 'quantity': quantity}) save_data(data) upload_db_to_hf() await bot.answer_callback_query(callback_query.id, "Товар добавлен в корзину!") else: await bot.answer_callback_query(callback_query.id, "Товар не найден") except Exception as e: logger.error(f"Ошибка при добавлении товара с количеством: {e}") await bot.answer_callback_query(callback_query.id, "Ошибка") @dp.callback_query(F.data.startswith("complete_")) async def complete_order(callback_query: types.CallbackQuery): try: user_id = int(callback_query.data.split('_')[1]) cart = next((o for o in data.get('orders', []) if o['user_id'] == user_id and not o.get('completed')), None) if cart and cart['items']: total = 0 cart_text = "Привет, я хочу сделать заказ:\n" for item in cart['items']: product = next((p for p in data.get('products', []) if p['id'] == item['product_id']), None) if product: cart_text += f"{product['name']} - {product['price']} сом x {item['quantity']}\n" total += product['price'] * item['quantity'] cart_text += f"\nИтого: {total} сом" encoded_text = urllib.parse.quote(cart_text) whatsapp_link = f"https://wa.me/996709513331?text={encoded_text}" cart['completed'] = True cart['date'] = datetime.now().isoformat() save_data(data) upload_db_to_hf() await bot.send_message(user_id, f"Ваш заказ принят! Для завершения оформите его через WhatsApp:\n{whatsapp_link}") await bot.answer_callback_query(callback_query.id) else: await bot.answer_callback_query(callback_query.id, "Ваша корзина пуста.") except Exception as e: logger.error(f"Ошибка при оформлении заказа: {e}") await bot.answer_callback_query(callback_query.id, "Ошибка") @dp.message(F.text == "📦 Заказы") async def show_orders(message: types.Message): user_id = message.from_user.id user_orders = [o for o in data.get('orders', []) if o.get('completed') and o['user_id'] == user_id] if not user_orders: await message.answer("У вас нет оформленных заказов.") return response_list = ["Ваши оформленные заказы:\n"] for order in user_orders: # Safely handle potential missing 'date' key or invalid format date_str = order.get('date', 'Неизвестная дата') try: date_obj = datetime.fromisoformat(date_str) formatted_date = date_obj.strftime('%Y-%m-%d %H:%M') except ValueError: formatted_date = date_str order_text = f"--- Заказ от {formatted_date} ---\n" total = 0 for item in order['items']: product = next((p for p in data.get('products', []) if p['id'] == item['product_id']), None) if product: order_text += f"🏷 {product['name']} - {product['price']} сом x {item['quantity']}\n" total += product['price'] * item['quantity'] else: order_text += f"Неизвестный товар (ID: {item['product_id']}) x {item['quantity']}\n" order_text += f"\nИтого: {total} сом\n" response_list.append(order_text) for text in response_list: await message.answer(text) admin_html = """ Админ-панель

Админ-панель

Отправка объявления

Всего пользователей: {{ total_users }}

Управление категориями

Существующие категории

{% if categories %} {% for category in categories %}
{{ category.name }} (ID: {{ category.id }})
{% endfor %} {% else %}

Нет категорий.

{% endif %}

Управление товарами

Существующие товары

{% if products %} {% for product in products %}
{{ product.name }} - {{ product.price }} сом (ID: {{ product.id }})
{{ product.description }}
{% if product.photos and product.photos|length > 0 %} {% for photo in product.photos %} {{ product.name }} {% endfor %} {% endif %}
{% endfor %} {% else %}

Нет товаров.

{% endif %}

Оформленные заказы

{% if orders %} {% set completed_orders = orders | selectattr('completed', 'equalto', True) | list %} {% if completed_orders %} {% for order in completed_orders %}
Пользователь: {{ order.user_id }}
Дата: {{ order.date | default('Неизвестная дата') }}
Товары: {% for item in order['items'] %} {% set product = products | selectattr('id', 'equalto', item.product_id) | first %} {% if product %} {{ item.quantity }} x {{ product.name }}
{% else %} {{ item.quantity }} x Неизвестный товар (ID: {{ item.product_id }})
{% endif %} {% endfor %}
{% endfor %} {% else %}

Нет оформленных заказов.

{% endif %} {% else %}

Нет оформленных заказов.

{% endif %}

Текущие корзины (незавершенные заказы)

{% if orders %} {% set pending_orders = orders | rejectattr('completed', 'equalto', True) | list %} {% if pending_orders %} {% for order in pending_orders %}
Пользователь: {{ order.user_id }}
Дата создания: {{ order.date | default('Неизвестная дата') }}
Товары в корзине: {% for item in order['items'] %} {% set product = products | selectattr('id', 'equalto', item.product_id) | first %} {% if product %} {{ item.quantity }} x {{ product.name }}
{% else %} {{ item.quantity }} x Неизвестный товар (ID: {{ item.product_id }})
{% endif %} {% endfor %}
{% endfor %} {% else %}

Нет текущих корзин.

{% endif %} {% else %}

Нет текущих корзин.

{% endif %}
""" update_event = threading.Event() @app.route('/') def admin_panel(): try: total_users = len(data.get('users', [])) return render_template_string(admin_html, products=data['products'], orders=data['orders'], categories=data['categories'], repo_id=REPO_ID, total_users=total_users) except Exception as e: logger.error(f"Ошибка в шаблоне: {e}") return "Ошибка сервера", 500 @app.route('/add_product', methods=['POST']) def add_product(): try: name = request.form['name'] price = float(request.form['price']) description = request.form['description'] category_id = int(request.form['category_id']) photos = request.files.getlist('photo') product_id = max((p['id'] for p in data.get('products', [])), default=0) + 1 photos_filenames = [] if HF_TOKEN_WRITE: api = HfApi() for photo in photos[:3]: if photo and photo.filename: unique_filename = f"{uuid.uuid4().hex}_{secure_filename(photo.filename)}" temp_path = os.path.join(".", unique_filename) try: photo.save(temp_path) api.upload_file( path_or_fileobj=temp_path, path_in_repo=f"photos/{unique_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"Добавлено фото {unique_filename} для товара {name}", create_pr=True ) photos_filenames.append(unique_filename) except Exception as upload_e: logger.error(f"Ошибка загрузки фото {unique_filename}: {upload_e}") finally: if os.path.exists(temp_path): os.remove(temp_path) else: logger.warning("HF_TOKEN_WRITE не установлен. Фотографии не будут загружены.") data['products'].append({ 'id': product_id, 'name': name, 'price': price, 'description': description, 'category_id': category_id, 'photos': photos_filenames }) save_data(data) upload_db_to_hf() update_event.set() return redirect("/") except Exception as e: logger.error(f"Ошибка при добавлении товара: {e}") # Redirect on error instead of jsonify for better admin panel flow return redirect("/") @app.route('/delete_product/', methods=['POST']) def delete_product(product_id): try: data['products'] = [p for p in data.get('products', []) if p['id'] != product_id] save_data(data) upload_db_to_hf() update_event.set() return jsonify({'status': 'success'}) except Exception as e: logger.error(f"Ошибка при удалении товара: {e}") return jsonify({'status': 'error', 'message': str(e)}), 500 @app.route('/add_category', methods=['POST']) def add_category(): try: name = request.form['name'] category_id = max((c['id'] for c in data.get('categories', [])), default=0) + 1 data['categories'].append({'id': category_id, 'name': name}) save_data(data) upload_db_to_hf() update_event.set() return redirect("/") except Exception as e: logger.error(f"Ошибка при добавлении категории: {e}") return redirect("/") @app.route('/delete_category/', methods=['POST']) def delete_category(category_id): try: data['categories'] = [c for c in data.get('categories', []) if c['id'] != category_id] save_data(data) upload_db_to_hf() update_event.set() return jsonify({'status': 'success'}) except Exception as e: logger.error(f"Ошибка при удалении категории: {e}") return jsonify({'status': 'error', 'message': str(e)}), 500 async def async_send_broadcast(bot: Bot, user_ids: list[int], text: str, media_url: str = None): logger.info(f"Запуск рассылки для {len(user_ids)} пользователей.") sent_count = 0 failed_count = 0 for user_id in user_ids: try: if media_url: await bot.send_photo(chat_id=user_id, photo=media_url, caption=text) else: await bot.send_message(chat_id=user_id, text=text) sent_count += 1 await asyncio.sleep(0.1) except Exception as e: failed_count += 1 # Log specific error for the user logger.error(f"Не удалось отправить объявление пользователю {user_id}: {e}") await asyncio.sleep(0.1) logger.info(f"Рассылка завершена. Успешно отправлено: {sent_count}, Ошибок: {failed_count}") @app.route('/send_announcement', methods=['POST']) def send_announcement(): try: text = request.form.get('text') media_file = request.files.get('media') media_url = None if not text: return jsonify({'status': 'error', 'message': 'Текст объявления обязателен'}), 400 # Check if the bot's asyncio loop is available and the bot is ready # Increased timeout to 20 seconds as startup might take longer if not bot_is_ready.wait(timeout=20) or asyncio_loop is None: logger.error("Бот не стал готов в течение 20 секунд или asyncio_loop не установлен.") # Redirect instead of jsonify for better admin panel UX on this specific error # return jsonify({'status': 'error', 'message': 'Бот еще не готов. Попробуйте позже.'}), 503 return redirect("/?error=bot_not_ready") # Redirect and indicate error if media_file and media_file.filename: if HF_TOKEN_WRITE: api = HfApi() unique_filename = f"announcement_{uuid.uuid4().hex}_{secure_filename(media_file.filename)}" temp_path = os.path.join(".", unique_filename) try: media_file.save(temp_path) api.upload_file( path_or_fileobj=temp_path, path_in_repo=f"photos/{unique_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"Добавлено медиа для объявления {unique_filename}", create_pr=True ) media_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{unique_filename}" logger.info(f"Медиа для объявления загружено на HF: {media_url}") except Exception as upload_e: logger.error(f"Ошибка загрузки медиа для объявления: {upload_e}") media_url = None logger.warning("Не удалось загрузить медиа для объявления. Объявление будет отправлено без медиа.") finally: if os.path.exists(temp_path): os.remove(temp_path) else: logger.warning("HF_TOKEN_WRITE не установлен. Медиа для объявления не будет загружено.") media_url = None user_ids_to_send = list(data.get('users', [])) if user_ids_to_send: asyncio_loop.call_soon_threadsafe( asyncio_loop.create_task, async_send_broadcast(bot, user_ids_to_send, text, media_url) ) logger.info(f"Задача рассылки поставлена в очередь для {len(user_ids_to_send)} пользователей.") else: logger.warning("Нет пользователей для отправки объявления.") return redirect("/") except Exception as e: logger.error(f"Ошибка при обработке запроса отправки объявления: {e}") # Redirect on unexpected errors as well return redirect("/?error=send_failed") # Redirect and indicate error @app.route('/updates') def sse_updates(): def stream(): while True: update_event.wait() yield "data: update\n\n" update_event.clear() return Response(stream(), mimetype="text/event-stream") @app.route('/broadcast_update', methods=['POST']) def broadcast_update(): update_event.set() return jsonify({'status': 'success'}) async def on_startup(_): global asyncio_loop asyncio_loop = asyncio.get_running_loop() bot_is_ready.set() logger.info("Бот запущен и asyncio_loop получен! Бот готов.") def run_flask(): app.run(host='0.0.0.0', port=7860, debug=True, use_reloader=False) if __name__ == '__main__': # Load data initially data = load_data() # Ensure data is loaded before starting Flask/bot flask_thread = threading.Thread(target=run_flask, daemon=True) flask_thread.start() logger.info("Flask запущен") start_periodic_backup() try: asyncio.run(dp.start_polling(bot, on_startup=on_startup)) except KeyboardInterrupt: logger.info("Остановка") finally: logger.info("Завершение работы.")