diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -14,45 +14,146 @@ from io import BytesIO import uuid import hmac import hashlib +from urllib.parse import unquote + +from telegram import Update, WebAppInfo, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes + +# --- CONFIGURATION --- +BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "6750208873:AAE2hvPlJ99dBdhGa_Brre0IIpUdOvXxHt4") +ADMIN_TELEGRAM_ID = os.getenv("ADMIN_TELEGRAM_ID", "YOUR_ADMIN_TELEGRAM_ID_HERE") # Replace with actual admin Telegram ID +FLASK_SECRET_KEY = os.getenv("FLASK_SECRET_KEY", "supersecretkey_telegram_mini_app_unique") +DATA_FILE = 'cloudeng_tg_data.json' +REPO_ID = os.getenv("HF_REPO_ID", "Eluza133/Z1e1u") # Default to your repo +HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE") +HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE +UPLOAD_FOLDER = 'uploads_tg' +WEB_APP_URL = os.getenv("WEB_APP_URL") # e.g., https://yourdomain.com +os.makedirs(UPLOAD_FOLDER, exist_ok=True) +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) app = Flask(__name__) -app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_folders_unique_telegram") -DATA_FILE = 'cloudeng_data_tg.json' -REPO_ID = os.getenv("HF_REPO_ID", "Eluza133/Z1e1u") # Ensure this is set in your environment -HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE") # Ensure this is set -HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE # Ensure this is set -ADMIN_TELEGRAM_IDS = os.getenv("ADMIN_TELEGRAM_IDS", "").split(',') # Comma-separated string of admin Telegram IDs -BOT_TOKEN = "6750208873:AAE2hvPlJ99dBdhGa_Brre0IIpUdOvXxHt4" +app.secret_key = FLASK_SECRET_KEY +cache = Cache(app, config={'CACHE_TYPE': 'simple'}) -UPLOAD_FOLDER = 'uploads_tg' -os.makedirs(UPLOAD_FOLDER, exist_ok=True) +# --- STYLES --- +BASE_STYLE = ''' +:root { + --primary: #0088cc; --secondary: #00ab6c; --accent: #536de6; + --background-light: #ffffff; --background-dark: #1c1c1e; + --card-bg: rgba(240, 240, 245, 0.95); --card-bg-dark: rgba(44, 44, 46, 0.95); + --text-light: #000000; --text-dark: #ffffff; --shadow: 0 8px 25px rgba(0, 0, 0, 0.15); + --glass-bg: rgba(200, 200, 200, 0.2); --transition: all 0.3s ease; --delete-color: #ff3b30; + --folder-color: #ff9500; +} +* { margin: 0; padding: 0; box-sizing: border-box; } +body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: var(--background-light); color: var(--text-light); line-height: 1.5; -webkit-font-smoothing: antialiased; } +body.dark { background: var(--background-dark); color: var(--text-dark); } +.container { margin: 0 auto; max-width: 100%; min-height: 100vh; padding: 15px; background: var(--background-light); overflow-x: hidden; } +body.dark .container { background: var(--background-dark); } +h1 { font-size: 1.8em; font-weight: 700; text-align: center; margin-bottom: 20px; color: var(--primary); } +h2 { font-size: 1.4em; margin-top: 25px; margin-bottom:10px; color: var(--text-light); } +body.dark h2 { color: var(--text-dark); } +h4 { font-size: 1em; margin-top: 12px; margin-bottom: 4px; 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 #ccc; border-radius: 10px; background: var(--glass-bg); color: var(--text-light); font-size: 1em; } +body.dark input, body.dark textarea { border-color: #444; color: var(--text-dark); background: rgba(70,70,70,0.3); } +input:focus, textarea:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 2px var(--primary); } +.btn { padding: 12px 24px; background: var(--primary); color: white; border: none; border-radius: 10px; cursor: pointer; font-size: 1em; font-weight: 600; transition: var(--transition); box-shadow: 0 4px 10px rgba(0,0,0,0.1); display: inline-block; text-decoration: none; margin-top: 4px; margin-right: 4px; text-align: center; } +.btn:hover { transform: translateY(-2px); background: #0077b3; box-shadow: 0 6px 15px rgba(0,0,0,0.15); } +.download-btn { background: var(--secondary); } +.download-btn:hover { background: #00965e; } +.delete-btn { background: var(--delete-color); } +.delete-btn:hover { background: #e03024; } +.folder-btn { background: var(--folder-color); } +.folder-btn:hover { background: #e08300; } +.flash { color: var(--primary); text-align: center; margin-bottom: 12px; padding: 8px; background: rgba(0, 136, 204, 0.1); border-radius: 8px; border: 1px solid var(--primary); } +.flash.error { color: var(--delete-color); background: rgba(255, 59, 48, 0.1); border-color: var(--delete-color); } +.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(--card-bg); border-radius: 12px; margin-bottom: 8px; box-shadow: var(--shadow); transition: var(--transition); } +body.dark .user-item { background: var(--card-bg-dark); } +.user-item:hover { transform: translateY(-3px); } +.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: 12px; border-radius: 12px; 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(-3px); } +.item-preview { 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(--folder-color); line-height: 100px; } +.item p { font-size: 0.85em; margin: 4px 0; word-break: break-all; } +.item a { color: var(--primary); text-decoration: none; } +.item a:hover { color: var(--accent); } +.item-actions { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 4px; justify-content: center; } +.item-actions .btn { font-size: 0.8em; padding: 6px 10px; } +.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); z-index: 2000; justify-content: center; align-items: center; } +.modal-content { max-width: 95%; max-height: 95%; background: var(--background-light); padding: 10px; border-radius: 12px; 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: 8px; } +.modal iframe { width: 90vw; height: 85vh; border: none; } +.modal pre { background: #eee; color: #333; padding: 12px; border-radius: 6px; 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: 10px; right: 15px; font-size: 24px; color: #aaa; cursor: pointer; background: rgba(0,0,0,0.3); border-radius: 50%; width: 28px; height: 28px; line-height: 28px; text-align: center; } +body.dark .modal-close-btn { color: #555; background: rgba(255,255,255,0.15); } +#progress-container { width: 100%; background: var(--glass-bg); border-radius: 8px; margin: 12px 0; display: none; position: relative; height: 18px; } +#progress-bar { width: 0%; height: 100%; background: var(--primary); border-radius: 8px; transition: width 0.3s ease; } +#progress-text { position: absolute; width: 100%; text-align: center; line-height: 18px; color: var(--text-dark); font-size: 0.8em; font-weight: bold; text-shadow: 1px 1px 1px rgba(0,0,0,0.2); } +.breadcrumbs { margin-bottom: 15px; font-size: 1em; } +.breadcrumbs a { color: var(--accent); text-decoration: none; } +.breadcrumbs a:hover { text-decoration: underline; } +.breadcrumbs span { margin: 0 4px; color: #999; } +.folder-actions { margin-top: 15px; margin-bottom: 8px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } +.folder-actions input[type=text] { width: auto; flex-grow: 1; margin: 0; min-width: 120px; } +.folder-actions .btn { margin: 0; flex-shrink: 0;} +#auth-message { text-align: center; padding: 20px; font-size: 1.2em; } +@media (max-width: 480px) { + .container { padding: 10px; } + .file-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 10px; } + .item-preview { height: 80px; } + .item.folder .item-preview { font-size: 40px; line-height: 80px; } + h1 { font-size: 1.6em; } + .btn { padding: 10px 20px; font-size: 0.9em; } + .item-actions .btn { padding: 5px 8px; font-size: 0.75em;} + .folder-actions { flex-direction: column; align-items: stretch; } +} +''' -cache = Cache(app, config={'CACHE_TYPE': 'simple'}) -logging.basicConfig(level=logging.INFO) +# --- HELPER FUNCTIONS --- +def verify_telegram_data(init_data_str, bot_token_val): + if not init_data_str or not bot_token_val: + return False + + parsed_data = {} + for pair in init_data_str.split('&'): + if '=' in pair: + key, value = pair.split('=', 1) + parsed_data[key] = unquote(value) + + received_hash = parsed_data.pop('hash', None) + if not received_hash: + return False + + data_check_arr = [f"{key}={value}" for key, value in sorted(parsed_data.items())] + data_check_string = "\n".join(data_check_arr) + + secret_key = hmac.new("WebAppData".encode(), bot_token_val.encode(), hashlib.sha256).digest() + calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest() + + return calculated_hash == received_hash -def verify_telegram_auth(init_data_str, bot_token): - try: - params = dict(item.split("=", 1) for item in init_data_str.split("&")) - hash_received = params.pop("hash", None) - if not hash_received: - return None - - data_check_arr = [] - for key, value in sorted(params.items()): - data_check_arr.append(f"{key}={value}") - data_check_string = "\n".join(data_check_arr) - - secret_key = hmac.new("WebAppData".encode(), bot_token.encode(), hashlib.sha256).digest() - calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest() - - if calculated_hash == hash_received: - user_data = json.loads(params.get("user", "{}")) - return user_data - return None - except Exception as e: - logging.error(f"Telegram auth verification error: {e}") - return None +def get_tg_user_display_name(tg_user_obj): + if not tg_user_obj: return "Unknown User" + first_name = tg_user_obj.get('first_name', '') + last_name = tg_user_obj.get('last_name', '') + username = tg_user_obj.get('username') + + if first_name and last_name: return f"{first_name} {last_name}" + if first_name: return first_name + if username: return username + return f"User {tg_user_obj.get('id')}" def find_node_by_id(filesystem, node_id): if not filesystem: return None, None @@ -62,7 +163,7 @@ def find_node_by_id(filesystem, node_id): while queue: current_node, parent = queue.pop(0) if current_node.get('type') == 'folder' and 'children' in current_node: - for i, child in enumerate(current_node['children']): + for child in current_node['children']: if child.get('id') == node_id: return child, current_node if child.get('type') == 'folder': @@ -83,8 +184,13 @@ def remove_node(filesystem, node_id): if node_to_remove and parent_node and 'children' in parent_node: parent_node['children'] = [child for child in parent_node['children'] if child.get('id') != node_id] return True + # Handle root node removal attempt if it's the target (should not happen for files/folders within root) + elif node_to_remove and node_id == filesystem.get('id') and not parent_node: + logger.error("Attempted to remove root node itself via remove_node, which is not supported this way.") + return False # Or handle as a special case if needed return False + def get_node_path_string(filesystem, node_id): path_list = [] current_id = node_id @@ -97,18 +203,11 @@ 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_if_needed(user_data, user_id_for_path_unused): - if 'filesystem' not in user_data or not isinstance(user_data['filesystem'], dict): +def initialize_user_filesystem(user_data): + 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 'files' list migration logic as it's complex with TG ID change - # and relied on session['username'] which is not available globally in load_data. - # New users will get the basic structure. Existing data might need manual migration if format is very old. - @cache.memoize(timeout=300) def load_data(): @@ -116,16 +215,15 @@ 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_id_str, user_data_item in data['users'].items(): - initialize_user_filesystem_if_needed(user_data_item, user_id_str) - logging.info("Data successfully loaded and initialized for TG app") - return data + if not isinstance(data, dict): + data = {'users': {}} + data.setdefault('users', {}) + for user_id, user_data_val in data['users'].items(): + initialize_user_filesystem(user_data_val) + logger.info("Data successfully loaded and initialized") + return data except Exception as e: - logging.error(f"Error loading data: {e}") + logger.error(f"Error loading data: {e}") return {'users': {}} def save_data(data): @@ -134,32 +232,29 @@ 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 TG app") + logger.info("Data saved and uploaded to HF") except Exception as e: - logging.error(f"Error saving data: {e}") + logger.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.") + logger.warning("HF_TOKEN_WRITE not set, skipping database upload.") return try: api = HfApi() api.upload_file( - path_or_fileobj=DATA_FILE, - path_in_repo=DATA_FILE, - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=f"Backup TG App {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + path_or_fileobj=DATA_FILE, path_in_repo=DATA_FILE, repo_id=REPO_ID, + repo_type="dataset", token=HF_TOKEN_WRITE, + commit_message=f"Backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" ) - logging.info("Database uploaded to Hugging Face (TG App)") + logger.info("Database uploaded to Hugging Face") except Exception as e: - logging.error(f"Error uploading database (TG App): {e}") + logger.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.") + logger.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 @@ -168,18 +263,21 @@ 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 (TG App)") + logger.info("Database downloaded from Hugging Face") + except (hf_utils.RepositoryNotFoundError, hf_utils.EntryNotFoundError): + logger.warning(f"{DATA_FILE} or repo {REPO_ID} not found. 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: - logging.error(f"Error downloading database (TG App): {e}") + logger.error(f"Error downloading database: {e}") if not os.path.exists(DATA_FILE): with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f) def periodic_backup(): while True: - time.sleep(1800) # Backup every 30 minutes + time.sleep(1800) upload_db_to_hf() - def get_file_type(filename): filename_lower = filename.lower() if filename_lower.endswith(('.mp4', '.mov', '.avi', '.webm', '.mkv')): return 'video' @@ -188,275 +286,436 @@ 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_user(): + return session.get('telegram_user_id') and str(session.get('telegram_user_id')) == str(ADMIN_TELEGRAM_ID) + +# --- HTML TEMPLATES --- +APP_SHELL_HTML = """ + + + + Zeus Cloud Mini App + + + + + + +
+ {% if not authenticated %} +
Аутентификация через Telegram...
+ + {% else %} + + {{ content | safe if content else "" }} + {% endif %} +
+ +""" + +DASHBOARD_CONTENT_HTML = """ +
+

Zeus Cloud

Пользователь: {{ 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 %} +
+ +
+ + +""" + +ADMIN_PANEL_HTML = """ -Zeus Cloud - Telegram App +Админ-панель - -

Zeus Cloud

-

Это приложение предназначено для использования внутри Telegram.

-

Пожалуйста, откройте его через вашего Telegram бота.

-
Пытаемся авторизоваться...
+ + + +

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

+{% 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 }} (ID: {{user.id}}) +

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

+

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

+
+ +
+{% else %}

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

{% endfor %}
+ +
""" + +ADMIN_USER_FILES_HTML = """ + +Файлы {{ user_display_name }} + + + + +

Файлы пользователя: {{ user_display_name }}

+Назад к пользователям +{% 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(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) + const repoId = "{{ repo_id }}"; function hfFileUrl(path, download=false) { let url = `https://huggingface.co/datasets/${repoId}/resolve/main/${path}`; if (download) url += '?download=true'; return url; } + async function openModal(srcOrUrl, type, itemId) { + const modal = document.getElementById('mediaModal'); const modalContent = document.getElementById('modalContent'); + modalContent.innerHTML = '

Загрузка...

'; modal.style.display = 'flex'; + try { + if (type === 'image') modalContent.innerHTML = `Просмотр изображения`; + else if (type === 'video') modalContent.innerHTML = ``; + else if (type === 'pdf') modalContent.innerHTML = ``; + else if (type === 'text') { + const response = await fetch(srcOrUrl); if (!response.ok) throw new Error(`Ошибка загрузки текста: ${response.statusText}`); + const text = await response.text(); const escapedText = text.replace(/&/g, "&").replace(//g, ">"); + modalContent.innerHTML = `
${escapedText}
`; + } else modalContent.innerHTML = '

Предпросмотр для этого типа файла не поддерживается.

'; + } catch (error) { console.error("Error loading modal content:", error); modalContent.innerHTML = `

Не удалось загрузить содержимое для предпросмотра. ${error.message}

`; } + } + function closeModal(event) { if (event.target === document.getElementById('mediaModal')) closeModalManual(); } + function closeModalManual() { + const modal = document.getElementById('mediaModal'); modal.style.display = 'none'; + const video = modal.querySelector('video'); if (video) video.pause(); + const iframe = modal.querySelector('iframe'); if (iframe) iframe.src = 'about:blank'; + document.getElementById('modalContent').innerHTML = ''; + } + if (window.Telegram && window.Telegram.WebApp) { window.Telegram.WebApp.BackButton.onClick(() => { window.location.href = "{{ url_for('admin_panel') }}"; }); window.Telegram.WebApp.BackButton.show(); } +""" + +# --- FLASK ROUTES --- +@app.route('/') +def index(): + if 'telegram_user_id' in session and 'user_display_name' in session: + return redirect(url_for('dashboard')) + return render_template_string(APP_SHELL_HTML, authenticated=False) -@app.route('/check_auth_status') -def check_auth_status(): - if 'telegram_user_id' in session: - return jsonify({'authenticated': True}) - return jsonify({'authenticated': False}) +@app.route('/telegram_auth_handler', methods=['POST']) +def telegram_auth_handler(): + auth_data = request.json + init_data_str = auth_data.get('initData') + if not verify_telegram_data(init_data_str, BOT_TOKEN): + logger.warning("Telegram data verification failed.") + return jsonify({'status': 'error', 'message': 'Верификация данных не удалась.'}), 403 -@app.route('/perform_telegram_auth', methods=['POST']) -def perform_telegram_auth(): - auth_data = request.json - init_data_str = auth_data.get('init_data') - - if not init_data_str: - return jsonify({'status': 'error', 'message': 'Отсутствуют данные для авторизации (initData).'}), 400 - - tg_user = verify_telegram_auth(init_data_str, BOT_TOKEN) - - if tg_user and 'id' in tg_user: - tg_user_id = str(tg_user['id']) - tg_username = tg_user.get('username', f"user{tg_user_id}") - tg_first_name = tg_user.get('first_name', '') - tg_last_name = tg_user.get('last_name', '') - - data = load_data() - if tg_user_id not in data['users']: - data['users'][tg_user_id] = { - 'telegram_username': tg_username, - 'telegram_first_name': tg_first_name, - 'telegram_last_name': tg_last_name, - 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - 'filesystem': {"type": "folder", "id": "root", "name": "root", "children": []} - } - try: - save_data(data) - except Exception as e: - logging.error(f"Error saving new TG user data {tg_user_id}: {e}") - return jsonify({'status': 'error', 'message': 'Ошибка сохранения данных нового пользователя.'}), 500 - - session['telegram_user_id'] = tg_user_id - session['telegram_username'] = tg_username - session['telegram_first_name'] = tg_first_name - session['telegram_last_name'] = tg_last_name - - logging.info(f"User {tg_username} (ID: {tg_user_id}) authenticated successfully.") - return jsonify({'status': 'success', 'message': 'Авторизация успешна.'}) - else: - logging.warning(f"Telegram authentication failed. Invalid initData or verification issue.") - return jsonify({'status': 'error', 'message': 'Неверные данные авторизации Telegram.'}), 403 + try: + user_data_json = dict(pair.split('=', 1) for pair in init_data_str.split('&') if pair.startswith('user=')) + user_info_str = unquote(user_data_json.get('user', '{}')) + tg_user = json.loads(user_info_str) + except Exception as e: + logger.error(f"Error parsing Telegram user data: {e}") + return jsonify({'status': 'error', 'message': 'Ошибка парсинга данных пользователя.'}), 400 + telegram_user_id = str(tg_user['id']) + user_display_name = get_tg_user_display_name(tg_user) + + session['telegram_user_id'] = telegram_user_id + session['user_display_name'] = user_display_name + session['raw_tg_user_data'] = tg_user -@app.route('/dashboard', methods=['GET', 'POST']) + data = load_data() + if telegram_user_id not in data['users']: + data['users'][telegram_user_id] = { + 'telegram_user_data': tg_user, + 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'filesystem': {"type": "folder", "id": "root", "name": "root", "children": []} + } + initialize_user_filesystem(data['users'][telegram_user_id]) + try: + save_data(data) + except Exception as e: + logger.error(f"Error saving data for new user {telegram_user_id}: {e}") + return jsonify({'status': 'error', 'message': 'Ошибка сохранения данных нового пользователя.'}), 500 + + return jsonify({'status': 'success', 'message': 'Аутентификация успешна.'}) + +@app.route('/dashboard', methods=['GET']) def dashboard(): if 'telegram_user_id' not in session: - flash('Пожалуйста, авторизуйтесь через Telegram!') - return redirect(url_for('index_page')) + return redirect(url_for('index')) - current_tg_user_id = session['telegram_user_id'] - current_tg_username = session['telegram_username'] + telegram_user_id = session['telegram_user_id'] + user_display_name = session.get('user_display_name', 'Пользователь') data = load_data() - if current_tg_user_id not in data['users']: + if telegram_user_id not in data['users']: session.clear() - flash('Пользователь не найден! Пожалуйста, перезайдите.') - return redirect(url_for('index_page')) + flash('Пользователь не найден!', 'error') + return redirect(url_for('index')) - user_data = data['users'][current_tg_user_id] - initialize_user_filesystem_if_needed(user_data, current_tg_user_id) # Ensure filesystem key exists + user_data = data['users'][telegram_user_id] + initialize_user_filesystem(user_data) # 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) @@ -466,84 +725,13 @@ def dashboard(): 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 {current_tg_user_id}") + logger.error(f"CRITICAL: Root folder not found for user {telegram_user_id}") flash('Критическая ошибка: корневая папка не найдена.', 'error') session.clear() - return redirect(url_for('index_page')) - + return redirect(url_for('index')) + 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)) - - 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)) - - if len(files) > 20: - flash('Максимум 20 файлов за раз!', 'error') - return redirect(url_for('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('dashboard')) - - api = HfApi() - uploaded_count = 0 - errors = [] - - for file_item in files: - if file_item and file_item.filename: - original_filename = secure_filename(file_item.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/{current_tg_user_id}/{target_folder_id}/{unique_filename}" - temp_path = os.path.join(UPLOAD_FOLDER, f"{file_id}_{unique_filename}") - - try: - file_item.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 {current_tg_username} (ID: {current_tg_user_id}) uploaded {original_filename} to folder {target_folder_id}" - ) - file_info = { - 'type': 'file', 'id': file_id, 'original_filename': original_filename, - 'unique_filename': unique_filename, 'path': hf_path, 'file_type': get_file_type(original_filename), - 'upload_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S') - } - 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} for user {current_tg_user_id}") - 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 {current_tg_user_id}: {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 {current_tg_user_id}: {e}") - if errors: - for error_msg in errors: flash(error_msg, 'error') - return redirect(url_for('dashboard', folder_id=target_folder_id)) - breadcrumbs = [] temp_id = current_folder_id while temp_id: @@ -552,205 +740,116 @@ def dashboard(): 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') + temp_id = parent.get('id') if parent else None breadcrumbs.reverse() - html = f''' - -Панель управления - Zeus Cloud - -
-

Zeus Cloud

Пользователь: {current_tg_username} (ID: {current_tg_user_id})

-{{% with messages = get_flashed_messages(with_categories=true) %}} - {{% if messages %}} - {{% for category, message in messages %}} -
{{{{ message }}}}
- {{% endfor %}} - {{% endif %}} -{{% endwith %}} + dashboard_html_content = render_template_string( + DASHBOARD_CONTENT_HTML, + 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, + 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(APP_SHELL_HTML, authenticated=True, content=dashboard_html_content) + +@app.route('/dashboard_upload', methods=['POST']) +def dashboard_upload_action(): + if 'telegram_user_id' not in session: + return jsonify({'status': 'error', 'message': 'Not authenticated'}), 401 - + telegram_user_id = session['telegram_user_id'] + data = load_data() + user_data = data['users'][telegram_user_id] -
-
- - - -
-
+ if not HF_TOKEN_WRITE: + flash('Загрузка невозможна: токен для записи не настроен.', 'error') + return redirect(url_for('dashboard', folder_id=request.form.get('current_folder_id', 'root'))) -
- - - -
-
0%
+ files = request.files.getlist('files') + target_folder_id = request.form.get('current_folder_id', 'root') -

Содержимое папки: {{{{ 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 not files or all(not f.filename for f in files): + flash('Файлы для загрузки не выбраны.', 'error') + return redirect(url_for('dashboard', folder_id=target_folder_id)) + + if len(files) > 20: + flash('Максимум 20 файлов за раз!', 'error') + return redirect(url_for('dashboard', folder_id=target_folder_id)) -Выйти -
+ 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')) + + 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) + 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_{telegram_user_id}/{target_folder_id}/{unique_filename}" + temp_path = os.path.join(UPLOAD_FOLDER, f"{file_id}_{unique_filename}") - + try: + file_obj.save(temp_path) + api.upload_file( + path_or_fileobj=temp_path, path_in_repo=hf_path, repo_id=REPO_ID, + repo_type="dataset", token=HF_TOKEN_WRITE, + commit_message=f"User tg_{telegram_user_id} uploaded {original_filename} to folder {target_folder_id}" + ) + file_info = { + 'type': 'file', 'id': file_id, 'original_filename': original_filename, + 'unique_filename': unique_filename, 'path': hf_path, + 'file_type': get_file_type(original_filename), + 'upload_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + if add_node(user_data['filesystem'], target_folder_id, file_info): + uploaded_count += 1 + else: + errors.append(f"Ошибка добавления метаданных для {original_filename}.") + logger.error(f"Failed to add node metadata for {file_id} to {target_folder_id} for tg_user {telegram_user_id}") + 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: logger.error(f"Failed to delete orphaned file {hf_path}: {del_err}") + except Exception as e: + logger.error(f"Error uploading file {original_filename} for tg_user {telegram_user_id}: {e}") + errors.append(f"Ошибка загрузки файла {original_filename}: {e}") + finally: + if os.path.exists(temp_path): os.remove(temp_path) - -''' - template_context = { - 'current_tg_user_id': current_tg_user_id, - 'current_tg_username': current_tg_username, - 'items': items_in_folder, - 'current_folder_id': current_folder_id, - 'current_folder': current_folder, - 'breadcrumbs': breadcrumbs, - 'hf_file_url_jinja': lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{path}{'?download=true' if download else ''}", - 'os': os - } - return render_template_string(html, **template_context) @app.route('/create_folder', methods=['POST']) def create_folder(): - if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован'}), 401 - current_tg_user_id = session['telegram_user_id'] + if 'telegram_user_id' not in session: + return jsonify({'status': 'error', 'message': 'Not authenticated'}), 401 + + telegram_user_id = session['telegram_user_id'] data = load_data() - user_data = data['users'].get(current_tg_user_id) - if not user_data: return jsonify({'status': 'error', 'message': 'Пользователь не найден'}), 404 + user_data = data['users'].get(telegram_user_id) + if not user_data: + return jsonify({'status': 'error', 'message': 'User not found'}), 404 parent_folder_id = request.form.get('parent_folder_id', 'root') folder_name = request.form.get('folder_name', '').strip() @@ -758,14 +857,17 @@ def create_folder(): if not folder_name: flash('Имя папки не может быть пустым!', 'error') return redirect(url_for('dashboard', folder_id=parent_folder_id)) + # Allow more characters in folder names - # if not folder_name.isalnum() and '_' not in folder_name and ' ' not in folder_name: - # flash('Имя папки может содержать буквы, цифры, пробелы и подчеркивания.', 'error') + # if not re.match(r"^[a-zA-Z0-9_.\- ]+$", folder_name): + # flash('Имя папки может содержать буквы, цифры, пробелы и символы ._-', 'error') # return redirect(url_for('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: @@ -773,55 +875,47 @@ def create_folder(): flash(f'Папка "{folder_name}" успешно создана.') except Exception as e: flash('Ошибка сохранения данных при создании папки.', 'error') - logging.error(f"Create folder save error for {current_tg_user_id}: {e}") + logger.error(f"Create folder save error: {e}") else: flash('Не удалось найти родительскую папку.', 'error') - return redirect(url_for('dashboard', folder_id=parent_folder_id)) -def is_current_user_admin(): - return 'telegram_user_id' in session and str(session['telegram_user_id']) in ADMIN_TELEGRAM_IDS + return redirect(url_for('dashboard', folder_id=parent_folder_id)) @app.route('/download/') def download_file(file_id): - user_is_admin_for_this_op = False - if 'telegram_user_id' not in session: - if not is_current_user_admin(): # Admin check even if no session, but this path is unlikely - flash('Пожалуйста, авторизуйтесь через Telegram!') - return redirect(url_for('index_page')) - user_is_admin_for_this_op = True # Admin trying to access without being a regular user - - current_tg_user_id = session.get('telegram_user_id') + can_access = False data = load_data() file_node = None - owner_tg_user_id = None - - if not user_is_admin_for_this_op and current_tg_user_id: - user_data = data['users'].get(current_tg_user_id) + + if 'telegram_user_id' in session: + telegram_user_id = session['telegram_user_id'] + user_data = data['users'].get(telegram_user_id) if user_data: - file_node, _ = find_node_by_id(user_data['filesystem'], file_id) - if file_node: owner_tg_user_id = current_tg_user_id + file_node_user, _ = find_node_by_id(user_data['filesystem'], file_id) + if file_node_user: + file_node = file_node_user + can_access = True - if not file_node and is_current_user_admin(): # If current user is admin and didn't find in own files (or isn't owner) - user_is_admin_for_this_op = True # Confirm admin powers are invoked - logging.info(f"Admin {session.get('telegram_user_id')} searching for file ID {file_id} across all users.") + if not file_node and is_admin_user(): # Admin can download any file + logger.info(f"Admin attempting to download file ID {file_id}") for uid, udata in data.get('users', {}).items(): node, _ = find_node_by_id(udata.get('filesystem', {}), file_id) if node and node.get('type') == 'file': file_node = node - owner_tg_user_id = uid - logging.info(f"Admin found file ID {file_id} belonging to user {owner_tg_user_id}") + can_access = True + logger.info(f"Admin found file ID {file_id} belonging to user {uid}") break - if not file_node or file_node.get('type') != 'file': - flash('Файл не найден!', 'error') - return redirect(request.referrer or url_for('dashboard' if current_tg_user_id else 'index_page')) + if not can_access or not file_node or file_node.get('type') != 'file': + flash('Файл не найден или доступ запрещен!', 'error') + return redirect(request.referrer or url_for('dashboard' if 'telegram_user_id' in session else 'index')) 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('dashboard' if current_tg_user_id else 'index_page')) + flash('Ошибка: Путь к файлу не найден.', 'error') + return redirect(request.referrer or url_for('dashboard' if 'telegram_user_id' in session else 'index')) file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{hf_path}?download=true" try: @@ -832,24 +926,22 @@ def download_file(file_id): file_content = BytesIO(response.content) return send_file(file_content, as_attachment=True, download_name=original_filename, mimetype='application/octet-stream') except Exception as e: - logging.error(f"Error downloading file from HF ({hf_path}): {e}") + logger.error(f"Error downloading file from HF ({hf_path}): {e}") flash(f'Ошибка скачивания файла {original_filename}! ({e})', 'error') - return redirect(request.referrer or url_for('dashboard' if current_tg_user_id else 'index_page')) + return redirect(request.referrer or url_for('dashboard' if 'telegram_user_id' in session else 'index')) + @app.route('/delete_file/', methods=['POST']) def delete_file(file_id): if 'telegram_user_id' not in session: - flash('Пожалуйста, авторизуйтесь!') - return redirect(url_for('index_page')) + flash('Пожалуйста, войдите в систему!') + return redirect(url_for('index')) - current_tg_user_id = session['telegram_user_id'] - current_tg_username = session['telegram_username'] + telegram_user_id = session['telegram_user_id'] data = load_data() - user_data = data['users'].get(current_tg_user_id) + user_data = data['users'].get(telegram_user_id) if not user_data: - session.clear() - flash('Пользователь не найден!') - return redirect(url_for('index_page')) + flash('Пользователь не найден!', 'error'); session.clear(); return redirect(url_for('index')) file_node, parent_node = find_node_by_id(user_data['filesystem'], file_id) current_view_folder_id = request.form.get('current_view_folder_id', 'root') @@ -857,17 +949,17 @@ def delete_file(file_id): if not file_node or file_node.get('type') != 'file' or not parent_node: flash('Файл не найден или не может быть удален.', 'error') return redirect(url_for('dashboard', 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') + 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}") + except Exception as e: flash('Ошибка сохранения.', 'error'); logger.error(f"Delete file metadata save error: {e}") return redirect(url_for('dashboard', folder_id=current_view_folder_id)) - + if not HF_TOKEN_WRITE: flash('Удаление невозможно: токен для записи не настроен.', 'error') return redirect(url_for('dashboard', folder_id=current_view_folder_id)) @@ -875,36 +967,41 @@ def delete_file(file_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 {current_tg_username} (ID: {current_tg_user_id}) deleted file {original_filename} (ID: {file_id})") - logging.info(f"Deleted file {hf_path} from HF Hub for user {current_tg_user_id}") + commit_message=f"User tg_{telegram_user_id} deleted file {original_filename} (ID: {file_id})") + logger.info(f"Deleted file {hf_path} from HF Hub for user tg_{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"Delete file DB update error: {e}") - else: flash('Файл удален с сервера, но не найден в БД.', 'error') + except Exception as e: flash('Файл удален, ошибка обновления базы.', 'error'); logger.error(f"Delete file DB update error: {e}") + else: flash('Файл удален с сервера, но не найден в базе.', 'error') except hf_utils.EntryNotFoundError: - logging.warning(f"File {hf_path} not found on HF Hub for user {current_tg_user_id}. Removing from DB.") + logger.warning(f"File {hf_path} not found on HF Hub. Removing from DB for tg_{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"Delete file metadata save error (HF not found): {e}") - else: flash('Файл не найден ни на сервере, ни в базе данных.', 'error') + except Exception as e: flash('Ошибка сохранения (файл не найден на сервере).', 'error'); logger.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 {current_tg_user_id}: {e}") + logger.error(f"Error deleting file {hf_path} for tg_{telegram_user_id}: {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('index_page')) - if folder_id == 'root': flash('Нельзя удалить корневую папку!', 'error'); return redirect(url_for('dashboard')) + if 'telegram_user_id' not in session: + flash('Пожалуйста, войдите в систему!') + return redirect(url_for('index')) + if folder_id == 'root': + flash('Нельзя удалить корневую папку!', 'error') + return redirect(url_for('dashboard')) - current_tg_user_id = session['telegram_user_id'] + telegram_user_id = session['telegram_user_id'] data = load_data() - user_data = data['users'].get(current_tg_user_id) - if not user_data: session.clear(); flash('Пользователь не найден!'); return redirect(url_for('index_page')) + user_data = data['users'].get(telegram_user_id) + if not user_data: + flash('Пользователь не найден!', 'error'); session.clear(); return redirect(url_for('index')) 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') + current_view_folder_id = request.form.get('current_view_folder_id', parent_node.get('id', 'root') if parent_node else 'root') + if not folder_node or folder_node.get('type') != 'folder' or not parent_node: flash('Папка не найдена или не может быть удалена.', 'error') @@ -916,305 +1013,279 @@ def delete_folder(folder_id): return redirect(url_for('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}") - else: flash('Не удалось удалить папку из базы данных.', 'error') + try: + save_data(data) + flash(f'Пустая папка "{folder_name}" успешно удалена.') + except Exception as e: + flash('Ошибка сохранения данных после удаления папки.', 'error') + logger.error(f"Delete empty folder save error: {e}") + else: + flash('Не удалось удалить папку из базы данных.', 'error') redirect_to_folder_id = parent_node.get('id', 'root') return redirect(url_for('dashboard', folder_id=redirect_to_folder_id)) + @app.route('/get_text_content/') def get_text_content(file_id): - user_is_admin_for_this_op = False - if 'telegram_user_id' not in session: - if not is_current_user_admin(): return Response("Не авторизован", status=401) - user_is_admin_for_this_op = True - - current_tg_user_id = session.get('telegram_user_id') + can_access = False data = load_data() file_node = None - owner_tg_user_id = None - if not user_is_admin_for_this_op and current_tg_user_id: - user_data = data['users'].get(current_tg_user_id) + if 'telegram_user_id' in session: + telegram_user_id = session['telegram_user_id'] + user_data = data['users'].get(telegram_user_id) if user_data: - node, _ = find_node_by_id(user_data['filesystem'], file_id) - if node and node.get('type') == 'file' and node.get('file_type') == 'text': - file_node = node - owner_tg_user_id = current_tg_user_id + file_node_user, _ = find_node_by_id(user_data['filesystem'], file_id) + if file_node_user and file_node_user.get('file_type') == 'text': + file_node = file_node_user + can_access = True - if not file_node and is_current_user_admin(): - user_is_admin_for_this_op = True + if not file_node and is_admin_user(): + logger.info(f"Admin attempting to get text content for file ID {file_id}") for uid, udata in data.get('users', {}).items(): node, _ = find_node_by_id(udata.get('filesystem', {}), file_id) if node and node.get('type') == 'file' and node.get('file_type') == 'text': file_node = node - owner_tg_user_id = uid + can_access = True + logger.info(f"Admin found text file ID {file_id} belonging to user {uid}") break - - if not file_node: return Response("Текстовый файл не найден", status=404) + + if not can_access or not file_node: + return Response("Текстовый файл не найден или доступ запрещен", status=404) + hf_path = file_node.get('path') 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 = {}; if HF_TOKEN_READ: headers["authorization"] = f"Bearer {HF_TOKEN_READ}" - response = requests.get(file_url, headers=headers) - 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='ignore') + except UnicodeDecodeError: text_content = response.content.decode('latin-1', errors='replace') return Response(text_content, mimetype='text/plain') except Exception as e: - logging.error(f"Error fetching text content from HF ({hf_path}): {e}") - return Response(f"Ошибка загрузки содержимого: {e}", status=502) + logger.error(f"Error fetching text content from HF ({hf_path}): {e}") + return Response(f"Ошибка загрузки: {e}", status=502) -@app.route('/logout') +@app.route('/logout', methods=['POST']) def logout(): session.clear() - flash('Вы успешно вышли из системы.') - return redirect(url_for('index_page')) + return jsonify({'status': 'success', 'message': 'Вы успешно вышли.'}) @app.route('/admhosto') def admin_panel(): - if not is_current_user_admin(): + if not is_admin_user(): flash('Доступ запрещен.', 'error') - return redirect(url_for('index_page')) + # For TG mini app, redirecting to index might trigger auth loop. + # Better to show an error page or close. + return render_template_string("Доступ запрещен. Откройте как администратор. "), 403 data = load_data() users = data.get('users', {}) user_details = [] - for tg_id_str, udata in users.items(): + for tg_id, udata in users.items(): file_count = 0 - q = [(udata.get('filesystem', {}))] + q = [udata.get('filesystem', {}).get('children', [])] while q: - current_folder_node = q.pop(0) - if not current_folder_node: continue - for item in current_folder_node.get('children', []): + current_level = q.pop(0) + for item in current_level: if item.get('type') == 'file': file_count += 1 - elif item.get('type') == 'folder': q.append(item) + elif item.get('type') == 'folder' and 'children' in item: q.append(item.get('children', [])) + + tg_user_data_from_db = udata.get('telegram_user_data', {'id': tg_id}) + display_name = get_tg_user_display_name(tg_user_data_from_db) + user_details.append({ - 'telegram_id_str': tg_id_str, - 'display_name': udata.get('telegram_username', f"user_{tg_id_str}"), - 'first_name': udata.get('telegram_first_name', ''), - 'last_name': udata.get('telegram_last_name', ''), - 'created_at': udata.get('created_at', 'N/A'), - 'file_count': file_count + 'id': tg_id, 'display_name': display_name, + 'created_at': udata.get('created_at', 'N/A'), 'file_count': file_count }) - - html = f''' - -Админ-панель - -

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

-Вернуться в приложение -{{% 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.first_name}}}} {{{{user.last_name}}}}) - ID: {{{{user.telegram_id_str}}}} -

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

-

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

-
- -
-
-{{% else %}}

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

{{% endfor %}}
''' - return render_template_string(html, user_details=user_details) + return render_template_string(ADMIN_PANEL_HTML, user_details=user_details) +@app.route('/admhosto/user/') +def admin_user_files(tg_user_id): + if not is_admin_user(): + flash('Доступ запрещен.', 'error'); return redirect(url_for('index')) # Or error page -@app.route('/admhosto/user/') -def admin_user_files(telegram_id_str): - if not is_current_user_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('index_page')) data = load_data() - user_data = data.get('users', {}).get(telegram_id_str) - if not user_data: flash(f'Пользователь ID {telegram_id_str} не найден.', 'error'); return redirect(url_for('admin_panel')) + user_data = data.get('users', {}).get(str(tg_user_id)) + if not user_data: + flash(f'Пользователь {tg_user_id} не найден.', 'error'); return redirect(url_for('admin_panel')) + user_display_name = get_tg_user_display_name(user_data.get('telegram_user_data', {'id': tg_user_id})) all_files = [] - def collect_files_recursive(folder_node, current_path_id='root'): - parent_path_str = get_node_path_string(user_data['filesystem'], current_path_id) - for item in folder_node.get('children', []): - if item.get('type') == 'file': - item_copy = item.copy() - item_copy['parent_path_str'] = parent_path_str - all_files.append(item_copy) - elif item.get('type') == 'folder': - collect_files_recursive(item, item.get('id')) - - collect_files_recursive(user_data.get('filesystem', {})) + def collect_files(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(item, item.get('id')) + collect_files(user_data.get('filesystem', {})) all_files.sort(key=lambda x: x.get('upload_date', ''), reverse=True) - display_username = user_data.get('telegram_username', f"user_{telegram_id_str}") + return render_template_string(ADMIN_USER_FILES_HTML, + user_display_name=user_display_name, + tg_user_id_for_route=tg_user_id, 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 ''}") - html = f''' -Файлы {display_username} - - -

Файлы пользователя: {display_username} (ID: {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(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, current_telegram_id_str_for_route=telegram_id_str, display_username=display_username, files=all_files, - 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(telegram_id_str): - if not is_current_user_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('index_page')) - if not HF_TOKEN_WRITE: flash('Удаление невозможно: токен для записи не настроен.', 'error'); return redirect(url_for('admin_panel')) +@app.route('/admhosto/delete_user/', methods=['POST']) +def admin_delete_user(tg_user_id): + if not is_admin_user(): flash('Доступ запрещен.', 'error'); return redirect(url_for('index')) + if not HF_TOKEN_WRITE: flash('Удаление невозможно: токен записи не настроен.', 'error'); return redirect(url_for('admin_panel')) data = load_data() - if telegram_id_str not in data['users']: flash('Пользователь не найден!', 'error'); return redirect(url_for('admin_panel')) - - user_to_delete_data = data['users'][telegram_id_str] - user_to_delete_username = user_to_delete_data.get('telegram_username', f"user_{telegram_id_str}") - logging.warning(f"ADMIN ACTION: Attempting to delete user {user_to_delete_username} (ID: {telegram_id_str}) and all their data.") + tg_user_id_str = str(tg_user_id) + if tg_user_id_str not in data['users']: + flash('Пользователь не найден!', 'error'); return redirect(url_for('admin_panel')) + logger.warning(f"ADMIN ACTION: Attempting to delete user tg_{tg_user_id_str} and all their data.") try: api = HfApi() - user_folder_path_on_hf = f"cloud_files/{telegram_id_str}" - logging.info(f"Attempting to delete HF Hub folder: {user_folder_path_on_hf} for user {telegram_id_str}") + user_folder_path_on_hf = f"cloud_files/tg_{tg_user_id_str}" + logger.info(f"Attempting to delete HF Hub folder: {user_folder_path_on_hf} for user tg_{tg_user_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 ACTION: Deleted all files/folders for user {user_to_delete_username} (ID: {telegram_id_str})") - logging.info(f"Successfully initiated deletion of folder {user_folder_path_on_hf} on HF Hub.") + commit_message=f"ADMIN ACTION: Deleted all files/folders for user tg_{tg_user_id_str}") + logger.info(f"Successfully initiated deletion of folder {user_folder_path_on_hf} on HF Hub.") 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. Skipping HF deletion.") + if e.response.status_code == 404: logger.warning(f"User folder {user_folder_path_on_hf} not found on HF Hub. Skipping HF deletion.") else: - logging.error(f"Error deleting user folder {user_folder_path_on_hf} from HF Hub: {e}") - flash(f'Ошибка при удалении файлов пользователя {user_to_delete_username} с сервера: {e}. Пользователь НЕ удален из базы.', 'error') + logger.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 Hub folder deletion for {user_to_delete_username}: {e}") - flash(f'Неожиданная ошибка при удалении файлов {user_to_delete_username} с сервера: {e}. Пользователь НЕ удален из базы.', 'error') + logger.error(f"Unexpected error during HF Hub folder deletion for tg_{tg_user_id_str}: {e}") + flash(f'Неожиданная ошибка при удалении файлов с сервера: {e}. Пользователь НЕ удален из базы.', 'error') return redirect(url_for('admin_panel')) try: - del data['users'][telegram_id_str] + del data['users'][tg_user_id_str] save_data(data) - flash(f'Пользователь {user_to_delete_username} (ID: {telegram_id_str}) и его файлы (запрос на удаление отправлен) успешно удалены из базы данных!') - logging.info(f"ADMIN ACTION: Successfully deleted user {user_to_delete_username} (ID: {telegram_id_str}) from database.") + flash(f'Пользователь tg_{tg_user_id_str} и его файлы (запрос на удаление отправлен) успешно удалены из базы!') + logger.info(f"ADMIN ACTION: Successfully deleted user tg_{tg_user_id_str} from database.") except Exception as e: - logging.error(f"Error saving data after deleting user {user_to_delete_username}: {e}") - flash(f'Файлы пользователя {user_to_delete_username} удалены с сервера, но ошибка при удалении из базы: {e}', 'error') + logger.error(f"Error saving data after deleting user tg_{tg_user_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_id_str, file_id): - if not is_current_user_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('index_page')) - if not HF_TOKEN_WRITE: flash('Удаление невозможно: токен для записи не настроен.', 'error'); return redirect(url_for('admin_user_files', telegram_id_str=telegram_id_str)) +@app.route('/admhosto/delete_file//', methods=['POST']) +def admin_delete_file(tg_user_id, file_id): + if not is_admin_user(): flash('Доступ запрещен.', 'error'); return redirect(url_for('index')) + if not HF_TOKEN_WRITE: flash('Удаление невозможно: токен записи не настроен.', 'error'); return redirect(url_for('admin_user_files', tg_user_id=tg_user_id)) data = load_data() - user_data = data.get('users', {}).get(telegram_id_str) - if not user_data: flash(f'Пользователь ID {telegram_id_str} не найден.', 'error'); return redirect(url_for('admin_panel')) - - user_display_name = user_data.get('telegram_username', f"user_{telegram_id_str}") - file_node, parent_node = find_node_by_id(user_data['filesystem'], file_id) + tg_user_id_str = str(tg_user_id) + user_data = data.get('users', {}).get(tg_user_id_str) + if not user_data: flash(f'Пользователь {tg_user_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', telegram_id_str=telegram_id_str)) + return redirect(url_for('admin_user_files', tg_user_id=tg_user_id_str)) hf_path = file_node.get('path') original_filename = file_node.get('original_filename', 'файл') if not hf_path: - flash(f'Ошибка: Путь к файлу {original_filename} не найден. Удаление только из базы.', 'error') + 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"Admin delete file metadata save error (no path): {e}") - return redirect(url_for('admin_user_files', telegram_id_str=telegram_id_str)) + except Exception as e: flash('Ошибка сохранения (путь отсутствовал).', 'error'); logger.error(f"Admin delete file metadata save error (no path): {e}") + return redirect(url_for('admin_user_files', tg_user_id=tg_user_id_str)) 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 ACTION: Deleted file {original_filename} (ID: {file_id}) for user {user_display_name} (ID: {telegram_id_str})") - logging.info(f"ADMIN ACTION: Deleted file {hf_path} from HF Hub for user {telegram_id_str}") + commit_message=f"ADMIN ACTION: Deleted file {original_filename} (ID: {file_id}) for user tg_{tg_user_id_str}") + logger.info(f"ADMIN ACTION: Deleted file {hf_path} from HF Hub for user tg_{tg_user_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 Exception as e: flash('Файл удален, ошибка обновления базы.', 'error'); logger.error(f"Admin delete file DB update error: {e}") + else: flash('Файл удален с сервера, но не найден в базе.', 'error') except hf_utils.EntryNotFoundError: - logging.warning(f"ADMIN ACTION: File {hf_path} not found on HF Hub for user {telegram_id_str}. Removing from DB.") + logger.warning(f"ADMIN ACTION: File {hf_path} not found on HF Hub for tg_{tg_user_id_str}. Removing from DB.") 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 save error (HF not found): {e}") - else: flash('Файл не найден ни на сервере, ни в базе данных.', 'error') + except Exception as e: flash('Ошибка сохранения (файл не найден на сервере).', 'error'); logger.error(f"Admin delete file metadata save error (HF not found): {e}") + else: flash('Файл не найден ни на сервере, ни в базе.', 'error') except Exception as e: - logging.error(f"ADMIN ACTION: Error deleting file {hf_path} for {telegram_id_str}: {e}") + logger.error(f"ADMIN ACTION: Error deleting file {hf_path} for tg_{tg_user_id_str}: {e}") flash(f'Ошибка удаления файла {original_filename}: {e}', 'error') - return redirect(url_for('admin_user_files', telegram_id_str=telegram_id_str)) + return redirect(url_for('admin_user_files', tg_user_id=tg_user_id_str)) + + +# --- TELEGRAM BOT --- +async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + user = update.effective_user + if not WEB_APP_URL: + await update.message.reply_text("URL веб-приложения не настроен.") + return + + keyboard = [[InlineKeyboardButton("☁️ Открыть Zeus Cloud", web_app=WebAppInfo(url=WEB_APP_URL))]] + reply_markup = InlineKeyboardMarkup(keyboard) + await update.message.reply_text(f"Привет, {user.mention_html()}! Нажми кнопку ниже, чтобы открыть облако.", reply_markup=reply_markup, parse_mode='HTML') + +async def admin_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + user_id = str(update.effective_user.id) + if user_id == str(ADMIN_TELEGRAM_ID): + if not WEB_APP_URL: + await update.message.reply_text("URL веб-приложения не настроен для админ-панели.") + return + + admin_url = WEB_APP_URL.strip('/') + '/admhosto' # Ensure it's the correct admin path + keyboard = [[InlineKeyboardButton("🛠️ Админ-панель", web_app=WebAppInfo(url=admin_url))]] + reply_markup = InlineKeyboardMarkup(keyboard) + await update.message.reply_text("Доступ к админ-панели:", reply_markup=reply_markup) + else: + await update.message.reply_text("У вас нет прав для доступа к админ-панели.") +def run_telegram_bot(): + application = Application.builder().token(BOT_TOKEN).build() + application.add_handler(CommandHandler("start", start)) + application.add_handler(CommandHandler("cloud", start)) # Alias for start + application.add_handler(CommandHandler("admin", admin_command)) # For admin panel + logger.info("Telegram bot started polling...") + application.run_polling() +# --- MAIN EXECUTION --- if __name__ == '__main__': - if not REPO_ID: logging.critical("HF_REPO_ID is not set. Application cannot function properly."); exit(1) - if not HF_TOKEN_WRITE: logging.warning("HF_TOKEN_WRITE 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_WRITE as fallback). File downloads/previews might fail for private repos.") - if not BOT_TOKEN: logging.critical("BOT_TOKEN is not set. Telegram authentication will fail."); exit(1) - - if not ADMIN_TELEGRAM_IDS or ADMIN_TELEGRAM_IDS == ['']: - logging.warning("ADMIN_TELEGRAM_IDS is not set. Admin panel will be inaccessible.") + if not WEB_APP_URL: + logger.critical("WEB_APP_URL environment variable is not set. The Mini App will not work correctly.") + # Decide if you want to exit or run with a warning + # exit(1) # Optional: exit if critical env var is missing else: - logging.info(f"Admin Telegram IDs configured: {ADMIN_TELEGRAM_IDS}") + logger.info(f"Web App URL is set to: {WEB_APP_URL}") + + + if not HF_TOKEN_WRITE: logger.warning("HF_TOKEN_WRITE not set. Uploads, deletions, backups will fail.") + if not HF_TOKEN_READ: logger.warning("HF_TOKEN_READ not set (or same as WRITE). Downloads/previews might fail for private repos if WRITE token also not set.") + + if ADMIN_TELEGRAM_ID == "YOUR_ADMIN_TELEGRAM_ID_HERE": + logger.warning("ADMIN_TELEGRAM_ID is not set. Admin panel functionality will not be secure or may not work as expected.") if HF_TOKEN_WRITE: - logging.info("Performing initial database download before starting background backup.") - download_db_from_hf() # Download once at start if possible - threading.Thread(target=periodic_backup, daemon=True).start() - logging.info("Periodic backup thread started.") + logger.info("Performing initial database download before starting background backup.") + download_db_from_hf() + backup_thread = threading.Thread(target=periodic_backup, daemon=True) + backup_thread.start() + logger.info("Periodic backup thread started.") else: - logging.warning("Periodic backup disabled because HF_TOKEN_WRITE is not set.") + logger.warning("Periodic backup disabled (HF_TOKEN_WRITE not set).") if HF_TOKEN_READ: - logging.info("Performing initial database download (read-only mode).") + logger.info("Performing initial database download (read-only mode).") download_db_from_hf() else: - logging.warning("No read or write token. Database operations with Hugging Face Hub are disabled.") - if not os.path.exists(DATA_FILE): + logger.warning("No read/write token. HF Hub DB operations disabled.") + if not os.path.exists(DATA_FILE): with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f) - logging.info(f"Created empty local database file: {DATA_FILE}") + logger.info(f"Created empty local DB file: {DATA_FILE}") + + bot_thread = threading.Thread(target=run_telegram_bot, daemon=True) + bot_thread.start() - app.run(debug=False, host='0.0.0.0', port=int(os.getenv("PORT", 7860))) \ No newline at end of file + logger.info("Flask app starting...") + # Use a production-ready WSGI server like gunicorn or waitress in production + # For development/simplicity, Flask's built-in server: + app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 7860)), debug=False) \ No newline at end of file