diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,2139 +1,586 @@ - -from flask import Flask, render_template_string, request, redirect, url_for, send_file, flash, jsonify +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 flask import Flask, request, jsonify, render_template_string, redirect import logging import threading -import time -from datetime import datetime from huggingface_hub import HfApi, hf_hub_download -from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError +from huggingface_hub.utils import RepositoryNotFoundError from werkzeug.utils import secure_filename -from dotenv import load_dotenv -import requests -import uuid -import telebot -load_dotenv() +# Настройка логирования +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) +# Инициализация бота и Flask +BOT_TOKEN = '8283649768:AAGYWatM-nUVQirgBiBwoAtWZgzfp3QnQjY' +bot = Bot(token=BOT_TOKEN) +dp = Dispatcher() app = Flask(__name__) -app.secret_key = 'your_unique_secret_key_soola_cosmetics_67890_no_login' -DATA_FILE = 'data.json' -SYNC_FILES = [DATA_FILE] +# Путь для хранения данных +DATA_FILE = 'data.json' +# Настройки Hugging Face REPO_ID = "Kgshop/aiocult" HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") -TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN") - -STORE_ADDRESS = " Алматы, «Байсат» 1 этаж, синий сектор 117-118 Бутик " - -CURRENCY_CODE = 'KZT' -CURRENCY_NAME = 'Казахстанский тенге' - -DOWNLOAD_RETRIES = 3 -DOWNLOAD_DELAY = 5 - -STATUS_MAP_RU = { - "new": "Новый", - "accepted": "Принят", - "prepared": "Собран", - "shipped": "Отправлен" -} - -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') - -data_lock = threading.Lock() -bot = None -if TELEGRAM_BOT_TOKEN: - bot = telebot.TeleBot(TELEGRAM_BOT_TOKEN, threaded=False) - -def load_data_internal(): - default_data = {'products': [], 'categories': [], 'orders': {}, 'telegram_users': {}} +# Функции для работы с данными +def load_data(): try: - with open(DATA_FILE, 'r', encoding='utf-8') as file: - data = json.load(file) - if not isinstance(data, dict): - return default_data - if 'products' not in data: data['products'] = [] - if 'categories' not in data: data['categories'] = [] - if 'orders' not in data: data['orders'] = {} - if 'telegram_users' not in data: data['telegram_users'] = {} - return data - except (FileNotFoundError, json.JSONDecodeError): - return default_data + 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': []} + if "categories" not in loaded_data: + loaded_data["categories"] = [] + return loaded_data + except Exception as e: + logger.error(f"Ошибка при загрузке данных: {e}") + return {'products': [], 'orders': [], 'categories': []} -def save_data_internal(data): +def save_data(data): try: - if 'products' not in data: data['products'] = [] - if 'categories' not in data: data['categories'] = [] - if 'orders' not in data: data['orders'] = {} - if 'telegram_users' not in data: data['telegram_users'] = {} - - with open(DATA_FILE, 'w', encoding='utf-8') as file: - json.dump(data, file, ensure_ascii=False, indent=4) + with open(DATA_FILE, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=4) + # upload_db_to_hf() убрано отсюда except Exception as e: - logging.error(f"Error saving data to {DATA_FILE}: {e}", exc_info=True) - -if bot: - @bot.message_handler(commands=['start']) - def send_welcome(message): - user_id = message.from_user.id - username = message.from_user.username - first_name = message.from_user.first_name - - with data_lock: - data = load_data_internal() - if 'telegram_users' not in data: - data['telegram_users'] = {} - - str_id = str(user_id) - data['telegram_users'][str_id] = { - 'username': username, - 'first_name': first_name, - 'joined_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S') - } - save_data_internal(data) - logging.info(f"New Telegram user registered: {user_id}") - - bot.send_message(message.chat.id, "Вы зарегистрированы. Теперь мы сможем присылать вам уведомления.") - -def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY): - if not HF_TOKEN_READ and not HF_TOKEN_WRITE: - logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.") + logger.error(f"Ошибка при сохранении данных: {e}") - token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE - - files_to_download = [specific_file] if specific_file else SYNC_FILES - logging.info(f"Attempting download for {files_to_download} from {REPO_ID}...") - all_successful = True - - for file_name in files_to_download: - success = False - for attempt in range(retries + 1): - try: - logging.info(f"Downloading {file_name} (Attempt {attempt + 1}/{retries + 1})...") - local_path = hf_hub_download( - repo_id=REPO_ID, - filename=file_name, - repo_type="dataset", - token=token_to_use, - local_dir=".", - local_dir_use_symlinks=False, - force_download=True, - resume_download=False - ) - logging.info(f"Successfully downloaded {file_name} to {local_path}.") - success = True - break - except RepositoryNotFoundError: - logging.error(f"Repository {REPO_ID} not found. Download cancelled for all files.") - return False - except HfHubHTTPError as e: - if e.response.status_code == 404: - logging.warning(f"File {file_name} not found in repo {REPO_ID} (404). Skipping this file.") - if attempt == 0 and not os.path.exists(file_name): - try: - if file_name == DATA_FILE: - with open(file_name, 'w', encoding='utf-8') as f: - json.dump({'products': [], 'categories': [], 'orders': {}, 'telegram_users': {}}, f) - logging.info(f"Created empty local file {file_name} because it was not found on HF.") - except Exception as create_e: - logging.error(f"Failed to create empty local file {file_name}: {create_e}") - success = False - break - else: - logging.error(f"HTTP error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...") - except requests.exceptions.RequestException as e: - logging.error(f"Network error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...") - except Exception as e: - logging.error(f"Unexpected error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...", exc_info=True) - - if attempt < retries: - time.sleep(delay) - - if not success: - logging.error(f"Failed to download {file_name} after {retries + 1} attempts.") - all_successful = False - - logging.info(f"Download process finished. Overall success: {all_successful}") - return all_successful - -def upload_db_to_hf(specific_file=None): - if not HF_TOKEN_WRITE: - logging.warning("HF_TOKEN (for writing) not set. Skipping upload to Hugging Face.") - return +def upload_db_to_hf(): try: api = HfApi() - files_to_upload = [specific_file] if specific_file else SYNC_FILES - logging.info(f"Starting upload of {files_to_upload} to HF repo {REPO_ID}...") + 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')}" + ) + logger.info("База загружена на Hugging Face") + except Exception as e: + logger.error(f"Ошибка при загрузке резервной копии: {e}") - for file_name in files_to_upload: - if os.path.exists(file_name): - try: - api.upload_file( - path_or_fileobj=file_name, - path_in_repo=file_name, - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" - ) - logging.info(f"File {file_name} successfully uploaded to Hugging Face.") - except Exception as e: - logging.error(f"Error uploading file {file_name} to Hugging Face: {e}") - else: - logging.warning(f"File {file_name} not found locally, skipping upload.") - logging.info("Finished uploading files to HF.") +def download_db_from_hf(): + try: + 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: - logging.error(f"General error during Hugging Face upload initialization or process: {e}", exc_info=True) + logger.error(f"Ошибка при скачивании: {e}") + raise -def periodic_backup(): - backup_interval = 1800 - logging.info(f"Setting up periodic backup every {backup_interval} seconds.") - while True: - time.sleep(backup_interval) - logging.info("Starting periodic backup...") +# Периодическое копирование каждые 30 секунд +def start_periodic_backup(): + def backup_loop(): upload_db_to_hf() - logging.info("Periodic backup finished.") - -def load_data(): - with data_lock: - default_data = {'products': [], 'categories': [], 'orders': {}, 'telegram_users': {}} - try: - with open(DATA_FILE, 'r', encoding='utf-8') as file: - data = json.load(file) - logging.info(f"Local data loaded successfully from {DATA_FILE}") - if not isinstance(data, dict): - logging.warning(f"Local {DATA_FILE} is not a dictionary. Attempting download.") - raise FileNotFoundError - if 'products' not in data: data['products'] = [] - if 'categories' not in data: data['categories'] = [] - if 'orders' not in data: data['orders'] = {} - if 'telegram_users' not in data: data['telegram_users'] = {} - return data - except FileNotFoundError: - logging.warning(f"Local file {DATA_FILE} not found. Attempting download from HF.") - except json.JSONDecodeError: - logging.error(f"Error decoding JSON in local {DATA_FILE}. File might be corrupt. Attempting download.") + # Запускаем следующий вызов через 30 секунд + 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): + 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()) - if download_db_from_hf(specific_file=DATA_FILE): - try: - with open(DATA_FILE, 'r', encoding='utf-8') as file: - data = json.load(file) - logging.info(f"Data loaded successfully from {DATA_FILE} after download.") - if not isinstance(data, dict): - return default_data - if 'products' not in data: data['products'] = [] - if 'categories' not in data: data['categories'] = [] - if 'orders' not in data: data['orders'] = {} - if 'telegram_users' not in data: data['telegram_users'] = {} - return data - except Exception as e: - logging.error(f"Unknown error loading downloaded {DATA_FILE}: {e}. Using default.", exc_info=True) - return default_data - else: - logging.error(f"Failed to download {DATA_FILE} from HF after retries. Using empty default data structure.") - if not os.path.exists(DATA_FILE): +@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] + + if not products_in_cat: + await bot.send_message(callback_query.from_user.id, "В этой категории нет товаров.") + await bot.answer_callback_query(callback_query.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: - with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump(default_data, f) - logging.info(f"Created empty local file {DATA_FILE} after failed download.") - except Exception as create_e: - logging.error(f"Failed to create empty local file {DATA_FILE}: {create_e}") - return default_data - -def save_data(data): - with data_lock: - try: - if 'products' not in data: data['products'] = [] - if 'categories' not in data: data['categories'] = [] - if 'orders' not in data: data['orders'] = {} - if 'telegram_users' not in data: data['telegram_users'] = {} - - with open(DATA_FILE, 'w', encoding='utf-8') as file: - json.dump(data, file, ensure_ascii=False, indent=4) - upload_db_to_hf(specific_file=DATA_FILE) - except Exception as e: - logging.error(f"Error saving data to {DATA_FILE}: {e}", exc_info=True) - -def start_bot_polling(): - if bot: - logging.info("Starting Telebot polling...") - bot.infinity_polling() - -def broadcast_message(user_ids, text): - if not bot: - return 0 - - count = 0 - for user_id in user_ids: - try: - bot.send_message(chat_id=user_id, text=text) - count += 1 - time.sleep(0.05) - except Exception as e: - logging.error(f"Failed to send message to {user_id}: {e}") - return count - -CATALOG_TEMPLATE = ''' - - -
- - -
-
- {% endif %}
- {{ product.get('description', '')[:50] }}{% if product.get('description', '')|length > 50 %}...{% endif %}
-Категория: {{ product.get('category', 'Без категории') }}
-Цена: {{ "%.2f"|format(product['price']) }} {{ currency_code }}
-Описание:
{{ product.get('description', 'Описание отсутствует.')|replace('\\n', '
')|safe }}
Доступные цвета/варианты: {{ colors|select('ne', '')|join(', ') }}
- {% endif %} - {% set models = product.get('models', []) %} - {% if models and models|select('ne', '')|list|length > 0 %} -Доступные модели/объемы: {{ models|select('ne', '')|join(', ') }}
- {% endif %} -Общая сумма товаров: {{ "%.2f"|format(order.total_price) }} {{ currency_code }}
-ИТОГО К ОПЛАТЕ: {{ "%.2f"|format(order.total_price) }} {{ currency_code }}
-Ваш текущий статус: {{ status_map_ru.get(order.status, order.status) }}
-Этот заказ был оформлен без входа в систему.
-Пожалуйста, свяжитесь с нами по WhatsApp для подтверждения и уточнения деталей.
-Заказ с таким ID не найден.
- ← Вернуться в каталог - {% endif %} -
- {{ variant_display }}
{% endif %} -Нет категорий.
+ {% endif %} +Категорий пока нет.
- {% endif %} -Магазин работает в режиме каталога, заказы оформляются анонимно и должны быть подтверждены через WhatsApp.
-Адрес магазина: {{ store_address }}
-Валюта: {{ currency_name }} ({{ currency_code }})
-
- {% endif %}
- Категория: {{ product.get('category', 'Без категории') }}
-Цена: {{ "%.2f"|format(product['price']) }} {{ currency_code }}
-Описание: {{ product.get('description', 'N/A')[:100] }}{% if product.get('description', '')|length > 100 %}...{% endif %}
- {% set colors = product.get('colors', []) %} - {% set models = product.get('models', []) %} -Цвета: {{ colors|select('ne', '')|join(', ') if colors|select('ne', '')|list|length > 0 else 'Нет' }}
-Модели: {{ models|select('ne', '')|join(', ') if models|select('ne', '')|list|length > 0 else 'Нет' }}
-Нет товаров.
+ {% endif %} +Товаров пока нет.
- {% endif %} -Нет заказов.
+ {% endif %}