diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,12 +1,13 @@ +# --- START OF FILE app.py --- import os import hmac import hashlib import json -from urllib.parse import unquote, parse_qsl, quote -from flask import Flask, request, jsonify, Response, send_file +from urllib.parse import unquote, parse_qsl +from flask import Flask, request, jsonify, Response, session, send_file +import time import logging import threading -import time from datetime import datetime from huggingface_hub import HfApi, hf_hub_download, utils as hf_utils from werkzeug.utils import secure_filename @@ -15,84 +16,91 @@ from io import BytesIO import uuid # --- Configuration --- -# VITAL: Set these environment variables or replace placeholders -BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN', 'YOUR_BOT_TOKEN') # Your Telegram Bot Token for validation -HF_TOKEN_WRITE = os.environ.get('HF_TOKEN') # Your Hugging Face WRITE token -HF_TOKEN_READ = os.environ.get('HF_TOKEN_READ') or HF_TOKEN_WRITE # Your Hugging Face READ token (falls back to WRITE) -REPO_ID = os.environ.get('HF_REP' , 'Eluza133/Z1e1u') # e.g., "Eluza133/Z1e1u" -FLASK_SECRET_KEY = os.getenv("FLASK_SECRET_KEY", "a_very_secret_key_for_flask") # For potential future session use, though limited here - -HOST = '0.0.0.0' -PORT = 7860 -DATA_FILE = 'cloudeng_mini_app_data.json' # Changed filename to avoid conflicts -UPLOAD_FOLDER = 'mini_app_uploads' +BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN', 'YOUR_BOT_TOKEN') # ВАЖНО: Замените или используйте env! +HF_TOKEN_WRITE = os.environ.get("HF_TOKEN") +HF_TOKEN_READ = os.environ.get("HF_TOKEN_READ") or HF_TOKEN_WRITE +FLASK_SECRET_KEY = os.environ.get("FLASK_SECRET_KEY", "supersecretkey_mini_app_unique") +REPO_ID = "Eluza133/Z1e1u" # Ваш репозиторий HF +DATA_FILE = 'cloudeng_telegram_data.json' # Файл данных для TG версии +UPLOAD_FOLDER = 'uploads_tg' os.makedirs(UPLOAD_FOLDER, exist_ok=True) +AUTH_DATA_LIFETIME = 3600 # Время жизни initData (1 час) # --- Flask App Initialization --- app = Flask(__name__) app.secret_key = FLASK_SECRET_KEY -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') - -# --- Telegram Validation Logic --- -AUTH_DATA_LIFETIME = 3600 # 1 hour validity for initData - -def check_telegram_authorization(auth_data: str, bot_token: str) -> dict | None: - if not auth_data: return None - try: - parsed_data = dict(parse_qsl(unquote(auth_data))) - if "hash" not in parsed_data: return None - telegram_hash = parsed_data.pop('hash') - auth_date_ts = int(parsed_data.get('auth_date', 0)) - if time.time() - auth_date_ts > AUTH_DATA_LIFETIME: return None # Expired - data_check_string = "\n".join(sorted([f"{k}={v}" for k, v in parsed_data.items()])) - secret_key = hmac.new("WebAppData".encode(), bot_token.encode(), hashlib.sha256).digest() - calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest() - if calculated_hash == telegram_hash: - user_data = parsed_data.get('user') - if user_data: return json.loads(user_data) - return {} # Valid but no user data? Return empty dict - return None # Hash mismatch - except Exception as e: - logging.error(f"Telegram validation error: {e}") - return None +logging.basicConfig(level=logging.INFO) -# --- Filesystem Logic --- +# --- Helper Functions --- def find_node_by_id(filesystem, node_id): - if not filesystem: return None, None - if filesystem.get('id') == node_id: return filesystem, None + if not filesystem or not isinstance(filesystem, dict): + return None, None + if filesystem.get('id') == node_id: + return filesystem, None queue = [(filesystem, None)] while queue: current_node, parent = queue.pop(0) - if current_node.get('type') == 'folder' and 'children' in current_node: + if current_node.get('type') == 'folder' and 'children' in current_node and isinstance(current_node['children'], list): for child in current_node['children']: - if child.get('id') == node_id: return child, current_node - if child.get('type') == 'folder': queue.append((child, current_node)) + if isinstance(child, dict): # Добавлена проверка типа + if child.get('id') == node_id: + return child, current_node + if 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'] = [] + if 'children' not in parent_node or not isinstance(parent_node['children'], list): + 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 child.get('id') != node_id] + if node_to_remove and parent_node and 'children' in parent_node and isinstance(parent_node['children'], list): + parent_node['children'] = [child for child in parent_node['children'] if not isinstance(child, dict) or child.get('id') != node_id] return True - elif node_to_remove and not parent_node: # Trying to remove root? Disallow. - return False - return False # Node not found or parent invalid - -def initialize_user_filesystem(user_id_str): - return { - "type": "folder", - "id": "root", - "name": "root", - "children": [] - } + return False + +def get_node_path_string(filesystem, node_id): + path_list = [] + current_id = node_id + visited = set() # Защита от циклов + + while current_id and current_id not in visited: + visited.add(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 initialize_user_filesystem(user_data): + if 'filesystem' not in user_data or not isinstance(user_data.get('filesystem'), dict): + user_data['filesystem'] = { + "type": "folder", + "id": "root", + "name": "root", + "children": [] + } + +def get_file_type(filename): + filename_lower = filename.lower() + if filename_lower.endswith(('.mp4', '.mov', '.avi', '.webm', '.mkv')): return 'video' + if filename_lower.endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg')): return 'image' + if filename_lower.endswith('.pdf'): return 'pdf' + if filename_lower.endswith('.txt'): return 'text' + if filename_lower.endswith(('.doc', '.docx')): return 'doc' + if filename_lower.endswith(('.xls', '.xlsx')): return 'xls' + if filename_lower.endswith(('.ppt', '.pptx')): return 'ppt' + if filename_lower.endswith(('.zip', '.rar', '.7z', '.tar', '.gz')): return 'archive' + if filename_lower.endswith(('.mp3', '.wav', '.ogg', '.aac', '.flac')): return 'audio' + return 'other' # --- Data Persistence --- data_lock = threading.Lock() @@ -101,229 +109,366 @@ def load_data(): with data_lock: try: download_db_from_hf() - if not os.path.exists(DATA_FILE): - logging.warning(f"{DATA_FILE} not found locally after potential download attempt. Initializing empty.") - return {'users': {}} - with open(DATA_FILE, 'r', encoding='utf-8') as file: - data = json.load(file) - if not isinstance(data, dict): return {'users': {}} - data.setdefault('users', {}) - # No filesystem initialization here, do it on first access if needed - return data + if os.path.exists(DATA_FILE) and os.path.getsize(DATA_FILE) > 0: + with open(DATA_FILE, 'r', encoding='utf-8') as file: + data = json.load(file) + if not isinstance(data, dict): + logging.warning(f"{DATA_FILE} is not a dict, initializing.") + data = {'users': {}} + else: + data = {'users': {}} + + data.setdefault('users', {}) + # Ensure all users have initialized filesystem + for user_id, user_data in data['users'].items(): + if isinstance(user_data, dict): # Check if user_data is a dict + initialize_user_filesystem(user_data) + else: + logging.warning(f"Invalid data format for user {user_id}, re-initializing.") + data['users'][user_id] = {} # Initialize as empty dict or default structure + initialize_user_filesystem(data['users'][user_id]) + + + logging.info("Data loaded/initialized") + return data + except json.JSONDecodeError: + logging.error(f"Error decoding JSON from {DATA_FILE}. Returning empty data.") + return {'users': {}} except Exception as e: logging.error(f"Error loading data: {e}") - return {'users': {}} # Return default structure on error + return {'users': {}} def save_data(data): - with data_lock: + with data_lock: try: - # Ensure user data integrity before saving + # Ensure filesystem structure is valid before saving for user_id, user_data in data.get('users', {}).items(): - if 'filesystem' not in user_data or not isinstance(user_data['filesystem'], dict): - logging.warning(f"Filesystem missing or invalid for user {user_id}. Reinitializing.") - user_data['filesystem'] = initialize_user_filesystem(user_id) - if 'user_info' not in user_data or not isinstance(user_data['user_info'], dict): - logging.warning(f"User info missing for user {user_id}.") - # Optionally add placeholder if needed: user_data['user_info'] = {'id': user_id} + if isinstance(user_data, dict): + initialize_user_filesystem(user_data) # Ensures filesystem exists and is a dict with open(DATA_FILE, 'w', encoding='utf-8') as file: json.dump(data, file, ensure_ascii=False, indent=4) upload_db_to_hf() - logging.info("Data saved and upload initiated.") + logging.info("Data saved and uploaded to HF") except Exception as e: logging.error(f"Error saving data: {e}") - # Optionally raise e to signal failure upstream + # Optionally re-raise or handle appropriately + # raise -# --- Hugging Face Integration --- def upload_db_to_hf(): - if not HF_TOKEN_WRITE or not REPO_ID: - logging.warning("HF_TOKEN_WRITE or REPO_ID not set, skipping database upload.") + if not HF_TOKEN_WRITE: + logging.warning("HF_TOKEN_WRITE not set, skipping database upload.") + return + if not os.path.exists(DATA_FILE): + logging.warning(f"{DATA_FILE} not found, skipping upload.") 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')}" + 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 TG App {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" ) - logging.info(f"Database {DATA_FILE} uploaded to HF dataset {REPO_ID}") + logging.info("Database uploaded to Hugging Face") except Exception as e: - logging.error(f"Error uploading database to HF: {e}") + logging.error(f"Error uploading database: {e}") def download_db_from_hf(): - if not HF_TOKEN_READ or not REPO_ID: - logging.warning("HF_TOKEN_READ or REPO_ID not set, skipping database download.") + if not HF_TOKEN_READ: + logging.warning("HF_TOKEN_READ not set, skipping database download.") + if not os.path.exists(DATA_FILE): + with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, 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, resume_download=True + local_dir=".", local_dir_use_symlinks=False, etag_timeout=60 # Increased timeout ) - logging.info(f"Database {DATA_FILE} downloaded from HF dataset {REPO_ID}") - return True + logging.info("Database downloaded from Hugging Face") except hf_utils.EntryNotFoundError: - logging.warning(f"{DATA_FILE} not found in HF repo {REPO_ID}. Will use/create local.") - return False + logging.warning(f"{DATA_FILE} not found in repo. Initializing empty DB locally.") + if not os.path.exists(DATA_FILE): + with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f) except Exception as e: - logging.error(f"Error downloading database from HF: {e}") - return False + 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': {}}, f) def periodic_backup(): while True: time.sleep(1800) # Backup every 30 minutes - logging.info("Initiating periodic data backup.") - all_data = load_data() # Load current state - save_data(all_data) # Save and upload + logging.info("Starting periodic backup...") + data = load_data() # Load current data before saving + save_data(data) -def get_file_type(filename): - filename_lower = filename.lower() - if filename_lower.endswith(('.mp4', '.mov', '.avi', '.webm', '.mkv')): return 'video' - if filename_lower.endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg')): return 'image' - if filename_lower.endswith('.pdf'): return 'pdf' - if filename_lower.endswith(('.txt', '.log', '.md', '.py', '.js', '.css', '.html', '.json', '.xml')): return 'text' - if filename_lower.endswith(('.mp3', '.wav', '.ogg', '.aac', '.flac')): return 'audio' - return 'other' +# --- Telegram Validation --- +def check_telegram_authorization(auth_data: str, bot_token: str) -> dict | None: + if not auth_data: return None + try: + parsed_data = dict(parse_qsl(unquote(auth_data))) + if "hash" not in parsed_data: return None + + telegram_hash = parsed_data.pop('hash') + auth_date_ts = int(parsed_data.get('auth_date', 0)) + current_ts = int(time.time()) + if current_ts - auth_date_ts > AUTH_DATA_LIFETIME: + logging.warning(f"Auth data expired: {current_ts - auth_date_ts} seconds old.") + return None -# --- HTML, CSS, JS Template --- + data_check_string = "\n".join(sorted([f"{k}={v}" for k, v in parsed_data.items()])) + secret_key = hmac.new("WebAppData".encode(), bot_token.encode(), hashlib.sha256).digest() + calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest() + + if calculated_hash == telegram_hash: + user_data_str = parsed_data.get('user') + if user_data_str: + try: + return json.loads(user_data_str) + except json.JSONDecodeError: + logging.error("Failed to decode user JSON from initData") + return None + return {} # Valid hash, but no user field? Return empty dict + else: + logging.warning("Hash mismatch during validation.") + return None + except Exception as e: + logging.error(f"Error during Telegram validation: {e}") + return None + +# --- HTML Template --- HTML_TEMPLATE = """ - Zeus Cloud Mini + Zeus Cloud +
-
-

Zeus Cloud

- -
- -
- - +
+

Загрузка данных...

-
- - - - + + -
-
-
- Загрузка... -
+