diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,3 +1,6 @@ +import flask +from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify, Response +from flask_caching import Cache import json import os import logging @@ -11,29 +14,36 @@ from io import BytesIO import uuid import hashlib import hmac -from urllib.parse import unquote - -from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify, Response -from flask_caching import Cache +from telegram import Update, WebAppInfo, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters app = Flask(__name__) -app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_folders_unique_telegram_mini_app") # Changed for new app -DATA_FILE = 'cloudeng_data_tg.json' # Changed data file name -REPO_ID = os.getenv("HF_REPO_ID", "Eluza133/Z1e1u") # Ensure this is set or use a default +app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_folders_telegram_unique") +DATA_FILE = 'cloudeng_data_telegram.json' +REPO_ID = "Eluza133/Z1e1u" # Replace with your actual repo ID if different HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE -UPLOAD_FOLDER = 'uploads_tg' # Changed upload folder +UPLOAD_FOLDER = 'uploads_telegram' os.makedirs(UPLOAD_FOLDER, exist_ok=True) -BOT_TOKEN = os.getenv("BOT_TOKEN", "6750208873:AAE2hvPlJ99dBdhGa_Brre0IIpUdOvXxHt4") -ADMIN_TELEGRAM_USER_ID = os.getenv("ADMIN_TELEGRAM_USER_ID") +BOT_TOKEN = "6750208873:AAE2hvPlJ99dBdhGa_Brre0IIpUdOvXxHt4" +ADMIN_TELEGRAM_IDS_STR = os.getenv("ADMIN_TELEGRAM_IDS", "") +ADMIN_TELEGRAM_IDS = [] +if ADMIN_TELEGRAM_IDS_STR: + try: + ADMIN_TELEGRAM_IDS = [int(id_str.strip()) for id_str in ADMIN_TELEGRAM_IDS_STR.split(',')] + except ValueError: + logging.error(f"Invalid ADMIN_TELEGRAM_IDS: {ADMIN_TELEGRAM_IDS_STR}. Must be comma-separated integers.") + ADMIN_TELEGRAM_IDS = [] + +if not ADMIN_TELEGRAM_IDS: + logging.warning("ADMIN_TELEGRAM_IDS is not set or is invalid. Admin panel will not be accessible to specific users.") cache = Cache(app, config={'CACHE_TYPE': 'simple'}) logging.basicConfig(level=logging.INFO) -# --- Helper Functions (largely unchanged, but context might change) --- def find_node_by_id(filesystem, node_id): if not filesystem: return None, None if filesystem.get('id') == node_id: @@ -77,15 +87,27 @@ def get_node_path_string(filesystem, node_id): current_id = parent.get('id') if parent else None return " / ".join(reversed(path_list)) or "Root" -def initialize_user_filesystem(user_data): # user_data is data['users'][tg_user_id_str] +def initialize_user_filesystem(user_data, user_telegram_id_str): if 'filesystem' not in user_data: user_data['filesystem'] = { - "type": "folder", - "id": "root", - "name": "root", - "children": [] + "type": "folder", "id": "root", "name": "root", "children": [] } - # Removed old file migration logic, assuming new users or already structured data for TG users + 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/{user_telegram_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'] @cache.memoize(timeout=300) def load_data(): @@ -97,11 +119,9 @@ def load_data(): logging.warning("Data is not in dict format, initializing empty database") return {'users': {}} data.setdefault('users', {}) - # Filesystem initialization now happens upon user login/first access if needed - # or when admin views a user that hasn't logged in yet. - for user_id_str, user_data_val in data['users'].items(): - initialize_user_filesystem(user_data_val) # Ensure all loaded users have fs structure - logging.info("Data successfully loaded and initialized") + for user_tg_id_str, user_data_val in data['users'].items(): + initialize_user_filesystem(user_data_val, user_tg_id_str) + logging.info("Data successfully loaded and initialized for Telegram users") return data except Exception as e: logging.error(f"Error loading data: {e}") @@ -113,7 +133,7 @@ def save_data(data): json.dump(data, file, ensure_ascii=False, indent=4) upload_db_to_hf() cache.clear() - logging.info("Data saved and uploaded to HF") + logging.info("Data saved and uploaded to HF for Telegram users") except Exception as e: logging.error(f"Error saving data: {e}") raise @@ -127,7 +147,7 @@ def upload_db_to_hf(): 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"TGMA Backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + commit_message=f"Backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" ) logging.info("Database uploaded to Hugging Face") except Exception as e: @@ -145,8 +165,12 @@ def download_db_from_hf(): token=HF_TOKEN_READ, local_dir=".", local_dir_use_symlinks=False ) logging.info("Database downloaded from Hugging Face") - except (hf_utils.RepositoryNotFoundError, hf_utils.EntryNotFoundError): - logging.warning(f"DB not found in repo {REPO_ID}. Initializing empty database.") + 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 repository {REPO_ID}. Initializing empty database.") 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: @@ -156,9 +180,8 @@ def download_db_from_hf(): def periodic_backup(): while True: - time.sleep(1800) # Backup every 30 minutes upload_db_to_hf() - + time.sleep(1800) def get_file_type(filename): filename_lower = filename.lower() @@ -168,106 +191,6 @@ def get_file_type(filename): elif filename_lower.endswith('.txt'): return 'text' return 'other' -# --- Telegram Auth --- -def check_telegram_authorization(auth_data_dict): - if not BOT_TOKEN: - logging.error("BOT_TOKEN is not set. Cannot verify Telegram authorization.") - return None - - check_hash = auth_data_dict.pop('hash', None) - if not check_hash: return None - - data_check_arr = [] - for key, value in sorted(auth_data_dict.items()): - data_check_arr.append(f"{key}={value}") - data_check_string = "\n".join(data_check_arr) - - secret_key = hashlib.sha256(BOT_TOKEN.encode()).digest() - calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest() - - if calculated_hash == check_hash: - try: - user_data = json.loads(auth_data_dict['user']) - return user_data - except (KeyError, json.JSONDecodeError) as e: - logging.error(f"Error parsing user data from Telegram auth: {e}") - return None - return None - -# --- Admin Check --- -def is_admin(): - if not ADMIN_TELEGRAM_USER_ID: return False - return 'telegram_user_id' in session and str(session['telegram_user_id']) == ADMIN_TELEGRAM_USER_ID - -# --- HTML Shell for Mini App --- -MINI_APP_SHELL_HTML = """ - - - - - - Zeus Cloud - - - - -
-

Загрузка Zeus Cloud...

-
-
- - - -""" - -# --- Base Style (remains largely the same) --- BASE_STYLE = ''' :root { --primary: #ff4d6d; --secondary: #00ddeb; --accent: #8b5cf6; @@ -278,148 +201,226 @@ BASE_STYLE = ''' --folder-color: #ffc107; } * { margin: 0; padding: 0; box-sizing: border-box; } -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - background: var(--tg-theme-bg-color, var(--background-light)); - color: var(--tg-theme-text-color, var(--text-light)); - line-height: 1.6; - padding-bottom: 70px; /* Space for potential MainButton */ -} -/* body.dark specific styles are less relevant as Telegram handles theme */ -.container { margin: 10px auto; max-width: 1200px; padding: 15px; background: var(--tg-theme-secondary-bg-color, var(--card-bg)); border-radius: 12px; box-shadow: var(--shadow); overflow-x: hidden; } -h1 { font-size: 1.8em; font-weight: 800; text-align: center; margin-bottom: 20px; background: linear-gradient(135deg, var(--primary), var(--accent)); -webkit-background-clip: text; color: transparent; } -h2 { font-size: 1.4em; margin-top: 25px; color: var(--tg-theme-text-color, var(--text-light)); } -h4 { font-size: 1.0em; margin-top: 12px; margin-bottom: 4px; color: var(--tg-theme-link-color, var(--accent)); } -ol, ul { margin-left: 20px; margin-bottom: 12px; } -li { margin-bottom: 4px; } -input, textarea { width: 100%; padding: 12px; margin: 10px 0; border: 1px solid var(--tg-theme-hint-color, #ccc); border-radius: 10px; background: var(--tg-theme-bg-color, var(--glass-bg)); color: var(--tg-theme-text-color, var(--text-light)); font-size: 1em; } -input:focus, textarea:focus { outline: none; border-color: var(--tg-theme-link-color, var(--primary)); box-shadow: 0 0 0 2px var(--tg-theme-link-color, var(--primary)); } -.btn { padding: 12px 24px; background: var(--tg-theme-button-color, var(--primary)); color: var(--tg-theme-button-text-color, white); border: none; border-radius: 10px; cursor: pointer; font-size: 1em; font-weight: 600; transition: var(--transition); box-shadow: var(--shadow); display: inline-block; text-decoration: none; margin-top: 5px; margin-right: 5px; } -.btn:hover { opacity: 0.9; transform: scale(1.03); } +body { font-family: 'Inter', sans-serif; background: var(--background-light); color: var(--text-light); line-height: 1.6; } +body.dark { background: var(--background-dark); color: var(--text-dark); } +.container { margin: 20px auto; max-width: 1200px; padding: 25px; background: var(--card-bg); border-radius: 20px; box-shadow: var(--shadow); overflow-x: hidden; } +body.dark .container { background: var(--card-bg-dark); } +h1 { font-size: 2em; font-weight: 800; text-align: center; margin-bottom: 25px; background: linear-gradient(135deg, var(--primary), var(--accent)); -webkit-background-clip: text; color: transparent; } +h2 { font-size: 1.5em; margin-top: 30px; color: var(--text-light); } +body.dark h2 { color: var(--text-dark); } +h4 { font-size: 1.1em; margin-top: 15px; margin-bottom: 5px; color: var(--accent); } +ol, ul { margin-left: 20px; margin-bottom: 15px; } +li { margin-bottom: 5px; } +input, textarea { width: 100%; padding: 14px; margin: 12px 0; border: none; border-radius: 14px; background: var(--glass-bg); color: var(--text-light); font-size: 1.1em; box-shadow: inset 0 3px 10px rgba(0, 0, 0, 0.1); } +body.dark input, body.dark textarea { color: var(--text-dark); } +input:focus, textarea:focus { outline: none; box-shadow: 0 0 0 4px var(--primary); } +.btn { padding: 14px 28px; background: var(--primary); color: white; border: none; border-radius: 14px; cursor: pointer; font-size: 1.1em; font-weight: 600; transition: var(--transition); box-shadow: var(--shadow); display: inline-block; text-decoration: none; margin-top: 5px; margin-right: 5px; } +.btn:hover { transform: scale(1.05); background: #e6415f; } .download-btn { background: var(--secondary); } .download-btn:hover { background: #00b8c5; } .delete-btn { background: var(--delete-color); } .delete-btn:hover { background: #cc3333; } .folder-btn { background: var(--folder-color); } .folder-btn:hover { background: #e6a000; } -.flash { color: var(--tg-theme-link-color, var(--secondary)); text-align: center; margin-bottom: 15px; padding: 10px; background: rgba(0, 221, 235, 0.1); border-radius: 10px; } -.flash.error { color: var(--tg-theme-destructive-text-color, var(--delete-color)); background: rgba(255, 68, 68, 0.1); } -.file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; margin-top: 15px; } -.user-list { margin-top: 15px; } -.user-item { padding: 12px; background: var(--tg-theme-secondary-bg-color, var(--card-bg)); border-radius: 12px; margin-bottom: 8px; box-shadow: var(--shadow); transition: var(--transition); } -.user-item:hover { transform: translateY(-3px); } -.user-item a { color: var(--tg-theme-link-color, var(--primary)); text-decoration: none; font-weight: 600; } -.item { background: var(--tg-theme-secondary-bg-color, var(--card-bg)); padding: 12px; border-radius: 12px; box-shadow: var(--shadow); text-align: center; transition: var(--transition); display: flex; flex-direction: column; justify-content: space-between; } -.item:hover { transform: translateY(-3px); } -.item-preview { max-width: 100%; height: 100px; object-fit: cover; border-radius: 8px; margin-bottom: 8px; cursor: pointer; display: block; margin-left: auto; margin-right: auto;} -.item.folder .item-preview { object-fit: contain; font-size: 50px; color: var(--tg-theme-link-color, var(--folder-color)); line-height: 100px; } -.item p { font-size: 0.85em; margin: 4px 0; word-break: break-all; } -.item-actions { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 4px; justify-content: center; } -.item-actions .btn { font-size: 0.8em; padding: 4px 8px; } +.flash { color: var(--secondary); text-align: center; margin-bottom: 15px; padding: 10px; background: rgba(0, 221, 235, 0.1); border-radius: 10px; } +.flash.error { color: var(--delete-color); background: rgba(255, 68, 68, 0.1); } +.file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; margin-top: 20px; } +.user-list { margin-top: 20px; } +.user-item { padding: 15px; background: var(--card-bg); border-radius: 16px; margin-bottom: 10px; box-shadow: var(--shadow); transition: var(--transition); } +body.dark .user-item { background: var(--card-bg-dark); } +.user-item:hover { transform: translateY(-5px); } +.user-item a { color: var(--primary); text-decoration: none; font-weight: 600; } +.user-item a:hover { color: var(--accent); } +.item { background: var(--card-bg); padding: 15px; border-radius: 16px; box-shadow: var(--shadow); text-align: center; transition: var(--transition); display: flex; flex-direction: column; justify-content: space-between; } +body.dark .item { background: var(--card-bg-dark); } +.item:hover { transform: translateY(-5px); } +.item-preview { max-width: 100%; height: 130px; object-fit: cover; border-radius: 10px; margin-bottom: 10px; cursor: pointer; display: block; margin-left: auto; margin-right: auto;} +.item.folder .item-preview { object-fit: contain; font-size: 60px; color: var(--folder-color); line-height: 130px; } +.item p { font-size: 0.9em; margin: 5px 0; word-break: break-all; } +.item a { color: var(--primary); text-decoration: none; } +.item a:hover { color: var(--accent); } +.item-actions { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 5px; justify-content: center; } +.item-actions .btn { font-size: 0.9em; padding: 5px 10px; } .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 { max-width: 95%; max-height: 95%; background: var(--tg-theme-secondary-bg-color, #fff); padding: 10px; border-radius: 15px; overflow: auto; position: relative; } +.modal-content { max-width: 95%; max-height: 95%; background: #fff; padding: 10px; border-radius: 15px; overflow: auto; position: relative; } +body.dark .modal-content { background: var(--card-bg-dark); } .modal img, .modal video, .modal iframe, .modal pre { max-width: 100%; max-height: 85vh; display: block; margin: auto; border-radius: 10px; } -.modal iframe { width: 90vw; height: 85vh; border: none; } /* Adjusted for TWA */ -.modal pre { background: var(--tg-theme-bg-color, #eee); color: var(--tg-theme-text-color, #333); padding: 15px; border-radius: 8px; white-space: pre-wrap; word-wrap: break-word; text-align: left; max-height: 85vh; overflow-y: auto;} -.modal-close-btn { position: absolute; top: 10px; right: 15px; font-size: 24px; color: var(--tg-theme-hint-color, #aaa); cursor: pointer; background: rgba(0,0,0,0.3); border-radius: 50%; width: 28px; height: 28px; line-height: 28px; text-align: center; } -#progress-container { width: 100%; background: var(--tg-theme-hint-color, var(--glass-bg)); border-radius: 10px; margin: 15px 0; display: none; position: relative; height: 20px; } -#progress-bar { width: 0%; height: 100%; background: var(--tg-theme-button-color, var(--primary)); border-radius: 10px; transition: width 0.3s ease; } -#progress-text { position: absolute; width: 100%; text-align: center; line-height: 20px; color: var(--tg-theme-button-text-color, white); font-size: 0.9em; font-weight: bold; text-shadow: 1px 1px 1px rgba(0,0,0,0.5); } -.breadcrumbs { margin-bottom: 15px; font-size: 1em; } -.breadcrumbs a { color: var(--tg-theme-link-color, var(--accent)); text-decoration: none; } +.modal iframe { width: 80vw; height: 85vh; border: none; } +.modal pre { background: #eee; color: #333; padding: 15px; border-radius: 8px; white-space: pre-wrap; word-wrap: break-word; text-align: left; max-height: 85vh; overflow-y: auto;} +body.dark .modal pre { background: #2b2a33; color: var(--text-dark); } +.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; } +body.dark .modal-close-btn { color: #555; background: rgba(255,255,255,0.2); } +#progress-container { width: 100%; background: var(--glass-bg); border-radius: 10px; margin: 15px 0; display: none; position: relative; height: 20px; } +#progress-bar { width: 0%; height: 100%; background: var(--primary); border-radius: 10px; transition: width 0.3s ease; } +#progress-text { position: absolute; width: 100%; text-align: center; line-height: 20px; color: white; font-size: 0.9em; font-weight: bold; text-shadow: 1px 1px 1px rgba(0,0,0,0.5); } +.breadcrumbs { margin-bottom: 20px; font-size: 1.1em; } +.breadcrumbs a { color: var(--accent); text-decoration: none; } .breadcrumbs a:hover { text-decoration: underline; } -.breadcrumbs span { margin: 0 5px; color: var(--tg-theme-hint-color, #aaa); } -.folder-actions { margin-top: 15px; margin-bottom: 8px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } +.breadcrumbs span { margin: 0 5px; color: #aaa; } +.folder-actions { margin-top: 20px; margin-bottom: 10px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; } .folder-actions input[type=text] { width: auto; flex-grow: 1; margin: 0; min-width: 150px; } .folder-actions .btn { margin: 0; flex-shrink: 0;} -@media (max-width: 768px) { /* These might need less adjustment for TWA */ - .file-grid { grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); } - .item-preview { height: 90px; } - .item.folder .item-preview { font-size: 40px; line-height: 90px; } +@media (max-width: 768px) { + .file-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); } + .folder-actions { flex-direction: column; align-items: stretch; } + .folder-actions input[type=text] { width: 100%; } + .item-preview { height: 100px; } + .item.folder .item-preview { font-size: 50px; line-height: 100px; } + h1 { font-size: 1.8em; } + .btn { padding: 12px 24px; font-size: 1em; } + .item-actions .btn { padding: 4px 8px; font-size: 0.8em;} } @media (max-width: 480px) { - .container { padding: 10px; } - .file-grid { grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 10px; } - .item-preview { height: 70px; } - .item.folder .item-preview { font-size: 35px; line-height: 70px; } + .container { padding: 15px; } + .file-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 15px; } + .item-preview { height: 80px; } + .item.folder .item-preview { font-size: 40px; line-height: 80px; } .item p { font-size: 0.8em;} - .breadcrumbs { font-size: 0.9em; } + .breadcrumbs { font-size: 1em; } + .btn { padding: 10px 20px; } } ''' -# --- Routes --- -@app.route('/') -def index_redirect(): - # Redirect to the Mini App entry point or an info page - return redirect(url_for('mini_app_entry')) +INITIAL_LOAD_HTML = ''' + + + + + + Zeus Cloud + + + + +
+

Аутентификация...

+
+ + + +''' -@app.route('/app') -def mini_app_entry(): - return render_template_string(MINI_APP_SHELL_HTML) +def is_telegram_admin(): + user_id = session.get('telegram_user_id') + return user_id in ADMIN_TELEGRAM_IDS -@app.route('/auth/telegram', methods=['POST']) -def auth_telegram(): +def get_current_user_display_name(): + if 'telegram_user_first_name' in session and session['telegram_user_first_name']: + return session['telegram_user_first_name'] + if 'telegram_username' in session and session['telegram_username']: + return session['telegram_username'] + return str(session.get('telegram_user_id', 'User')) + + +@app.route('/', methods=['GET']) +def root_path(): + return render_template_string(INITIAL_LOAD_HTML) + +@app.route('/api/telegram_authenticate', methods=['POST']) +def telegram_authenticate(): try: - init_data_str = request.form.get('initData') - if not init_data_str: - return jsonify({'status': 'error', 'message': 'No initData received'}), 400 + payload = request.json + init_data_str = payload.get('initData') + user_info = payload.get('user') - auth_data_dict = dict(param.split('=', 1) for param in unquote(init_data_str).split('&')) + if not init_data_str or not user_info or not user_info.get('id'): + return jsonify({'status': 'error', 'message': 'Отсутствуют данные для аутентификации'}), 400 - tg_user = check_telegram_authorization(auth_data_dict.copy()) - - if tg_user: - user_id_str = str(tg_user['id']) - session['telegram_user_id'] = tg_user['id'] - session['telegram_user_info'] = tg_user - - data = load_data() - if user_id_str not in data['users']: - data['users'][user_id_str] = { - 'tg_username': tg_user.get('username'), - 'tg_first_name': tg_user.get('first_name'), - 'tg_last_name': tg_user.get('last_name'), - 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S') - # Filesystem initialized by initialize_user_filesystem below - } - - # Ensure filesystem structure exists for the user - initialize_user_filesystem(data['users'][user_id_str]) - + data_check_string = "\n".join(sorted([f"{k}={v}" for k, v in經urllib.parse.parse_qsl(init_data_str) if k != 'hash'])) + 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() + + received_hash = urllib.parse.parse_qs(init_data_str).get('hash', [None])[0] + + if calculated_hash != received_hash: + # For dev, allow unsafe if hash fails, but log it. In prod, this should be an error. + logging.warning(f"Telegram data hash mismatch. Rec: {received_hash}, Calc: {calculated_hash}. Using unsafe data for user {user_info.get('id')}") + # return jsonify({'status': 'error', 'message': 'Ошибка проверки данных Telegram.'}), 403 + + + user_id = user_info['id'] + username = user_info.get('username') + first_name = user_info.get('first_name') + last_name = user_info.get('last_name') + + session['telegram_user_id'] = user_id + session['telegram_username'] = username + session['telegram_user_first_name'] = first_name + session['telegram_user_last_name'] = last_name + + user_id_str = str(user_id) + data = load_data() + if user_id_str not in data['users']: + data['users'][user_id_str] = { + 'telegram_username': username, + 'telegram_first_name': first_name, + 'telegram_last_name': last_name, + 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'filesystem': { "type": "folder", "id": "root", "name": "root", "children": [] } + } + initialize_user_filesystem(data['users'][user_id_str], user_id_str) try: - save_data(data) # Save if new user or if filesystem was just initialized + save_data(data) + logging.info(f"New Telegram user {user_id_str} registered.") except Exception as e: - logging.error(f"Error saving user data for TGID {user_id_str}: {e}") - return jsonify({'status': 'error', 'message': 'Error saving user data'}), 500 - - return jsonify({'status': 'success', 'user': tg_user, 'redirect_url': url_for('dashboard')}) - else: - return jsonify({'status': 'error', 'message': 'Invalid Telegram authorization'}), 403 + logging.error(f"Error saving data for new Telegram user {user_id_str}: {e}") + return jsonify({'status': 'error', 'message': 'Ошибка сохранения данных пользователя.'}), 500 + + return jsonify({'status': 'success'}) except Exception as e: - logging.error(f"Error in Telegram auth: {e}") + logging.error(f"Error in /api/telegram_authenticate: {e}") return jsonify({'status': 'error', 'message': str(e)}), 500 -@app.route('/dashboard', methods=['GET', 'POST']) -def dashboard(): +@app.route('/app', methods=['GET', 'POST']) +def main_app_view_and_upload(): if 'telegram_user_id' not in session: - flash('Пожалуйста, авторизуйтесь через Telegram.', 'error') - # In a mini app, this usually means redirecting to the auth flow or showing an error. - # Since /app handles auth, direct access to /dashboard shouldn't happen unauthenticated. - return render_template_string("

Доступ запрещен. Пожалуйста, откройте приложение через Telegram.

"), 403 - - tg_user_id_str = str(session['telegram_user_id']) - tg_user_info = session.get('telegram_user_info', {}) - user_display_name = tg_user_info.get('first_name', tg_user_info.get('username', f"User {tg_user_id_str}")) + flash('Пожалуйста, пройдите аутентификацию через Telegram.') + return redirect(url_for('root_path')) + user_telegram_id = session['telegram_user_id'] + user_telegram_id_str = str(user_telegram_id) + data = load_data() - if tg_user_id_str not in data['users']: + if user_telegram_id_str not in data['users']: session.clear() - flash('Данные пользователя не найдены. Пожалуйста, перезайдите.', 'error') - return render_template_string("

Данные пользователя не найдены. Пожалуйста, перезайдите через Telegram.

"), 403 + flash('Пользователь не найден!') + return redirect(url_for('root_path')) - user_data = data['users'][tg_user_id_str] - initialize_user_filesystem(user_data) # Ensure filesystem exists + user_data = data['users'][user_telegram_id_str] + if 'filesystem' not in user_data: + initialize_user_filesystem(user_data, user_telegram_id_str) current_folder_id = request.args.get('folder_id', 'root') current_folder, parent_folder = find_node_by_id(user_data['filesystem'], current_folder_id) @@ -428,57 +429,54 @@ def dashboard(): flash('Папка не найдена!', 'error') current_folder_id = 'root' current_folder, parent_folder = find_node_by_id(user_data['filesystem'], current_folder_id) - if not current_folder: # Should not happen if initialized - logging.error(f"CRITICAL: Root folder not found for user TGID {tg_user_id_str}") + if not current_folder: + logging.error(f"CRITICAL: Root folder not found for user {user_telegram_id_str}") flash('Критическая ошибка: корневая папка не найдена.', 'error') session.clear() - return redirect(url_for('mini_app_entry')) # Re-auth + return redirect(url_for('root_path')) items_in_folder = sorted(current_folder.get('children', []), key=lambda x: (x['type'] != 'folder', x.get('name', x.get('original_filename', '')).lower())) if request.method == 'POST': if not HF_TOKEN_WRITE: flash('Загрузка невозможна: токен для записи не настроен.', 'error') - return redirect(url_for('dashboard', folder_id=current_folder_id)) + return redirect(url_for('main_app_view_and_upload', 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('dashboard', folder_id=current_folder_id)) + return redirect(url_for('main_app_view_and_upload', folder_id=current_folder_id)) - if len(files) > 20: # Limit number of files + if len(files) > 20: flash('Максимум 20 файлов за раз!', 'error') - return redirect(url_for('dashboard', folder_id=current_folder_id)) - + return redirect(url_for('main_app_view_and_upload', folder_id=current_folder_id)) target_folder_id = request.form.get('current_folder_id', 'root') target_folder_node, _ = find_node_by_id(user_data['filesystem'], target_folder_id) if not target_folder_node or target_folder_node.get('type') != 'folder': flash('Целевая папка для загрузки не найдена!', 'error') - return redirect(url_for('dashboard')) + return redirect(url_for('main_app_view_and_upload')) api = HfApi() uploaded_count = 0 errors = [] - for file_obj in files: - if file_obj and file_obj.filename: - original_filename = secure_filename(file_obj.filename) + for file_in_request in files: + if file_in_request and file_in_request.filename: + original_filename = secure_filename(file_in_request.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/{tg_user_id_str}/{target_folder_id}/{unique_filename}" + hf_path = f"cloud_files/{user_telegram_id_str}/{target_folder_id}/{unique_filename}" temp_path = os.path.join(UPLOAD_FOLDER, f"{file_id}_{unique_filename}") - try: - file_obj.save(temp_path) + file_in_request.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, - commit_message=f"User TGID:{tg_user_id_str} uploaded {original_filename} to folder {target_folder_id}" + commit_message=f"User {user_telegram_id_str} uploaded {original_filename} to folder {target_folder_id}" ) file_info = { 'type': 'file', 'id': file_id, 'original_filename': original_filename, @@ -488,27 +486,26 @@ def dashboard(): } if add_node(user_data['filesystem'], target_folder_id, file_info): uploaded_count += 1 - else: # Should not happen if target_folder_node is valid + else: errors.append(f"Ошибка добавления метаданных для {original_filename}.") - logging.error(f"Failed to add node metadata for file {file_id} to folder {target_folder_id} for user TGID {tg_user_id_str}") + logging.error(f"Failed to add node metadata for file {file_id} to folder {target_folder_id} for user {user_telegram_id_str}") try: api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) except Exception as del_err: logging.error(f"Failed to delete orphaned file {hf_path} from HF Hub: {del_err}") except Exception as e: - logging.error(f"Error uploading file {original_filename} for TGID {tg_user_id_str}: {e}") + logging.error(f"Error uploading file {original_filename} for {user_telegram_id_str}: {e}") errors.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 as e: flash('Файлы загружены на сервер, но произошла ошибка сохранения метаданных.', 'error') - logging.error(f"Error saving data after upload for TGID {tg_user_id_str}: {e}") + logging.error(f"Error saving data after upload for {user_telegram_id_str}: {e}") if errors: for error_msg in errors: flash(error_msg, 'error') - return redirect(url_for('dashboard', folder_id=target_folder_id)) + return redirect(url_for('main_app_view_and_upload', folder_id=target_folder_id)) breadcrumbs = [] temp_id = current_folder_id @@ -522,58 +519,76 @@ def dashboard(): breadcrumbs.reverse() html = ''' - + Zeus Cloud
-

Zeus Cloud

Пользователь: {{ user_display_name }}

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

Zeus Cloud

Пользователь: {{ current_user_display_name }}

+{% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %}{% for category, message in messages %}
{{ message }}
{% endfor %}{% endif %} +{% endwith %} + {% for crumb in breadcrumbs %} + {% if crumb.is_link %}{{ crumb.name if crumb.id != 'root' else 'Главная' }} + {% else %}{{ crumb.name if crumb.id != 'root' else 'Главная' }}{% endif %} + {% if not loop.last %}/{% endif %} + {% endfor %} +
-
+ -
-
+ +
+ +
-
+ +
0%

Содержимое папки: {{ current_folder.name if current_folder_id != 'root' else 'Главная' }}

- {% for item in items %}
- {% if item.type == 'folder' %}📁

{{ item.name }}

-
Открыть -
-
- {% elif item.type == 'file' %}{% set previewable = item.file_type in ['image', 'video', 'pdf', 'text'] %} - {% if item.file_type == 'image' %}{{ item.original_filename|e }} - {% elif item.file_type == 'video' %} - {% elif item.file_type == 'pdf' %}
📄
- {% elif item.file_type == 'text' %}
📝
- {% else %}
{% endif %} -

{{ item.original_filename | truncate(25, True) }}

{{ item.upload_date }}

-
- Скачать - {% if previewable %}{% endif %} -
-
{% endif %}
- {% endfor %}{% if not items %}

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

{% endif %}
- + {% for item in items %} +
+ {% if item.type == 'folder' %} + 📁 +

{{ item.name }}

+
+ Открыть +
+ + +
+
+ {% elif item.type == 'file' %} + {% set previewable = item.file_type in ['image', 'video', 'pdf', 'text'] %} + {% if item.file_type == 'image' %}{{ item.original_filename }} + {% elif item.file_type == 'video' %} + {% elif item.file_type == 'pdf' %}
📄
+ {% elif item.file_type == 'text' %}
📝
+ {% else %}
{% endif %} +

{{ item.original_filename | truncate(25, True) }}

+

{{ item.upload_date }}

+
+ Скачать + {% if previewable %}{% endif %} +
+ + +
+
+ {% endif %} +
+ {% endfor %} + {% if not items %}

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

{% endif %} + + - + ''' template_context = { - 'user_display_name': user_display_name, 'items': items_in_folder, 'current_folder_id': current_folder_id, - 'current_folder': current_folder, 'breadcrumbs': breadcrumbs, 'repo_id': REPO_ID, + 'current_user_display_name': get_current_user_display_name(), + 'items': items_in_folder, 'current_folder_id': current_folder_id, + 'current_folder': current_folder, 'breadcrumbs': breadcrumbs, + 'repo_id': REPO_ID, 'HF_TOKEN_READ': HF_TOKEN_READ, 'hf_file_url': lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{path}{'?download=true' if download else ''}", + 'os': os } return render_template_string(html, **template_context) -@app.route('/create_folder', methods=['POST']) -def create_folder(): +@app.route('/api/create_folder', methods=['POST']) +def create_folder_api(): if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован'}), 401 - tg_user_id_str = str(session['telegram_user_id']) + user_telegram_id_str = str(session['telegram_user_id']) data = load_data() - user_data = data['users'].get(tg_user_id_str) + user_data = data['users'].get(user_telegram_id_str) if not user_data: return jsonify({'status': 'error', 'message': 'Пользователь не найден'}), 404 parent_folder_id = request.form.get('parent_folder_id', 'root') @@ -639,342 +666,327 @@ def create_folder(): if not folder_name: flash('Имя папки не может быть пустым!', 'error') - elif not all(c.isalnum() or c.isspace() or c in '_-.' for c in folder_name) or len(folder_name) > 50 : # Basic validation - 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 as e: flash('Ошибка сохранения данных.', 'error'); logging.error(f"Create folder save error: {e}") - else: flash('Не удалось найти родительскую папку.', 'error') - return redirect(url_for('dashboard', folder_id=parent_folder_id)) - + return redirect(url_for('main_app_view_and_upload', folder_id=parent_folder_id)) + if not all(c.isalnum() or c in [' ', '_', '-'] for c in folder_name): + flash('Имя папки может содержать буквы, цифры, пробелы, дефисы и подчеркивания.', 'error') + return redirect(url_for('main_app_view_and_upload', folder_id=parent_folder_id)) + + 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 as e: flash('Ошибка сохранения данных при создании папки.', 'error'); logging.error(f"Create folder save error: {e}") + else: flash('Не удалось найти родительскую папку.', 'error') + return redirect(url_for('main_app_view_and_upload', folder_id=parent_folder_id)) @app.route('/download/') def download_file(file_id): - allow_access = False - current_user_tg_id_str = str(session.get('telegram_user_id')) if 'telegram_user_id' in session else None + is_admin_access = is_telegram_admin() and (request.referrer and 'admhosto' in request.referrer) + if 'telegram_user_id' not in session and not is_admin_access: + flash('Пожалуйста, пройдите аутентификацию.') + return redirect(url_for('root_path')) + data = load_data() file_node = None - # file_owner_tg_id_str = None # Not strictly needed here, but good for logging if implemented - - # Check if current user is admin - is_current_user_admin = is_admin() - - if current_user_tg_id_str: - user_data = data['users'].get(current_user_tg_id_str) - if user_data: - _file_node, _ = find_node_by_id(user_data.get('filesystem', {}), file_id) - if _file_node and _file_node.get('type') == 'file': - file_node = _file_node - allow_access = True # Owner has access - - if not file_node and is_current_user_admin: # If admin and file not found under their own ID (or they are searching) - for tg_id, udata in data.get('users', {}).items(): - if tg_id == current_user_tg_id_str: continue # Already checked - node, _ = find_node_by_id(udata.get('filesystem', {}), file_id) + user_context_id_str = str(session.get('telegram_user_id')) if 'telegram_user_id' in session else None + + if user_context_id_str: + user_data = data['users'].get(user_context_id_str) + if user_data: file_node, _ = find_node_by_id(user_data['filesystem'], file_id) + + if not file_node and is_telegram_admin(): # Admin can download any file + for uid_str, udata_val in data.get('users', {}).items(): + node, _ = find_node_by_id(udata_val.get('filesystem', {}), file_id) if node and node.get('type') == 'file': - file_node = node; allow_access = True; break + file_node = node; user_context_id_str = uid_str; break - if not allow_access or not file_node: - flash('Файл не найден или доступ запрещен!', 'error') - return redirect(url_for('dashboard') if current_user_tg_id_str else url_for('mini_app_entry')) + if not file_node or file_node.get('type') != 'file': + flash('Файл не найден!', 'error') + return redirect(request.referrer or url_for('main_app_view_and_upload' if 'telegram_user_id' in session else 'root_path')) hf_path = file_node.get('path') original_filename = file_node.get('original_filename', 'downloaded_file') if not hf_path: flash('Ошибка: Путь к файлу не найден.', 'error') - return redirect(url_for('dashboard', folder_id=request.args.get('folder_id', 'root'))) # Stay in current folder + return redirect(request.referrer or url_for('main_app_view_and_upload' if 'telegram_user_id' in session else 'root_path')) file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{hf_path}?download=true" try: - headers = {} + headers = {}; if HF_TOKEN_READ: headers["authorization"] = f"Bearer {HF_TOKEN_READ}" - response = requests.get(file_url, headers=headers, stream=True) - response.raise_for_status() + response = requests.get(file_url, headers=headers, stream=True); response.raise_for_status() return send_file(BytesIO(response.content), as_attachment=True, download_name=original_filename, mimetype='application/octet-stream') - except requests.exceptions.RequestException as e: - logging.error(f"Error downloading file from HF ({hf_path}): {e}") - flash(f'Ошибка скачивания файла: {e}', 'error') except Exception as e: - logging.error(f"Unexpected error during download ({hf_path}): {e}") - flash('Непредвиденная ошибка при скачивании.', 'error') - return redirect(url_for('dashboard', folder_id=request.args.get('folder_id', 'root'))) - + logging.error(f"Error downloading file from HF ({hf_path}): {e}") + flash(f'Ошибка скачивания файла {original_filename}! ({e})', 'error') + return redirect(request.referrer or url_for('main_app_view_and_upload' if 'telegram_user_id' in session else 'root_path')) -@app.route('/delete_file/', methods=['POST']) -def delete_file(file_id): - if 'telegram_user_id' not in session: flash('Пожалуйста, авторизуйтесь.'); return redirect(url_for('mini_app_entry')) - tg_user_id_str = str(session['telegram_user_id']) +@app.route('/api/delete_file/', methods=['POST']) +def delete_file_api(file_id): + if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован'}), 401 + user_telegram_id_str = str(session['telegram_user_id']) data = load_data() - user_data = data['users'].get(tg_user_id_str) - if not user_data: flash('Пользователь не найден!'); session.clear(); return redirect(url_for('mini_app_entry')) + user_data = data['users'].get(user_telegram_id_str) + if not user_data: flash('Пользователь не найден!', 'error'); session.clear(); return redirect(url_for('root_path')) file_node, parent_node = find_node_by_id(user_data['filesystem'], file_id) - current_view_folder_id = request.form.get('current_view_folder_id', parent_node.get('id', 'root') if parent_node else 'root') - + current_view_folder_id = request.form.get('current_view_folder_id', 'root') if not file_node or file_node.get('type') != 'file' or not parent_node: flash('Файл не найден или не может быть удален.', 'error') - elif not HF_TOKEN_WRITE: - flash('Удаление невозможно: токен для записи не настроен.', 'error') - else: - hf_path = file_node.get('path') - original_filename = file_node.get('original_filename', 'файл') - try: - if hf_path: - api = HfApi() - api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, - commit_message=f"User TGID:{tg_user_id_str} deleted file {original_filename}") - if remove_node(user_data['filesystem'], file_id): save_data(data) - flash(f'Файл {original_filename} успешно удален!') - except hf_utils.EntryNotFoundError: # File not on HF, remove from DB - if remove_node(user_data['filesystem'], file_id): save_data(data) - flash(f'Файл {original_filename} не найден на сервере, удален из базы.') - except Exception as e: - logging.error(f"Error deleting file {hf_path} for TGID {tg_user_id_str}: {e}") - flash(f'Ошибка удаления файла {original_filename}: {e}', 'error') - return redirect(url_for('dashboard', folder_id=current_view_folder_id)) - -@app.route('/delete_folder/', methods=['POST']) -def delete_folder(folder_id): - if 'telegram_user_id' not in session: flash('Пожалуйста, авторизуйтесь.'); return redirect(url_for('mini_app_entry')) - if folder_id == 'root': flash('Нельзя удалить корневую папку!', 'error'); return redirect(url_for('dashboard')) - - tg_user_id_str = str(session['telegram_user_id']) - data = load_data() - user_data = data['users'].get(tg_user_id_str) - if not user_data: flash('Пользователь не найден!'); session.clear(); return redirect(url_for('mini_app_entry')) + return redirect(url_for('main_app_view_and_upload', folder_id=current_view_folder_id)) + + hf_path = file_node.get('path'); original_filename = file_node.get('original_filename', 'файл') + if not hf_path: + flash(f'Ошибка: Путь к файлу {original_filename} не найден. Удаление только из базы.', 'error') + if remove_node(user_data['filesystem'], file_id): + try: save_data(data); flash(f'Метаданные файла {original_filename} удалены.') + except Exception as e: flash('Ошибка сохранения данных.', 'error'); logging.error(f"Delete file metadata save error: {e}") + return redirect(url_for('main_app_view_and_upload', folder_id=current_view_folder_id)) + if not HF_TOKEN_WRITE: flash('Удаление невозможно: токен не настроен.', 'error'); return redirect(url_for('main_app_view_and_upload', folder_id=current_view_folder_id)) + try: + api = HfApi() + api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"User {user_telegram_id_str} deleted {file_id}") + if remove_node(user_data['filesystem'], file_id): + try: save_data(data); flash(f'Файл {original_filename} успешно удален!') + except Exception as e: flash('Файл удален, ошибка обновления базы.', 'error'); logging.error(f"Delete file DB update error: {e}") + else: flash('Файл удален, но не найден в базе.', 'error') + except hf_utils.EntryNotFoundError: + if remove_node(user_data['filesystem'], file_id): + try: save_data(data); flash(f'Файл {original_filename} не найден на сервере, удален из базы.') + except Exception as e: flash('Ошибка сохранения (файл не на сервере).', 'error'); logging.error(f"Delete file metadata save error (HF not found): {e}") + else: flash('Файл не найден нигде.', 'error') + except Exception as e: logging.error(f"Error deleting file {hf_path} for {user_telegram_id_str}: {e}"); flash(f'Ошибка удаления {original_filename}: {e}', 'error') + return redirect(url_for('main_app_view_and_upload', folder_id=current_view_folder_id)) + +@app.route('/api/delete_folder/', methods=['POST']) +def delete_folder_api(folder_id): + if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован'}), 401 + if folder_id == 'root': flash('Нельзя удалить корневую папку!', 'error'); return redirect(url_for('main_app_view_and_upload')) + user_telegram_id_str = str(session['telegram_user_id']) + data = load_data() + user_data = data['users'].get(user_telegram_id_str) + if not user_data: flash('Пользователь не найден!', 'error'); session.clear(); return redirect(url_for('root_path')) + folder_node, parent_node = find_node_by_id(user_data['filesystem'], folder_id) - current_view_folder_id = request.form.get('current_view_folder_id', parent_node.get('id', 'root') if parent_node else 'root') + current_view_folder_id = request.form.get('current_view_folder_id', 'root') if not folder_node or folder_node.get('type') != 'folder' or not parent_node: flash('Папка не найдена или не может быть удалена.', 'error') - elif folder_node.get('children'): - flash(f'Папку "{folder_node.get("name", "папка")}" можно удалить только если она пуста.', 'error') - else: - if remove_node(user_data['filesystem'], folder_id): - try: save_data(data); flash(f'Папка "{folder_node.get("name", "папка")}" удалена.') - except Exception as e: flash('Ошибка сохранения данных.', 'error'); logging.error(f"Delete folder save error: {e}") - else: flash('Не удалось удалить папку.', 'error') - return redirect(url_for('dashboard', folder_id=current_view_folder_id)) - + return redirect(url_for('main_app_view_and_upload', folder_id=current_view_folder_id)) + folder_name = folder_node.get('name', 'папка') + if folder_node.get('children'): + flash(f'Папку "{folder_name}" можно удалить только если она пуста.', 'error') + return redirect(url_for('main_app_view_and_upload', folder_id=current_view_folder_id)) + if remove_node(user_data['filesystem'], folder_id): + try: save_data(data); flash(f'Папка "{folder_name}" удалена.') + except Exception as e: flash('Ошибка сохранения данных.', 'error'); logging.error(f"Delete empty folder save error: {e}") + else: flash('Не удалось удалить папку.', 'error') + return redirect(url_for('main_app_view_and_upload', folder_id=parent_node.get('id', 'root'))) @app.route('/get_text_content/') def get_text_content(file_id): - allow_access = False - current_user_tg_id_str = str(session.get('telegram_user_id')) if 'telegram_user_id' in session else None - + is_admin_access = is_telegram_admin() and (request.referrer and 'admhosto' in request.referrer) + if 'telegram_user_id' not in session and not is_admin_access: return Response("Не авторизован", status=401) + data = load_data() file_node = None - is_current_user_admin = is_admin() - - if current_user_tg_id_str: - user_data = data['users'].get(current_user_tg_id_str) - if user_data: - _file_node, _ = find_node_by_id(user_data.get('filesystem', {}), file_id) - if _file_node and _file_node.get('type') == 'file' and _file_node.get('file_type') == 'text': - file_node = _file_node; allow_access = True - - if not file_node and is_current_user_admin: - for tg_id, udata in data.get('users', {}).items(): - if tg_id == current_user_tg_id_str: continue - node, _ = find_node_by_id(udata.get('filesystem', {}), file_id) + user_context_id_str = str(session.get('telegram_user_id')) if 'telegram_user_id' in session else None + + if user_context_id_str: + user_data = data['users'].get(user_context_id_str) + if user_data: file_node, _ = find_node_by_id(user_data['filesystem'], file_id) + + if not file_node and is_telegram_admin(): + for uid_str, udata_val in data.get('users', {}).items(): + node, _ = find_node_by_id(udata_val.get('filesystem', {}), file_id) if node and node.get('type') == 'file' and node.get('file_type') == 'text': - file_node = node; allow_access = True; break + file_node = node; break - if not allow_access or not file_node: return Response("Текстовый файл не найден или доступ запрещен", status=404) - + if not file_node or file_node.get('type') != 'file' or file_node.get('file_type') != 'text': return Response("Файл не найден", status=404) hf_path = file_node.get('path') - if not hf_path: return Response("Ошибка: путь к файлу отсутствует", status=500) - + if not hf_path: return Response("Путь к файлу отсутствует", status=500) file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{hf_path}?download=true" try: - headers = {}; + headers = {}; if HF_TOKEN_READ: headers["authorization"] = f"Bearer {HF_TOKEN_READ}" - response = requests.get(file_url, headers=headers, timeout=10) - response.raise_for_status() - if len(response.content) > 1 * 1024 * 1024: return Response("Файл слишком большой для предпросмотра.", status=413) + response = requests.get(file_url, headers=headers); response.raise_for_status() + if len(response.content) > 1 * 1024 * 1024: return Response("Файл слишком большой.", status=413) try: text_content = response.content.decode('utf-8') - except UnicodeDecodeError: text_content = response.content.decode('latin-1', errors='replace') + except UnicodeDecodeError: text_content = response.content.decode('latin-1') return Response(text_content, mimetype='text/plain') - except requests.exceptions.RequestException as e: return Response(f"Ошибка загрузки: {e}", status=502) - except Exception as e: return Response("Внутренняя ошибка", status=500) + except Exception as e: logging.error(f"Error fetching text from HF ({hf_path}): {e}"); return Response(f"Ошибка: {e}", status=502) + -# --- Admin Routes --- @app.route('/admhosto') def admin_panel(): - if not is_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('mini_app_entry')) - data = load_data() - users = data.get('users', {}) - user_details = [] - for tg_id_str, udata in users.items(): - file_count = 0; q = [udata.get('filesystem', {}).get('children', [])] + if not is_telegram_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('root_path')) + data = load_data(); users = data.get('users', {}); user_details = [] + for uid_str, udata_val in users.items(): + file_count = 0; q = [udata_val.get('filesystem', {}).get('children', [])] while q: current_level = q.pop(0) - for item in current_level: - if item.get('type') == 'file': file_count += 1 - elif item.get('type') == 'folder' and 'children' in item: q.append(item.get('children', [])) - user_details.append({ - 'telegram_user_id': tg_id_str, - 'display_name': udata.get('tg_first_name', udata.get('tg_username', f"TGID: {tg_id_str}")), - 'created_at': udata.get('created_at', 'N/A'), 'file_count': file_count - }) + for item_val in current_level: + if item_val.get('type') == 'file': file_count += 1 + elif item_val.get('type') == 'folder' and 'children' in item_val: q.append(item_val.get('children', [])) + user_display = udata_val.get('telegram_first_name', '') or udata_val.get('telegram_username', uid_str) + user_details.append({'id_str': uid_str, 'display_name': user_display, 'created_at': udata_val.get('created_at', 'N/A'), 'file_count': file_count}) html = ''' -Админ-панель -

Админ-панель

- + +Админ-панель +

Админ-панель

+Вернуться в приложение {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}
{{ message }}
{% endfor %}{% endif %}{% endwith %}

Пользователи

-{% for user in user_details %}
- {{ user.display_name }} ({{user.telegram_user_id}}) -

Зарегистрирован: {{ user.created_at }}

Файлов: {{ user.file_count }}

-
+{% for user_item_detail in user_details %}
+ {{ user_item_detail.display_name }} (ID: {{ user_item_detail.id_str }}) +

Зарегистрирован: {{ user_item_detail.created_at }}

Файлов: {{ user_item_detail.file_count }}

+
-{% else %}

Пользователей нет.

{% endfor %}
-''' +{% else %}

Пользователей нет.

{% endfor %}
''' return render_template_string(html, user_details=user_details) -@app.route('/admhosto/user/') -def admin_user_files(telegram_user_id): - if not is_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('mini_app_entry')) - data = load_data() - user_data = data.get('users', {}).get(telegram_user_id) - if not user_data: flash(f'Пользователь {telegram_user_id} не найден.', 'error'); return redirect(url_for('admin_panel')) - - initialize_user_filesystem(user_data) # Ensure fs for this user if accessed by admin first time - - user_display_name = user_data.get('tg_first_name', user_data.get('tg_username', f"TGID: {telegram_user_id}")) - all_files = [] - def collect_files_recursive(folder_node, current_path_str_list): - for item in folder_node.get('children', []): - if item.get('type') == 'file': - item_copy = item.copy() - item_copy['parent_path_str'] = " / ".join(current_path_str_list) or "Root" - all_files.append(item_copy) - elif item.get('type') == 'folder': - collect_files_recursive(item, current_path_str_list + [item.get('name', 'Unnamed Folder')]) - - collect_files_recursive(user_data.get('filesystem', {}), []) - all_files.sort(key=lambda x: x.get('upload_date', ''), reverse=True) - +@app.route('/admhosto/user/') +def admin_user_files(user_telegram_id_str): + if not is_telegram_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('root_path')) + data = load_data(); user_data = data.get('users', {}).get(user_telegram_id_str) + if not user_data: flash(f'Пользователь {user_telegram_id_str} не найден.', 'error'); return redirect(url_for('admin_panel')) + all_files = []; user_display_name = user_data.get('telegram_first_name', '') or user_data.get('telegram_username', user_telegram_id_str) + def collect_files(folder, current_path_id='root'): + parent_path_str = get_node_path_string(user_data['filesystem'], current_path_id) + for item_val in folder.get('children', []): + if item_val.get('type') == 'file': item_val['parent_path_str'] = parent_path_str; all_files.append(item_val) + elif item_val.get('type') == 'folder': collect_files(item_val, item_val.get('id')) + collect_files(user_data.get('filesystem', {})); all_files.sort(key=lambda x_file: x_file.get('upload_date', ''), reverse=True) html = ''' -Файлы {{ user_display_name }} -

Файлы: {{ user_display_name }} ({{telegram_user_id}})

-Назад +Файлы {{ user_display_name }} + +

Файлы пользовате��я: {{ user_display_name }} (ID: {{ user_telegram_id_str }})

+Назад к пользователям {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}
{{ message }}
{% endfor %}{% endif %}{% endwith %} -
-{% for file in files %}
- {% if file.file_type == 'image' %} - {% elif file.file_type == 'video' %} - {% elif file.file_type == 'pdf' %}
📄
- {% elif file.file_type == 'text' %}
📝
- {% else %}
{% endif %} -

{{ file.original_filename | truncate(25) }}

-

В папке: {{ file.parent_path_str }}

-

Загружен: {{ file.upload_date }}

-

ID: {{ file.id }}

-

Path: {{ file.path }}

+
+{% for file_item_val in files %}
+ {% if file_item_val.file_type == 'image' %} + {% elif file_item_val.file_type == 'video' %} + {% elif file_item_val.file_type == 'pdf' %}
📄
+ {% elif file_item_val.file_type == 'text' %}
📝
+ {% else %}
{% endif %} +

{{ file_item_val.original_filename | truncate(30) }}

+

В папке: {{ file_item_val.parent_path_str }}

+

Загружен: {{ file_item_val.upload_date }}

+

ID: {{ file_item_val.id }}

+

Path: {{ file_item_val.path }}

- Скачать - {% set previewable = file.file_type in ['image', 'video', 'pdf', 'text'] %} - {% if previewable %}{% endif %} -
+ Скачать + {% set previewable = file_item_val.file_type in ['image', 'video', 'pdf', 'text'] %} + {% if previewable %}{% endif %} +
{% else %}

У пользователя нет файлов.

{% endfor %}
- + ''' - return render_template_string(html, telegram_user_id=telegram_user_id, user_display_name=user_display_name, files=all_files, repo_id=REPO_ID, - hf_file_url=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{path}{'?download=true' if download else ''}") + return render_template_string(html, user_telegram_id_str=user_telegram_id_str, user_display_name=user_display_name, files=all_files, repo_id=REPO_ID, hf_file_url=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{path}{'?download=true' if download else ''}") -@app.route('/admhosto/delete_user/', methods=['POST']) -def admin_delete_user(telegram_user_id): - if not is_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('mini_app_entry')) +@app.route('/admhosto/delete_user/', methods=['POST']) +def admin_delete_user(user_telegram_id_str): + if not is_telegram_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('root_path')) if not HF_TOKEN_WRITE: flash('Удаление невозможно: токен не настроен.', 'error'); return redirect(url_for('admin_panel')) - data = load_data() - if telegram_user_id not in data['users']: flash('Пользователь не найден!', 'error'); return redirect(url_for('admin_panel')) - - logging.warning(f"ADMIN ACTION: Attempting to delete user TGID {telegram_user_id} and all their data.") + if user_telegram_id_str not in data['users']: flash('Пользователь не найден!', 'error'); return redirect(url_for('admin_panel')) + logging.warning(f"ADMIN ACTION: Deleting user {user_telegram_id_str}.") try: - api = HfApi(); user_folder_path_on_hf = f"cloud_files/{telegram_user_id}" - logging.info(f"Attempting to delete HF Hub folder: {user_folder_path_on_hf} for user TGID {telegram_user_id}") - api.delete_folder(folder_path=user_folder_path_on_hf, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, - commit_message=f"ADMIN ACTION: Deleted all files/folders for user TGID {telegram_user_id}") - logging.info(f"Successfully initiated deletion of folder {user_folder_path_on_hf} on HF Hub.") + api = HfApi(); user_folder_path_on_hf = f"cloud_files/{user_telegram_id_str}" + api.delete_folder(folder_path=user_folder_path_on_hf, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"ADMIN: Deleted all files for user {user_telegram_id_str}") except hf_utils.HfHubHTTPError as e: - if e.response.status_code == 404: logging.warning(f"User folder {user_folder_path_on_hf} not found on HF Hub.") - else: logging.error(f"Error deleting user folder {user_folder_path_on_hf} from HF Hub: {e}"); flash(f'Ошибка удаления файлов с сервера: {e}. Пользователь НЕ удален из базы.', 'error'); return redirect(url_for('admin_panel')) - except Exception as e: logging.error(f"Unexpected error during HF folder deletion: {e}"); flash(f'Неожиданная ошибка удаления файлов с сервера: {e}. Пользователь НЕ удален из базы.', 'error'); return redirect(url_for('admin_panel')) - + if e.response.status_code == 404: logging.warning(f"User folder {user_folder_path_on_hf} not found on HF. Skipping HF deletion.") + else: logging.error(f"Error deleting user folder from HF for {user_telegram_id_str}: {e}"); flash(f'Ошибка удаления файлов {user_telegram_id_str} с сервера: {e}.', 'error'); return redirect(url_for('admin_panel')) + except Exception as e: logging.error(f"Unexpected error deleting HF folder for {user_telegram_id_str}: {e}"); flash(f'Ошибка удаления файлов {user_telegram_id_str} с сервера: {e}.', 'error'); return redirect(url_for('admin_panel')) try: - del data['users'][telegram_user_id]; save_data(data) - flash(f'Пользователь TGID {telegram_user_id} и его файлы удалены/запрошены к удалению!') - logging.info(f"ADMIN ACTION: Successfully deleted user TGID {telegram_user_id} from database.") - except Exception as e: logging.error(f"Error saving data after deleting user {telegram_user_id}: {e}"); flash(f'Файлы удалены с сервера, но ошибка удаления из базы: {e}', 'error') + del data['users'][user_telegram_id_str]; save_data(data) + flash(f'Пользователь {user_telegram_id_str} и его файлы удалены!') + logging.info(f"ADMIN ACTION: Deleted user {user_telegram_id_str} from database.") + except Exception as e: logging.error(f"Error saving data after deleting user {user_telegram_id_str}: {e}"); flash(f'Файлы удалены, ошибка удаления из базы: {e}', 'error') return redirect(url_for('admin_panel')) -@app.route('/admhosto/delete_file//', methods=['POST']) -def admin_delete_file(telegram_user_id, file_id): - if not is_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('mini_app_entry')) - if not HF_TOKEN_WRITE: flash('Удаление невозможно: токен не настроен.', 'error'); return redirect(url_for('admin_user_files', telegram_user_id=telegram_user_id)) - - data = load_data() - user_data = data.get('users', {}).get(telegram_user_id) - if not user_data: flash(f'Пользователь {telegram_user_id} не найден.', 'error'); return redirect(url_for('admin_panel')) - - file_node, _ = find_node_by_id(user_data.get('filesystem',{}), file_id) - if not file_node or file_node.get('type') != 'file': flash('Файл не найден.', 'error'); return redirect(url_for('admin_user_files', telegram_user_id=telegram_user_id)) - - hf_path = file_node.get('path') - original_filename = file_node.get('original_filename', 'файл') +@app.route('/admhosto/delete_file//', methods=['POST']) +def admin_delete_file(user_telegram_id_str, file_id): + if not is_telegram_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('root_path')) + if not HF_TOKEN_WRITE: flash('Удаление невозможно: токен не настроен.', 'error'); return redirect(url_for('admin_user_files', user_telegram_id_str=user_telegram_id_str)) + data = load_data(); user_data = data.get('users', {}).get(user_telegram_id_str) + if not user_data: flash(f'Пользователь {user_telegram_id_str} не найден.', 'error'); return redirect(url_for('admin_panel')) + file_node, parent_node = find_node_by_id(user_data['filesystem'], file_id) + if not file_node or file_node.get('type') != 'file' or not parent_node: flash('Файл не найден.', 'error'); return redirect(url_for('admin_user_files', user_telegram_id_str=user_telegram_id_str)) + hf_path = file_node.get('path'); original_filename = file_node.get('original_filename', 'файл') + if not hf_path: + if remove_node(user_data['filesystem'], file_id): + try: save_data(data); flash(f'Метаданные {original_filename} удалены (путь отсутствовал).') + except Exception as e: flash('Ошибка сохранения (путь отсутствовал).', 'error'); logging.error(f"Admin delete file metadata (no path): {e}") + return redirect(url_for('admin_user_files', user_telegram_id_str=user_telegram_id_str)) try: - if hf_path: - api = HfApi() - api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, - commit_message=f"ADMIN ACTION: Deleted file {original_filename} for user TGID {telegram_user_id}") - if remove_node(user_data['filesystem'], file_id): save_data(data) - flash(f'Файл {original_filename} удален!') + api = HfApi(); api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"ADMIN: Deleted {file_id} for {user_telegram_id_str}") + if remove_node(user_data['filesystem'], file_id): + try: save_data(data); flash(f'Файл {original_filename} удален!') + except Exception as e: flash('Файл удален, ошибка обновления базы.', 'error'); logging.error(f"Admin delete file DB update error: {e}") + else: flash('Файл удален, но не найден в базе.', 'error') except hf_utils.EntryNotFoundError: - if remove_node(user_data['filesystem'], file_id): save_data(data) - flash(f'Файл {original_filename} не найден на сервере, удален из базы.') - except Exception as e: - logging.error(f"ADMIN ACTION: Error deleting file {hf_path} for TGID {telegram_user_id}: {e}") - flash(f'Ошибка удаления файла {original_filename}: {e}', 'error') - return redirect(url_for('admin_user_files', telegram_user_id=telegram_user_id)) + if remove_node(user_data['filesystem'], file_id): + try: save_data(data); flash(f'Файл {original_filename} не найден на сервере, удален из базы.') + except Exception as e: flash('Ошибка сохранения (файл не на сервере).', 'error'); logging.error(f"Admin delete metadata (HF not found): {e}") + else: flash('Файл не найден нигде.', 'error') + except Exception as e: logging.error(f"ADMIN: Error deleting {hf_path} for {user_telegram_id_str}: {e}"); flash(f'Ошибка удаления {original_filename}: {e}', 'error') + return redirect(url_for('admin_user_files', user_telegram_id_str=user_telegram_id_str)) + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + if update.effective_chat is None: return + keyboard = [[InlineKeyboardButton("Открыть Zeus Cloud", web_app=WebAppInfo(url=flask.url_for('root_path', _external=True)))]] + reply_markup = InlineKeyboardMarkup(keyboard) + await update.message.reply_text('Нажмите кнопку ниже, чтобы запустить Zeus Cloud:', reply_markup=reply_markup) + +async def plain_message_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + if update.message and update.message.web_app_data: + logging.info(f"Received web_app_data: {update.message.web_app_data.data}") + await update.message.reply_text(f"Получены данные из Web App: {update.message.web_app_data.data}") if __name__ == '__main__': - if not BOT_TOKEN: logging.critical("BOT_TOKEN is not set. Telegram authentication will FAIL.") - if not ADMIN_TELEGRAM_USER_ID: logging.warning("ADMIN_TELEGRAM_USER_ID is not set. Admin panel will be inaccessible.") - if not HF_TOKEN_WRITE: logging.warning("HF_TOKEN (write access) is not set. File uploads, deletions, and backups will fail.") - if not HF_TOKEN_READ: logging.warning("HF_TOKEN_READ is not set (or HF_TOKEN). Downloads/previews might fail for private repos.") - + if not HF_TOKEN_WRITE: logging.warning("HF_TOKEN (write) not set. Uploads/deletions/backups fail.") + if not HF_TOKEN_READ: logging.warning("HF_TOKEN_READ not set. Using HF_TOKEN. Downloads/previews might fail.") if HF_TOKEN_WRITE: - logging.info("Performing initial database download before starting background backup.") - download_db_from_hf() # Download once at start + download_db_from_hf() threading.Thread(target=periodic_backup, daemon=True).start() - logging.info("Periodic backup thread started.") else: - logging.warning("Periodic backup disabled (HF_TOKEN_WRITE not set).") if HF_TOKEN_READ: download_db_from_hf() - elif not os.path.exists(DATA_FILE): # No tokens and no local 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}") - - app.run(debug=False, host='0.0.0.0', port=int(os.getenv("PORT", 7860))) \ No newline at end of file + else: + if not os.path.exists(DATA_FILE): + with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f) + + bot_app = Application.builder().token(BOT_TOKEN).build() + bot_app.add_handler(CommandHandler("start", start_command)) + bot_app.add_handler(MessageHandler(filters.StatusUpdate.WEB_APP_DATA, plain_message_handler)) + + flask_thread = threading.Thread(target=lambda: app.run(debug=False, host='0.0.0.0', port=7860, use_reloader=False)) + flask_thread.start() + + logging.info("Flask app started. Starting Telegram bot polling.") + bot_app.run_polling() \ No newline at end of file