import flask from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify, Response, stream_with_context from flask_caching import Cache import json import os import logging import threading import time from datetime import datetime, timedelta from huggingface_hub import HfApi, hf_hub_download, utils as hf_utils from werkzeug.utils import secure_filename import requests from io import BytesIO import uuid from functools import wraps from urllib.parse import quote, quote_plus import zipfile import tempfile import pytz from zoneinfo import ZoneInfo, ZoneInfoNotFoundError import re app = Flask(__name__) app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_folders_unique_tma") DATA_FILE = 'cloudeng_data_tma.json' REPO_ID = "Eluza133/Z1e1u" HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "6750208873:AAE2hvPlJ99dBdhGa_Brre0IIpUdOvXxHt4") ADMIN_TELEGRAM_ID = os.getenv("ADMIN_TELEGRAM_ID", "YOUR_ADMIN_TELEGRAM_USER_ID_HERE") ADMIN_USERNAME = os.getenv("ADMIN_USERNAME", "admin") ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "zeusadminpass") UPLOAD_FOLDER = 'uploads_tma' os.makedirs(UPLOAD_FOLDER, exist_ok=True) cache = Cache(app, config={'CACHE_TYPE': 'simple'}) logging.basicConfig(level=logging.INFO) save_data_lock = threading.Lock() BASE_STYLE = ''' :root { --primary: #ff4d6d; --secondary: #00ddeb; --accent: #8b5cf6; --background-dark: #121212; --card-bg-dark: #1e1e1e; --text-dark: #e0e0e0; --text-muted: #888; --shadow: 0 10px 30px rgba(0, 0, 0, 0.2); --glass-bg: rgba(30, 30, 30, 0.7); --transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); --delete-color: #ff4444; --folder-color: #ffc107; --selection-color: rgba(139, 92, 246, 0.3); --note-color: #6a5acd; --share-color: #4caf50; --archive-color: #78909c; --todolist-color: #29b6f6; --shoppinglist-color: #ffa726; --business-color: #fd7e14; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } * { margin: 0; padding: 0; box-sizing: border-box; } html { scroll-behavior: smooth; } body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background: var(--background-dark); color: var(--text-dark); line-height: 1.6; -webkit-tap-highlight-color: transparent; } .container { margin: 0 auto; max-width: 1200px; padding: 75px 15px 15px 15px; } .app-header { position: fixed; top: 0; left: 0; right: 0; background: var(--glass-bg); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border-bottom: 1px solid rgba(255,255,255,0.1); z-index: 1000; padding: 10px 20px; display: flex; justify-content: space-between; align-items: center; } .user-info { font-weight: 600; } .view-toggle { display: flex; align-items: center; gap: 5px; } .view-toggle button, .view-toggle a { background: none; border: none; color: var(--text-muted); font-size: 1.2em; padding: 5px; cursor: pointer; transition: var(--transition); text-decoration: none;} .view-toggle button:hover, .view-toggle button.active, .view-toggle a:hover { color: var(--primary); } h1, h2, h3, h4, h5 { color: var(--text-dark); } h2 { font-size: 1.3em; margin-bottom: 15px; margin-top: 15px; } .breadcrumbs { font-size: 1em; margin-bottom: 20px; white-space: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; } .breadcrumbs a { color: var(--accent); text-decoration: none; } .breadcrumbs span { margin: 0 5px; color: var(--text-muted); } input, select, textarea, label { width: 100%; padding: 14px; margin: 8px 0; border: 1px solid #333; border-radius: 12px; background: #2a2a2a; color: var(--text-dark); font-size: 1em; } label { padding: 0; margin: 0; border: none; background: none; } .checkbox-label { display: flex; align-items: center; gap: 10px; width: auto; } .checkbox-label input[type="checkbox"] { width: auto; margin: 0; } .btn { padding: 12px 24px; background: var(--primary); color: white; border: none; border-radius: 12px; cursor: pointer; font-size: 1em; font-weight: 600; transition: var(--transition); text-decoration: none; display: inline-block; text-align: center; } .btn:hover { filter: brightness(1.2); } .btn:active { transform: scale(0.98); } .download-btn { background: var(--secondary); } .delete-btn { background: var(--delete-color); } .folder-btn { background: var(--folder-color); } .share-btn { background: var(--share-color); } .archive-btn { background: var(--archive-color); } .business-btn { background: var(--business-color); } .flash { text-align: center; margin-bottom: 15px; padding: 12px; border-radius: 10px; background: rgba(0, 221, 235, 0.1); color: var(--secondary); } .flash.error { background: rgba(255, 68, 68, 0.1); color: var(--delete-color); } .file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; } .item { background: var(--card-bg-dark); border-radius: 16px; text-align: center; transition: var(--transition); position: relative; border: 2px solid transparent; user-select: none; padding: 10px; display: flex; flex-direction: column; cursor: pointer; } .item:hover { transform: translateY(-5px); box-shadow: var(--shadow); } .item:active { transform: scale(0.97); } .item.selected { border-color: var(--accent); background-color: var(--selection-color); } .item-preview-wrapper { position: relative; width: 100%; padding-top: 75%; border-radius: 10px; overflow: hidden; margin-bottom: 10px; background: #2a2a2a; } .item-preview { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; } .item.folder .item-preview { object-fit: contain; font-size: 3.5em; color: var(--folder-color); } .item.note .item-preview { object-fit: contain; font-size: 3.5em; color: var(--note-color); } .item.todolist .item-preview { object-fit: contain; font-size: 3.5em; color: var(--todolist-color); } .item.shoppinglist .item-preview { object-fit: contain; font-size: 3.5em; color: var(--shoppinglist-color); } .item-name { font-size: 0.9em; font-weight: 500; word-break: break-all; margin: 5px 0; flex-grow: 1; } .item-info { font-size: 0.75em; color: var(--text-muted); } .file-grid.list-view { display: flex; flex-direction: column; gap: 8px; } .file-grid.list-view .item { flex-direction: row; align-items: center; text-align: left; padding: 8px; } .file-grid.list-view .item:hover { transform: translateY(0); } .file-grid.list-view .item-preview-wrapper { width: 45px; height: 45px; padding-top: 0; margin-bottom: 0; margin-right: 15px; flex-shrink: 0; } .file-grid.list-view .item.folder .item-preview, .file-grid.list-view .item.note .item-preview, .file-grid.list-view .item.todolist .item-preview, .file-grid.list-view .item.shoppinglist .item-preview { font-size: 1.8em; } .file-grid.list-view .item-name-info { flex-grow: 1; } .modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.85); z-index: 2000; justify-content: center; align-items: center; } .modal-content { display: flex; flex-direction: column; max-width: 95%; max-height: 95%; background: var(--card-bg-dark); padding: 10px; border-radius: 15px; overflow: hidden; position: relative; } .modal-main-content { flex-grow: 1; overflow-y: auto; padding: 10px; } .modal-main-content img, .modal-main-content video, .modal-main-content iframe, .modal-main-content pre { max-width: 100%; max-height: 85vh; display: block; margin: auto; border-radius: 10px; } .modal-main-content iframe { width: 80vw; height: 85vh; border: none; } .modal-main-content pre { background: #121212; padding: 15px; border-radius: 8px; white-space: pre-wrap; word-wrap: break-word; max-height: 85vh; color: var(--text-dark); } .modal-actions { padding: 10px; text-align: center; border-top: 1px solid #333; } .modal-close-btn { position: absolute; top: 15px; right: 25px; font-size: 30px; color: #aaa; cursor: pointer; background: rgba(0,0,0,0.5); border-radius: 50%; width: 30px; height: 30px; line-height: 30px; text-align: center; z-index: 2001;} #progress-container { width: 100%; background: #333; border-radius: 10px; margin: 15px 0; display: none; height: 10px; } #progress-bar { width: 0%; height: 100%; background: var(--primary); border-radius: 10px; transition: width 0.3s ease; } #selection-bar { position: fixed; bottom: -120px; left: 10px; right: 10px; background: var(--glass-bg); backdrop-filter: blur(10px); padding: 10px; border-radius: 15px; box-shadow: var(--shadow); z-index: 1000; display: flex; justify-content: space-around; align-items: center; transition: bottom 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); } #selection-bar.visible { bottom: 10px; } #selection-bar .btn { margin: 0 5px; padding: 10px 15px; font-size: 0.9em; flex-grow: 1; } #move-modal .modal-content { padding: 20px; max-width: 400px; } .fab-container { position: fixed; bottom: 20px; right: 20px; z-index: 1050; } .fab { width: 56px; height: 56px; background: var(--accent); border-radius: 50%; border: none; box-shadow: var(--shadow); color: white; font-size: 24px; display: flex; justify-content: center; align-items: center; cursor: pointer; transition: transform 0.3s; } .fab:active { transform: scale(0.9); } .loading-spinner { border: 4px solid #f3f3f3; border-top: 4px solid var(--primary); border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: auto; } #fab-modal .modal-content { padding: 20px; max-width: 500px; background: var(--card-bg-dark); text-align: center; } .fab-options { display: grid; grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); gap: 15px; margin-bottom: 20px; } .fab-option { display: flex; flex-direction: column; align-items: center; justify-content: center; background: #2a2a2a; border-radius: 12px; padding: 15px; cursor: pointer; transition: var(--transition); text-decoration:none; color: var(--text-dark); } .fab-option:hover { background: #333; transform: translateY(-3px); } .fab-option i { font-size: 2em; margin-bottom: 8px; } #fab-option-upload i { color: var(--secondary); } #fab-option-note i { color: var(--note-color); } #fab-option-folder i { color: var(--folder-color); } #fab-option-todolist i { color: var(--todolist-color); } #fab-option-shoppinglist i { color: var(--shoppinglist-color); } #fab-option-business i { color: var(--business-color); } #create-folder-form { display: none; margin-top: 15px; } .shared-link-item { display: flex; justify-content: space-between; align-items: center; padding: 8px; border-bottom: 1px solid #333; } .shared-link-item:last-child { border-bottom: none; } .shared-link-info { text-align: left; } .shared-link-info strong { word-break: break-all; } .shared-link-info small { color: var(--text-muted); display: block; } .shared-link-actions button { background: none; border: none; color: var(--text-muted); font-size: 1.1em; cursor: pointer; padding: 5px; } .list-editor-item { display: flex; align-items: center; gap: 10px; padding: 8px; border-radius: 8px; } .list-editor-item:hover { background-color: #2a2a2a; } .list-editor-item input[type=checkbox] { width: 20px; height: 20px; flex-shrink: 0; } .list-editor-item input[type=text] { margin: 0; flex-grow: 1; } .list-editor-item .quantity-controls { display: flex; align-items: center; gap: 5px; } .list-editor-item .quantity-controls input { width: 50px; text-align: center; padding: 8px; margin: 0; } .list-editor-item .quantity-controls button { background: #333; border: none; color: white; border-radius: 50%; width: 28px; height: 28px; font-weight: bold; cursor: pointer; } .list-editor-item .delete-item-btn { background: none; border: none; color: var(--text-muted); cursor: pointer; font-size: 1.2em; } .list-editor-item.completed span { text-decoration: line-through; color: var(--text-muted); } .public-list-item { display: flex; align-items: center; gap: 15px; padding: 12px; border-bottom: 1px solid #333; } .public-list-item input[type=checkbox] { width: 22px; height: 22px; cursor: pointer; } .public-list-item label { flex-grow: 1; cursor: pointer; } .public-list-item.purchased label { text-decoration: line-through; color: var(--text-muted); } .public-list-item .quantity { font-weight: bold; color: var(--secondary); background: #2a2a2a; padding: 2px 8px; border-radius: 6px; } .form-group { margin-bottom: 15px; text-align: left; } .form-group small { color: var(--text-muted); font-size: 0.8em; margin-top: 4px; display: block; } ''' PUBLIC_SHARE_PAGE_HTML = ''' Общая папка: {{ folder.name }}

Общая папка

{{ folder.name }}

Автор: {{ user.first_name or user.telegram_username }}

{% for item in items %}
{% if item.type == 'folder' %}
{% elif item.type == 'note' %}
{% elif item.file_type == 'image' %}
{% else %}
{% endif %}

{% if item.type == 'folder' %} {{ item.name }} {% else %} {{ item.title if item.type == 'note' else item.original_filename }} {% endif %}

{% if item.type == 'file' %}{{ item.upload_date }}{% elif item.type == 'note' %}{{ item.modified_date }}{% endif %}

{% if item.type != 'folder' %} {% endif %}
{% endfor %} {% if not items %}

Эта папка пуста.

{% endif %}
''' PUBLIC_SHOPPING_LIST_HTML = ''' Список покупок: {{ list_data.title }}

Список покупок

{{ list_data.title }}

Автор: {{ user.first_name or user.telegram_username }}

''' PUBLIC_BUSINESS_PAGE_HTML = ''' {{ page.org_name }}
{% if page.avatar_path %} Avatar {% endif %}

{{ page.org_name }}

{% if page.products %}
{% for product in page.products %}
{% if product.photo_path %} {{ product.name }} {% endif %}

{{ product.name }}

{{ product.description }}

{% if page.show_prices and product.price %}

{{ "%.2f"|format(product.price|float) }} {{ page.currency }}

{% endif %}
{% endfor %}
{% else %}

Товары скоро появятся.

{% endif %}
{% set phone_number = page.contact_number | replace('+', '') | replace(' ', '') %} {% if page.order_destination == 'whatsapp' %} Заказать {% elif page.order_destination == 'telegram' %} Заказать {% endif %}
{% if page.show_prices %}
0

Корзина

Корзина пуста.

{% endif %} ''' PUBLIC_ORDER_PAGE_HTML = ''' Заказ {{ order.id[:8] }}

Ваш заказ для {{ business.org_name }}

Номер заказа: {{ order.id[:8] }}

Дата: {{ order.timestamp }}

{% for item in order.items %}
{% if item.image %} {{ item.name }} {% else %}
{% endif %}
{{ item.name }}
{{ item.quantity }} x {{ "%.2f"|format(item.price|float) }} {{ business.currency }}
{{ "%.2f"|format(item.price|float * item.quantity|int) }} {{ business.currency }}
{% endfor %}
Итого: {{ "%.2f"|format(order.total_price|float) }} {{ business.currency }}
{% set phone_number = business.contact_number | replace('+', '') | replace(' ', '') %} {% set message = "Здравствуйте, хочу сделать заказ. Ссылка на мой заказ: " + url_for('public_order_page', order_id=order.id, _external=True) %} {% set encoded_message = quote_plus(message) %} {% if business.order_destination == 'whatsapp' %} Отправить заказ {% elif business.order_destination == 'telegram' %} Отправить заказ {% endif %}
''' def find_node_by_id(filesystem, node_id): if not filesystem: return None, None if filesystem.get('id') == node_id: return filesystem, None queue = [(filesystem, None)] while queue: current_node, parent = queue.pop(0) if 'children' in current_node and isinstance(current_node['children'], list): for i, child in enumerate(current_node['children']): if isinstance(child, dict) and child.get('id') == node_id: return child, current_node if isinstance(child, dict) and child.get('type') == 'folder': queue.append((child, current_node)) return None, None def add_node(filesystem, parent_id, node_data): parent_node, _ = find_node_by_id(filesystem, parent_id) if parent_node and parent_node.get('type') == 'folder': if 'children' not in parent_node: parent_node['children'] = [] parent_node['children'].append(node_data) return True return False def remove_node(filesystem, node_id): node_to_remove, parent_node = find_node_by_id(filesystem, node_id) if node_to_remove and parent_node and 'children' in parent_node: parent_node['children'] = [child for child in parent_node['children'] if not (isinstance(child, dict) and child.get('id') == node_id)] return True, node_to_remove return False, None def get_node_path_string(filesystem, node_id): path_list = [] current_id = node_id while current_id: node, parent = find_node_by_id(filesystem, current_id) if not node: break if node.get('id') != 'root': path_list.append(node.get('name', node.get('original_filename', ''))) if not parent: break current_id = parent.get('id') if parent else None return " / ".join(reversed(path_list)) or "Root" def get_all_folders(filesystem, exclude_ids=None): if exclude_ids is None: exclude_ids = set() folders = [] def traverse(node, path_prefix): if node.get('type') == 'folder': if node.get('id') not in exclude_ids: folder_name = f"{path_prefix}{node.get('name', 'Unnamed')}" if path_prefix else (node.get('name') if node.get('id') != 'root' else 'Главная (Root)') folders.append({'id': node.get('id'), 'name': folder_name}) new_prefix = f"{path_prefix}{node.get('name', '')}/" if node.get('id') != 'root' else "" for child in node.get('children', []): traverse(child, new_prefix) traverse(filesystem, "") return sorted(folders, key=lambda x: x['name'].lower()) def get_all_archived_items(filesystem): archived_items = [] def traverse(node): if isinstance(node, dict): if node.get('is_archived'): archived_items.append(node) if 'children' in node: for child in node.get('children', []): traverse(child) traverse(filesystem) return archived_items def count_items_recursive(node): if not node or not isinstance(node, dict): return 0 count = 0 if node.get('type') in ['file', 'note', 'todolist', 'shoppinglist']: count += 1 if node.get('type') == 'folder' and 'children' in node: for child in node.get('children', []): count += count_items_recursive(child) return count def initialize_user_filesystem_tma(user_data, tma_user_id_str): if 'filesystem' not in user_data or not isinstance(user_data['filesystem'], dict): user_data['filesystem'] = {"type": "folder", "id": "root", "name": "root", "children": []} if 'files' in user_data and isinstance(user_data['files'], list): for old_file in user_data['files']: file_id = old_file.get('id', uuid.uuid4().hex) original_filename = old_file.get('filename', 'unknown_file') name_part, ext_part = os.path.splitext(original_filename) unique_suffix = uuid.uuid4().hex[:8] unique_filename = f"{name_part}_{unique_suffix}{ext_part}" hf_path = f"cloud_files/{tma_user_id_str}/root/{unique_filename}" file_node = { 'type': 'file', 'id': file_id, 'original_filename': original_filename, 'unique_filename': unique_filename, 'path': hf_path, 'file_type': get_file_type(original_filename), 'upload_date': old_file.get('upload_date', datetime.now().strftime('%Y-%m-%d %H:%M:%S')) } add_node(user_data['filesystem'], 'root', file_node) del user_data['files'] user_data.setdefault('owned_business_pages', []) @cache.memoize(timeout=300) def load_data(): try: download_db_from_hf() with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file) if not isinstance(data, dict): data = {'users': {}, 'shared_links': {}, 'business_pages': {}, 'orders': {}} data.setdefault('users', {}) data.setdefault('shared_links', {}) data.setdefault('business_pages', {}) data.setdefault('orders', {}) for tma_user_id_str, user_data_item in data['users'].items(): initialize_user_filesystem_tma(user_data_item, tma_user_id_str) user_data_item.setdefault('reminders', []) return data except Exception as e: logging.error(f"Error loading data: {e}") return {'users': {}, 'shared_links': {}, 'business_pages': {}, 'orders': {}} def save_data(data): with save_data_lock: try: with open(DATA_FILE, 'w', encoding='utf-8') as file: json.dump(data, file, ensure_ascii=False, indent=4) upload_db_to_hf() cache.clear() except Exception as e: logging.error(f"Error saving data: {e}") raise def upload_db_to_hf(): if not HF_TOKEN_WRITE: return try: 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"Backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") except Exception as e: logging.error(f"Error uploading database: {e}") def download_db_from_hf(): if not HF_TOKEN_READ: if not os.path.exists(DATA_FILE): with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}, 'business_pages': {}, 'orders': {}}, f) return 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) except (hf_utils.RepositoryNotFoundError, hf_utils.EntryNotFoundError): if not os.path.exists(DATA_FILE): with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}, 'business_pages': {}, 'orders': {}}, f) except Exception as e: logging.error(f"Error downloading database: {e}") if not os.path.exists(DATA_FILE): with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}, 'business_pages': {}, 'orders': {}}, f) def periodic_backup(): while True: upload_db_to_hf() time.sleep(1800) def send_telegram_message(chat_id, text): if not TELEGRAM_BOT_TOKEN: logging.warning("TELEGRAM_BOT_TOKEN is not set. Cannot send message.") return url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage" payload = {"chat_id": chat_id, "text": text, "parse_mode": "HTML"} try: response = requests.post(url, json=payload) response.raise_for_status() logging.info(f"Sent message to {chat_id}") except requests.exceptions.RequestException as e: logging.error(f"Failed to send Telegram message to {chat_id}: {e}") def check_reminders(): while True: try: data = load_data() now_utc = datetime.now(pytz.utc) made_changes = False for user_id, user_data in data.get('users', {}).items(): if 'reminders' in user_data: for reminder in user_data['reminders']: if not reminder.get('notified', False): due_time_str = reminder.get('due_datetime_utc') if due_time_str: due_time_utc = datetime.fromisoformat(due_time_str.replace('Z', '+00:00')).replace(tzinfo=pytz.utc) if now_utc >= due_time_utc: telegram_id = user_data.get('telegram_id') if telegram_id: message_text = f"🔔 Напоминание:\n\n{reminder['text']}" send_telegram_message(telegram_id, message_text) reminder['notified'] = True made_changes = True if made_changes: save_data(data) except Exception as e: logging.error(f"Error in check_reminders thread: {e}") time.sleep(60) def get_file_type(filename): filename_lower = filename.lower() if filename_lower.endswith(('.mp4', '.mov', '.avi', '.webm', '.mkv')): return 'video' elif filename_lower.endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg')): return 'image' elif filename_lower.endswith('.pdf'): return 'pdf' elif filename_lower.endswith('.txt'): return 'text' return 'other' def is_admin_tma(): if not ADMIN_TELEGRAM_ID or ADMIN_TELEGRAM_ID == "YOUR_ADMIN_TELEGRAM_USER_ID_HERE": return False return 'telegram_user_id' in session and str(session['telegram_user_id']) == str(ADMIN_TELEGRAM_ID) def admin_browser_login_required(f): @wraps(f) def decorated_function(*args, **kwargs): if not session.get('admin_browser_logged_in'): return redirect(url_for('admin_login', next=request.url)) return f(*args, **kwargs) return decorated_function TMA_ENTRY_HTML = ''' Zeus Cloud TMA
Загрузка приложения...
''' @app.route('/tma') def tma_entry_page(): return render_template_string(TMA_ENTRY_HTML) @app.route('/') def root_redirect(): return redirect(url_for('tma_entry_page')) @app.route('/auth_via_telegram', methods=['POST']) def auth_via_telegram(): try: payload = request.json tg_user_data = payload.get('user') if not tg_user_data or not tg_user_data.get('id'): return jsonify({'status': 'error', 'message': 'Отсутствуют данные пользователя Telegram.'}), 400 tma_user_id_str = str(tg_user_data['id']) data = load_data() user_info = { 'telegram_id': tg_user_data['id'], 'telegram_username': tg_user_data.get('username'), 'first_name': tg_user_data.get('first_name'), 'last_name': tg_user_data.get('last_name'), 'photo_url': tg_user_data.get('photo_url') } if tma_user_id_str not in data['users']: user_info['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') user_info['filesystem'] = {"type": "folder", "id": "root", "name": "root", "children": []} user_info['reminders'] = [] user_info['owned_business_pages'] = [] data['users'][tma_user_id_str] = user_info initialize_user_filesystem_tma(data['users'][tma_user_id_str], tma_user_id_str) else: data['users'][tma_user_id_str].update(user_info) data['users'][tma_user_id_str].setdefault('owned_business_pages', []) try: save_data(data) except Exception as e: logging.error(f"Save data error for TMA user {tma_user_id_str}: {e}") return jsonify({'status': 'error', 'message': 'Ошибка сохранения данных пользователя.'}), 500 session['telegram_user_id'] = tma_user_id_str display_name = tg_user_data.get('first_name') or tg_user_data.get('username') or f"User {tma_user_id_str}" session['telegram_display_name'] = display_name return jsonify({'status': 'success', 'redirect_url': url_for('tma_dashboard')}) except Exception as e: logging.error(f"Error in auth_via_telegram: {e}") return jsonify({'status': 'error', 'message': 'Внутренняя ошибка сервера при авторизации.'}), 500 TMA_DASHBOARD_HTML_TEMPLATE = ''' Zeus Cloud
{{ display_name }}
{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %}{% endwith %}

{{ current_folder.name if current_folder_id != 'root' else 'Главная' }}

{% for item in items %}
{% if item.type == 'folder' %}
{% elif item.type == 'note' %}
{% elif item.type == 'todolist' %}
{% elif item.type == 'shoppinglist' %}
{% elif item.type == 'file' %} {% if item.file_type == 'image' %} {% elif item.file_type == 'video' %} {% elif item.file_type == 'pdf' %}
{% elif item.file_type == 'text' %}
{% else %}
{% endif %} {% endif %}

{{ (item.title if item.type in ['note', 'todolist', 'shoppinglist'] else item.name if item.type == 'folder' else item.original_filename) | truncate(30, True) }}

{% if item.type == 'file' %}

{{ item.upload_date }}

{% elif item.type in ['note', 'todolist', 'shoppinglist'] %}

{{ item.modified_date }}

{% endif %}
{% endfor %} {% if not items %}

Эта папка пуста.

{% endif %}
''' ARCHIVED_LISTS_HTML = ''' Архив
{{ display_name }}

Архив

{% for item in items %}
{% if item.type == 'todolist' %}
{% elif item.type == 'shoppinglist' %}
{% endif %}

{{ item.title }}

Заархивировано

{% endfor %} {% if not items %}

Архив пуст.

{% endif %}
''' TMA_MANAGE_BUSINESS_HTML = ''' Мои бизнес страницы
{{ display_name }}

Мои бизнес страницы

Создать
{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %}{% endwith %}
{% for page in pages %}
{% if page.avatar_path %} {% else %}
{% endif %}

{{ page.org_name }}

/business/{{ page.login }}

{% endfor %} {% if not pages %}

У вас еще нет бизнес страниц.

{% endif %}
''' TMA_CREATE_EDIT_BUSINESS_FORM_HTML = ''' {{ 'Редактировать' if page else 'Создать' }} страницу
{{ display_name }}

{{ 'Редактировать' if page else 'Создать' }} бизнес страницу

{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %}{% endwith %}
Только латинские буквы, цифры и символы (_, ., -). Это будет в ссылке: /business/логин
Для WhatsApp: номер с кодом страны (e.g., +77001234567). Для Telegram: @username (без @).
{% if page %}
{% endif %}
''' TMA_MANAGE_PRODUCTS_HTML = ''' Управление товарами
{{ page.org_name }}

Товары

{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %}{% endwith %}
{% for product in page.products %}
{% if product.photo_path %} {% else %}
{% endif %}

{{ product.name }}

{% if page.show_prices %}

{{ "%.2f"|format(product.price|float) }} {{ page.currency }}

{% endif %}
{% endfor %} {% if not page.products %}

У вас еще нет товаров.

{% endif %}
''' @app.route('/tma_dashboard', methods=['GET', 'POST']) def tma_dashboard(): if 'telegram_user_id' not in session: flash('Пожалуйста, авторизуйтесь через Telegram.', 'error') return redirect(url_for('tma_entry_page')) tma_user_id = session['telegram_user_id'] display_name = session.get('telegram_display_name', 'Пользователь') data = load_data() if tma_user_id not in data['users']: session.clear() flash('Пользователь не найден. Пожалуйста, перезапустите приложение.', 'error') return redirect(url_for('tma_entry_page')) user_data = data['users'][tma_user_id] initialize_user_filesystem_tma(user_data, tma_user_id) current_folder_id = request.args.get('folder_id', 'root') current_folder, parent_folder = find_node_by_id(user_data['filesystem'], current_folder_id) if not current_folder or current_folder.get('type') != 'folder': flash('Папка не найдена!', 'error') current_folder_id = 'root' current_folder, parent_folder = find_node_by_id(user_data['filesystem'], 'root') parent_folder_id = parent_folder.get('id', 'root') if parent_folder else 'root' items_in_folder = [item for item in current_folder.get('children', []) if not item.get('is_archived')] items_in_folder = sorted(items_in_folder, key=lambda x: (x['type'] not in ['folder', 'note', 'todolist', 'shoppinglist'], x.get('name', x.get('original_filename', x.get('title', ''))).lower())) if request.method == 'POST': if not HF_TOKEN_WRITE: flash('Загрузка невозможна: токен для записи не настроен.', 'error') return redirect(url_for('tma_dashboard', folder_id=current_folder_id)) files = request.files.getlist('files') if not files or all(not f.filename for f in files): flash('Файлы для загрузки не выбраны.', 'error') return redirect(url_for('tma_dashboard', folder_id=current_folder_id)) target_folder_id = request.form.get('current_folder_id', 'root') uploaded_count = 0 errors_list = [] api = HfApi() for file_obj in files: if file_obj and file_obj.filename: original_filename = secure_filename(file_obj.filename) name_part, ext_part = os.path.splitext(original_filename) unique_suffix = uuid.uuid4().hex[:8] unique_filename = f"{name_part}_{unique_suffix}{ext_part}" file_id = uuid.uuid4().hex hf_path = f"cloud_files/{tma_user_id}/{target_folder_id}/{unique_filename}" temp_path = os.path.join(UPLOAD_FOLDER, f"{file_id}_{unique_filename}") try: file_obj.save(temp_path) api.upload_file(path_or_fileobj=temp_path, path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) file_info = {'type': 'file', 'id': file_id, 'original_filename': original_filename, 'unique_filename': unique_filename, 'path': hf_path, 'file_type': get_file_type(original_filename), 'upload_date': datetime.now().strftime('%Y-%m-%d')} if add_node(user_data['filesystem'], target_folder_id, file_info): uploaded_count += 1 except Exception as e: errors_list.append(f"Ошибка загрузки {original_filename}: {e}") finally: if os.path.exists(temp_path): os.remove(temp_path) if uploaded_count > 0: try: save_data(data); flash(f'{uploaded_count} файл(ов) успешно загружено!') except Exception: flash('Файлы загружены, но ошибка сохранения метаданных.', 'error') if errors_list: for error_msg in errors_list: flash(error_msg, 'error') return redirect(url_for('tma_dashboard', folder_id=target_folder_id)) breadcrumbs = [] temp_id = current_folder_id while temp_id: node, parent_node_bc = find_node_by_id(user_data['filesystem'], temp_id) if not node: break is_link = (node['id'] != current_folder_id) breadcrumbs.append({'id': node['id'], 'name': node.get('name', 'Root'), 'is_link': is_link}) if not parent_node_bc: break temp_id = parent_node_bc.get('id') breadcrumbs.reverse() all_folders_for_move = get_all_folders(user_data['filesystem']) return render_template_string(TMA_DASHBOARD_HTML_TEMPLATE, display_name=display_name, items=items_in_folder, current_folder_id=current_folder_id, current_folder=current_folder, parent_folder_id=parent_folder_id, breadcrumbs=breadcrumbs, hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(path)}{'?download=true' if download else ''}", is_tma_user_admin_flag=is_admin_tma(), all_folders_for_move=all_folders_for_move) @app.route('/tma_archive') def tma_archive_view(): if 'telegram_user_id' not in session: return redirect(url_for('tma_entry_page')) tma_user_id = session['telegram_user_id'] display_name = session.get('telegram_display_name', 'Пользователь') data = load_data() user_data = data['users'].get(tma_user_id) if not user_data: return redirect(url_for('tma_entry_page')) archived_items = get_all_archived_items(user_data.get('filesystem')) sorted_items = sorted(archived_items, key=lambda x: x.get('modified_date', ''), reverse=True) return render_template_string(ARCHIVED_LISTS_HTML, display_name=display_name, items=sorted_items) @app.route('/create_folder_tma', methods=['POST']) def create_folder_tma(): if 'telegram_user_id' not in session: return redirect(url_for('tma_entry_page')) tma_user_id = session['telegram_user_id'] data = load_data() user_data = data['users'].get(tma_user_id) if not user_data: return redirect(url_for('tma_entry_page')) parent_folder_id = request.form.get('parent_folder_id', 'root') folder_name = request.form.get('folder_name', '').strip() if not folder_name: flash('Имя папки не может быть пустым!', 'error') else: folder_id = uuid.uuid4().hex folder_data = {'type': 'folder', 'id': folder_id, 'name': folder_name, 'children': []} if add_node(user_data['filesystem'], parent_folder_id, folder_data): try: save_data(data); flash(f'Папка "{folder_name}" создана.') except Exception: flash('Ошибка сохранения данных.', 'error') else: flash('Не удалось найти родительскую папку.', 'error') return redirect(url_for('tma_dashboard', folder_id=parent_folder_id)) def get_item_node_for_user(item_id): if not (session.get('telegram_user_id') or session.get('admin_browser_logged_in')): return None data = load_data() if session.get('admin_browser_logged_in'): for uid, udata in data.get('users', {}).items(): node, _ = find_node_by_id(udata.get('filesystem', {}), item_id) if node: return node else: user_data = data['users'].get(session['telegram_user_id']) if user_data: node, _ = find_node_by_id(user_data.get('filesystem', {}), item_id) if node: return node return None def get_file_node_for_admin(tma_user_id_str, file_id): if not session.get('admin_browser_logged_in'): return None data = load_data() user_data = data['users'].get(tma_user_id_str) if user_data: node, _ = find_node_by_id(user_data.get('filesystem', {}), file_id) if node and node.get('type') == 'file': return node return None @app.route('/download_tma/') def download_tma(file_id): file_node = get_item_node_for_user(file_id) if not file_node or file_node.get('type') != 'file': return jsonify({'status': 'error', 'message': 'Файл не найден или доступ запрещен'}), 404 token = uuid.uuid4().hex cache.set(f"download_token_{token}", file_node, timeout=300) public_url = url_for('public_download', token=token, _external=True) return jsonify({'status': 'success', 'url': public_url}) @app.route('/public_download/') def public_download(token): file_node = cache.get(f"download_token_{token}") if not file_node: return Response("Ссылка для скачивания недействительна или истекла.", status=404) hf_path = file_node.get('path') original_filename = file_node.get('original_filename', 'downloaded_file') if not hf_path: return Response("Ошибка: Путь к файлу не найден.", status=500) try: hf_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(hf_path)}" headers = {} if HF_TOKEN_READ: headers["Authorization"] = f"Bearer {HF_TOKEN_READ}" req = requests.get(hf_url, headers=headers, stream=True, allow_redirects=True) req.raise_for_status() encoded_filename = quote(original_filename) response_headers = { 'Content-Type': req.headers.get('Content-Type', 'application/octet-stream'), 'Content-Disposition': f"attachment; filename*=UTF-8''{encoded_filename}", 'Content-Length': req.headers.get('Content-Length') } response_headers = {k: v for k, v in response_headers.items() if v is not None} return Response(stream_with_context(req.iter_content(chunk_size=8192)), headers=response_headers) except requests.exceptions.HTTPError as e: if e.response.status_code == 404: logging.error(f"File not found on Hugging Face during streaming: {hf_path}") return Response("Файл не найден на удаленном хранилище.", status=404) else: logging.error(f"HTTP error downloading from HF via streaming: {e}") return Response(f'Ошибка HTTP при скачивании файла: {e}', status=502) except Exception as e: logging.error(f"Error streaming with token {token} from HF: {e}") return Response(f'Ошибка скачивания файла: {e}', status=502) @app.route('/batch_download_tma') def batch_download_tma(): if 'telegram_user_id' not in session: return Response("Unauthorized", 401) file_ids_str = request.args.get('file_ids') if not file_ids_str: return Response("No file IDs provided", 400) file_ids = file_ids_str.split(',') temp_zip_file = tempfile.NamedTemporaryFile(delete=False, suffix=".zip") try: with zipfile.ZipFile(temp_zip_file.name, 'w', zipfile.ZIP_DEFLATED) as zf: for file_id in file_ids: file_node = get_item_node_for_user(file_id) if file_node and file_node.get('path'): hf_path = file_node['path'] original_filename = file_node.get('original_filename', file_id) try: local_file_path = hf_hub_download( repo_id=REPO_ID, filename=hf_path, repo_type="dataset", token=HF_TOKEN_READ, cache_dir=os.path.join(UPLOAD_FOLDER, 'hf_download_cache') ) zf.write(local_file_path, arcname=original_filename) except Exception as e: logging.error(f"Failed to download and add {original_filename} to zip: {e}") return send_file(temp_zip_file.name, as_attachment=True, download_name='archive.zip', mimetype='application/zip') finally: if os.path.exists(temp_zip_file.name): os.unlink(temp_zip_file.name) @app.route('/batch_delete_tma', methods=['POST']) def batch_delete_tma(): if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401 tma_user_id = session['telegram_user_id'] data = load_data() user_data = data['users'].get(tma_user_id) if not user_data: return jsonify({'status': 'error', 'message': 'Пользователь не найден.'}), 404 item_ids = request.json.get('item_ids', []) if not item_ids: return jsonify({'status': 'error', 'message': 'Не выбраны элементы.'}), 400 api = HfApi() success_count = 0; errors = [] for item_id in item_ids: node, _ = find_node_by_id(user_data['filesystem'], item_id) if not node: errors.append(f"Элемент {item_id} не найден.") continue node_type = node.get('type') node_name = node.get('name') or node.get('title') or node.get('original_filename', 'элемент') if node_type == 'folder': if node.get('children'): errors.append(f'Папка "{node_name}" не пуста.'); continue if node_type in ['folder', 'note', 'todolist', 'shoppinglist', 'file']: if node_type == 'file': try: if node.get('path') and HF_TOKEN_WRITE: api.delete_file(path_in_repo=node['path'], repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) except hf_utils.EntryNotFoundError: pass except Exception as e: errors.append(f'Ошибка удаления "{node_name}" с сервера: {e}'); continue if remove_node(user_data['filesystem'], item_id)[0]: success_count += 1 else: errors.append(f'Ошибка удаления "{node_name}" из базы.') if success_count > 0: try: save_data(data) except Exception as e: return jsonify({'status': 'error', 'message': f'Ошибка сохранения: {e}'}) if errors: return jsonify({'status': 'error', 'message': f'Удалено {success_count}. Ошибки: ' + "; ".join(errors)}) return jsonify({'status': 'success', 'message': f'Удалено {success_count} элемент(ов).'}) @app.route('/batch_move_tma', methods=['POST']) def batch_move_tma(): if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401 tma_user_id = session['telegram_user_id'] data = load_data() user_data = data['users'].get(tma_user_id) if not user_data: return jsonify({'status': 'error', 'message': 'Пользователь не найден.'}), 404 item_ids = request.json.get('item_ids', []) destination_id = request.json.get('destination_id') if not item_ids or not destination_id: return jsonify({'status': 'error', 'message': 'Не указаны файлы или папка.'}), 400 destination_node, _ = find_node_by_id(user_data['filesystem'], destination_id) if not destination_node or destination_node.get('type') != 'folder': return jsonify({'status': 'error', 'message': 'Папка назначения не найдена.'}), 404 descendant_ids = set() for item_id in item_ids: node, _ = find_node_by_id(user_data['filesystem'], item_id) if node and node.get('type') == 'folder': queue = [node] while queue: curr = queue.pop(0) descendant_ids.add(curr.get('id')) if 'children' in curr: queue.extend(curr['children']) if destination_id in descendant_ids: return jsonify({'status': 'error', 'message': 'Нельзя переместить папку в саму себя.'}) moved_count = 0; errors = [] for item_id in item_ids: if item_id == destination_id: continue removed, node_to_move = remove_node(user_data['filesystem'], item_id) if removed and node_to_move: node_to_move['is_archived'] = False if add_node(user_data['filesystem'], destination_id, node_to_move): moved_count += 1 else: errors.append(f'Ошибка добавления {item_id} в новую папку.') else: errors.append(f'Не удалось извлечь {item_id}.') if moved_count > 0: try: save_data(data) except Exception as e: return jsonify({'status': 'error', 'message': f'Ошибка сохранения: {e}'}) if errors: return jsonify({'status': 'error', 'message': f'Перемещено {moved_count}. Ошибки: ' + "; ".join(errors)}) return jsonify({'status': 'success', 'message': f'Перемещено {moved_count} элемент(ов).'}) @app.route('/batch_archive_tma', methods=['POST']) def batch_archive_tma(): if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401 tma_user_id = session['telegram_user_id'] data = load_data() user_data = data['users'].get(tma_user_id) if not user_data: return jsonify({'status': 'error', 'message': 'Пользователь не найден.'}), 404 item_ids = request.json.get('item_ids', []) if not item_ids: return jsonify({'status': 'error', 'message': 'Не выбраны элементы.'}), 400 archived_count = 0 for item_id in item_ids: node, _ = find_node_by_id(user_data['filesystem'], item_id) if node and node.get('type') in ['todolist', 'shoppinglist']: node['is_archived'] = True archived_count += 1 if archived_count > 0: try: save_data(data) return jsonify({'status': 'success', 'message': f'Архивировано {archived_count} списк(ов).'}) except Exception as e: return jsonify({'status': 'error', 'message': f'Ошибка сохранения: {e}'}), 500 return jsonify({'status': 'error', 'message': 'Не найдено списков для архивации.'}) @app.route('/batch_unarchive_tma', methods=['POST']) def batch_unarchive_tma(): if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401 tma_user_id = session['telegram_user_id'] data = load_data() user_data = data['users'].get(tma_user_id) if not user_data: return jsonify({'status': 'error', 'message': 'Пользователь не найден.'}), 404 item_ids = request.json.get('item_ids', []) if not item_ids: return jsonify({'status': 'error', 'message': 'Не выбраны элементы.'}), 400 unarchived_count = 0 for item_id in item_ids: node, _ = find_node_by_id(user_data['filesystem'], item_id) if node and node.get('is_archived'): node['is_archived'] = False unarchived_count += 1 if unarchived_count > 0: try: save_data(data) return jsonify({'status': 'success', 'message': f'Восстановлено {unarchived_count} списк(ов).'}) except Exception as e: return jsonify({'status': 'error', 'message': f'Ошибка сохранения: {e}'}), 500 return jsonify({'status': 'error', 'message': 'Не найдено списков для восстановления.'}) @app.route('/get_text_content_tma/') def get_text_content_tma(file_id): file_node = get_item_node_for_user(file_id) if not file_node or file_node.get('file_type') != 'text': return Response("Текстовый файл не найден", 404) hf_path = file_node.get('path') if not hf_path: return Response("Ошибка: путь к файлу отсутствует", 500) file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(hf_path)}?download=true" try: req_headers = {}; if HF_TOKEN_READ: req_headers["authorization"] = f"Bearer {HF_TOKEN_READ}" response = requests.get(file_url, headers=req_headers) response.raise_for_status() if len(response.content) > 1 * 1024 * 1024: return Response("Файл слишком большой для предпросмотра.", 413) try: text_content = response.content.decode('utf-8') except UnicodeDecodeError: text_content = response.content.decode('latin-1', errors='ignore') return Response(text_content, mimetype='text/plain') except Exception as e: return Response(f"Ошибка загрузки: {e}", 502) @app.route('/get_note_tma/') def get_note_tma(note_id): if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Unauthorized'}), 401 note_node = get_item_node_for_user(note_id) if not note_node or note_node.get('type') != 'note': return jsonify({'status': 'error', 'message': 'Note not found'}), 404 return jsonify({'status': 'success', 'note': note_node}) @app.route('/create_or_update_note_tma', methods=['POST']) def create_or_update_note_tma(): if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Unauthorized'}), 401 tma_user_id = session['telegram_user_id'] data = load_data() user_data = data['users'].get(tma_user_id) if not user_data: return jsonify({'status': 'error', 'message': 'User not found'}), 404 payload = request.json note_id = payload.get('note_id') title = payload.get('title', '').strip() content = payload.get('content', '') parent_folder_id = payload.get('parent_folder_id', 'root') now_str = datetime.now().strftime('%Y-%m-%d %H:%M') if not title: return jsonify({'status': 'error', 'message': 'Title cannot be empty.'}), 400 if note_id: node, _ = find_node_by_id(user_data['filesystem'], note_id) if not node or node.get('type') != 'note': return jsonify({'status': 'error', 'message': 'Note not found'}), 404 node['title'] = title node['content'] = content node['modified_date'] = now_str else: new_note_id = uuid.uuid4().hex note_data = { 'type': 'note', 'id': new_note_id, 'title': title, 'content': content, 'created_date': now_str, 'modified_date': now_str } if not add_node(user_data['filesystem'], parent_folder_id, note_data): return jsonify({'status': 'error', 'message': 'Parent folder not found'}), 404 try: save_data(data) return jsonify({'status': 'success', 'message': 'Note saved.'}) except Exception as e: return jsonify({'status': 'error', 'message': f'Failed to save data: {e}'}), 500 @app.route('/get_list_tma/') def get_list_tma(list_id): if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Unauthorized'}), 401 list_node = get_item_node_for_user(list_id) if not list_node or list_node.get('type') not in ['todolist', 'shoppinglist']: return jsonify({'status': 'error', 'message': 'List not found'}), 404 return jsonify({'status': 'success', 'list': list_node}) @app.route('/create_or_update_list_tma', methods=['POST']) def create_or_update_list_tma(): if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Unauthorized'}), 401 tma_user_id = session['telegram_user_id'] data = load_data() user_data = data['users'].get(tma_user_id) if not user_data: return jsonify({'status': 'error', 'message': 'User not found'}), 404 payload = request.json list_id = payload.get('list_id') list_type = payload.get('type') title = payload.get('title', '').strip() items = payload.get('items', []) parent_folder_id = payload.get('parent_folder_id', 'root') now_str = datetime.now().strftime('%Y-%m-%d %H:%M') if not title: return jsonify({'status': 'error', 'message': 'Title cannot be empty.'}), 400 if list_type not in ['todolist', 'shoppinglist']: return jsonify({'status': 'error', 'message': 'Invalid list type.'}), 400 for item in items: if not item.get('id'): item['id'] = uuid.uuid4().hex if list_id: node, _ = find_node_by_id(user_data['filesystem'], list_id) if not node or node.get('type') != list_type: return jsonify({'status': 'error', 'message': 'List not found'}), 404 node['title'] = title node['items'] = items node['modified_date'] = now_str else: new_list_id = uuid.uuid4().hex list_data = { 'type': list_type, 'id': new_list_id, 'title': title, 'items': items, 'created_date': now_str, 'modified_date': now_str, 'is_archived': False } if not add_node(user_data['filesystem'], parent_folder_id, list_data): return jsonify({'status': 'error', 'message': 'Parent folder not found'}), 404 try: save_data(data) return jsonify({'status': 'success', 'message': 'List saved.'}) except Exception as e: return jsonify({'status': 'error', 'message': f'Failed to save data: {e}'}), 500 @app.route('/get_reminders_tma') def get_reminders_tma(): if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Unauthorized'}), 401 user_data = load_data()['users'].get(session['telegram_user_id']) if not user_data: return jsonify({'status': 'error', 'message': 'User not found'}), 404 reminders = sorted(user_data.get('reminders', []), key=lambda r: r.get('due_datetime_utc', '')) return jsonify({'status': 'success', 'reminders': reminders}) @app.route('/create_reminder_tma', methods=['POST']) def create_reminder_tma(): if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Unauthorized'}), 401 tma_user_id = session['telegram_user_id'] data = load_data() user_data = data['users'].get(tma_user_id) if not user_data: return jsonify({'status': 'error', 'message': 'User not found'}), 404 payload = request.json text = payload.get('text', '').strip() dt_local_str = payload.get('datetime_local') user_tz_str = payload.get('user_timezone', 'UTC') if not text or not dt_local_str: return jsonify({'status': 'error', 'message': 'Missing required fields'}), 400 try: user_tz = ZoneInfo(user_tz_str) except ZoneInfoNotFoundError: user_tz = pytz.timezone('UTC') dt_local = datetime.fromisoformat(dt_local_str) dt_aware = dt_local.astimezone(user_tz) dt_utc = dt_aware.astimezone(pytz.utc) new_reminder = { 'id': uuid.uuid4().hex, 'text': text, 'due_datetime_utc': dt_utc.isoformat().replace('+00:00', 'Z'), 'due_datetime_local': dt_local.isoformat(), 'user_timezone': user_tz_str, 'notified': False } user_data.setdefault('reminders', []).append(new_reminder) try: save_data(data) return jsonify({'status': 'success'}) except Exception as e: return jsonify({'status': 'error', 'message': f'Failed to save: {e}'}), 500 @app.route('/delete_reminder_tma/', methods=['POST']) def delete_reminder_tma(reminder_id): if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Unauthorized'}), 401 tma_user_id = session['telegram_user_id'] data = load_data() user_data = data['users'].get(tma_user_id) if not user_data: return jsonify({'status': 'error', 'message': 'User not found'}), 404 reminders = user_data.get('reminders', []) user_data['reminders'] = [r for r in reminders if r.get('id') != reminder_id] try: save_data(data) return jsonify({'status': 'success'}) except Exception as e: return jsonify({'status': 'error', 'message': f'Failed to save: {e}'}), 500 @app.route('/tma_logout') def tma_logout(): session.clear() flash('Вы вышли из сессии приложения.') return redirect(url_for('tma_entry_page')) @app.route('/create_public_link', methods=['POST']) def create_public_link(): if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401 tma_user_id = session['telegram_user_id'] data = load_data() user_data = data['users'].get(tma_user_id) if not user_data: return jsonify({'status': 'error', 'message': 'Пользователь не найден.'}), 404 payload = request.json item_id = payload.get('item_id') item_type = payload.get('item_type') name = payload.get('name') duration_hours = payload.get('duration_hours', 0) item_node, _ = find_node_by_id(user_data['filesystem'], item_id) if not item_node or item_node.get('type') != item_type or item_type not in ['folder', 'shoppinglist']: return jsonify({'status': 'error', 'message': 'Элемент не найден или не может быть опубликован.'}), 404 now = datetime.now(pytz.utc) expires_at = None if duration_hours > 0: expires_at = now + timedelta(hours=duration_hours) expires_at_iso = expires_at.isoformat() else: expires_at_iso = None link_id = uuid.uuid4().hex link_data = { 'id': link_id, 'user_id': tma_user_id, 'item_id': item_id, 'item_type': item_type, 'name': name, 'created_at': now.isoformat(), 'expires_at': expires_at_iso } data['shared_links'][link_id] = link_data item_node.setdefault('public_links', []).append(link_id) try: save_data(data) public_url = url_for('shared_item_view', link_id=link_id, _external=True) return jsonify({'status': 'success', 'url': public_url}) except Exception as e: return jsonify({'status': 'error', 'message': f'Ошибка сохранения: {e}'}), 500 @app.route('/delete_public_link', methods=['POST']) def delete_public_link(): if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401 tma_user_id = session['telegram_user_id'] data = load_data() link_id = request.json.get('link_id') link_data = data['shared_links'].get(link_id) if not link_data or link_data.get('user_id') != tma_user_id: return jsonify({'status': 'error', 'message': 'Ссылка не найдена или нет доступа.'}), 404 item_id = link_data.get('item_id') user_data = data['users'].get(tma_user_id) if user_data: item, _ = find_node_by_id(user_data['filesystem'], item_id) if item and 'public_links' in item: item['public_links'] = [l for l in item['public_links'] if l != link_id] del data['shared_links'][link_id] try: save_data(data) return jsonify({'status': 'success'}) except Exception as e: return jsonify({'status': 'error', 'message': f'Ошибка сохранения: {e}'}), 500 @app.route('/get_public_links/') def get_public_links(item_id): if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован.'}), 401 tma_user_id = session['telegram_user_id'] data = load_data() user_data = data['users'].get(tma_user_id) if not user_data: return jsonify({'status': 'error', 'message': 'Пользователь не найден.'}), 404 item, _ = find_node_by_id(user_data['filesystem'], item_id) if not item: return jsonify({'status': 'error', 'message': 'Элемент не найден.'}), 404 link_ids = item.get('public_links', []) links_details = [] for link_id in link_ids: link_data = data['shared_links'].get(link_id) if link_data: link_data['url'] = url_for('shared_item_view', link_id=link_id, _external=True) links_details.append(link_data) return jsonify({'status': 'success', 'links': links_details}) @app.route('/shared/') def shared_item_view(link_id): data = load_data() link_data = data.get('shared_links', {}).get(link_id) if not link_data: return "Ссылка недействительна.", 404 if link_data.get('expires_at'): expires_at = datetime.fromisoformat(link_data['expires_at']) if datetime.now(pytz.utc) > expires_at: return "Срок действия ссылки истек.", 410 user_id = link_data['user_id'] user_data = data['users'].get(user_id) if not user_data: return "Владелец не найден.", 404 item_id = link_data['item_id'] item_node, _ = find_node_by_id(user_data['filesystem'], item_id) if not item_node: return "Элемент не найден.", 404 if link_data['item_type'] == 'folder': return redirect(url_for('shared_folder_view', link_id=link_id)) elif link_data['item_type'] == 'shoppinglist': return render_template_string(PUBLIC_SHOPPING_LIST_HTML, list_data=item_node, user=user_data, link=link_data) else: return "Неподдерживаемый тип элемента для обмена.", 400 @app.route('/shared//folder') @app.route('/shared//folder/') def shared_folder_view(link_id, subfolder_id=None): data = load_data() link_data = data['shared_links'].get(link_id) if not link_data or link_data['item_type'] != 'folder': return "Ссылка недействительна.", 404 if link_data.get('expires_at'): expires_at = datetime.fromisoformat(link_data['expires_at']) if datetime.now(pytz.utc) > expires_at: return "Срок действия ссылки истек.", 410 user_id = link_data['user_id'] user_data = data['users'].get(user_id) if not user_data: return "Владелец не найден.", 404 folder_id_to_show = subfolder_id if subfolder_id else link_data['item_id'] folder_node, _ = find_node_by_id(user_data['filesystem'], folder_id_to_show) if not folder_node or folder_node.get('type') != 'folder': return "Папка не найдена.", 404 items_in_folder = sorted(folder_node.get('children', []), key=lambda x: (x['type'] != 'folder', x.get('name', x.get('original_filename', x.get('title', ''))).lower())) return render_template_string(PUBLIC_SHARE_PAGE_HTML, folder=folder_node, items=items_in_folder, user=user_data, link=link_data, hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(path)}{'?download=true' if download else ''}") @app.route('/public_download//') def public_download_via_link(link_id, item_id): data = load_data() link_data = data['shared_links'].get(link_id) if not link_data: return Response("Ссылка недействительна.", status=404) if link_data.get('expires_at'): expires_at = datetime.fromisoformat(link_data['expires_at']) if datetime.now(pytz.utc) > expires_at: return Response("Срок действия ссылки истек.", status=410) user_id = link_data['user_id'] user_data = data['users'].get(user_id) if not user_data: return Response("Владелец не найден.", status=404) item_node, _ = find_node_by_id(user_data['filesystem'], item_id) if not item_node: return Response("Элемент не найден.", status=404) token = uuid.uuid4().hex cache.set(f"download_token_{token}", item_node, timeout=300) return redirect(url_for('public_download', token=token)) @app.route('/api/public_list_data/') def public_list_data(link_id): data = load_data() link_data = data.get('shared_links', {}).get(link_id) if not link_data: return jsonify({'status': 'error', 'message': 'Ссылка не найдена.'}), 404 if link_data.get('expires_at') and datetime.now(pytz.utc) > datetime.fromisoformat(link_data['expires_at']): return jsonify({'status': 'error', 'message': 'Срок действия ссылки истек.'}), 410 user_id = link_data['user_id'] user_data = data['users'].get(user_id) if not user_data: return jsonify({'status': 'error', 'message': 'Владелец не найден.'}), 404 list_node, _ = find_node_by_id(user_data['filesystem'], link_data['item_id']) if not list_node: return jsonify({'status': 'error', 'message': 'Список не найден.'}), 404 return jsonify({'status': 'success', 'list': list_node}) @app.route('/api/public_toggle_item//', methods=['POST']) def public_toggle_item(link_id, item_id): data = load_data() link_data = data.get('shared_links', {}).get(link_id) if not link_data: return jsonify({'status': 'error', 'message': 'Ссылка не найдена.'}), 404 if link_data.get('expires_at') and datetime.now(pytz.utc) > datetime.fromisoformat(link_data['expires_at']): return jsonify({'status': 'error', 'message': 'Срок действия ссылки истек.'}), 410 user_id = link_data['user_id'] user_data = data['users'].get(user_id) if not user_data: return jsonify({'status': 'error', 'message': 'Владелец не найден.'}), 404 list_node, _ = find_node_by_id(user_data['filesystem'], link_data['item_id']) if not list_node: return jsonify({'status': 'error', 'message': 'Список не найден.'}), 404 item_found = False for item in list_node.get('items', []): if item.get('id') == item_id: item['purchased'] = not item.get('purchased', False) item_found = True break if item_found: list_node['modified_date'] = datetime.now().strftime('%Y-%m-%d %H:%M') try: save_data(data) return jsonify({'status': 'success'}) except Exception as e: return jsonify({'status': 'error', 'message': f'Ошибка сохранения: {e}'}), 500 else: return jsonify({'status': 'error', 'message': 'Элемент в списке не найден.'}), 404 @app.route('/tma_business') def tma_manage_business_pages(): if 'telegram_user_id' not in session: return redirect(url_for('tma_entry_page')) tma_user_id = session['telegram_user_id'] display_name = session.get('telegram_display_name', 'Пользователь') data = load_data() user_data = data['users'].get(tma_user_id) if not user_data: return redirect(url_for('tma_entry_page')) owned_logins = user_data.get('owned_business_pages', []) pages = [data['business_pages'][login] for login in owned_logins if login in data['business_pages']] return render_template_string(TMA_MANAGE_BUSINESS_HTML, display_name=display_name, pages=pages, hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(path)}{'?download=true' if download else ''}") @app.route('/tma_business/create', methods=['GET', 'POST']) def tma_create_business_page(): if 'telegram_user_id' not in session: return redirect(url_for('tma_entry_page')) tma_user_id = session['telegram_user_id'] display_name = session.get('telegram_display_name', 'Пользователь') if request.method == 'POST': data = load_data() login = request.form.get('login', '').strip().lower() if not re.match(r'^[a-z0-9_.-]+$', login): flash('Логин содержит недопустимые символы.', 'error') return redirect(url_for('tma_create_business_page')) if login in data['business_pages']: flash('Этот логин уже занят.', 'error') return redirect(url_for('tma_create_business_page')) new_page = { 'owner_id': tma_user_id, 'login': login, 'org_name': request.form.get('org_name'), 'currency': request.form.get('currency'), 'show_prices': 'show_prices' in request.form, 'order_destination': request.form.get('order_destination'), 'contact_number': request.form.get('contact_number').replace('@', ''), 'avatar_path': None, 'products': [] } avatar = request.files.get('avatar') if avatar and avatar.filename: if not HF_TOKEN_WRITE: flash('Загрузка аватара невозможна: токен для записи не настроен.', 'error') return redirect(url_for('tma_create_business_page')) try: api = HfApi() unique_filename = f"avatar_{uuid.uuid4().hex[:8]}{os.path.splitext(secure_filename(avatar.filename))[1]}" hf_path = f"business_pages/{login}/{unique_filename}" api.upload_file(path_or_fileobj=BytesIO(avatar.read()), path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) new_page['avatar_path'] = hf_path except Exception as e: flash(f'Ошибка загрузки аватара: {e}', 'error') return redirect(url_for('tma_create_business_page')) data['business_pages'][login] = new_page data['users'][tma_user_id].setdefault('owned_business_pages', []).append(login) try: save_data(data) flash('Бизнес страница успешно создана!', 'success') return redirect(url_for('tma_manage_business_pages')) except Exception as e: flash(f'Ошибка сохранения данных: {e}', 'error') return render_template_string(TMA_CREATE_EDIT_BUSINESS_FORM_HTML, display_name=display_name, page=None) @app.route('/tma_business/edit/', methods=['GET', 'POST']) def tma_edit_business_page(login): if 'telegram_user_id' not in session: return redirect(url_for('tma_entry_page')) tma_user_id = session['telegram_user_id'] display_name = session.get('telegram_display_name', 'Пользователь') data = load_data() page = data.get('business_pages', {}).get(login) if not page or page.get('owner_id') != tma_user_id: flash('Страница не найдена или у вас нет доступа.', 'error') return redirect(url_for('tma_manage_business_pages')) if request.method == 'POST': page['org_name'] = request.form.get('org_name') page['currency'] = request.form.get('currency') page['show_prices'] = 'show_prices' in request.form page['order_destination'] = request.form.get('order_destination') page['contact_number'] = request.form.get('contact_number').replace('@', '') avatar = request.files.get('avatar') if avatar and avatar.filename: if not HF_TOKEN_WRITE: flash('Загрузка аватара невозможна: токен для записи не настроен.', 'error') else: try: api = HfApi() if page.get('avatar_path'): try: api.delete_file(path_in_repo=page['avatar_path'], repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) except hf_utils.EntryNotFoundError: pass unique_filename = f"avatar_{uuid.uuid4().hex[:8]}{os.path.splitext(secure_filename(avatar.filename))[1]}" hf_path = f"business_pages/{login}/{unique_filename}" api.upload_file(path_or_fileobj=BytesIO(avatar.read()), path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) page['avatar_path'] = hf_path except Exception as e: flash(f'Ошибка загрузки аватара: {e}', 'error') try: save_data(data) flash('Изменения сохранены.', 'success') return redirect(url_for('tma_manage_business_pages')) except Exception as e: flash(f'Ошибка сохранения данных: {e}', 'error') return render_template_string(TMA_CREATE_EDIT_BUSINESS_FORM_HTML, display_name=display_name, page=page) @app.route('/tma_business/delete/', methods=['POST']) def tma_delete_business_page(login): if 'telegram_user_id' not in session: return redirect(url_for('tma_entry_page')) tma_user_id = session['telegram_user_id'] data = load_data() page = data.get('business_pages', {}).get(login) if not page or page.get('owner_id') != tma_user_id: flash('Страница не найдена или у вас нет доступа.', 'error') return redirect(url_for('tma_manage_business_pages')) if HF_TOKEN_WRITE: api = HfApi() try: api.delete_folder(folder_path=f"business_pages/{login}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) except Exception as e: logging.error(f"Could not delete business page folder from HF: {e}") del data['business_pages'][login] if login in data['users'][tma_user_id].get('owned_business_pages', []): data['users'][tma_user_id]['owned_business_pages'].remove(login) try: save_data(data) flash('Бизнес страница удалена.', 'success') except Exception as e: flash(f'Ошибка сохранения после удаления: {e}', 'error') return redirect(url_for('tma_manage_business_pages')) @app.route('/business/') def public_business_page(login): data = load_data() page = data.get('business_pages', {}).get(login) if not page: return "Страница не найдена.", 404 return render_template_string(PUBLIC_BUSINESS_PAGE_HTML, page=page, hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(path)}{'?download=true' if download else ''}") @app.route('/tma_business/manage/') def tma_manage_products(login): if 'telegram_user_id' not in session: return redirect(url_for('tma_entry_page')) tma_user_id = session['telegram_user_id'] data = load_data() page = data.get('business_pages', {}).get(login) if not page or page.get('owner_id') != tma_user_id: flash('Страница не найдена или у вас нет доступа.', 'error') return redirect(url_for('tma_manage_business_pages')) return render_template_string(TMA_MANAGE_PRODUCTS_HTML, page=page, hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(path)}{'?download=true' if download else ''}") @app.route('/tma_business/product/add/', methods=['POST']) def tma_add_product(login): if 'telegram_user_id' not in session: return redirect(url_for('tma_entry_page')) tma_user_id = session['telegram_user_id'] data = load_data() page = data.get('business_pages', {}).get(login) if not page or page.get('owner_id') != tma_user_id: flash('Страница не найдена или у вас нет доступа.', 'error') return redirect(url_for('tma_manage_business_pages')) new_product = { 'id': uuid.uuid4().hex, 'name': request.form.get('name'), 'description': request.form.get('description', ''), 'price': request.form.get('price', 0), 'photo_path': None } photo = request.files.get('photo') if photo and photo.filename: if not HF_TOKEN_WRITE: flash('Загрузка фото невозможна: токен не настроен.', 'error') else: try: api = HfApi() unique_filename = f"product_{new_product['id'][:8]}{os.path.splitext(secure_filename(photo.filename))[1]}" hf_path = f"business_pages/{login}/{unique_filename}" api.upload_file(path_or_fileobj=BytesIO(photo.read()), path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) new_product['photo_path'] = hf_path except Exception as e: flash(f'Ошибка загрузки фото: {e}', 'error') page.setdefault('products', []).append(new_product) try: save_data(data) flash('Товар добавлен.', 'success') except Exception as e: flash(f'Ошибка сохранения: {e}', 'error') return redirect(url_for('tma_manage_products', login=login)) @app.route('/tma_business/product/edit/', methods=['POST']) def tma_edit_product(login): if 'telegram_user_id' not in session: return redirect(url_for('tma_entry_page')) tma_user_id = session['telegram_user_id'] data = load_data() page = data.get('business_pages', {}).get(login) product_id = request.form.get('product_id') if not page or page.get('owner_id') != tma_user_id or not product_id: flash('Ошибка доступа.', 'error'); return redirect(url_for('tma_manage_business_pages')) product_to_edit = next((p for p in page.get('products', []) if p['id'] == product_id), None) if not product_to_edit: flash('Товар не найден.', 'error'); return redirect(url_for('tma_manage_products', login=login)) product_to_edit['name'] = request.form.get('name') product_to_edit['description'] = request.form.get('description', '') product_to_edit['price'] = request.form.get('price', 0) photo = request.files.get('photo') if photo and photo.filename: if not HF_TOKEN_WRITE: flash('Загрузка фото невозможна: токен не настроен.', 'error') else: try: api = HfApi() if product_to_edit.get('photo_path'): try: api.delete_file(path_in_repo=product_to_edit['photo_path'], repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) except hf_utils.EntryNotFoundError: pass unique_filename = f"product_{product_id[:8]}{os.path.splitext(secure_filename(photo.filename))[1]}" hf_path = f"business_pages/{login}/{unique_filename}" api.upload_file(path_or_fileobj=BytesIO(photo.read()), path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) product_to_edit['photo_path'] = hf_path except Exception as e: flash(f'Ошибка загрузки фото: {e}', 'error') try: save_data(data) flash('Товар обновлен.', 'success') except Exception as e: flash(f'Ошибка сохранения: {e}', 'error') return redirect(url_for('tma_manage_products', login=login)) @app.route('/tma_business/product/delete//', methods=['POST']) def tma_delete_product(login, product_id): if 'telegram_user_id' not in session: return redirect(url_for('tma_entry_page')) tma_user_id = session['telegram_user_id'] data = load_data() page = data.get('business_pages', {}).get(login) if not page or page.get('owner_id') != tma_user_id: flash('Ошибка доступа.', 'error'); return redirect(url_for('tma_manage_business_pages')) product_to_delete = next((p for p in page.get('products', []) if p['id'] == product_id), None) if product_to_delete and product_to_delete.get('photo_path') and HF_TOKEN_WRITE: try: api = HfApi() api.delete_file(path_in_repo=product_to_delete['photo_path'], repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) except Exception as e: logging.error(f"Could not delete product photo from HF: {e}") page['products'] = [p for p in page.get('products', []) if p['id'] != product_id] try: save_data(data) flash('Товар удален.', 'success') except Exception as e: flash(f'Ошибка сохранения: {e}', 'error') return redirect(url_for('tma_manage_products', login=login)) @app.route('/api/business//create_order', methods=['POST']) def create_order(login): data = load_data() page = data.get('business_pages', {}).get(login) if not page: return jsonify({'status': 'error', 'message': 'Business page not found.'}), 404 cart_data = request.json if not cart_data: return jsonify({'status': 'error', 'message': 'Cart data is empty.'}), 400 order_items = [] total_price = 0 products_on_server = {p['id']: p for p in page.get('products', [])} for product_id, cart_item in cart_data.items(): server_product = products_on_server.get(product_id) if not server_product: continue quantity = int(cart_item.get('quantity', 0)) if quantity <= 0: continue price = float(server_product.get('price', 0)) order_items.append({ 'id': product_id, 'name': server_product['name'], 'price': price, 'quantity': quantity, 'image': server_product.get('photo_path') }) total_price += price * quantity if not order_items: return jsonify({'status': 'error', 'message': 'No valid items in the order.'}), 400 order_id = uuid.uuid4().hex new_order = { 'id': order_id, 'business_login': login, 'items': order_items, 'total_price': total_price, 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S') } data.setdefault('orders', {})[order_id] = new_order try: save_data(data) return jsonify({ 'status': 'success', 'order_url': url_for('public_order_page', order_id=order_id, _external=True) }) except Exception as e: logging.error(f"Error saving order: {e}") return jsonify({'status': 'error', 'message': 'Could not save the order.'}), 500 @app.route('/order/') def public_order_page(order_id): data = load_data() order = data.get('orders', {}).get(order_id) if not order: return "Заказ не найден.", 404 business_page = data.get('business_pages', {}).get(order['business_login']) if not business_page: return "Страница бизнеса, связанная с этим заказом, не найдена.", 404 return render_template_string(PUBLIC_ORDER_PAGE_HTML, order=order, business=business_page, hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(path)}{'?download=true' if download else ''}") ADMIN_LOGIN_HTML = ''' Admin Login ''' ADMIN_PANEL_HTML = ''' Admin Panel

Admin Panel ({{ users|length }})

Logout
    {% for user_id, user in users %}
  • {% if user.photo_url %}Avatar {% else %}
    {{ user.get('first_name', 'U')[0] }}
    {% endif %}
  • {% else %}
  • No users found.
  • {% endfor %}
''' ADMIN_USER_FILES_HTML = ''' Admin - User Files

Items for {{ user.get('first_name', 'N/A') }}

@{{ user.get('telegram_username', 'N/A') }} (ID: {{ user_id }})
{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %}{% endwith %}

{{ current_folder.name if current_folder_id != 'root' else 'Root Folder' }}

{% for item in items %}
{% if item.type == 'folder' %}
{% elif item.type == 'note' %}
{% elif item.type == 'todolist' %}
{% elif item.type == 'shoppinglist' %}
{% elif item.type == 'file' %} {% if item.file_type == 'image' %} {% elif item.file_type == 'video' %} {% elif item.file_type == 'pdf' %}
{% elif item.file_type == 'text' %}
{% else %}
{% endif %} {% endif %}

{{ (item.title if item.type in ['note', 'todolist', 'shoppinglist'] else item.name if item.type == 'folder' else item.original_filename) | truncate(30, True) }}

{% if item.type == 'file' %}

{{ item.upload_date }}

{% elif item.type in ['note', 'todolist', 'shoppinglist'] %}

{{ item.modified_date }}

{% endif %}
{% endfor %} {% if not items %}

This folder is empty.

{% endif %}
''' ADMIN_USER_REMINDERS_HTML = ''' Admin - User Reminders

Reminders for {{ user.get('first_name', 'N/A') }}

@{{ user.get('telegram_username', 'N/A') }} (ID: {{ user_id }})
{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %}{% endwith %}
    {% for reminder in reminders %}
  • {{ reminder.text }} Due (UTC): {{ reminder.due_datetime_utc }} Notified: {{ 'Yes' if reminder.notified else 'No' }}
  • {% else %}
  • No reminders for this user.
  • {% endfor %}
''' @app.route('/admin') def admin_redirect(): return redirect(url_for('admin_login')) @app.route('/admhosto/login', methods=['GET', 'POST']) def admin_login(): if session.get('admin_browser_logged_in'): return redirect(url_for('admin_panel')) if request.method == 'POST': if request.form.get('username') == ADMIN_USERNAME and request.form.get('password') == ADMIN_PASSWORD: session['admin_browser_logged_in'] = True next_url = request.form.get('next') or url_for('admin_panel') return redirect(next_url) else: flash('Invalid credentials.', 'error') return render_template_string(ADMIN_LOGIN_HTML) @app.route('/admhosto/logout') def admin_logout(): session.pop('admin_browser_logged_in', None) flash('You have been logged out.') return redirect(url_for('admin_login')) @app.route('/admhosto') @admin_browser_login_required def admin_panel(): data = load_data() all_users = data.get('users', {}) search_query = request.args.get('q', '').lower() processed_users = {} for user_id, user_data in all_users.items(): user_data['item_count'] = count_items_recursive(user_data.get('filesystem')) processed_users[user_id] = user_data if search_query: filtered_users = {} for user_id, user_data in processed_users.items(): full_name = f"{user_data.get('first_name', '')} {user_data.get('last_name', '')}".lower() username = user_data.get('telegram_username', '').lower() if search_query in full_name or search_query in username: filtered_users[user_id] = user_data users_to_display = filtered_users else: users_to_display = processed_users sorted_users = sorted( users_to_display.items(), key=lambda item: item[1].get('created_at', '0000-00-00 00:00:00'), reverse=True ) return render_template_string(ADMIN_PANEL_HTML, users=sorted_users, search_query=request.args.get('q', '')) @app.route('/admhosto/user/') @admin_browser_login_required def admin_user_files(tma_user_id_str): data = load_data() user_data = data['users'].get(tma_user_id_str) if not user_data: flash('User not found.', 'error') return redirect(url_for('admin_panel')) initialize_user_filesystem_tma(user_data, tma_user_id_str) current_folder_id = request.args.get('folder_id', 'root') current_folder, _ = find_node_by_id(user_data['filesystem'], current_folder_id) if not current_folder or current_folder.get('type') != 'folder': flash('Folder not found!', 'error') current_folder_id = 'root' current_folder, _ = find_node_by_id(user_data['filesystem'], 'root') items_in_folder = [item for item in current_folder.get('children', []) if not item.get('is_archived')] items_in_folder = sorted(items_in_folder, key=lambda x: (x['type'] not in ['folder', 'note', 'todolist', 'shoppinglist'], x.get('name', x.get('original_filename', x.get('title', ''))).lower())) breadcrumbs = [] temp_id = current_folder_id while temp_id: node, parent_node_bc = find_node_by_id(user_data['filesystem'], temp_id) if not node: break is_link = (node['id'] != current_folder_id) breadcrumbs.append({'id': node['id'], 'name': node.get('name', 'Root'), 'is_link': is_link}) if not parent_node_bc: break temp_id = parent_node_bc.get('id') breadcrumbs.reverse() return render_template_string(ADMIN_USER_FILES_HTML, user_id=tma_user_id_str, user=user_data, items=items_in_folder, current_folder_id=current_folder_id, current_folder=current_folder, breadcrumbs=breadcrumbs, hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(path)}{'?download=true' if download else ''}") @app.route('/admhosto/user//reminders') @admin_browser_login_required def admin_user_reminders(tma_user_id_str): data = load_data() user_data = data['users'].get(tma_user_id_str) if not user_data: flash('User not found.', 'error') return redirect(url_for('admin_panel')) reminders = sorted(user_data.get('reminders', []), key=lambda r: r.get('due_datetime_utc', ''), reverse=True) return render_template_string(ADMIN_USER_REMINDERS_HTML, user_id=tma_user_id_str, user=user_data, reminders=reminders) @app.route('/admhosto/download//') @admin_browser_login_required def admin_download_file(tma_user_id_str, file_id): file_node = get_file_node_for_admin(tma_user_id_str, file_id) if not file_node: return jsonify({'status': 'error', 'message': 'File not found or access denied!'}), 404 token = uuid.uuid4().hex cache.set(f"download_token_{token}", file_node, timeout=300) public_url = url_for('public_download', token=token, _external=True) return jsonify({'status': 'success', 'url': public_url}) @app.route('/admhosto/text//') @admin_browser_login_required def admin_get_text_content(tma_user_id_str, file_id): file_node = get_file_node_for_admin(tma_user_id_str, file_id) if not file_node or file_node.get('file_type') != 'text': return Response("Text file not found", 404) hf_path = file_node.get('path') if not hf_path: return Response("Error: file path is missing", 500) file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(hf_path)}?download=true" try: req_headers = {} if HF_TOKEN_READ: req_headers["authorization"] = f"Bearer {HF_TOKEN_READ}" response = requests.get(file_url, headers=req_headers) response.raise_for_status() if len(response.content) > 1 * 1024 * 1024: return Response("File too large for preview.", 413) try: text_content = response.content.decode('utf-8') except UnicodeDecodeError: text_content = response.content.decode('latin-1', errors='ignore') return Response(text_content, mimetype='text/plain') except Exception as e: return Response(f"Download error: {e}", 502) @app.route('/admhosto/item//') @admin_browser_login_required def admin_get_item(tma_user_id_str, item_id): data = load_data() user_data = data['users'].get(tma_user_id_str) if not user_data: return jsonify({'status': 'error', 'message': 'User not found'}), 404 node, _ = find_node_by_id(user_data['filesystem'], item_id) if not node: return jsonify({'status': 'error', 'message': 'Item not found'}), 404 return jsonify({'status': 'success', 'item': node}) @app.route('/admhosto/delete_item//', methods=['POST']) @admin_browser_login_required def admin_delete_item(tma_user_id_str, item_id): data = load_data() user_data = data['users'].get(tma_user_id_str) current_folder_id = request.form.get('current_folder_id', 'root') if not user_data: flash('User not found.', 'error'); return redirect(url_for('admin_panel')) node, _ = find_node_by_id(user_data['filesystem'], item_id) if not node: flash('Item not found.', 'error') else: node_type = node.get('type') if node_type == 'file': hf_path = node.get('path') if not HF_TOKEN_WRITE: flash('Deletion not possible: write token not configured.', 'error') else: try: api = HfApi() if hf_path: api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) except hf_utils.EntryNotFoundError: pass except Exception as e: flash(f'Deletion error from remote storage: {e}', 'error') return redirect(url_for('admin_user_files', tma_user_id_str=tma_user_id_str, folder_id=current_folder_id)) elif node_type == 'folder' and node.get('children'): flash('Folder is not empty.', 'error') return redirect(url_for('admin_user_files', tma_user_id_str=tma_user_id_str, folder_id=current_folder_id)) if remove_node(user_data['filesystem'], item_id)[0]: try: save_data(data) flash(f'{node_type.capitalize()} deleted.') except Exception as e: flash(f'DB update failed after deletion: {e}', 'error') else: flash('Failed to remove item from filesystem.', 'error') return redirect(url_for('admin_user_files', tma_user_id_str=tma_user_id_str, folder_id=current_folder_id)) @app.route('/admhosto/user//delete_reminder/', methods=['POST']) @admin_browser_login_required def admin_delete_reminder(tma_user_id_str, reminder_id): data = load_data() user_data = data['users'].get(tma_user_id_str) if not user_data: flash('User not found.', 'error'); return redirect(url_for('admin_panel')) reminders = user_data.get('reminders', []) initial_len = len(reminders) user_data['reminders'] = [r for r in reminders if r.get('id') != reminder_id] if len(user_data['reminders']) < initial_len: try: save_data(data) flash('Reminder deleted successfully.', 'success') except Exception as e: flash(f'Failed to save data: {e}', 'error') else: flash('Reminder not found.', 'error') return redirect(url_for('admin_user_reminders', tma_user_id_str=tma_user_id_str)) if __name__ == '__main__': if not HF_TOKEN_WRITE: logging.warning("HF_TOKEN (write) is not set. Uploads/deletions will fail.") if not HF_TOKEN_READ: logging.warning("HF_TOKEN_READ is not set. Downloads/previews might fail.") if ADMIN_TELEGRAM_ID == "YOUR_ADMIN_TELEGRAM_USER_ID_HERE": logging.warning("ADMIN_TELEGRAM_ID is not set.") if ADMIN_USERNAME == "admin" and ADMIN_PASSWORD == "zeusadminpass": logging.warning("Using default admin credentials. Please change them.") if HF_TOKEN_WRITE or HF_TOKEN_READ: download_db_from_hf() else: if not os.path.exists(DATA_FILE): with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}, 'shared_links': {}, 'business_pages': {}, 'orders': {}}, f) if HF_TOKEN_WRITE: threading.Thread(target=periodic_backup, daemon=True).start() threading.Thread(target=check_reminders, daemon=True).start() app.run(debug=False, host='0.0.0.0', port=7860)