diff --git "a/app.py" "b/app.py" deleted file mode 100644--- "a/app.py" +++ /dev/null @@ -1,2534 +0,0 @@ -import os -import io -import base64 -import json -import logging -import threading -import time -import math -import re -import urllib.parse -from datetime import datetime, timedelta, timezone -from uuid import uuid4 -import random -import string -from flask import Flask, render_template_string, request, redirect, url_for, flash, jsonify, session, make_response -from PIL import Image -import google.generativeai as genai -from huggingface_hub import HfApi, hf_hub_download -from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError -from werkzeug.utils import secure_filename -from dotenv import load_dotenv -import requests - -load_dotenv() - -app = Flask(__name__) -app.secret_key = 'your_unique_secret_key_gippo_312_shop_54321_no_login' -DATA_FILE = 'data.json' -SYNC_FILES = [DATA_FILE] -REPO_ID = "Kgshop/bc" -HF_TOKEN_WRITE = os.getenv("HF_TOKEN") -HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") -GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY") - -DOWNLOAD_RETRIES = 3 -DOWNLOAD_DELAY = 5 -ALMATY_TZ = timezone(timedelta(hours=6)) - -db_lock = threading.RLock() - -CURRENCIES = { - 'KGS': 'Кыргызский сом', - 'KZT': 'Казахстанский тенге', - 'UAH': 'Украинская гривна', - 'RUB': 'Российский рубль', - 'USD': 'Доллар США', - 'EUR': 'Евро', - 'UZS': 'Узбекский сум' -} - -COLOR_SCHEMES = { - 'default': 'Бирюзовый (по умолч.)', - 'forest': 'Лесной зеленый', - 'ocean': 'Глубокий синий', - 'sunset': 'Закатный оранжевый', - 'lavender': 'Лавандовый', - 'vintage': 'Винтажный', - 'dark': 'Полночь (тёмная)', - 'cosmic': 'Космическая ночь', - 'minty': 'Свежая мята', - 'mocha': 'Кофейный мокко', - 'crimson': 'Багровый рассвет', - 'solar': 'Солнечная вспышка', - 'cyberpunk': 'Киберпанк неон', - 'neon': 'Неоновая вспышка', - 'pastel': 'Пастельный (светлый)', - 'emerald': 'Изумрудный город', - 'gold': 'Роскошное золото', - 'sakura': 'Цветение сакуры (светлый)', - 'arctic': 'Арктический лед (светлый)', - 'volcano': 'Магма', - 'monochrome_light': 'Классика (Светлая)', - 'monochrome_dark': 'Классика (Темная)', - 'nord': 'Скандинавский Норд', - 'dracula': 'Дракула (Темный)', - 'ruby': 'Глубокий Рубин', - 'sapphire': 'Королевский Сапфир', - 'amethyst': 'Аметистовый блеск' -} - -ICONS = { - 'fa-link': 'Ссылка (вебсайт)', - 'fa-phone': 'Телефон', - 'fa-whatsapp': 'WhatsApp', - 'fa-telegram': 'Telegram', - 'fa-instagram': 'Instagram', - 'fa-envelope': 'Email', - 'fa-map-marker-alt': 'Локация/Адрес', - 'fa-youtube': 'YouTube', - 'fa-tiktok': 'TikTok' -} - -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') - -def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY): - if not HF_TOKEN_READ and not HF_TOKEN_WRITE: - pass - 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 - all_successful = True - for file_name in files_to_download: - success = False - for attempt in range(retries + 1): - try: - 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 - ) - success = True - break - except RepositoryNotFoundError: - return False - except HfHubHTTPError as e: - if e.response.status_code == 404: - 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({}, f) - except Exception: - pass - success = False - break - else: - pass - except requests.exceptions.RequestException: - pass - except Exception: - pass - if attempt < retries: - time.sleep(delay) - if not success: - all_successful = False - return all_successful - -def upload_db_to_hf(specific_file=None): - if not HF_TOKEN_WRITE: - return - try: - api = HfApi() - files_to_upload = [specific_file] if specific_file else SYNC_FILES - 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(ALMATY_TZ).strftime('%Y-%m-%d %H:%M:%S')}" - ) - except Exception: - pass - except Exception: - pass - -def periodic_backup(): - backup_interval = 1800 - while True: - time.sleep(backup_interval) - upload_db_to_hf() - -def load_data(): - with db_lock: - try: - with open(DATA_FILE, 'r', encoding='utf-8') as f: - data = json.load(f) - if not isinstance(data, dict): - data = {} - except (FileNotFoundError, json.JSONDecodeError): - if download_db_from_hf(specific_file=DATA_FILE): - try: - with open(DATA_FILE, 'r', encoding='utf-8') as f: - data = json.load(f) - if not isinstance(data, dict): - data = {} - except (FileNotFoundError, json.JSONDecodeError): - data = {} - else: - data = {} - return data - -def save_data(data): - with db_lock: - try: - with open(DATA_FILE, 'w', encoding='utf-8') as file: - json.dump(data, file, ensure_ascii=False, indent=4) - except Exception: - pass - upload_db_to_hf(specific_file=DATA_FILE) - -def get_env_data(env_id): - with db_lock: - all_data = load_data() - default_settings = { - "vcard_firstname": "Имя", - "vcard_lastname": "Фамилия", - "vcard_job": "Специалист", - "organization_name": "Моя Компания", - "currency_code": "KGS", - "chat_avatar": None, - "color_scheme": "default", - "admin_password_enabled": False, - "admin_password": "", - "categories_as_lines": False, - "about_text": "Привет! Это моя онлайн-визитка.", - "enable_cart": False, - "order_messenger": "whatsapp", - "order_contact": "" - } - - env_data = all_data.get(env_id, {}) - if not env_data: - env_data = { - 'products': [], 'categories':[], 'blocks':[], 'orders': [], - 'settings': default_settings - } - - if 'products' not in env_data: env_data['products'] = [] - if 'categories' not in env_data: env_data['categories'] = [] - if 'settings' not in env_data: env_data['settings'] = default_settings - if 'blocks' not in env_data: env_data['blocks'] = [] - if 'orders' not in env_data: env_data['orders'] = [] - - settings_changed = False - for key, value in default_settings.items(): - if key not in env_data['settings']: - env_data['settings'][key] = value - settings_changed = True - - products_changed = False - for product in env_data['products']: - if 'product_id' not in product: - product['product_id'] = uuid4().hex - products_changed = True - if 'views' not in product: - product['views'] = 0 - products_changed = True - if 'price' not in product: - product['price'] = 0.0 - products_changed = True - - if products_changed or settings_changed: - save_env_data(env_id, env_data) - - return env_data - -def save_env_data(env_id, env_data): - with db_lock: - all_data = load_data() - all_data[env_id] = env_data - save_data(all_data) - -def configure_gemini(): - if not GOOGLE_API_KEY: - return False - try: - genai.configure(api_key=GOOGLE_API_KEY) - return True - except Exception: - return False - -def generate_ai_description_from_image(image_data, language): - if not configure_gemini(): - raise ValueError("Google AI API не настроен.") - try: - if not image_data: - raise ValueError("Файл изображения не найден.") - image_stream = io.BytesIO(image_data) - image = Image.open(image_stream).convert('RGB') - except Exception: - raise ValueError("Не удалось обработать изображение.") - base_prompt = "Напиши привлекательное описание для этого товара/услуги. Текст должен быть ем��им, продающим, с использованием эмодзи. Не пиши цены, адреса и телефоны." - lang_suffix = "" - if language == "Русский": - lang_suffix = " Пиши на русском языке." - elif language == "Кыргызский": - lang_suffix = " Пиши на кыргызском языке." - elif language == "Казахский": - lang_suffix = " Пиши на казахском языке." - elif language == "Узбекский": - lang_suffix = " Пиши на узбекском языке." - final_prompt = f"{base_prompt}{lang_suffix}" - try: - model = genai.GenerativeModel('gemma-4-31b-it') - response = model.generate_content([final_prompt, image]) - if hasattr(response, 'text'): - return response.text - else: - if response.parts: - return "".join(part.text for part in response.parts if hasattr(part, 'text')) - else: - response.resolve() - return response.text - except Exception as e: - raise ValueError(f"Ошибка при генерации контента: {e}") - -LANDING_PAGE_TEMPLATE = ''' - - - - - - Платформа Онлайн-Визиток - - - - - - -''' - -LOGIN_TEMPLATE = ''' - - - - - - Вход в Админ-панель - - - - -
-

Вход

- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} -
{{ message }}
- {% endfor %} - {% endif %} - {% endwith %} -
- - -
-
- - -''' - -ADMHOSTO_TEMPLATE = ''' - - - - - - Главная Админ-панель - - - - - -
-

Управление Визитками

- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} -
{{ message }}
- {% endfor %} - {% endif %} - {% endwith %} -
-
- -
-
-
- -
-
-

Существующие визитки

- {% if environments %} - - {% else %} -

Пока не создано ни одной визитки.

- {% endif %} -
-
- - - -''' - -CATALOG_TEMPLATE = ''' - - - - - - {{ settings.vcard_firstname }} {{ settings.vcard_lastname }} - Визитка - - - - - - - -
-
- -
- -
- Avatar -
{{ settings.vcard_firstname }} {{ settings.vcard_lastname }}
- {% if settings.vcard_job %}
{{ settings.vcard_job }}
{% endif %} - {% if settings.organization_name %}
{{ settings.organization_name }}
{% endif %} - {% if settings.about_text %}
{{ settings.about_text|replace('\\n', '
')|safe }}
{% endif %} -
- - - Сохранить в контакты - - - {% if blocks %} -
- {% for block in blocks %} - {% if block.type == 'link' %} - - {% if block.icon %} - {% set prefix = 'fab' if block.icon in ['fa-whatsapp', 'fa-telegram', 'fa-instagram', 'fa-youtube', 'fa-tiktok'] else 'fas' %} - - {% endif %} - {{ block.title }} - - {% elif block.type == 'text' %} -
- {% if block.title %}

{{ block.title }}

{% endif %} -

{{ block.content|replace('\\n', '
')|safe }}

-
- {% elif block.type == 'pdf' %} - - {% endif %} - {% endfor %} -
- {% endif %} - - {% if products_json != '[]' %} -
-

Каталог / Услуги

-
- - -
- -
-
-
- -
-
- {% endif %} -
- -
- - 0 -
- - - - - - - - - - - - - -''' - -ADMIN_TEMPLATE = ''' - - - - - - Настройки Визитки - {{ settings.organization_name }} - - - - - -
-
-

Сохранение данных...

-
-
-
-
- Logo -

Настройки Визитки

-
-
- Открыть визитку - - История заказов - {% if new_orders_count > 0 %} - {{ new_orders_count }} - {% endif %} - - - {% if settings.admin_password_enabled %} - Выход - {% endif %} -
-
- - {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} -
{{ message }}
- {% endfor %} - {% endif %} - {% endwith %} - -
-
- Профиль и Внешний вид -
-
- - -
-
- - -
-
- - -
-
- - - - - - - - - - - - - {% if settings.chat_avatar %} -

Текущий аватар:

- {% endif %} - - - - - - - -

Настройки корзины и заказов

- - - - - - -
-

Пароль для входа сюда

- - - -
- - -
-
-
-
- -
-

Кнопки и Ссылки (для Визитки)

-
- Добавить кнопку -
-
- - - - - - -
- - -
- -
- - -
- - - -
-
-
-
- {% if blocks %} - {% for block in blocks %} -
-
- - {% if block.icon %} - {% set prefix = 'fab' if block.icon in ['fa-whatsapp', 'fa-telegram', 'fa-instagram', 'fa-youtube', 'fa-tiktok'] else 'fas' %} - - {% endif %} - {{ block.title }} - - - ({% if block.type == 'link' %}Ссылка{% elif block.type == 'pdf' %}PDF{% else %}Текст{% endif %}) - - {% if block.type == 'link' %}
{{ block.url }}{% endif %} -
-
- -
- - - -
-
- - - -
-
- - - -
-
-
-
-
- - - - - - - - - -
- - -
- -
- - -
- - - -
-
- {% endfor %} - {% else %} -

Кнопки не добавлены.

- {% endif %} -
- -
- -
-

Категории товаров/услуг

-
- Добавить новую категорию -
-
- - - - -
-
-
- {% if categories %} -
- {% for category in categories %} -
- {{ category }} -
- - - -
-
- {% endfor %} -
- {% else %} -

Категорий пока нет.

- {% endif %} -
- -
-

Товары и Услуги

-
- Добавить товар/услугу -
-
- - - - - - - - - - - - - -
- - -
- - - - - -
-
-
- -

Список товаров/услуг:

-
- - - {% if search_q %} - Сброс - {% endif %} -
- - {% if paginated_products %} -
- {% for product in paginated_products %} -
-
-
- {% if product.get('photos') %} - Фото - {% else %} - Нет фото - {% endif %} -
-
-

{{ product['name'] }}

-

Категория: {{ product.get('category', 'Без категории') }}

-

Цена: {% if product.get('price', 0) > 0 %}{{ "%.2f"|format(product.get('price', 0)) }} {{ currency_code }}{% else %}Не указана{% endif %}

-
-
- -
- -
- - - -
-
- -
-

Редактирование

-
- - - - - - - - - - - - - - - - - - - -
-
-
- {% endfor %} -
- - {% if total_pages > 1 %} - - {% endif %} - - {% else %} -

Записей пока нет или по вашему запросу ничего не найдено.

- {% endif %} -
-
- - - - -''' - -ORDER_TEMPLATE = ''' - - - - - - Заказ #{{ order['id'] }} - - - - - -
-
-

Накладная заказа

-

#{{ order['id'] }} от {{ order['date'] }}

-
-
-

Покупатель: {{ order['customer_name'] }}

-

Телефон: {{ order['customer_phone'] }}

-

Продавец: {{ settings['organization_name'] }}

-
- -
- {% for pid, item in order['items'].items() %} -
- {% if item['photo'] %} - {{ item['name'] }} - {% else %} - Нет фото - {% endif %} -
-
{{ item['name'] }}
-
{{ item['qty'] }} шт. × {{ "%.2f"|format(item['price']|float) }} {{ currency }}
-
-
- {{ "%.2f"|format(item['qty']|int * item['price']|float) }} {{ currency }} -
-
- {% endfor %} -
- -
- Итого: {{ "%.2f"|format(order['total']|float) }} {{ currency }} -
- - {% if send_link != "#" %} - - Отправить продавцу ({{ messenger_name }}) - - {% endif %} - - Вернуться в каталог -
- - -''' - -ADMIN_ORDERS_TEMPLATE = ''' - - - - - - История заказов - {{ settings.organization_name }} - - - - - -
-
-

История заказов

- Назад в панель -
- {% if orders %} - {% for order in orders %} -
-
-
- Заказ #{{ order['id'] }} - {{ order['date'] }} -
-
- Накладная -
- -
-
-
-
-

Покупатель: {{ order['customer_name'] }}

-

Телефон: {{ order['customer_phone'] }}

-
-
- {% for pid, item in order['items'].items() %} -
- {{ item['name'] }} ({{ item['qty'] }} шт.) - {{ "%.2f"|format(item['price']|float * item['qty']|int) }} {{ currency }} -
- {% endfor %} -
- Итого: {{ "%.2f"|format(order['total']|float) }} {{ currency }} -
-
-
- {% endfor %} - {% else %} -
- -

История заказов пуста.

-
- {% endif %} -
- - -''' - -@app.route('/') -def index(): - return render_template_string(LANDING_PAGE_TEMPLATE) - -@app.route('/admhosto', methods=['GET']) -def admhosto(): - data = load_data() - environments_data = [] - for env_id, env_data in data.items(): - settings = env_data.get('settings', {}) - org_name = settings.get("organization_name", f"Визитка {env_id}") - environments_data.append({ - "id": env_id, - "org_name": org_name, - "pwd_enabled": settings.get("admin_password_enabled", False), - "password": settings.get("admin_password", "") - }) - environments_data.sort(key=lambda x: x['id']) - return render_template_string(ADMHOSTO_TEMPLATE, environments=environments_data) - -@app.route('/admhosto/create', methods=['POST']) -def create_environment(): - all_data = load_data() - while True: - new_id = ''.join(random.choices(string.digits, k=6)) - if new_id not in all_data: - break - all_data[new_id] = { - 'products': [], 'categories':[], 'blocks':[], 'orders': [], - 'settings': { - "vcard_firstname": "Имя", - "vcard_lastname": "Фамилия", - "vcard_job": "Специалист", - "organization_name": "Моя Компания", - "currency_code": "KGS", - "chat_avatar": None, - "color_scheme": "default", - "admin_password_enabled": False, - "admin_password": "", - "categories_as_lines": False, - "about_text": "Привет! Это моя онлайн-визитка.", - "enable_cart": False, - "order_messenger": "whatsapp", - "order_contact": "" - } - } - save_data(all_data) - flash(f'Новая визитка с ID {new_id} успешно создана.', 'success') - return redirect(url_for('admhosto')) - -@app.route('/admhosto/update_pwd/', methods=['POST']) -def update_env_pwd(env_id): - all_data = load_data() - if env_id in all_data: - pwd_enabled = 'pwd_enabled' in request.form - password = request.form.get('password', '').strip() - all_data[env_id]['settings']['admin_password_enabled'] = pwd_enabled - all_data[env_id]['settings']['admin_password'] = password - save_data(all_data) - flash(f'Пароль для визитки {env_id} обновлен.', 'success') - else: - flash(f'Визитка {env_id} не найдена.', 'error') - return redirect(url_for('admhosto')) - -@app.route('/admhosto/delete/', methods=['POST']) -def delete_environment(env_id): - all_data = load_data() - if env_id in all_data: - del all_data[env_id] - save_data(all_data) - flash(f'Визитка {env_id} была удалена.', 'success') - else: - flash(f'Визитка {env_id} не найдена.', 'error') - return redirect(url_for('admhosto')) - -@app.route('//login', methods=['GET', 'POST']) -def admin_login(env_id): - data = get_env_data(env_id) - settings = data.get('settings', {}) - - if not settings.get('admin_password_enabled'): - return redirect(url_for('admin', env_id=env_id)) - - if request.method == 'POST': - pwd = request.form.get('password', '') - if pwd == settings.get('admin_password', ''): - session[f'admin_auth_{env_id}'] = True - return redirect(url_for('admin', env_id=env_id)) - else: - flash('Неверный пароль', 'error') - - return render_template_string(LOGIN_TEMPLATE, env_id=env_id) - -@app.route('//logout') -def admin_logout(env_id): - session.pop(f'admin_auth_{env_id}', None) - return redirect(url_for('admin_login', env_id=env_id)) - -@app.route('//vcard') -def download_vcard(env_id): - data = get_env_data(env_id) - settings = data.get('settings', {}) - blocks = data.get('blocks', []) - - first_name = settings.get("vcard_firstname", "") - last_name = settings.get("vcard_lastname", "") - org = settings.get("organization_name", "") - title = settings.get("vcard_job", "") - note = settings.get("about_text", "").replace('\n', '\\n') - - card_url = url_for('catalog', env_id=env_id, _external=True) - - vcard = [ - "BEGIN:VCARD", - "VERSION:3.0", - f"N:{last_name};{first_name};;;", - f"FN:{first_name} {last_name}".strip(), - ] - - if org: vcard.append(f"ORG:{org}") - if title: vcard.append(f"TITLE:{title}") - if note: vcard.append(f"NOTE:{note}") - - vcard.append(f"URL;type=pref:{card_url}") - - for b in blocks: - if b.get('type') == 'link' and b.get('url'): - url = b.get('url', '').strip() - if url.startswith('+') or url.replace('-', '').replace(' ', '').isdigit(): - vcard.append(f"TEL;TYPE=CELL:{url}") - elif '@' in url and not url.startswith('http'): - vcard.append(f"EMAIL;TYPE=WORK:{url.replace('mailto:', '')}") - else: - vcard.append(f"URL:{url}") - - vcard.append("END:VCARD") - vcard_str = "\r\n".join(vcard) - - response = make_response(vcard_str) - response.headers["Content-Disposition"] = f"attachment; filename=contact_{env_id}.vcf" - response.headers["Content-Type"] = "text/vcard; charset=utf-8" - return response - -@app.route('//catalog') -def catalog(env_id): - data = get_env_data(env_id) - all_products = data.get('products',[]) - settings = data.get('settings', {}) - blocks = data.get('blocks',[]) - - product_categories = set(p.get('category', 'Без категории') for p in all_products) - admin_categories = set(data.get('categories',[])) - all_cat_names = sorted(list(product_categories.union(admin_categories))) - - products_sorted_for_js = sorted(all_products, key=lambda p: p.get('name', '').lower()) - - products_by_category = {cat:[] for cat in all_cat_names} - for product in all_products: - products_by_category[product.get('category', 'Без категории')].append(product) - ordered_categories = [cat for cat in all_cat_names if products_by_category.get(cat)] - - chat_avatar_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/avatars/{settings['chat_avatar']}" if settings.get('chat_avatar') else "https://huggingface.co/spaces/gippo312/admin/resolve/main/Picsart_25-11-04_12-02-21-390.png" - - return render_template_string( - CATALOG_TEMPLATE, ordered_categories=ordered_categories, - products_json=json.dumps(products_sorted_for_js), repo_id=REPO_ID, currency_code=settings.get('currency_code', 'KGS'), - settings=settings, chat_avatar_url=chat_avatar_url, env_id=env_id, blocks=blocks - ) - -@app.route('//checkout', methods=['POST']) -def checkout(env_id): - req_data = request.get_json() - env_data = get_env_data(env_id) - - items = req_data.get('items', {}) - if not items: - return jsonify({"success": False, "error": "Корзина пуста"}) - - total = sum(item.get('qty', 0) * item.get('price', 0) for item in items.values()) - - order = { - "id": ''.join(random.choices(string.digits, k=8)), - "date": datetime.now(ALMATY_TZ).strftime('%Y-%m-%d %H:%M:%S'), - "customer_name": req_data.get('customer_name', ''), - "customer_phone": req_data.get('customer_phone', ''), - "items": items, - "total": total, - "viewed": False - } - - if 'orders' not in env_data: - env_data['orders'] = [] - env_data['orders'].append(order) - save_env_data(env_id, env_data) - - return jsonify({"success": True, "url": url_for('order_invoice', env_id=env_id, order_id=order['id'])}) - -@app.route('//order/') -def order_invoice(env_id, order_id): - env_data = get_env_data(env_id) - settings = env_data.get('settings', {}) - orders = env_data.get('orders', []) - - order = next((o for o in orders if o.get('id') == order_id), None) - if not order: - return "Заказ не найден", 404 - - currency = settings.get('currency_code', 'KGS') - - text = f"Здравствуйте! Я оформил заказ #{order['id']} на сумму {order['total']} {currency}.\nСсылка на накладную: {request.url}" - encoded_text = urllib.parse.quote(text) - - messenger = settings.get('order_messenger', 'whatsapp') - contact = settings.get('order_contact', '') - - if messenger == 'whatsapp': - num = re.sub(r'[^0-9+]', '', contact) - send_link = f"https://wa.me/{num}?text={encoded_text}" - messenger_name = "WhatsApp" - elif messenger == 'telegram': - username = contact.replace('@', '').strip() - send_link = f"tg://resolve?domain={username}&text={encoded_text}" - messenger_name = "Telegram" - else: - send_link = "#" - messenger_name = "Нет" - - return render_template_string( - ORDER_TEMPLATE, - order=order, - currency=currency, - send_link=send_link, - messenger_name=messenger_name, - repo_id=REPO_ID, - settings=settings, - env_id=env_id - ) - -@app.route('//admin', methods=['GET', 'POST']) -def admin(env_id): - data = get_env_data(env_id) - settings = data.get('settings', {}) - - if settings.get('admin_password_enabled') and not session.get(f'admin_auth_{env_id}'): - return redirect(url_for('admin_login', env_id=env_id)) - - products = data.get('products',[]) - categories = data.get('categories',[]) - blocks = data.get('blocks',[]) - - page = request.args.get('p', 1, type=int) - search_q = request.args.get('q', '').strip() - - if request.method == 'POST': - action = request.form.get('action') - try: - if action == 'add_block': - b_type = request.form.get('block_type') - b_title = request.form.get('block_title', '').strip() - b_icon = request.form.get('block_icon', '') - b_url = request.form.get('block_url', '').strip() - b_content = request.form.get('block_content', '').strip() - - if b_type == 'link' and b_url: - if not b_url.startswith(('http://', 'https://', 'mailto:', 'tel:')) and not b_url.startswith('+'): - if '@' in b_url: - b_url = 'mailto:' + b_url - else: - b_url = 'https://' + b_url - elif b_url.startswith('+'): - b_url = 'tel:' + b_url.replace(' ', '').replace('-', '') - elif b_type == 'pdf': - b_url = '' - pdf_file = request.files.get('block_pdf') - if pdf_file and pdf_file.filename and HF_TOKEN_WRITE: - try: - ext = os.path.splitext(pdf_file.filename)[1].lower() - pdf_filename = f"pdf_{uuid4().hex[:8]}{ext}" - uploads_dir = 'uploads_temp' - os.makedirs(uploads_dir, exist_ok=True) - temp_path = os.path.join(uploads_dir, pdf_filename) - pdf_file.save(temp_path) - HfApi().upload_file(path_or_fileobj=temp_path, path_in_repo=f"pdfs/{pdf_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) - os.remove(temp_path) - b_url = pdf_filename - except Exception as e: - flash(f"Ошибка загрузки PDF: {e}", 'error') - - blocks.append({ - 'id': uuid4().hex[:8], - 'type': b_type, - 'title': b_title, - 'icon': b_icon if b_type in ['link', 'pdf'] else '', - 'url': b_url, - 'content': b_content - }) - data['blocks'] = blocks - save_env_data(env_id, data) - flash("Блок добавлен.", "success") - - elif action == 'edit_block': - b_id = request.form.get('block_id') - b_type = request.form.get('block_type') - b_title = request.form.get('block_title', '').strip() - b_icon = request.form.get('block_icon', '') - b_url = request.form.get('block_url', '').strip() - b_content = request.form.get('block_content', '').strip() - old_url = request.form.get('old_block_url', '') - - for b in blocks: - if b.get('id') == b_id: - b['type'] = b_type - b['title'] = b_title - b['icon'] = b_icon if b_type in ['link', 'pdf'] else '' - b['content'] = b_content - - if b_type == 'link': - if b_url: - if not b_url.startswith(('http://', 'https://', 'mailto:', 'tel:')) and not b_url.startswith('+'): - if '@' in b_url: - b_url = 'mailto:' + b_url - else: - b_url = 'https://' + b_url - elif b_url.startswith('+'): - b_url = 'tel:' + b_url.replace(' ', '').replace('-', '') - b['url'] = b_url - elif b_type == 'pdf': - pdf_file = request.files.get('block_pdf') - if pdf_file and pdf_file.filename and HF_TOKEN_WRITE: - if old_url and old_url.startswith('pdf_'): - try: - HfApi().delete_files(repo_id=REPO_ID, paths_in_repo=[f"pdfs/{old_url}"], repo_type="dataset", token=HF_TOKEN_WRITE) - except Exception: pass - try: - ext = os.path.splitext(pdf_file.filename)[1].lower() - pdf_filename = f"pdf_{uuid4().hex[:8]}{ext}" - uploads_dir = 'uploads_temp' - os.makedirs(uploads_dir, exist_ok=True) - temp_path = os.path.join(uploads_dir, pdf_filename) - pdf_file.save(temp_path) - HfApi().upload_file(path_or_fileobj=temp_path, path_in_repo=f"pdfs/{pdf_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) - os.remove(temp_path) - b['url'] = pdf_filename - except Exception as e: - flash(f"Ошибка загрузки PDF: {e}", 'error') - b['url'] = old_url - else: - b['url'] = old_url - else: - b['url'] = '' - - break - - data['blocks'] = blocks - save_env_data(env_id, data) - flash("Блок обновлен.", "success") - - elif action == 'delete_block': - b_id = request.form.get('block_id') - block_to_del = next((b for b in blocks if b.get('id') == b_id), None) - if block_to_del: - if block_to_del.get('type') == 'pdf' and block_to_del.get('url') and block_to_del.get('url').startswith('pdf_') and HF_TOKEN_WRITE: - try: - HfApi().delete_files(repo_id=REPO_ID, paths_in_repo=[f"pdfs/{block_to_del.get('url')}"], repo_type="dataset", token=HF_TOKEN_WRITE) - except Exception: pass - blocks = [b for b in blocks if b.get('id') != b_id] - data['blocks'] = blocks - save_env_data(env_id, data) - flash("Блок удален.", "success") - - elif action == 'move_block_up': - b_id = request.form.get('block_id') - idx = next((i for i, b in enumerate(blocks) if b.get('id') == b_id), -1) - if idx > 0: - blocks[idx], blocks[idx-1] = blocks[idx-1], blocks[idx] - data['blocks'] = blocks - save_env_data(env_id, data) - flash("Блок перемещен выше.", "success") - - elif action == 'move_block_down': - b_id = request.form.get('block_id') - idx = next((i for i, b in enumerate(blocks) if b.get('id') == b_id), -1) - if idx != -1 and idx < len(blocks) - 1: - blocks[idx], blocks[idx+1] = blocks[idx+1], blocks[idx] - data['blocks'] = blocks - save_env_data(env_id, data) - flash("Блок перемещен ниже.", "success") - - elif action == 'add_category': - category_name = request.form.get('category_name', '').strip() - if category_name and category_name not in categories: - categories.append(category_name) - data['categories'] = categories - save_env_data(env_id, data) - flash(f"Категория '{category_name}' успешно добавлена.", 'success') - elif not category_name: - flash("Название категории не может быть пустым.", 'error') - else: - flash(f"Категория '{category_name}' уже существует.", 'error') - - elif action == 'delete_category': - category_to_delete = request.form.get('category_name') - if category_to_delete and category_to_delete in categories: - categories.remove(category_to_delete) - updated_count = 0 - for product in products: - if product.get('category') == category_to_delete: - product['category'] = 'Без категории' - updated_count += 1 - data['categories'] = categories - data['products'] = products - save_env_data(env_id, data) - flash(f"Категория '{category_to_delete}' удалена. {updated_count} товаров/услуг обновлено.", 'success') - else: - flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error') - - elif action == 'update_settings': - settings['admin_password_enabled'] = 'admin_password_enabled' in request.form - settings['admin_password'] = request.form.get('admin_password', '').strip() - - settings['vcard_firstname'] = request.form.get('vcard_firstname', '').strip() - settings['vcard_lastname'] = request.form.get('vcard_lastname', '').strip() - settings['vcard_job'] = request.form.get('vcard_job', '').strip() - settings['organization_name'] = request.form.get('organization_name', '').strip() - settings['about_text'] = request.form.get('about_text', '').strip() - - settings['currency_code'] = request.form.get('currency_code', 'KGS') - settings['color_scheme'] = request.form.get('color_scheme', 'default') - - settings['enable_cart'] = 'enable_cart' in request.form - settings['order_messenger'] = request.form.get('order_messenger', 'whatsapp') - settings['order_contact'] = request.form.get('order_contact', '').strip() - - avatar_file = request.files.get('chat_avatar') - if avatar_file and avatar_file.filename: - if HF_TOKEN_WRITE: - try: - api = HfApi() - old_avatar = settings.get('chat_avatar') - if old_avatar: - try: api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"avatars/{old_avatar}"], repo_type="dataset", token=HF_TOKEN_WRITE) - except Exception: pass - ext = os.path.splitext(avatar_file.filename)[1].lower() - avatar_filename = f"avatar_{env_id}_{int(time.time())}{ext}" - uploads_dir = 'uploads_temp' - os.makedirs(uploads_dir, exist_ok=True) - temp_path = os.path.join(uploads_dir, avatar_filename) - avatar_file.save(temp_path) - api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"avatars/{avatar_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) - settings['chat_avatar'] = avatar_filename - os.remove(temp_path) - flash("Аватар успешно обновлен.", 'success') - except Exception as e: - flash(f"Ошибка при загрузке аватара: {e}", 'error') - else: - flash("HF_TOKEN не настроен. Аватар не был загружен.", "warning") - - data['settings'] = settings - save_env_data(env_id, data) - flash("Настройки успешно обновлены.", 'success') - - elif action == 'add_product' or action == 'edit_product': - product_id = request.form.get('product_id') - product_data = {} - is_edit = action == 'edit_product' - - if is_edit: - product_data = next((p for p in products if p.get('product_id') == product_id), None) - if not product_data: - flash(f"Ошибка: запись не найдена.", 'error') - return redirect(url_for('admin', env_id=env_id, p=page, q=search_q)) - else: - product_data['views'] = 0 - - product_data['name'] = request.form.get('name', '').strip() - product_data['description'] = request.form.get('description', '').strip() - - try: - product_data['price'] = float(request.form.get('price', 0)) - except ValueError: - product_data['price'] = 0.0 - - category = request.form.get('category') - product_data['category'] = category if category in categories else 'Без категории' - - if not product_data['name']: - flash("Название обязательно.", 'error') - return redirect(url_for('admin', env_id=env_id, p=page, q=search_q)) - - photos_files = request.files.getlist('photos') - if photos_files and any(f.filename for f in photos_files): - if HF_TOKEN_WRITE: - uploads_dir = 'uploads_temp' - os.makedirs(uploads_dir, exist_ok=True) - api = HfApi() - new_photos_list = [] - photo_limit = 10 - uploaded_count = 0 - for photo in photos_files: - if uploaded_count >= photo_limit: break - if photo and photo.filename: - try: - ext = os.path.splitext(photo.filename)[1].lower() - if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']: continue - safe_name = secure_filename(product_data['name'].replace(' ', '_'))[:50] - photo_filename = f"{safe_name}_{uuid4().hex[:8]}{ext}" - temp_path = os.path.join(uploads_dir, photo_filename) - photo.save(temp_path) - api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"photos/{photo_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) - new_photos_list.append(photo_filename) - os.remove(temp_path) - uploaded_count += 1 - except Exception as e: - flash(f"Ошибка при загрузке фото {photo.filename}: {e}", 'error') - if new_photos_list and is_edit and product_data.get('photos'): - try: api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in product_data['photos']], repo_type="dataset", token=HF_TOKEN_WRITE) - except Exception: pass - if new_photos_list: - product_data['photos'] = new_photos_list - else: - flash("HF_TOKEN не настроен. Фотографии не загружены.", "warning") - - if is_edit: - product_index = next((i for i, p in enumerate(products) if p.get('product_id') == product_id), -1) - if product_index != -1: - products[product_index] = product_data - flash(f"Запись '{product_data['name']}' обновлена.", 'success') - else: - product_data['product_id'] = uuid4().hex - products.append(product_data) - flash(f"Запись '{product_data['name']}' добавлена.", 'success') - - data['products'] = products - save_env_data(env_id, data) - - elif action == 'delete_product': - product_id = request.form.get('product_id') - product_index = next((i for i, p in enumerate(products) if p.get('product_id') == product_id), -1) - if product_index == -1: - flash(f"Ошибка удаления: запись не найдена.", 'error') - return redirect(url_for('admin', env_id=env_id, p=page, q=search_q)) - deleted_product = products.pop(product_index) - product_name = deleted_product.get('name', 'N/A') - photos_to_delete = deleted_product.get('photos', []) - if photos_to_delete and HF_TOKEN_WRITE: - try: - api = HfApi() - api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in photos_to_delete], repo_type="dataset", token=HF_TOKEN_WRITE) - except Exception: pass - data['products'] = products - save_env_data(env_id, data) - flash(f"Запись '{product_name}' удалена.", 'success') - else: - flash(f"Неизвестное действие: {action}", 'warning') - return redirect(url_for('admin', env_id=env_id, p=page, q=search_q)) - except Exception as e: - flash(f"Ошибка при выполнении действия: {e}", 'error') - return redirect(url_for('admin', env_id=env_id, p=page, q=search_q)) - - filtered_products = products - if search_q: - q_lower = search_q.lower() - filtered_products = [p for p in products if q_lower in p.get('name', '').lower() or q_lower in p.get('description', '').lower()] - - filtered_products = sorted(filtered_products, key=lambda p: p.get('name', '').lower()) - - PER_PAGE = 20 - total_items = len(filtered_products) - total_pages = math.ceil(total_items / PER_PAGE) if total_items > 0 else 1 - - if page < 1: page = 1 - if page > total_pages: page = total_pages - - start_idx = (page - 1) * PER_PAGE - end_idx = start_idx + PER_PAGE - paginated_products = filtered_products[start_idx:end_idx] - - display_categories = sorted(categories) - display_settings = settings - chat_avatar_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/avatars/{display_settings['chat_avatar']}" if display_settings.get('chat_avatar') else "https://huggingface.co/spaces/gippo312/admin/resolve/main/Picsart_25-11-04_12-02-21-390.png" - - new_orders_count = sum(1 for o in data.get('orders', []) if not o.get('viewed', False)) - - return render_template_string( - ADMIN_TEMPLATE, paginated_products=paginated_products, total_pages=total_pages, page=page, search_q=search_q, categories=display_categories, - settings=display_settings, blocks=blocks, repo_id=REPO_ID, currency_code=display_settings.get('currency_code', 'KGS'), chat_avatar_url=chat_avatar_url, - currencies=CURRENCIES, color_schemes=COLOR_SCHEMES, icons=ICONS, env_id=env_id, new_orders_count=new_orders_count - ) - -@app.route('//admin/orders') -def admin_orders(env_id): - data = get_env_data(env_id) - settings = data.get('settings', {}) - if settings.get('admin_password_enabled') and not session.get(f'admin_auth_{env_id}'): - return redirect(url_for('admin_login', env_id=env_id)) - - orders = data.get('orders', []) - changed = False - for o in orders: - if not o.get('viewed', False): - o['viewed'] = True - changed = True - if changed: - save_env_data(env_id, data) - - orders_sorted = sorted(orders, key=lambda x: x.get('date', ''), reverse=True) - return render_template_string(ADMIN_ORDERS_TEMPLATE, orders=orders_sorted, env_id=env_id, settings=settings, currency=settings.get('currency_code', 'KGS')) - -@app.route('//admin/orders/delete/', methods=['POST']) -def delete_order(env_id, order_id): - data = get_env_data(env_id) - settings = data.get('settings', {}) - if settings.get('admin_password_enabled') and not session.get(f'admin_auth_{env_id}'): - return redirect(url_for('admin_login', env_id=env_id)) - - orders = data.get('orders', []) - data['orders'] = [o for o in orders if o.get('id') != order_id] - save_env_data(env_id, data) - flash("Заказ удален из истории.", "success") - return redirect(url_for('admin_orders', env_id=env_id)) - -@app.route('/generate_description_ai', methods=['POST']) -def handle_generate_description_ai(): - request_data = request.get_json() - base64_image = request_data.get('image') - language = request_data.get('language', 'Русский') - if not base64_image: return jsonify({"error": "Изображение не найдено в запросе."}), 400 - try: - image_data = base64.b64decode(base64_image) - result_text = generate_ai_description_from_image(image_data, language) - return jsonify({"text": result_text}) - except ValueError as ve: return jsonify({"error": str(ve)}), 400 - except Exception as e: return jsonify({"error": f"Внутренняя ошибка сервера: {e}"}), 500 - -if __name__ == '__main__': - configure_gemini() - download_db_from_hf() - load_data() - if HF_TOKEN_WRITE: - backup_thread = threading.Thread(target=periodic_backup, daemon=True) - backup_thread.start() - port = int(os.environ.get('PORT', 7860)) - app.run(debug=False, host='0.0.0.0', port=port)