diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -12,37 +12,111 @@ from werkzeug.utils import secure_filename import requests from io import BytesIO import uuid -import hashlib -import hmac -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_telegram_unique") -DATA_FILE = 'cloudeng_data_telegram.json' -REPO_ID = "Eluza133/Z1e1u" # Replace with your actual repo ID if different +app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_folders_unique_tma") +DATA_FILE = 'cloudeng_data_tma.json' +REPO_ID = "Eluza133/Z1e1u" # Needs to be accessible by the tokens HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE -UPLOAD_FOLDER = 'uploads_telegram' -os.makedirs(UPLOAD_FOLDER, exist_ok=True) - -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.") +TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "6750208873:AAE2hvPlJ99dBdhGa_Brre0IIpUdOvXxHt4") +ADMIN_TELEGRAM_ID = os.getenv("ADMIN_TELEGRAM_ID", "YOUR_ADMIN_TELEGRAM_USER_ID_HERE") # IMPORTANT: Set this! +UPLOAD_FOLDER = 'uploads_tma' +os.makedirs(UPLOAD_FOLDER, exist_ok=True) cache = Cache(app, config={'CACHE_TYPE': 'simple'}) logging.basicConfig(level=logging.INFO) +BASE_STYLE = ''' +:root { + --primary: #ff4d6d; --secondary: #00ddeb; --accent: #8b5cf6; + --background-light: #f5f6fa; --background-dark: #1a1625; + --card-bg: rgba(255, 255, 255, 0.95); --card-bg-dark: rgba(40, 35, 60, 0.95); + --text-light: #2a1e5a; --text-dark: #e8e1ff; --shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + --glass-bg: rgba(255, 255, 255, 0.15); --transition: all 0.3s ease; --delete-color: #ff4444; + --folder-color: #ffc107; +} +* { margin: 0; padding: 0; box-sizing: border-box; } +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(--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: #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: 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: #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) { + .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: 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: 1em; } + .btn { padding: 10px 20px; } +} +''' def find_node_by_id(filesystem, node_id): if not filesystem: return None, None @@ -87,27 +161,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_telegram_id_str): +def initialize_user_filesystem_tma(user_data, tma_user_id_str): if 'filesystem' not in user_data: user_data['filesystem'] = { "type": "folder", "id": "root", "name": "root", "children": [] } - if 'files' in user_data and isinstance(user_data['files'], list): - for old_file in user_data['files']: - file_id = old_file.get('id', uuid.uuid4().hex) - original_filename = old_file.get('filename', 'unknown_file') - name_part, ext_part = os.path.splitext(original_filename) - unique_suffix = uuid.uuid4().hex[:8] - unique_filename = f"{name_part}_{unique_suffix}{ext_part}" - hf_path = f"cloud_files/{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'] + if 'files' in user_data and isinstance(user_data['files'], list): # Migration logic + for old_file in user_data['files']: + file_id = old_file.get('id', uuid.uuid4().hex) + original_filename = old_file.get('filename', 'unknown_file') + name_part, ext_part = os.path.splitext(original_filename) + unique_suffix = uuid.uuid4().hex[:8] + unique_filename = f"{name_part}_{unique_suffix}{ext_part}" + hf_path = f"cloud_files/{tma_user_id_str}/root/{unique_filename}" + file_node = { + 'type': 'file', 'id': file_id, 'original_filename': original_filename, + 'unique_filename': unique_filename, 'path': hf_path, + 'file_type': get_file_type(original_filename), + 'upload_date': old_file.get('upload_date', datetime.now().strftime('%Y-%m-%d %H:%M:%S')) + } + add_node(user_data['filesystem'], 'root', file_node) + del user_data['files'] @cache.memoize(timeout=300) def load_data(): @@ -115,14 +189,12 @@ def load_data(): 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 is not in dict format, initializing empty database") - return {'users': {}} - data.setdefault('users', {}) - 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 + if not isinstance(data, dict): + data = {'users': {}} + data.setdefault('users', {}) + for tma_user_id_str, user_data_item in data['users'].items(): + initialize_user_filesystem_tma(user_data_item, tma_user_id_str) + return data except Exception as e: logging.error(f"Error loading data: {e}") return {'users': {}} @@ -133,15 +205,12 @@ 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 for Telegram users") except Exception as e: logging.error(f"Error saving data: {e}") raise def upload_db_to_hf(): - if not HF_TOKEN_WRITE: - logging.warning("HF_TOKEN_WRITE not set, skipping database upload.") - return + if not HF_TOKEN_WRITE: return try: api = HfApi() api.upload_file( @@ -149,13 +218,11 @@ def upload_db_to_hf(): repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"Backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" ) - logging.info("Database uploaded to Hugging Face") except Exception as e: logging.error(f"Error uploading database: {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) return @@ -164,13 +231,7 @@ def download_db_from_hf(): repo_id=REPO_ID, filename=DATA_FILE, repo_type="dataset", token=HF_TOKEN_READ, local_dir=".", local_dir_use_symlinks=False ) - 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 repository {REPO_ID}. Initializing empty database.") + except (hf_utils.RepositoryNotFoundError, hf_utils.EntryNotFoundError): if not os.path.exists(DATA_FILE): with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f) except Exception as e: @@ -191,292 +252,294 @@ def get_file_type(filename): elif filename_lower.endswith('.txt'): return 'text' return 'other' -BASE_STYLE = ''' -:root { - --primary: #ff4d6d; --secondary: #00ddeb; --accent: #8b5cf6; - --background-light: #f5f6fa; --background-dark: #1a1625; - --card-bg: rgba(255, 255, 255, 0.95); --card-bg-dark: rgba(40, 35, 60, 0.95); - --text-light: #2a1e5a; --text-dark: #e8e1ff; --shadow: 0 10px 30px rgba(0, 0, 0, 0.2); - --glass-bg: rgba(255, 255, 255, 0.15); --transition: all 0.3s ease; --delete-color: #ff4444; - --folder-color: #ffc107; -} -* { margin: 0; padding: 0; box-sizing: border-box; } -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(--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: #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: 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: #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) { - .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: 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: 1em; } - .btn { padding: 10px 20px; } -} -''' +def is_admin_tma(): + if not ADMIN_TELEGRAM_ID or ADMIN_TELEGRAM_ID == "YOUR_ADMIN_TELEGRAM_USER_ID_HERE": + return False + return 'telegram_user_id' in session and session['telegram_user_id'] == ADMIN_TELEGRAM_ID -INITIAL_LOAD_HTML = ''' - - - - - - Zeus Cloud - - - - -
-

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

-
- - - +TMA_ENTRY_HTML = ''' + +Zeus Cloud TMA + +
Загрузка приложения...
+ ''' -def is_telegram_admin(): - user_id = session.get('telegram_user_id') - return user_id in ADMIN_TELEGRAM_IDS - -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('/tma') +def tma_entry_page(): + return render_template_string(TMA_ENTRY_HTML) +@app.route('/') +def root_redirect(): + return redirect(url_for('tma_entry_page')) -@app.route('/', methods=['GET']) -def root_path(): - return render_template_string(INITIAL_LOAD_HTML) -@app.route('/api/telegram_authenticate', methods=['POST']) -def telegram_authenticate(): +@app.route('/auth_via_telegram', methods=['POST']) +def auth_via_telegram(): try: payload = request.json - init_data_str = payload.get('initData') - user_info = payload.get('user') - - if not init_data_str or not user_info or not user_info.get('id'): - return jsonify({'status': 'error', 'message': 'Отсутствуют данные для аутентификации'}), 400 - - 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 + tg_user_data = payload.get('user') + # init_data_str = payload.get('initData') # For future validation if needed + if not tg_user_data or not tg_user_data.get('id'): + return jsonify({'status': 'error', 'message': 'Отсутствуют данные пользователя Telegram.'}), 400 - 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) + tma_user_id_str = str(tg_user_data['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, + + if tma_user_id_str not in data['users']: + data['users'][tma_user_id_str] = { + 'telegram_id': tg_user_data['id'], + 'telegram_username': tg_user_data.get('username'), + 'first_name': tg_user_data.get('first_name'), + 'last_name': tg_user_data.get('last_name'), 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - 'filesystem': { "type": "folder", "id": "root", "name": "root", "children": [] } + 'filesystem': {"type": "folder", "id": "root", "name": "root", "children": []} } - initialize_user_filesystem(data['users'][user_id_str], user_id_str) + initialize_user_filesystem_tma(data['users'][tma_user_id_str], tma_user_id_str) try: save_data(data) - logging.info(f"New Telegram user {user_id_str} registered.") except Exception as e: - logging.error(f"Error saving data for new Telegram user {user_id_str}: {e}") - return jsonify({'status': 'error', 'message': 'Ошибка сохранения данных пользователя.'}), 500 + logging.error(f"Save data error for new TMA user {tma_user_id_str}: {e}") + return jsonify({'status': 'error', 'message': 'Ошибка сохранения данных нового пользователя.'}), 500 + + session['telegram_user_id'] = tma_user_id_str + display_name = tg_user_data.get('first_name') or tg_user_data.get('username') or f"User {tma_user_id_str}" + session['telegram_display_name'] = display_name - return jsonify({'status': 'success'}) + if is_admin_tma(): + session['is_admin_flag'] = True # Optional flag + + return jsonify({'status': 'success', 'redirect_url': url_for('tma_dashboard')}) + except Exception as e: - logging.error(f"Error in /api/telegram_authenticate: {e}") - return jsonify({'status': 'error', 'message': str(e)}), 500 + logging.error(f"Error in auth_via_telegram: {e}") + return jsonify({'status': 'error', 'message': 'Внутренняя ошибка сервера при авторизации.'}), 500 +TMA_DASHBOARD_HTML_TEMPLATE = ''' + +Zeus Cloud + +
+

Zeus Cloud

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

+{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %} + {% for category, message in messages %}
{{ message }}
{% endfor %} +{% endif %}{% endwith %} + +
+
+ + + +
+
+
+ + + +
+
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 }} + {% 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 %} +
+Выйти (очистить сессию) +{% if is_admin %} +Админ-панель +{% endif %} +
+ + +''' -@app.route('/app', methods=['GET', 'POST']) -def main_app_view_and_upload(): +@app.route('/tma_dashboard', methods=['GET', 'POST']) +def tma_dashboard(): if 'telegram_user_id' not in session: - flash('Пожалуйста, пройдите аутентификацию через Telegram.') - return redirect(url_for('root_path')) + flash('Пожалуйста, авторизуйтесь через Telegram.', 'error') + return redirect(url_for('tma_entry_page')) - user_telegram_id = session['telegram_user_id'] - user_telegram_id_str = str(user_telegram_id) - + tma_user_id = session['telegram_user_id'] + display_name = session.get('telegram_display_name', 'Пользователь') data = load_data() - if user_telegram_id_str not in data['users']: + + if tma_user_id not in data['users']: session.clear() - flash('Пользователь не найден!') - return redirect(url_for('root_path')) + flash('Пользователь не найден в системе. Пожалуйста, перезапустите приложение.', 'error') + return redirect(url_for('tma_entry_page')) - user_data = data['users'][user_telegram_id_str] - if 'filesystem' not in user_data: - initialize_user_filesystem(user_data, user_telegram_id_str) + user_data = data['users'][tma_user_id] + initialize_user_filesystem_tma(user_data, tma_user_id) # Ensure filesystem exists current_folder_id = request.args.get('folder_id', 'root') - current_folder, parent_folder = find_node_by_id(user_data['filesystem'], current_folder_id) + current_folder, _ = find_node_by_id(user_data['filesystem'], current_folder_id) if not current_folder or current_folder.get('type') != 'folder': flash('Папка не найдена!', 'error') current_folder_id = 'root' - current_folder, parent_folder = find_node_by_id(user_data['filesystem'], current_folder_id) - if not current_folder: - logging.error(f"CRITICAL: Root folder not found for user {user_telegram_id_str}") + current_folder, _ = find_node_by_id(user_data['filesystem'], current_folder_id) + if not current_folder: # Should not happen if init is correct flash('Критическая ошибка: корневая папка не найдена.', 'error') session.clear() - return redirect(url_for('root_path')) - + return redirect(url_for('tma_entry_page')) + 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('main_app_view_and_upload', folder_id=current_folder_id)) + return redirect(url_for('tma_dashboard', folder_id=current_folder_id)) files = request.files.getlist('files') if not files or all(not f.filename for f in files): flash('Файлы для загрузки не выбраны.', 'error') - return redirect(url_for('main_app_view_and_upload', folder_id=current_folder_id)) - + return redirect(url_for('tma_dashboard', folder_id=current_folder_id)) if len(files) > 20: flash('Максимум 20 файлов за раз!', 'error') - return redirect(url_for('main_app_view_and_upload', folder_id=current_folder_id)) + return redirect(url_for('tma_dashboard', 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('main_app_view_and_upload')) + return redirect(url_for('tma_dashboard')) api = HfApi() uploaded_count = 0 - errors = [] - - for file_in_request in files: - if file_in_request and file_in_request.filename: - original_filename = secure_filename(file_in_request.filename) + errors_list = [] + for file in files: + if file and file.filename: + original_filename = secure_filename(file.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/{user_telegram_id_str}/{target_folder_id}/{unique_filename}" + hf_path = f"cloud_files/{tma_user_id}/{target_folder_id}/{unique_filename}" temp_path = os.path.join(UPLOAD_FOLDER, f"{file_id}_{unique_filename}") try: - file_in_request.save(temp_path) + file.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 {user_telegram_id_str} uploaded {original_filename} to folder {target_folder_id}" + commit_message=f"UserTMA {tma_user_id} uploaded {original_filename} to folder {target_folder_id}" ) file_info = { 'type': 'file', 'id': file_id, 'original_filename': original_filename, @@ -487,13 +550,11 @@ def main_app_view_and_upload(): if add_node(user_data['filesystem'], target_folder_id, file_info): uploaded_count += 1 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 {user_telegram_id_str}") + errors_list.append(f"Ошибка добавления метаданных для {original_filename}.") 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 {user_telegram_id_str}: {e}") - errors.append(f"Ошибка загрузки файла {original_filename}: {e}") + errors_list.append(f"Ошибка загрузки файла {original_filename}: {e}") finally: if os.path.exists(temp_path): os.remove(temp_path) if uploaded_count > 0: @@ -501,492 +562,398 @@ def main_app_view_and_upload(): save_data(data) flash(f'{uploaded_count} файл(ов) успешно загружено!') except Exception as e: - flash('Файлы загру��ены на сервер, но произошла ошибка сохранения метаданных.', 'error') - 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('main_app_view_and_upload', folder_id=target_folder_id)) + flash('Файлы загружены, но ошибка сохранения метаданных.', 'error') + if errors_list: + for error_msg in errors_list: flash(error_msg, 'error') + return redirect(url_for('tma_dashboard', folder_id=target_folder_id)) breadcrumbs = [] temp_id = current_folder_id while temp_id: - node, parent = find_node_by_id(user_data['filesystem'], temp_id) + node, parent_node_bc = find_node_by_id(user_data['filesystem'], temp_id) if not node: break is_link = (node['id'] != current_folder_id) breadcrumbs.append({'id': node['id'], 'name': node.get('name', 'Root'), 'is_link': is_link}) - if not parent: break - temp_id = parent.get('id') + if not parent_node_bc: break + temp_id = parent_node_bc.get('id') breadcrumbs.reverse() - html = ''' - -Zeus Cloud - -
-

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 %} - -
-
- - - -
-
-
- - - -
-
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 }} - {% 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 = { - '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) + return render_template_string(TMA_DASHBOARD_HTML_TEMPLATE, + display_name=display_name, items=items_in_folder, + current_folder_id=current_folder_id, current_folder=current_folder, + breadcrumbs=breadcrumbs, repo_id_js=REPO_ID, HF_TOKEN_READ_js=HF_TOKEN_READ, + hf_file_url_jinja=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{path}{'?download=true' if download else ''}", + is_admin=is_admin_tma()) + -@app.route('/api/create_folder', methods=['POST']) -def create_folder_api(): - if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован'}), 401 - user_telegram_id_str = str(session['telegram_user_id']) +@app.route('/create_folder_tma', methods=['POST']) +def create_folder_tma(): + if 'telegram_user_id' not in session: + return jsonify({'status': 'error', 'message': 'Не авторизован'}), 401 + tma_user_id = session['telegram_user_id'] data = load_data() - user_data = data['users'].get(user_telegram_id_str) + user_data = data['users'].get(tma_user_id) if not user_data: return jsonify({'status': 'error', 'message': 'Пользователь не найден'}), 404 parent_folder_id = request.form.get('parent_folder_id', 'root') folder_name = request.form.get('folder_name', '').strip() - if not folder_name: flash('Имя папки не может быть пустым!', 'error') - 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)) + return redirect(url_for('tma_dashboard', folder_id=parent_folder_id)) folder_id = uuid.uuid4().hex - folder_data = {'type': 'folder', 'id': folder_id, 'name': folder_name, 'children': [] } + 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): - 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')) + try: + save_data(data) + flash(f'Папка "{folder_name}" успешно создана.') + except Exception as e: flash('Ошибка сохранения данных при создании папки.', 'error') + else: + flash('Не удалось найти родительскую папку.', 'error') + return redirect(url_for('tma_dashboard', folder_id=parent_folder_id)) + +@app.route('/download_tma/') +def download_tma(file_id): + current_tma_user_id = session.get('telegram_user_id') + if not current_tma_user_id: + flash('Пожалуйста, авторизуйтесь.', 'error') + return redirect(url_for('tma_entry_page')) data = load_data() file_node = None - 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) + + user_data = data['users'].get(current_tma_user_id) + 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 not file_node and is_admin_tma(): + for uid_str, udata_iter in data.get('users', {}).items(): + node, _ = find_node_by_id(udata_iter.get('filesystem', {}), file_id) if node and node.get('type') == 'file': - file_node = node; user_context_id_str = uid_str; break + file_node = node + break 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')) + flash('Файл не найден или доступ запрещен!', 'error') + return redirect(request.referrer or url_for('tma_dashboard')) hf_path = file_node.get('path') original_filename = file_node.get('original_filename', 'downloaded_file') if not hf_path: flash('Ошибка: Путь к файлу не найден.', 'error') - return redirect(request.referrer or url_for('main_app_view_and_upload' if 'telegram_user_id' in session else 'root_path')) + return redirect(request.referrer or url_for('tma_dashboard')) 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() - return send_file(BytesIO(response.content), as_attachment=True, download_name=original_filename, mimetype='application/octet-stream') + 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) except Exception as e: - 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('/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']) + flash(f'Ошибка скачивания файла: {e}', 'error') + return redirect(request.referrer or url_for('tma_dashboard')) + +@app.route('/delete_file_tma/', methods=['POST']) +def delete_file_tma(file_id): + if 'telegram_user_id' not in session: + flash('Пожалуйста, авторизуйтесь.', 'error') + return redirect(url_for('tma_entry_page')) + tma_user_id = 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')) + user_data = data['users'].get(tma_user_id) + if not user_data: + session.clear(); flash('Пользователь не найден.', 'error'); return redirect(url_for('tma_entry_page')) - file_node, parent_node = find_node_by_id(user_data['filesystem'], file_id) + file_node, _ = find_node_by_id(user_data['filesystem'], file_id) current_view_folder_id = request.form.get('current_view_folder_id', 'root') + if not file_node or file_node.get('type') != 'file': + flash('Файл не найден.', 'error') + return redirect(url_for('tma_dashboard', folder_id=current_view_folder_id)) - if not file_node or file_node.get('type') != 'file' or not parent_node: - flash('Файл не найден или не может быть удален.', 'error') - 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', 'файл') + 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)) + try: save_data(data); flash(f'Метаданные файла {original_filename} удалены (путь отсутствовал).') + except Exception as e: flash('Ошибка сохранения данных после удаления метаданных.', 'error') + return redirect(url_for('tma_dashboard', 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)) + if not HF_TOKEN_WRITE: + flash('Удаление невозможно: токен для записи не настроен.', 'error') + return redirect(url_for('tma_dashboard', 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}") + api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) 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 Exception as e: flash('Файл удален с сервера, но ошибка обновления базы.', 'error') + 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']) + except Exception as e: flash('Ошибка сохранения (файл не на сервере).', 'error') + except Exception as e: + flash(f'Ошибка удаления файла {original_filename}: {e}', 'error') + return redirect(url_for('tma_dashboard', folder_id=current_view_folder_id)) + +@app.route('/delete_folder_tma/', methods=['POST']) +def delete_folder_tma(folder_id): + if 'telegram_user_id' not in session: + flash('Пожалуйста, авторизуйтесь.', 'error'); return redirect(url_for('tma_entry_page')) + if folder_id == 'root': + flash('Нельзя удалить корневую папку!', 'error'); return redirect(url_for('tma_dashboard')) + tma_user_id = 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')) - + user_data = data['users'].get(tma_user_id) + if not user_data: + session.clear(); flash('Пользователь не найден.', 'error'); return redirect(url_for('tma_entry_page')) + folder_node, parent_node = find_node_by_id(user_data['filesystem'], folder_id) 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') - return redirect(url_for('main_app_view_and_upload', folder_id=current_view_folder_id)) - folder_name = folder_node.get('name', 'папка') + flash('Папка не найдена.', 'error') + return redirect(url_for('tma_dashboard', folder_id=current_view_folder_id)) 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)) + flash(f'Папку "{folder_node.get("name")}" можно удалить только если она пуста.', 'error') + return redirect(url_for('tma_dashboard', 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}") + try: save_data(data); flash(f'Папка "{folder_node.get("name")}" удалена.') + except Exception as e: flash('Ошибка сохранения после удаления папки.', 'error') 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): - 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) + return redirect(url_for('tma_dashboard', folder_id=parent_node.get('id', 'root'))) +@app.route('/get_text_content_tma/') +def get_text_content_tma(file_id): + current_tma_user_id = session.get('telegram_user_id') + if not current_tma_user_id: return Response("Не авторизован", status=401) + data = load_data() file_node = None - user_context_id_str = str(session.get('telegram_user_id')) if 'telegram_user_id' in session else None + user_data = data['users'].get(current_tma_user_id) + if user_data: + file_node, _ = find_node_by_id(user_data['filesystem'], file_id) - 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 not file_node and is_admin_tma(): + for uid_str, udata_iter in data.get('users', {}).items(): + node, _ = find_node_by_id(udata_iter.get('filesystem', {}), file_id) if node and node.get('type') == 'file' and node.get('file_type') == 'text': file_node = node; break - if not file_node or file_node.get('type') != 'file' or file_node.get('file_type') != 'text': 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); response.raise_for_status() + 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') + except UnicodeDecodeError: text_content = response.content.decode('latin-1', errors='ignore') return Response(text_content, mimetype='text/plain') - except Exception as e: logging.error(f"Error fetching text from HF ({hf_path}): {e}"); return Response(f"Ошибка: {e}", status=502) + except Exception as e: return Response(f"Ошибка загрузки: {e}", status=502) +@app.route('/tma_logout') +def tma_logout(): + session.clear() + flash('Вы вышли из сессии приложения.') + return redirect(url_for('tma_entry_page')) -@app.route('/admhosto') -def admin_panel(): - 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_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 = ''' +ADMIN_PANEL_HTML_TMA_TEMPLATE = ''' Админ-панель

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

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

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

-{% 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 }}

-
-
+{% for user in user_details %} +
+ {{ user.display_name }} (ID: {{user.id_key}}) +

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

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

+
+ +
+
{% else %}

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

{% endfor %}
''' - return render_template_string(html, user_details=user_details) - -@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 }} (ID: {{ user_telegram_id_str }})

-Назад к пользователям + +@app.route('/admhosto') +def admin_panel_tma(): + if not is_admin_tma(): + flash('Доступ запрещен.', 'error'); return redirect(url_for('tma_dashboard')) + data = load_data() + users = data.get('users', {}) + user_details = [] + for tma_id_str, udata in users.items(): + file_count = 0 + q = [udata.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({ + 'id_key': tma_id_str, + 'display_name': udata.get('first_name', udata.get('telegram_username', f"User {tma_id_str}")), + 'created_at': udata.get('created_at', 'N/A'), 'file_count': file_count + }) + return render_template_string(ADMIN_PANEL_HTML_TMA_TEMPLATE, user_details=user_details) + +ADMIN_USER_FILES_HTML_TMA_TEMPLATE = ''' + +Файлы {{ display_name_admin_view }} +

Файлы пользователя: {{ display_name_admin_view }} (ID: {{ tma_user_id_str_admin_view }})

+Назад к пользователям {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}
{{ message }}
{% endfor %}{% endif %}{% endwith %}
-{% 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' %}
📝
+{% 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_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_item_val.file_type in ['image', 'video', 'pdf', 'text'] %} - {% if previewable %}{% endif %} -
-
-{% else %}

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

{% endfor %}
- +

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

+

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

+

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

+

ID: {{ file.id }}

+

Path: {{ file.path }}

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

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

{% endfor %}
+ ''' - 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(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')) +@app.route('/admhosto/user/') +def admin_user_files_tma(tma_user_id_str): + if not is_admin_tma(): + flash('Доступ запрещен.', 'error'); return redirect(url_for('tma_dashboard')) + data = load_data() + user_data = data.get('users', {}).get(tma_user_id_str) + if not user_data: + flash(f'Пользователь ID {tma_user_id_str} не найден.', 'error'); return redirect(url_for('admin_panel_tma')) + + all_files = [] + def collect_files_admin(folder, current_path_id='root'): + parent_path_str = get_node_path_string(user_data['filesystem'], current_path_id) + for item in folder.get('children', []): + if item.get('type') == 'file': item['parent_path_str'] = parent_path_str; all_files.append(item) + elif item.get('type') == 'folder': collect_files_admin(item, item.get('id')) + collect_files_admin(user_data.get('filesystem', {})) + all_files.sort(key=lambda x: x.get('upload_date', ''), reverse=True) + + display_name_admin_view = user_data.get('first_name', user_data.get('telegram_username', f"User {tma_user_id_str}")) + return render_template_string(ADMIN_USER_FILES_HTML_TMA_TEMPLATE, + tma_user_id_str_admin_view=tma_user_id_str, display_name_admin_view=display_name_admin_view, files=all_files, + repo_id_js_admin=REPO_ID, + hf_file_url_jinja=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_tma(tma_user_id_str): + if not is_admin_tma(): + flash('Доступ запрещен.', 'error'); return redirect(url_for('tma_dashboard')) + if not HF_TOKEN_WRITE: + flash('Удаление невозможно: токен для записи не настроен.', 'error'); return redirect(url_for('admin_panel_tma')) data = load_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}.") + if tma_user_id_str not in data['users']: + flash('Пользователь не найден!', 'error'); return redirect(url_for('admin_panel_tma')) try: - 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}") + api = HfApi() + user_folder_path_on_hf = f"cloud_files/{tma_user_id_str}" + api.delete_folder(folder_path=user_folder_path_on_hf, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) 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. 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')) + if e.response.status_code != 404: + flash(f'Ошибка удаления файлов пользователя {tma_user_id_str} с сервера: {e}.', 'error'); return redirect(url_for('admin_panel_tma')) + except Exception as e: + flash(f'Ошибка удаления файлов {tma_user_id_str} с сервера: {e}.', 'error'); return redirect(url_for('admin_panel_tma')) try: - 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(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', 'файл') + del data['users'][tma_user_id_str] + save_data(data) + flash(f'Пользователь {tma_user_id_str} и его файлы удалены.') + except Exception as e: + flash(f'Файлы удалены, но ошибка удаления из базы: {e}', 'error') + return redirect(url_for('admin_panel_tma')) + +@app.route('/admhosto/delete_file//', methods=['POST']) +def admin_delete_file_tma(tma_user_id_str_form, file_id): + if not is_admin_tma(): + flash('Доступ запрещен.', 'error'); return redirect(url_for('tma_dashboard')) + if not HF_TOKEN_WRITE: + flash('Удаление невозможно: токен не настроен.', 'error'); return redirect(url_for('admin_user_files_tma', tma_user_id_str=tma_user_id_str_form)) + data = load_data() + user_data = data.get('users', {}).get(tma_user_id_str_form) + if not user_data: + flash(f'Пользователь {tma_user_id_str_form} не найден.', 'error'); return redirect(url_for('admin_panel_tma')) + file_node, _ = find_node_by_id(user_data['filesystem'], file_id) + if not file_node or file_node.get('type') != 'file': + flash('Файл не найден.', 'error'); return redirect(url_for('admin_user_files_tma', tma_user_id_str=tma_user_id_str_form)) + 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)) + except Exception as e: flash('Ошибка сохранения (путь отсутствовал).', 'error') + return redirect(url_for('admin_user_files_tma', tma_user_id_str=tma_user_id_str_form)) 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"ADMIN: Deleted {file_id} for {user_telegram_id_str}") + api = HfApi() + api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) 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 Exception as e: 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"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}") + except Exception as e: flash('Ошибка сохранения (файл не на сервере).', 'error') + except Exception as e: + flash(f'Ошибка удаления файла {original_filename}: {e}', 'error') + return redirect(url_for('admin_user_files_tma', tma_user_id_str=tma_user_id_str_form)) if __name__ == '__main__': - 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 not HF_TOKEN_WRITE: logging.warning("HF_TOKEN (write access) is not set. Uploads/deletions/backups will fail.") + if not HF_TOKEN_READ: logging.warning("HF_TOKEN_READ is not set. Using HF_TOKEN. Downloads/previews might fail for private repos if HF_TOKEN also lacks read.") + if ADMIN_TELEGRAM_ID == "YOUR_ADMIN_TELEGRAM_USER_ID_HERE": logging.warning("ADMIN_TELEGRAM_ID is not set. Admin panel will not be accessible.") + if HF_TOKEN_WRITE: download_db_from_hf() threading.Thread(target=periodic_backup, daemon=True).start() + elif HF_TOKEN_READ: + download_db_from_hf() else: - if HF_TOKEN_READ: download_db_from_hf() - else: - if not os.path.exists(DATA_FILE): - with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, 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 + if not os.path.exists(DATA_FILE): + with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f) + + app.run(debug=False, host='0.0.0.0', port=7860) \ No newline at end of file