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 }} - Визитка
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Сохранить в контакты
-
-
- {% if blocks %}
-
- {% endif %}
-
- {% if products_json != '[]' %}
-
-
Каталог / Услуги
-
-
-
-
-
-
-
-
-
- {% endif %}
-
-
-
-
- 0
-
-
-
-
-
-
-
-
Мой QR-код
-
-
![QR Code]()
-
-
Отсканируйте, чтобы открыть визитку
-
-
-
-
-
-
-
-
-
Корзина
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-'''
-
-ADMIN_TEMPLATE = '''
-
-
-
-
-
- Настройки Визитки - {{ settings.organization_name }}
-
-
-
-
-
-
-
-
Сохранение данных...
-
-
-
-
- {% with messages = get_flashed_messages(with_categories=true) %}
- {% if messages %}
- {% for category, message in messages %}
-
{{ message }}
- {% endfor %}
- {% endif %}
- {% endwith %}
-
-
-
- Профиль и Внешний вид
-
-
-
-
-
-
Кнопки и Ссылки (для Визитки)
-
- Добавить кнопку
-
-
-
- {% 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 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['customer_name'] }}
-
Телефон: {{ order['customer_phone'] }}
-
Продавец: {{ settings['organization_name'] }}
-
-
-
- {% for pid, item in order['items'].items() %}
-
- {% if item['photo'] %}
-
![{{ item['name'] }}](https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ item['photo'] }})
- {% 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 %}
-
-
-
-
- {% 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)