diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -2,9 +2,11 @@ import os import hmac import hashlib import json -from urllib.parse import unquote, parse_qsl -from flask import Flask, request, jsonify, Response, session, redirect, url_for, flash, render_template_string, send_file +from urllib.parse import unquote, parse_qsl, urlencode +from flask import Flask, request, jsonify, Response, send_file from flask_caching import Cache +import logging +import threading import time from datetime import datetime from huggingface_hub import HfApi, hf_hub_download, utils as hf_utils @@ -12,37 +14,282 @@ from werkzeug.utils import secure_filename import requests from io import BytesIO import uuid -import threading -import logging - -logging.basicConfig(level=logging.INFO) +# --- Configuration --- app = Flask(__name__) -app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_folders_unique_telegram") - -BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN', 'YOUR_BOT_TOKEN') -HOST = '0.0.0.0' -PORT = 7860 -AUTH_DATA_LIFETIME = 3600 -DATA_FILE = 'cloudeng_data_telegram.json' -REPO_ID = "Eluza133/Z1e1u" +app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_mini_app_unique") +BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN', 'YOUR_BOT_TOKEN') # MUST be set +DATA_FILE = 'cloudeng_mini_app_data.json' +REPO_ID = "Eluza133/Z1e1u" # Same HF Repo HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE -UPLOAD_FOLDER = 'uploads' +UPLOAD_FOLDER = 'uploads_mini_app' os.makedirs(UPLOAD_FOLDER, exist_ok=True) +# --- Caching and Logging --- cache = Cache(app, config={'CACHE_TYPE': 'simple'}) +logging.basicConfig(level=logging.INFO) + +# --- Constants --- +AUTH_DATA_LIFETIME = 3600 # 1 hour validity for initData + +# --- Filesystem Utilities --- +def find_node_by_id(filesystem, node_id): + if not filesystem or not isinstance(filesystem, dict): + return None, None + if filesystem.get('id') == node_id: + return filesystem, None + + queue = [(filesystem, None)] + visited = {filesystem.get('id')} + + while queue: + current_node, parent = queue.pop(0) + if current_node.get('type') == 'folder' and 'children' in current_node: + for child in current_node.get('children', []): + child_id = child.get('id') + if not child_id: continue # Skip nodes without id + + if child_id == node_id: + return child, current_node + if child_id not in visited and child.get('type') == 'folder': + visited.add(child_id) + 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'] = [] + # Prevent adding duplicates by id + existing_ids = {child.get('id') for child in parent_node['children']} + if node_data.get('id') not in existing_ids: + 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: + original_length = len(parent_node['children']) + parent_node['children'] = [child for child in parent_node['children'] if child.get('id') != node_id] + return len(parent_node['children']) < original_length # Return True if something was removed + # Handle root node deletion attempt (should not happen normally) + if node_to_remove and node_id == filesystem.get('id'): + logging.warning("Attempted to remove root node directly.") + return False + return False + +def get_node_path_list(filesystem, node_id): + path_list = [] + current_id = node_id + + processed_ids = set() + + while current_id and current_id not in processed_ids: + processed_ids.add(current_id) + node, parent = find_node_by_id(filesystem, current_id) + if not node: + break + path_list.append({ + 'id': node.get('id'), + 'name': node.get('name', node.get('original_filename', 'Unknown')) + }) + if not parent: + break + parent_id = parent.get('id') + if parent_id == current_id: # Prevent infinite loop if parent is self + logging.error(f"Filesystem loop detected at node {current_id}") + break + current_id = parent_id + + # Ensure root is always first if found, otherwise add default root + if not any(p['id'] == 'root' for p in path_list): + path_list.append({'id': 'root', 'name': 'Root'}) + + # Filter out potential duplicates while preserving order, then reverse + final_path = [] + seen_ids = set() + for item in reversed(path_list): + if item['id'] not in seen_ids: + final_path.append(item) + seen_ids.add(item['id']) + + return final_path + + +def initialize_user_filesystem(user_data): + if 'filesystem' not in user_data or not isinstance(user_data['filesystem'], dict): + user_data['filesystem'] = { + "type": "folder", + "id": "root", + "name": "Root", + "children": [] + } + +# --- Data Loading/Saving --- +@cache.memoize(timeout=120) # Cache for 2 minutes +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): + logging.warning("Data file is not a dict, initializing empty.") + return {'users': {}} + data.setdefault('users', {}) + # Ensure all users have a valid filesystem structure + for user_id, user_data in data['users'].items(): + initialize_user_filesystem(user_data) + logging.info("Data loaded and filesystems checked/initialized.") + return data + except FileNotFoundError: + logging.warning(f"{DATA_FILE} not found locally. Initializing empty data.") + return {'users': {}} + 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': {}} + +def save_data(data): + try: + with open(DATA_FILE, 'w', encoding='utf-8') as file: + json.dump(data, file, ensure_ascii=False, indent=4) + # Upload immediately after saving + upload_db_to_hf() + cache.clear() # Clear cache after saving + logging.info("Data saved locally and upload to HF initiated.") + except Exception as e: + logging.error(f"Error saving data: {e}") + # Consider not raising here to potentially allow app to continue + # raise + +def upload_db_to_hf(): + if not HF_TOKEN_WRITE: + logging.warning("HF_TOKEN_WRITE not set, skipping database 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 MiniApp {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", + run_as_future=True # Upload in background + ) + logging.info("Database upload to Hugging Face scheduled.") + except Exception as e: + logging.error(f"Error scheduling database upload: {e}") + +def download_db_from_hf(): + 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) + logging.info(f"Created empty local database file: {DATA_FILE}") + 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, + force_download=True, # Ensure we get the latest + etag_timeout=10 # Short timeout for checking freshness + ) + logging.info("Database downloaded from Hugging Face") + except hf_utils.RepositoryNotFoundError: + logging.error(f"Repository {REPO_ID} not found.") + if not os.path.exists(DATA_FILE): + with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f) + except hf_utils.EntryNotFoundError: + logging.warning(f"{DATA_FILE} not found in repo {REPO_ID}. Using/Creating local.") + if not os.path.exists(DATA_FILE): + with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f) + except requests.exceptions.ConnectionError as e: + logging.error(f"Connection error downloading DB from HF: {e}. Using local version if available.") + 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': {}}, f) + +# --- File Type Helper --- +def get_file_type(filename): + if not filename or '.' not in filename: return 'other' + ext = filename.lower().split('.')[-1] + if ext in ['mp4', 'mov', 'avi', 'webm', 'mkv']: return 'video' + if ext in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']: return 'image' + if ext == 'pdf': return 'pdf' + if ext == 'txt': return 'text' + return 'other' + +# --- Telegram Validation --- +def check_telegram_authorization(auth_data: str, bot_token: str) -> dict | None: + if not auth_data or not bot_token or bot_token == 'YOUR_BOT_TOKEN': + logging.warning("Validation skipped: Missing auth_data or valid BOT_TOKEN.") + return None + try: + parsed_data = dict(parse_qsl(unquote(auth_data))) + if "hash" not in parsed_data: + logging.error("Hash not found in auth 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 abs(current_ts - auth_date_ts) > AUTH_DATA_LIFETIME: + logging.warning(f"Auth data expired (Auth: {auth_date_ts}, Now: {current_ts}, Diff: {current_ts - auth_date_ts})") + return None + + 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: + user_info = json.loads(user_data_str) + if 'id' not in user_info: + logging.error("Validated user data missing 'id'") + return None + return user_info # Success + except json.JSONDecodeError: + logging.error("Failed to decode user JSON from auth data") + return None + else: + logging.warning("No 'user' field in validated auth data") + return None # Require user field + else: + logging.warning("Hash mismatch during validation") + return None + except Exception as e: + logging.error(f"Exception during validation: {e}") + return None + +# --- HTML, CSS, JS Template --- HTML_TEMPLATE = """ - Zeus Cloud TG + Zeus Cloud Mini App + +
Загрузка и проверка данных Telegram...
+ +
+

Zeus Cloud

+
+
-
Загрузка данных Telegram...
- - -
-
- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} -
{{ message }}
- {% endfor %} - {% endif %} - {% endwith %} -
- -
-

Ваш профиль

-

Загрузка...

-
- - +
-
- - - -
+ +
-
- +
0%
- -

Содержимое папки: Загрузка...

-
-

Загрузка...

+

Содержимое папки

+
+
-
-