diff --git "a/app.py" "b/app.py"
--- "a/app.py"
+++ "b/app.py"
@@ -1,3 +1,6 @@
+import flask
+from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify, Response
+from flask_caching import Cache
import json
import os
import logging
@@ -11,29 +14,36 @@ from io import BytesIO
import uuid
import hashlib
import hmac
-from urllib.parse import unquote
-
-from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify, Response
-from flask_caching import Cache
+from telegram import Update, WebAppInfo, InlineKeyboardButton, InlineKeyboardMarkup
+from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters
app = Flask(__name__)
-app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_folders_unique_telegram_mini_app") # Changed for new app
-DATA_FILE = 'cloudeng_data_tg.json' # Changed data file name
-REPO_ID = os.getenv("HF_REPO_ID", "Eluza133/Z1e1u") # Ensure this is set or use a default
+app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey_folders_telegram_unique")
+DATA_FILE = 'cloudeng_data_telegram.json'
+REPO_ID = "Eluza133/Z1e1u" # Replace with your actual repo ID if different
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE
-UPLOAD_FOLDER = 'uploads_tg' # Changed upload folder
+UPLOAD_FOLDER = 'uploads_telegram'
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
-BOT_TOKEN = os.getenv("BOT_TOKEN", "6750208873:AAE2hvPlJ99dBdhGa_Brre0IIpUdOvXxHt4")
-ADMIN_TELEGRAM_USER_ID = os.getenv("ADMIN_TELEGRAM_USER_ID")
+BOT_TOKEN = "6750208873:AAE2hvPlJ99dBdhGa_Brre0IIpUdOvXxHt4"
+ADMIN_TELEGRAM_IDS_STR = os.getenv("ADMIN_TELEGRAM_IDS", "")
+ADMIN_TELEGRAM_IDS = []
+if ADMIN_TELEGRAM_IDS_STR:
+ try:
+ ADMIN_TELEGRAM_IDS = [int(id_str.strip()) for id_str in ADMIN_TELEGRAM_IDS_STR.split(',')]
+ except ValueError:
+ logging.error(f"Invalid ADMIN_TELEGRAM_IDS: {ADMIN_TELEGRAM_IDS_STR}. Must be comma-separated integers.")
+ ADMIN_TELEGRAM_IDS = []
+
+if not ADMIN_TELEGRAM_IDS:
+ logging.warning("ADMIN_TELEGRAM_IDS is not set or is invalid. Admin panel will not be accessible to specific users.")
cache = Cache(app, config={'CACHE_TYPE': 'simple'})
logging.basicConfig(level=logging.INFO)
-# --- Helper Functions (largely unchanged, but context might change) ---
def find_node_by_id(filesystem, node_id):
if not filesystem: return None, None
if filesystem.get('id') == node_id:
@@ -77,15 +87,27 @@ def get_node_path_string(filesystem, node_id):
current_id = parent.get('id') if parent else None
return " / ".join(reversed(path_list)) or "Root"
-def initialize_user_filesystem(user_data): # user_data is data['users'][tg_user_id_str]
+def initialize_user_filesystem(user_data, user_telegram_id_str):
if 'filesystem' not in user_data:
user_data['filesystem'] = {
- "type": "folder",
- "id": "root",
- "name": "root",
- "children": []
+ "type": "folder", "id": "root", "name": "root", "children": []
}
- # Removed old file migration logic, assuming new users or already structured data for TG users
+ if 'files' in user_data and isinstance(user_data['files'], list):
+ for old_file in user_data['files']:
+ file_id = old_file.get('id', uuid.uuid4().hex)
+ original_filename = old_file.get('filename', 'unknown_file')
+ name_part, ext_part = os.path.splitext(original_filename)
+ unique_suffix = uuid.uuid4().hex[:8]
+ unique_filename = f"{name_part}_{unique_suffix}{ext_part}"
+ hf_path = f"cloud_files/{user_telegram_id_str}/root/{unique_filename}"
+ file_node = {
+ 'type': 'file', 'id': file_id, 'original_filename': original_filename,
+ 'unique_filename': unique_filename, 'path': hf_path,
+ 'file_type': get_file_type(original_filename),
+ 'upload_date': old_file.get('upload_date', datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
+ }
+ add_node(user_data['filesystem'], 'root', file_node)
+ del user_data['files']
@cache.memoize(timeout=300)
def load_data():
@@ -97,11 +119,9 @@ def load_data():
logging.warning("Data is not in dict format, initializing empty database")
return {'users': {}}
data.setdefault('users', {})
- # Filesystem initialization now happens upon user login/first access if needed
- # or when admin views a user that hasn't logged in yet.
- for user_id_str, user_data_val in data['users'].items():
- initialize_user_filesystem(user_data_val) # Ensure all loaded users have fs structure
- logging.info("Data successfully loaded and initialized")
+ for user_tg_id_str, user_data_val in data['users'].items():
+ initialize_user_filesystem(user_data_val, user_tg_id_str)
+ logging.info("Data successfully loaded and initialized for Telegram users")
return data
except Exception as e:
logging.error(f"Error loading data: {e}")
@@ -113,7 +133,7 @@ def save_data(data):
json.dump(data, file, ensure_ascii=False, indent=4)
upload_db_to_hf()
cache.clear()
- logging.info("Data saved and uploaded to HF")
+ logging.info("Data saved and uploaded to HF for Telegram users")
except Exception as e:
logging.error(f"Error saving data: {e}")
raise
@@ -127,7 +147,7 @@ def upload_db_to_hf():
api.upload_file(
path_or_fileobj=DATA_FILE, path_in_repo=DATA_FILE, repo_id=REPO_ID,
repo_type="dataset", token=HF_TOKEN_WRITE,
- commit_message=f"TGMA Backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
+ commit_message=f"Backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
)
logging.info("Database uploaded to Hugging Face")
except Exception as e:
@@ -145,8 +165,12 @@ def download_db_from_hf():
token=HF_TOKEN_READ, local_dir=".", local_dir_use_symlinks=False
)
logging.info("Database downloaded from Hugging Face")
- except (hf_utils.RepositoryNotFoundError, hf_utils.EntryNotFoundError):
- logging.warning(f"DB not found in repo {REPO_ID}. Initializing empty database.")
+ except hf_utils.RepositoryNotFoundError:
+ logging.error(f"Repository {REPO_ID} not found.")
+ if not os.path.exists(DATA_FILE):
+ with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f)
+ except hf_utils.EntryNotFoundError:
+ logging.warning(f"{DATA_FILE} not found in repository {REPO_ID}. Initializing empty database.")
if not os.path.exists(DATA_FILE):
with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f)
except Exception as e:
@@ -156,9 +180,8 @@ def download_db_from_hf():
def periodic_backup():
while True:
- time.sleep(1800) # Backup every 30 minutes
upload_db_to_hf()
-
+ time.sleep(1800)
def get_file_type(filename):
filename_lower = filename.lower()
@@ -168,106 +191,6 @@ def get_file_type(filename):
elif filename_lower.endswith('.txt'): return 'text'
return 'other'
-# --- Telegram Auth ---
-def check_telegram_authorization(auth_data_dict):
- if not BOT_TOKEN:
- logging.error("BOT_TOKEN is not set. Cannot verify Telegram authorization.")
- return None
-
- check_hash = auth_data_dict.pop('hash', None)
- if not check_hash: return None
-
- data_check_arr = []
- for key, value in sorted(auth_data_dict.items()):
- data_check_arr.append(f"{key}={value}")
- data_check_string = "\n".join(data_check_arr)
-
- secret_key = hashlib.sha256(BOT_TOKEN.encode()).digest()
- calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest()
-
- if calculated_hash == check_hash:
- try:
- user_data = json.loads(auth_data_dict['user'])
- return user_data
- except (KeyError, json.JSONDecodeError) as e:
- logging.error(f"Error parsing user data from Telegram auth: {e}")
- return None
- return None
-
-# --- Admin Check ---
-def is_admin():
- if not ADMIN_TELEGRAM_USER_ID: return False
- return 'telegram_user_id' in session and str(session['telegram_user_id']) == ADMIN_TELEGRAM_USER_ID
-
-# --- HTML Shell for Mini App ---
-MINI_APP_SHELL_HTML = """
-
-
-
-
-
- Zeus Cloud
-
-
-
-
-
-
Загрузка Zeus Cloud...
-
-
-
-
-
-"""
-
-# --- Base Style (remains largely the same) ---
BASE_STYLE = '''
:root {
--primary: #ff4d6d; --secondary: #00ddeb; --accent: #8b5cf6;
@@ -278,148 +201,226 @@ BASE_STYLE = '''
--folder-color: #ffc107;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
-body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
- background: var(--tg-theme-bg-color, var(--background-light));
- color: var(--tg-theme-text-color, var(--text-light));
- line-height: 1.6;
- padding-bottom: 70px; /* Space for potential MainButton */
-}
-/* body.dark specific styles are less relevant as Telegram handles theme */
-.container { margin: 10px auto; max-width: 1200px; padding: 15px; background: var(--tg-theme-secondary-bg-color, var(--card-bg)); border-radius: 12px; box-shadow: var(--shadow); overflow-x: hidden; }
-h1 { font-size: 1.8em; font-weight: 800; text-align: center; margin-bottom: 20px; background: linear-gradient(135deg, var(--primary), var(--accent)); -webkit-background-clip: text; color: transparent; }
-h2 { font-size: 1.4em; margin-top: 25px; color: var(--tg-theme-text-color, var(--text-light)); }
-h4 { font-size: 1.0em; margin-top: 12px; margin-bottom: 4px; color: var(--tg-theme-link-color, var(--accent)); }
-ol, ul { margin-left: 20px; margin-bottom: 12px; }
-li { margin-bottom: 4px; }
-input, textarea { width: 100%; padding: 12px; margin: 10px 0; border: 1px solid var(--tg-theme-hint-color, #ccc); border-radius: 10px; background: var(--tg-theme-bg-color, var(--glass-bg)); color: var(--tg-theme-text-color, var(--text-light)); font-size: 1em; }
-input:focus, textarea:focus { outline: none; border-color: var(--tg-theme-link-color, var(--primary)); box-shadow: 0 0 0 2px var(--tg-theme-link-color, var(--primary)); }
-.btn { padding: 12px 24px; background: var(--tg-theme-button-color, var(--primary)); color: var(--tg-theme-button-text-color, white); border: none; border-radius: 10px; cursor: pointer; font-size: 1em; font-weight: 600; transition: var(--transition); box-shadow: var(--shadow); display: inline-block; text-decoration: none; margin-top: 5px; margin-right: 5px; }
-.btn:hover { opacity: 0.9; transform: scale(1.03); }
+body { font-family: 'Inter', sans-serif; background: var(--background-light); color: var(--text-light); line-height: 1.6; }
+body.dark { background: var(--background-dark); color: var(--text-dark); }
+.container { margin: 20px auto; max-width: 1200px; padding: 25px; background: var(--card-bg); border-radius: 20px; box-shadow: var(--shadow); overflow-x: hidden; }
+body.dark .container { background: var(--card-bg-dark); }
+h1 { font-size: 2em; font-weight: 800; text-align: center; margin-bottom: 25px; background: linear-gradient(135deg, var(--primary), var(--accent)); -webkit-background-clip: text; color: transparent; }
+h2 { font-size: 1.5em; margin-top: 30px; color: var(--text-light); }
+body.dark h2 { color: var(--text-dark); }
+h4 { font-size: 1.1em; margin-top: 15px; margin-bottom: 5px; color: var(--accent); }
+ol, ul { margin-left: 20px; margin-bottom: 15px; }
+li { margin-bottom: 5px; }
+input, textarea { width: 100%; padding: 14px; margin: 12px 0; border: none; border-radius: 14px; background: var(--glass-bg); color: var(--text-light); font-size: 1.1em; box-shadow: inset 0 3px 10px rgba(0, 0, 0, 0.1); }
+body.dark input, body.dark textarea { color: var(--text-dark); }
+input:focus, textarea:focus { outline: none; box-shadow: 0 0 0 4px var(--primary); }
+.btn { padding: 14px 28px; background: var(--primary); color: white; border: none; border-radius: 14px; cursor: pointer; font-size: 1.1em; font-weight: 600; transition: var(--transition); box-shadow: var(--shadow); display: inline-block; text-decoration: none; margin-top: 5px; margin-right: 5px; }
+.btn:hover { transform: scale(1.05); background: #e6415f; }
.download-btn { background: var(--secondary); }
.download-btn:hover { background: #00b8c5; }
.delete-btn { background: var(--delete-color); }
.delete-btn:hover { background: #cc3333; }
.folder-btn { background: var(--folder-color); }
.folder-btn:hover { background: #e6a000; }
-.flash { color: var(--tg-theme-link-color, var(--secondary)); text-align: center; margin-bottom: 15px; padding: 10px; background: rgba(0, 221, 235, 0.1); border-radius: 10px; }
-.flash.error { color: var(--tg-theme-destructive-text-color, var(--delete-color)); background: rgba(255, 68, 68, 0.1); }
-.file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; margin-top: 15px; }
-.user-list { margin-top: 15px; }
-.user-item { padding: 12px; background: var(--tg-theme-secondary-bg-color, var(--card-bg)); border-radius: 12px; margin-bottom: 8px; box-shadow: var(--shadow); transition: var(--transition); }
-.user-item:hover { transform: translateY(-3px); }
-.user-item a { color: var(--tg-theme-link-color, var(--primary)); text-decoration: none; font-weight: 600; }
-.item { background: var(--tg-theme-secondary-bg-color, var(--card-bg)); padding: 12px; border-radius: 12px; box-shadow: var(--shadow); text-align: center; transition: var(--transition); display: flex; flex-direction: column; justify-content: space-between; }
-.item:hover { transform: translateY(-3px); }
-.item-preview { max-width: 100%; height: 100px; object-fit: cover; border-radius: 8px; margin-bottom: 8px; cursor: pointer; display: block; margin-left: auto; margin-right: auto;}
-.item.folder .item-preview { object-fit: contain; font-size: 50px; color: var(--tg-theme-link-color, var(--folder-color)); line-height: 100px; }
-.item p { font-size: 0.85em; margin: 4px 0; word-break: break-all; }
-.item-actions { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 4px; justify-content: center; }
-.item-actions .btn { font-size: 0.8em; padding: 4px 8px; }
+.flash { color: var(--secondary); text-align: center; margin-bottom: 15px; padding: 10px; background: rgba(0, 221, 235, 0.1); border-radius: 10px; }
+.flash.error { color: var(--delete-color); background: rgba(255, 68, 68, 0.1); }
+.file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; margin-top: 20px; }
+.user-list { margin-top: 20px; }
+.user-item { padding: 15px; background: var(--card-bg); border-radius: 16px; margin-bottom: 10px; box-shadow: var(--shadow); transition: var(--transition); }
+body.dark .user-item { background: var(--card-bg-dark); }
+.user-item:hover { transform: translateY(-5px); }
+.user-item a { color: var(--primary); text-decoration: none; font-weight: 600; }
+.user-item a:hover { color: var(--accent); }
+.item { background: var(--card-bg); padding: 15px; border-radius: 16px; box-shadow: var(--shadow); text-align: center; transition: var(--transition); display: flex; flex-direction: column; justify-content: space-between; }
+body.dark .item { background: var(--card-bg-dark); }
+.item:hover { transform: translateY(-5px); }
+.item-preview { max-width: 100%; height: 130px; object-fit: cover; border-radius: 10px; margin-bottom: 10px; cursor: pointer; display: block; margin-left: auto; margin-right: auto;}
+.item.folder .item-preview { object-fit: contain; font-size: 60px; color: var(--folder-color); line-height: 130px; }
+.item p { font-size: 0.9em; margin: 5px 0; word-break: break-all; }
+.item a { color: var(--primary); text-decoration: none; }
+.item a:hover { color: var(--accent); }
+.item-actions { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 5px; justify-content: center; }
+.item-actions .btn { font-size: 0.9em; padding: 5px 10px; }
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.85); z-index: 2000; justify-content: center; align-items: center; }
-.modal-content { max-width: 95%; max-height: 95%; background: var(--tg-theme-secondary-bg-color, #fff); padding: 10px; border-radius: 15px; overflow: auto; position: relative; }
+.modal-content { max-width: 95%; max-height: 95%; background: #fff; padding: 10px; border-radius: 15px; overflow: auto; position: relative; }
+body.dark .modal-content { background: var(--card-bg-dark); }
.modal img, .modal video, .modal iframe, .modal pre { max-width: 100%; max-height: 85vh; display: block; margin: auto; border-radius: 10px; }
-.modal iframe { width: 90vw; height: 85vh; border: none; } /* Adjusted for TWA */
-.modal pre { background: var(--tg-theme-bg-color, #eee); color: var(--tg-theme-text-color, #333); padding: 15px; border-radius: 8px; white-space: pre-wrap; word-wrap: break-word; text-align: left; max-height: 85vh; overflow-y: auto;}
-.modal-close-btn { position: absolute; top: 10px; right: 15px; font-size: 24px; color: var(--tg-theme-hint-color, #aaa); cursor: pointer; background: rgba(0,0,0,0.3); border-radius: 50%; width: 28px; height: 28px; line-height: 28px; text-align: center; }
-#progress-container { width: 100%; background: var(--tg-theme-hint-color, var(--glass-bg)); border-radius: 10px; margin: 15px 0; display: none; position: relative; height: 20px; }
-#progress-bar { width: 0%; height: 100%; background: var(--tg-theme-button-color, var(--primary)); border-radius: 10px; transition: width 0.3s ease; }
-#progress-text { position: absolute; width: 100%; text-align: center; line-height: 20px; color: var(--tg-theme-button-text-color, white); font-size: 0.9em; font-weight: bold; text-shadow: 1px 1px 1px rgba(0,0,0,0.5); }
-.breadcrumbs { margin-bottom: 15px; font-size: 1em; }
-.breadcrumbs a { color: var(--tg-theme-link-color, var(--accent)); text-decoration: none; }
+.modal iframe { width: 80vw; height: 85vh; border: none; }
+.modal pre { background: #eee; color: #333; padding: 15px; border-radius: 8px; white-space: pre-wrap; word-wrap: break-word; text-align: left; max-height: 85vh; overflow-y: auto;}
+body.dark .modal pre { background: #2b2a33; color: var(--text-dark); }
+.modal-close-btn { position: absolute; top: 15px; right: 25px; font-size: 30px; color: #aaa; cursor: pointer; background: rgba(0,0,0,0.5); border-radius: 50%; width: 30px; height: 30px; line-height: 30px; text-align: center; }
+body.dark .modal-close-btn { color: #555; background: rgba(255,255,255,0.2); }
+#progress-container { width: 100%; background: var(--glass-bg); border-radius: 10px; margin: 15px 0; display: none; position: relative; height: 20px; }
+#progress-bar { width: 0%; height: 100%; background: var(--primary); border-radius: 10px; transition: width 0.3s ease; }
+#progress-text { position: absolute; width: 100%; text-align: center; line-height: 20px; color: white; font-size: 0.9em; font-weight: bold; text-shadow: 1px 1px 1px rgba(0,0,0,0.5); }
+.breadcrumbs { margin-bottom: 20px; font-size: 1.1em; }
+.breadcrumbs a { color: var(--accent); text-decoration: none; }
.breadcrumbs a:hover { text-decoration: underline; }
-.breadcrumbs span { margin: 0 5px; color: var(--tg-theme-hint-color, #aaa); }
-.folder-actions { margin-top: 15px; margin-bottom: 8px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
+.breadcrumbs span { margin: 0 5px; color: #aaa; }
+.folder-actions { margin-top: 20px; margin-bottom: 10px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.folder-actions input[type=text] { width: auto; flex-grow: 1; margin: 0; min-width: 150px; }
.folder-actions .btn { margin: 0; flex-shrink: 0;}
-@media (max-width: 768px) { /* These might need less adjustment for TWA */
- .file-grid { grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); }
- .item-preview { height: 90px; }
- .item.folder .item-preview { font-size: 40px; line-height: 90px; }
+@media (max-width: 768px) {
+ .file-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
+ .folder-actions { flex-direction: column; align-items: stretch; }
+ .folder-actions input[type=text] { width: 100%; }
+ .item-preview { height: 100px; }
+ .item.folder .item-preview { font-size: 50px; line-height: 100px; }
+ h1 { font-size: 1.8em; }
+ .btn { padding: 12px 24px; font-size: 1em; }
+ .item-actions .btn { padding: 4px 8px; font-size: 0.8em;}
}
@media (max-width: 480px) {
- .container { padding: 10px; }
- .file-grid { grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 10px; }
- .item-preview { height: 70px; }
- .item.folder .item-preview { font-size: 35px; line-height: 70px; }
+ .container { padding: 15px; }
+ .file-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 15px; }
+ .item-preview { height: 80px; }
+ .item.folder .item-preview { font-size: 40px; line-height: 80px; }
.item p { font-size: 0.8em;}
- .breadcrumbs { font-size: 0.9em; }
+ .breadcrumbs { font-size: 1em; }
+ .btn { padding: 10px 20px; }
}
'''
-# --- Routes ---
-@app.route('/')
-def index_redirect():
- # Redirect to the Mini App entry point or an info page
- return redirect(url_for('mini_app_entry'))
+INITIAL_LOAD_HTML = '''
+
+
+
+
+
+ Zeus Cloud
+
+
+
+
+
+
+
+
+'''
-@app.route('/app')
-def mini_app_entry():
- return render_template_string(MINI_APP_SHELL_HTML)
+def is_telegram_admin():
+ user_id = session.get('telegram_user_id')
+ return user_id in ADMIN_TELEGRAM_IDS
-@app.route('/auth/telegram', methods=['POST'])
-def auth_telegram():
+def get_current_user_display_name():
+ if 'telegram_user_first_name' in session and session['telegram_user_first_name']:
+ return session['telegram_user_first_name']
+ if 'telegram_username' in session and session['telegram_username']:
+ return session['telegram_username']
+ return str(session.get('telegram_user_id', 'User'))
+
+
+@app.route('/', methods=['GET'])
+def root_path():
+ return render_template_string(INITIAL_LOAD_HTML)
+
+@app.route('/api/telegram_authenticate', methods=['POST'])
+def telegram_authenticate():
try:
- init_data_str = request.form.get('initData')
- if not init_data_str:
- return jsonify({'status': 'error', 'message': 'No initData received'}), 400
+ payload = request.json
+ init_data_str = payload.get('initData')
+ user_info = payload.get('user')
- auth_data_dict = dict(param.split('=', 1) for param in unquote(init_data_str).split('&'))
+ if not init_data_str or not user_info or not user_info.get('id'):
+ return jsonify({'status': 'error', 'message': 'Отсутствуют данные для аутентификации'}), 400
- tg_user = check_telegram_authorization(auth_data_dict.copy())
-
- if tg_user:
- user_id_str = str(tg_user['id'])
- session['telegram_user_id'] = tg_user['id']
- session['telegram_user_info'] = tg_user
-
- data = load_data()
- if user_id_str not in data['users']:
- data['users'][user_id_str] = {
- 'tg_username': tg_user.get('username'),
- 'tg_first_name': tg_user.get('first_name'),
- 'tg_last_name': tg_user.get('last_name'),
- 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
- # Filesystem initialized by initialize_user_filesystem below
- }
-
- # Ensure filesystem structure exists for the user
- initialize_user_filesystem(data['users'][user_id_str])
-
+ data_check_string = "\n".join(sorted([f"{k}={v}" for k, v in經urllib.parse.parse_qsl(init_data_str) if k != 'hash']))
+ secret_key = hmac.new("WebAppData".encode(), BOT_TOKEN.encode(), hashlib.sha256).digest()
+ calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest()
+
+ received_hash = urllib.parse.parse_qs(init_data_str).get('hash', [None])[0]
+
+ if calculated_hash != received_hash:
+ # For dev, allow unsafe if hash fails, but log it. In prod, this should be an error.
+ logging.warning(f"Telegram data hash mismatch. Rec: {received_hash}, Calc: {calculated_hash}. Using unsafe data for user {user_info.get('id')}")
+ # return jsonify({'status': 'error', 'message': 'Ошибка проверки данных Telegram.'}), 403
+
+
+ user_id = user_info['id']
+ username = user_info.get('username')
+ first_name = user_info.get('first_name')
+ last_name = user_info.get('last_name')
+
+ session['telegram_user_id'] = user_id
+ session['telegram_username'] = username
+ session['telegram_user_first_name'] = first_name
+ session['telegram_user_last_name'] = last_name
+
+ user_id_str = str(user_id)
+ data = load_data()
+ if user_id_str not in data['users']:
+ data['users'][user_id_str] = {
+ 'telegram_username': username,
+ 'telegram_first_name': first_name,
+ 'telegram_last_name': last_name,
+ 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+ 'filesystem': { "type": "folder", "id": "root", "name": "root", "children": [] }
+ }
+ initialize_user_filesystem(data['users'][user_id_str], user_id_str)
try:
- save_data(data) # Save if new user or if filesystem was just initialized
+ save_data(data)
+ logging.info(f"New Telegram user {user_id_str} registered.")
except Exception as e:
- logging.error(f"Error saving user data for TGID {user_id_str}: {e}")
- return jsonify({'status': 'error', 'message': 'Error saving user data'}), 500
-
- return jsonify({'status': 'success', 'user': tg_user, 'redirect_url': url_for('dashboard')})
- else:
- return jsonify({'status': 'error', 'message': 'Invalid Telegram authorization'}), 403
+ logging.error(f"Error saving data for new Telegram user {user_id_str}: {e}")
+ return jsonify({'status': 'error', 'message': 'Ошибка сохранения данных пользователя.'}), 500
+
+ return jsonify({'status': 'success'})
except Exception as e:
- logging.error(f"Error in Telegram auth: {e}")
+ logging.error(f"Error in /api/telegram_authenticate: {e}")
return jsonify({'status': 'error', 'message': str(e)}), 500
-@app.route('/dashboard', methods=['GET', 'POST'])
-def dashboard():
+@app.route('/app', methods=['GET', 'POST'])
+def main_app_view_and_upload():
if 'telegram_user_id' not in session:
- flash('Пожалуйста, авторизуйтесь через Telegram.', 'error')
- # In a mini app, this usually means redirecting to the auth flow or showing an error.
- # Since /app handles auth, direct access to /dashboard shouldn't happen unauthenticated.
- return render_template_string("Доступ запрещен. Пожалуйста, откройте приложение через Telegram.
"), 403
-
- tg_user_id_str = str(session['telegram_user_id'])
- tg_user_info = session.get('telegram_user_info', {})
- user_display_name = tg_user_info.get('first_name', tg_user_info.get('username', f"User {tg_user_id_str}"))
+ flash('Пожалуйста, пройдите аутентификацию через Telegram.')
+ return redirect(url_for('root_path'))
+ user_telegram_id = session['telegram_user_id']
+ user_telegram_id_str = str(user_telegram_id)
+
data = load_data()
- if tg_user_id_str not in data['users']:
+ if user_telegram_id_str not in data['users']:
session.clear()
- flash('Данные пользователя не найдены. Пожалуйста, перезайдите.', 'error')
- return render_template_string("Данные пользователя не найдены. Пожалуйста, перезайдите через Telegram.
"), 403
+ flash('Пользователь не найден!')
+ return redirect(url_for('root_path'))
- user_data = data['users'][tg_user_id_str]
- initialize_user_filesystem(user_data) # Ensure filesystem exists
+ user_data = data['users'][user_telegram_id_str]
+ if 'filesystem' not in user_data:
+ initialize_user_filesystem(user_data, user_telegram_id_str)
current_folder_id = request.args.get('folder_id', 'root')
current_folder, parent_folder = find_node_by_id(user_data['filesystem'], current_folder_id)
@@ -428,57 +429,54 @@ def dashboard():
flash('Папка не найдена!', 'error')
current_folder_id = 'root'
current_folder, parent_folder = find_node_by_id(user_data['filesystem'], current_folder_id)
- if not current_folder: # Should not happen if initialized
- logging.error(f"CRITICAL: Root folder not found for user TGID {tg_user_id_str}")
+ if not current_folder:
+ logging.error(f"CRITICAL: Root folder not found for user {user_telegram_id_str}")
flash('Критическая ошибка: корневая папка не найдена.', 'error')
session.clear()
- return redirect(url_for('mini_app_entry')) # Re-auth
+ return redirect(url_for('root_path'))
items_in_folder = sorted(current_folder.get('children', []), key=lambda x: (x['type'] != 'folder', x.get('name', x.get('original_filename', '')).lower()))
if request.method == 'POST':
if not HF_TOKEN_WRITE:
flash('Загрузка невозможна: токен для записи не настроен.', 'error')
- return redirect(url_for('dashboard', folder_id=current_folder_id))
+ return redirect(url_for('main_app_view_and_upload', folder_id=current_folder_id))
files = request.files.getlist('files')
if not files or all(not f.filename for f in files):
flash('Файлы для загрузки не выбраны.', 'error')
- return redirect(url_for('dashboard', folder_id=current_folder_id))
+ return redirect(url_for('main_app_view_and_upload', folder_id=current_folder_id))
- if len(files) > 20: # Limit number of files
+ if len(files) > 20:
flash('Максимум 20 файлов за раз!', 'error')
- return redirect(url_for('dashboard', folder_id=current_folder_id))
-
+ return redirect(url_for('main_app_view_and_upload', folder_id=current_folder_id))
target_folder_id = request.form.get('current_folder_id', 'root')
target_folder_node, _ = find_node_by_id(user_data['filesystem'], target_folder_id)
if not target_folder_node or target_folder_node.get('type') != 'folder':
flash('Целевая папка для загрузки не найдена!', 'error')
- return redirect(url_for('dashboard'))
+ return redirect(url_for('main_app_view_and_upload'))
api = HfApi()
uploaded_count = 0
errors = []
- for file_obj in files:
- if file_obj and file_obj.filename:
- original_filename = secure_filename(file_obj.filename)
+ for file_in_request in files:
+ if file_in_request and file_in_request.filename:
+ original_filename = secure_filename(file_in_request.filename)
name_part, ext_part = os.path.splitext(original_filename)
unique_suffix = uuid.uuid4().hex[:8]
unique_filename = f"{name_part}_{unique_suffix}{ext_part}"
file_id = uuid.uuid4().hex
-
- hf_path = f"cloud_files/{tg_user_id_str}/{target_folder_id}/{unique_filename}"
+ hf_path = f"cloud_files/{user_telegram_id_str}/{target_folder_id}/{unique_filename}"
temp_path = os.path.join(UPLOAD_FOLDER, f"{file_id}_{unique_filename}")
-
try:
- file_obj.save(temp_path)
+ file_in_request.save(temp_path)
api.upload_file(
path_or_fileobj=temp_path, path_in_repo=hf_path, repo_id=REPO_ID,
repo_type="dataset", token=HF_TOKEN_WRITE,
- commit_message=f"User TGID:{tg_user_id_str} uploaded {original_filename} to folder {target_folder_id}"
+ commit_message=f"User {user_telegram_id_str} uploaded {original_filename} to folder {target_folder_id}"
)
file_info = {
'type': 'file', 'id': file_id, 'original_filename': original_filename,
@@ -488,27 +486,26 @@ def dashboard():
}
if add_node(user_data['filesystem'], target_folder_id, file_info):
uploaded_count += 1
- else: # Should not happen if target_folder_node is valid
+ else:
errors.append(f"Ошибка добавления метаданных для {original_filename}.")
- logging.error(f"Failed to add node metadata for file {file_id} to folder {target_folder_id} for user TGID {tg_user_id_str}")
+ logging.error(f"Failed to add node metadata for file {file_id} to folder {target_folder_id} for user {user_telegram_id_str}")
try: api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
except Exception as del_err: logging.error(f"Failed to delete orphaned file {hf_path} from HF Hub: {del_err}")
except Exception as e:
- logging.error(f"Error uploading file {original_filename} for TGID {tg_user_id_str}: {e}")
+ logging.error(f"Error uploading file {original_filename} for {user_telegram_id_str}: {e}")
errors.append(f"Ошибка загрузки файла {original_filename}: {e}")
finally:
if os.path.exists(temp_path): os.remove(temp_path)
-
if uploaded_count > 0:
try:
save_data(data)
flash(f'{uploaded_count} файл(ов) успешно загружено!')
except Exception as e:
flash('Файлы загружены на сервер, но произошла ошибка сохранения метаданных.', 'error')
- logging.error(f"Error saving data after upload for TGID {tg_user_id_str}: {e}")
+ logging.error(f"Error saving data after upload for {user_telegram_id_str}: {e}")
if errors:
for error_msg in errors: flash(error_msg, 'error')
- return redirect(url_for('dashboard', folder_id=target_folder_id))
+ return redirect(url_for('main_app_view_and_upload', folder_id=target_folder_id))
breadcrumbs = []
temp_id = current_folder_id
@@ -522,58 +519,76 @@ def dashboard():
breadcrumbs.reverse()
html = '''
-
+
Zeus Cloud
-
Zeus Cloud Пользователь: {{ user_display_name }}
-{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}
- {% for category, message in messages %}
{{ message }}
{% endfor %}
-{% endif %}{% endwith %}
+
Zeus Cloud Пользователь: {{ current_user_display_name }}
+{% with messages = get_flashed_messages(with_categories=true) %}
+ {% if messages %}{% for category, message in messages %}
{{ message }}
{% endfor %}{% endif %}
+{% endwith %}
+ {% for crumb in breadcrumbs %}
+ {% if crumb.is_link %}
{{ crumb.name if crumb.id != 'root' else 'Главная' }}
+ {% else %}
{{ crumb.name if crumb.id != 'root' else 'Главная' }} {% endif %}
+ {% if not loop.last %}
/ {% endif %}
+ {% endfor %}
+
-
-
+
+
+ Загрузить файлы сюда
+
Содержимое папки: {{ 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' %}
- {% elif item.file_type == 'video' %}
- {% elif item.file_type == 'pdf' %}
📄
- {% elif item.file_type == 'text' %}
📝
- {% else %}
❓
{% endif %}
-
{{ item.original_filename | truncate(25, True) }}
{{ item.upload_date }}
-
-
Скачать
- {% if previewable %}
Просмотр {% endif %}
-
{% endif %}
- {% endfor %}{% if not items %}
Эта папка пуста.
{% endif %}
-Закрыть
+ {% for item in items %}
+
+ {% if item.type == 'folder' %}
+
📁
+
{{ item.name }}
+
+ {% elif item.type == 'file' %}
+ {% set previewable = item.file_type in ['image', 'video', 'pdf', 'text'] %}
+ {% if item.file_type == 'image' %}
+ {% elif item.file_type == 'video' %}
+ {% elif item.file_type == 'pdf' %}
📄
+ {% elif item.file_type == 'text' %}
📝
+ {% else %}
❓
{% endif %}
+
{{ item.original_filename | truncate(25, True) }}
+
{{ item.upload_date }}
+
+
Скачать
+ {% if previewable %}
Просмотр {% endif %}
+
+
+ {% endif %}
+
+ {% endfor %}
+ {% if not items %} Эта папка пуста.
{% endif %}
+
+Закрыть
-
+
'''
template_context = {
- 'user_display_name': user_display_name, 'items': items_in_folder, 'current_folder_id': current_folder_id,
- 'current_folder': current_folder, 'breadcrumbs': breadcrumbs, 'repo_id': REPO_ID,
+ 'current_user_display_name': get_current_user_display_name(),
+ 'items': items_in_folder, 'current_folder_id': current_folder_id,
+ 'current_folder': current_folder, 'breadcrumbs': breadcrumbs,
+ 'repo_id': REPO_ID, 'HF_TOKEN_READ': HF_TOKEN_READ,
'hf_file_url': lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{path}{'?download=true' if download else ''}",
+ 'os': os
}
return render_template_string(html, **template_context)
-@app.route('/create_folder', methods=['POST'])
-def create_folder():
+@app.route('/api/create_folder', methods=['POST'])
+def create_folder_api():
if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован'}), 401
- tg_user_id_str = str(session['telegram_user_id'])
+ user_telegram_id_str = str(session['telegram_user_id'])
data = load_data()
- user_data = data['users'].get(tg_user_id_str)
+ user_data = data['users'].get(user_telegram_id_str)
if not user_data: return jsonify({'status': 'error', 'message': 'Пользователь не найден'}), 404
parent_folder_id = request.form.get('parent_folder_id', 'root')
@@ -639,342 +666,327 @@ def create_folder():
if not folder_name:
flash('Имя папки не может быть пустым!', 'error')
- elif not all(c.isalnum() or c.isspace() or c in '_-.' for c in folder_name) or len(folder_name) > 50 : # Basic validation
- flash('Имя папки содержит недопустимые символы или слишком длинное.', 'error')
- else:
- folder_id = uuid.uuid4().hex
- folder_data = {'type': 'folder', 'id': folder_id, 'name': folder_name, 'children': []}
- if add_node(user_data['filesystem'], parent_folder_id, folder_data):
- try: save_data(data); flash(f'Папка "{folder_name}" успешно создана.')
- except Exception as e: flash('Ошибка сохранения данных.', 'error'); logging.error(f"Create folder save error: {e}")
- else: flash('Не удалось найти родительскую папку.', 'error')
- return redirect(url_for('dashboard', folder_id=parent_folder_id))
-
+ return redirect(url_for('main_app_view_and_upload', folder_id=parent_folder_id))
+ if not all(c.isalnum() or c in [' ', '_', '-'] for c in folder_name):
+ flash('Имя папки может содержать буквы, цифры, пробелы, дефисы и подчеркивания.', 'error')
+ return redirect(url_for('main_app_view_and_upload', folder_id=parent_folder_id))
+
+ folder_id = uuid.uuid4().hex
+ folder_data = {'type': 'folder', 'id': folder_id, 'name': folder_name, 'children': [] }
+ if add_node(user_data['filesystem'], parent_folder_id, folder_data):
+ try: save_data(data); flash(f'Папка "{folder_name}" успешно создана.')
+ except Exception as e: flash('Ошибка сохранения данных при создании папки.', 'error'); logging.error(f"Create folder save error: {e}")
+ else: flash('Не удалось найти родительскую папку.', 'error')
+ return redirect(url_for('main_app_view_and_upload', folder_id=parent_folder_id))
@app.route('/download/')
def download_file(file_id):
- allow_access = False
- current_user_tg_id_str = str(session.get('telegram_user_id')) if 'telegram_user_id' in session else None
+ is_admin_access = is_telegram_admin() and (request.referrer and 'admhosto' in request.referrer)
+ if 'telegram_user_id' not in session and not is_admin_access:
+ flash('Пожалуйста, пройдите аутентификацию.')
+ return redirect(url_for('root_path'))
+
data = load_data()
file_node = None
- # file_owner_tg_id_str = None # Not strictly needed here, but good for logging if implemented
-
- # Check if current user is admin
- is_current_user_admin = is_admin()
-
- if current_user_tg_id_str:
- user_data = data['users'].get(current_user_tg_id_str)
- if user_data:
- _file_node, _ = find_node_by_id(user_data.get('filesystem', {}), file_id)
- if _file_node and _file_node.get('type') == 'file':
- file_node = _file_node
- allow_access = True # Owner has access
-
- if not file_node and is_current_user_admin: # If admin and file not found under their own ID (or they are searching)
- for tg_id, udata in data.get('users', {}).items():
- if tg_id == current_user_tg_id_str: continue # Already checked
- node, _ = find_node_by_id(udata.get('filesystem', {}), file_id)
+ user_context_id_str = str(session.get('telegram_user_id')) if 'telegram_user_id' in session else None
+
+ if user_context_id_str:
+ user_data = data['users'].get(user_context_id_str)
+ if user_data: file_node, _ = find_node_by_id(user_data['filesystem'], file_id)
+
+ if not file_node and is_telegram_admin(): # Admin can download any file
+ for uid_str, udata_val in data.get('users', {}).items():
+ node, _ = find_node_by_id(udata_val.get('filesystem', {}), file_id)
if node and node.get('type') == 'file':
- file_node = node; allow_access = True; break
+ file_node = node; user_context_id_str = uid_str; break
- if not allow_access or not file_node:
- flash('Файл не найден или доступ запрещен!', 'error')
- return redirect(url_for('dashboard') if current_user_tg_id_str else url_for('mini_app_entry'))
+ if not file_node or file_node.get('type') != 'file':
+ flash('Файл не найден!', 'error')
+ return redirect(request.referrer or url_for('main_app_view_and_upload' if 'telegram_user_id' in session else 'root_path'))
hf_path = file_node.get('path')
original_filename = file_node.get('original_filename', 'downloaded_file')
if not hf_path:
flash('Ошибка: Путь к файлу не найден.', 'error')
- return redirect(url_for('dashboard', folder_id=request.args.get('folder_id', 'root'))) # Stay in current folder
+ return redirect(request.referrer or url_for('main_app_view_and_upload' if 'telegram_user_id' in session else 'root_path'))
file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{hf_path}?download=true"
try:
- headers = {}
+ headers = {};
if HF_TOKEN_READ: headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
- response = requests.get(file_url, headers=headers, stream=True)
- response.raise_for_status()
+ response = requests.get(file_url, headers=headers, stream=True); response.raise_for_status()
return send_file(BytesIO(response.content), as_attachment=True, download_name=original_filename, mimetype='application/octet-stream')
- except requests.exceptions.RequestException as e:
- logging.error(f"Error downloading file from HF ({hf_path}): {e}")
- flash(f'Ошибка скачивания файла: {e}', 'error')
except Exception as e:
- logging.error(f"Unexpected error during download ({hf_path}): {e}")
- flash('Непредвиденная ошибка при скачивании.', 'error')
- return redirect(url_for('dashboard', folder_id=request.args.get('folder_id', 'root')))
-
+ logging.error(f"Error downloading file from HF ({hf_path}): {e}")
+ flash(f'Ошибка скачивания файла {original_filename}! ({e})', 'error')
+ return redirect(request.referrer or url_for('main_app_view_and_upload' if 'telegram_user_id' in session else 'root_path'))
-@app.route('/delete_file/', methods=['POST'])
-def delete_file(file_id):
- if 'telegram_user_id' not in session: flash('Пожалуйста, авторизуйтесь.'); return redirect(url_for('mini_app_entry'))
- tg_user_id_str = str(session['telegram_user_id'])
+@app.route('/api/delete_file/', methods=['POST'])
+def delete_file_api(file_id):
+ if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован'}), 401
+ user_telegram_id_str = str(session['telegram_user_id'])
data = load_data()
- user_data = data['users'].get(tg_user_id_str)
- if not user_data: flash('Пользователь не найден!'); session.clear(); return redirect(url_for('mini_app_entry'))
+ user_data = data['users'].get(user_telegram_id_str)
+ if not user_data: flash('Пользователь не найден!', 'error'); session.clear(); return redirect(url_for('root_path'))
file_node, parent_node = find_node_by_id(user_data['filesystem'], file_id)
- current_view_folder_id = request.form.get('current_view_folder_id', parent_node.get('id', 'root') if parent_node else 'root')
-
+ current_view_folder_id = request.form.get('current_view_folder_id', 'root')
if not file_node or file_node.get('type') != 'file' or not parent_node:
flash('Файл не найден или не может быть удален.', 'error')
- elif not HF_TOKEN_WRITE:
- flash('Удаление невозможно: токен для записи не настроен.', 'error')
- else:
- hf_path = file_node.get('path')
- original_filename = file_node.get('original_filename', 'файл')
- try:
- if hf_path:
- api = HfApi()
- api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE,
- commit_message=f"User TGID:{tg_user_id_str} deleted file {original_filename}")
- if remove_node(user_data['filesystem'], file_id): save_data(data)
- flash(f'Файл {original_filename} успешно удален!')
- except hf_utils.EntryNotFoundError: # File not on HF, remove from DB
- if remove_node(user_data['filesystem'], file_id): save_data(data)
- flash(f'Файл {original_filename} не найден на сервере, удален из базы.')
- except Exception as e:
- logging.error(f"Error deleting file {hf_path} for TGID {tg_user_id_str}: {e}")
- flash(f'Ошибка удаления файла {original_filename}: {e}', 'error')
- return redirect(url_for('dashboard', folder_id=current_view_folder_id))
-
-@app.route('/delete_folder/', methods=['POST'])
-def delete_folder(folder_id):
- if 'telegram_user_id' not in session: flash('Пожалуйста, авторизуйтесь.'); return redirect(url_for('mini_app_entry'))
- if folder_id == 'root': flash('Нельзя удалить корневую папку!', 'error'); return redirect(url_for('dashboard'))
-
- tg_user_id_str = str(session['telegram_user_id'])
- data = load_data()
- user_data = data['users'].get(tg_user_id_str)
- if not user_data: flash('Пользователь не найден!'); session.clear(); return redirect(url_for('mini_app_entry'))
+ return redirect(url_for('main_app_view_and_upload', folder_id=current_view_folder_id))
+
+ hf_path = file_node.get('path'); original_filename = file_node.get('original_filename', 'файл')
+ if not hf_path:
+ flash(f'Ошибка: Путь к файлу {original_filename} не найден. Удаление только из базы.', 'error')
+ if remove_node(user_data['filesystem'], file_id):
+ try: save_data(data); flash(f'Метаданные файла {original_filename} удалены.')
+ except Exception as e: flash('Ошибка сохранения данных.', 'error'); logging.error(f"Delete file metadata save error: {e}")
+ return redirect(url_for('main_app_view_and_upload', folder_id=current_view_folder_id))
+ if not HF_TOKEN_WRITE: flash('Удаление невозможно: токен не настроен.', 'error'); return redirect(url_for('main_app_view_and_upload', folder_id=current_view_folder_id))
+ try:
+ api = HfApi()
+ api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"User {user_telegram_id_str} deleted {file_id}")
+ if remove_node(user_data['filesystem'], file_id):
+ try: save_data(data); flash(f'Файл {original_filename} успешно удален!')
+ except Exception as e: flash('Файл удален, ошибка обновления базы.', 'error'); logging.error(f"Delete file DB update error: {e}")
+ else: flash('Файл удален, но не найден в базе.', 'error')
+ except hf_utils.EntryNotFoundError:
+ if remove_node(user_data['filesystem'], file_id):
+ try: save_data(data); flash(f'Файл {original_filename} не найден на сервере, удален из базы.')
+ except Exception as e: flash('Ошибка сохранения (файл не на сервере).', 'error'); logging.error(f"Delete file metadata save error (HF not found): {e}")
+ else: flash('Файл не найден нигде.', 'error')
+ except Exception as e: logging.error(f"Error deleting file {hf_path} for {user_telegram_id_str}: {e}"); flash(f'Ошибка удаления {original_filename}: {e}', 'error')
+ return redirect(url_for('main_app_view_and_upload', folder_id=current_view_folder_id))
+
+@app.route('/api/delete_folder/', methods=['POST'])
+def delete_folder_api(folder_id):
+ if 'telegram_user_id' not in session: return jsonify({'status': 'error', 'message': 'Не авторизован'}), 401
+ if folder_id == 'root': flash('Нельзя удалить корневую папку!', 'error'); return redirect(url_for('main_app_view_and_upload'))
+ user_telegram_id_str = str(session['telegram_user_id'])
+ data = load_data()
+ user_data = data['users'].get(user_telegram_id_str)
+ if not user_data: flash('Пользователь не найден!', 'error'); session.clear(); return redirect(url_for('root_path'))
+
folder_node, parent_node = find_node_by_id(user_data['filesystem'], folder_id)
- current_view_folder_id = request.form.get('current_view_folder_id', parent_node.get('id', 'root') if parent_node else 'root')
+ current_view_folder_id = request.form.get('current_view_folder_id', 'root')
if not folder_node or folder_node.get('type') != 'folder' or not parent_node:
flash('Папка не найдена или не может быть удалена.', 'error')
- elif folder_node.get('children'):
- flash(f'Папку "{folder_node.get("name", "папка")}" можно удалить только если она пуста.', 'error')
- else:
- if remove_node(user_data['filesystem'], folder_id):
- try: save_data(data); flash(f'Папка "{folder_node.get("name", "папка")}" удалена.')
- except Exception as e: flash('Ошибка сохранения данных.', 'error'); logging.error(f"Delete folder save error: {e}")
- else: flash('Не удалось удалить папку.', 'error')
- return redirect(url_for('dashboard', folder_id=current_view_folder_id))
-
+ return redirect(url_for('main_app_view_and_upload', folder_id=current_view_folder_id))
+ folder_name = folder_node.get('name', 'папка')
+ if folder_node.get('children'):
+ flash(f'Папку "{folder_name}" можно удалить только если она пуста.', 'error')
+ return redirect(url_for('main_app_view_and_upload', folder_id=current_view_folder_id))
+ if remove_node(user_data['filesystem'], folder_id):
+ try: save_data(data); flash(f'Папка "{folder_name}" удалена.')
+ except Exception as e: flash('Ошибка сохранения данных.', 'error'); logging.error(f"Delete empty folder save error: {e}")
+ else: flash('Не удалось удалить папку.', 'error')
+ return redirect(url_for('main_app_view_and_upload', folder_id=parent_node.get('id', 'root')))
@app.route('/get_text_content/')
def get_text_content(file_id):
- allow_access = False
- current_user_tg_id_str = str(session.get('telegram_user_id')) if 'telegram_user_id' in session else None
-
+ is_admin_access = is_telegram_admin() and (request.referrer and 'admhosto' in request.referrer)
+ if 'telegram_user_id' not in session and not is_admin_access: return Response("Не авторизован", status=401)
+
data = load_data()
file_node = None
- is_current_user_admin = is_admin()
-
- if current_user_tg_id_str:
- user_data = data['users'].get(current_user_tg_id_str)
- if user_data:
- _file_node, _ = find_node_by_id(user_data.get('filesystem', {}), file_id)
- if _file_node and _file_node.get('type') == 'file' and _file_node.get('file_type') == 'text':
- file_node = _file_node; allow_access = True
-
- if not file_node and is_current_user_admin:
- for tg_id, udata in data.get('users', {}).items():
- if tg_id == current_user_tg_id_str: continue
- node, _ = find_node_by_id(udata.get('filesystem', {}), file_id)
+ user_context_id_str = str(session.get('telegram_user_id')) if 'telegram_user_id' in session else None
+
+ if user_context_id_str:
+ user_data = data['users'].get(user_context_id_str)
+ if user_data: file_node, _ = find_node_by_id(user_data['filesystem'], file_id)
+
+ if not file_node and is_telegram_admin():
+ for uid_str, udata_val in data.get('users', {}).items():
+ node, _ = find_node_by_id(udata_val.get('filesystem', {}), file_id)
if node and node.get('type') == 'file' and node.get('file_type') == 'text':
- file_node = node; allow_access = True; break
+ file_node = node; break
- if not allow_access or not file_node: return Response("Текстовый файл не найден или доступ запрещен", status=404)
-
+ if not file_node or file_node.get('type') != 'file' or file_node.get('file_type') != 'text': return Response("Файл не найден", status=404)
hf_path = file_node.get('path')
- if not hf_path: return Response("Ошибка: путь к файлу отсутствует", status=500)
-
+ if not hf_path: return Response("Путь к файлу отсутствует", status=500)
file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{hf_path}?download=true"
try:
- headers = {};
+ headers = {};
if HF_TOKEN_READ: headers["authorization"] = f"Bearer {HF_TOKEN_READ}"
- response = requests.get(file_url, headers=headers, timeout=10)
- response.raise_for_status()
- if len(response.content) > 1 * 1024 * 1024: return Response("Файл слишком большой для предпросмотра.", status=413)
+ response = requests.get(file_url, headers=headers); response.raise_for_status()
+ if len(response.content) > 1 * 1024 * 1024: return Response("Файл слишком большой.", status=413)
try: text_content = response.content.decode('utf-8')
- except UnicodeDecodeError: text_content = response.content.decode('latin-1', errors='replace')
+ except UnicodeDecodeError: text_content = response.content.decode('latin-1')
return Response(text_content, mimetype='text/plain')
- except requests.exceptions.RequestException as e: return Response(f"Ошибка загрузки: {e}", status=502)
- except Exception as e: return Response("Внутренняя ошибка", status=500)
+ except Exception as e: logging.error(f"Error fetching text from HF ({hf_path}): {e}"); return Response(f"Ошибка: {e}", status=502)
+
-# --- Admin Routes ---
@app.route('/admhosto')
def admin_panel():
- if not is_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('mini_app_entry'))
- data = load_data()
- users = data.get('users', {})
- user_details = []
- for tg_id_str, udata in users.items():
- file_count = 0; q = [udata.get('filesystem', {}).get('children', [])]
+ if not is_telegram_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('root_path'))
+ data = load_data(); users = data.get('users', {}); user_details = []
+ for uid_str, udata_val in users.items():
+ file_count = 0; q = [udata_val.get('filesystem', {}).get('children', [])]
while q:
current_level = q.pop(0)
- for item in current_level:
- if item.get('type') == 'file': file_count += 1
- elif item.get('type') == 'folder' and 'children' in item: q.append(item.get('children', []))
- user_details.append({
- 'telegram_user_id': tg_id_str,
- 'display_name': udata.get('tg_first_name', udata.get('tg_username', f"TGID: {tg_id_str}")),
- 'created_at': udata.get('created_at', 'N/A'), 'file_count': file_count
- })
+ for item_val in current_level:
+ if item_val.get('type') == 'file': file_count += 1
+ elif item_val.get('type') == 'folder' and 'children' in item_val: q.append(item_val.get('children', []))
+ user_display = udata_val.get('telegram_first_name', '') or udata_val.get('telegram_username', uid_str)
+ user_details.append({'id_str': uid_str, 'display_name': user_display, 'created_at': udata_val.get('created_at', 'N/A'), 'file_count': file_count})
html = '''
-Админ-панель
-Админ-панель
-
Закрыть Админку
+
+
Админ-панель
+
Админ-панель
+
Вернуться в приложение
{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}
{{ message }}
{% endfor %}{% endif %}{% endwith %}
Пользователи
-{% for user in user_details %}
-'''
+{% else %}
Пользователей нет.
{% endfor %}
'''
return render_template_string(html, user_details=user_details)
-@app.route('/admhosto/user/')
-def admin_user_files(telegram_user_id):
- if not is_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('mini_app_entry'))
- data = load_data()
- user_data = data.get('users', {}).get(telegram_user_id)
- if not user_data: flash(f'Пользователь {telegram_user_id} не найден.', 'error'); return redirect(url_for('admin_panel'))
-
- initialize_user_filesystem(user_data) # Ensure fs for this user if accessed by admin first time
-
- user_display_name = user_data.get('tg_first_name', user_data.get('tg_username', f"TGID: {telegram_user_id}"))
- all_files = []
- def collect_files_recursive(folder_node, current_path_str_list):
- for item in folder_node.get('children', []):
- if item.get('type') == 'file':
- item_copy = item.copy()
- item_copy['parent_path_str'] = " / ".join(current_path_str_list) or "Root"
- all_files.append(item_copy)
- elif item.get('type') == 'folder':
- collect_files_recursive(item, current_path_str_list + [item.get('name', 'Unnamed Folder')])
-
- collect_files_recursive(user_data.get('filesystem', {}), [])
- all_files.sort(key=lambda x: x.get('upload_date', ''), reverse=True)
-
+@app.route('/admhosto/user/')
+def admin_user_files(user_telegram_id_str):
+ if not is_telegram_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('root_path'))
+ data = load_data(); user_data = data.get('users', {}).get(user_telegram_id_str)
+ if not user_data: flash(f'Пользователь {user_telegram_id_str} не найден.', 'error'); return redirect(url_for('admin_panel'))
+ all_files = []; user_display_name = user_data.get('telegram_first_name', '') or user_data.get('telegram_username', user_telegram_id_str)
+ def collect_files(folder, current_path_id='root'):
+ parent_path_str = get_node_path_string(user_data['filesystem'], current_path_id)
+ for item_val in folder.get('children', []):
+ if item_val.get('type') == 'file': item_val['parent_path_str'] = parent_path_str; all_files.append(item_val)
+ elif item_val.get('type') == 'folder': collect_files(item_val, item_val.get('id'))
+ collect_files(user_data.get('filesystem', {})); all_files.sort(key=lambda x_file: x_file.get('upload_date', ''), reverse=True)
html = '''
-Файлы {{ user_display_name }}
-Файлы: {{ user_display_name }} ({{telegram_user_id}})
-
Назад
+
Файлы {{ user_display_name }}
+
+
Файлы пользовате��я: {{ user_display_name }} (ID: {{ user_telegram_id_str }})
+
Назад к пользователям
{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}
{{ message }}
{% endfor %}{% endif %}{% endwith %}
-
-{% for file in files %}
- {% if file.file_type == 'image' %}
- {% elif file.file_type == 'video' %}
- {% elif file.file_type == 'pdf' %}
📄
- {% elif file.file_type == 'text' %}
📝
- {% else %}
❓
{% endif %}
-
{{ file.original_filename | truncate(25) }}
-
В папке: {{ file.parent_path_str }}
-
Загружен: {{ file.upload_date }}
-
ID: {{ file.id }}
-
Path: {{ file.path }}
+
+{% for file_item_val in files %}
+ {% if file_item_val.file_type == 'image' %}
+ {% elif file_item_val.file_type == 'video' %}
+ {% elif file_item_val.file_type == 'pdf' %}
📄
+ {% elif file_item_val.file_type == 'text' %}
📝
+ {% else %}
❓
{% endif %}
+
{{ file_item_val.original_filename | truncate(30) }}
+
В папке: {{ file_item_val.parent_path_str }}
+
Загружен: {{ file_item_val.upload_date }}
+
ID: {{ file_item_val.id }}
+
Path: {{ file_item_val.path }}
-
Скачать
- {% set previewable = file.file_type in ['image', 'video', 'pdf', 'text'] %}
- {% if previewable %}
Просмотр {% endif %}
-
+ Скачать
+ {% set previewable = file_item_val.file_type in ['image', 'video', 'pdf', 'text'] %}
+ {% if previewable %}Просмотр {% endif %}
+
Удалить
{% else %}
У пользователя нет файлов.
{% endfor %}
-
+
'''
- return render_template_string(html, telegram_user_id=telegram_user_id, user_display_name=user_display_name, files=all_files, repo_id=REPO_ID,
- hf_file_url=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{path}{'?download=true' if download else ''}")
+ return render_template_string(html, user_telegram_id_str=user_telegram_id_str, user_display_name=user_display_name, files=all_files, repo_id=REPO_ID, hf_file_url=lambda path, download=False: f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{path}{'?download=true' if download else ''}")
-@app.route('/admhosto/delete_user/
', methods=['POST'])
-def admin_delete_user(telegram_user_id):
- if not is_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('mini_app_entry'))
+@app.route('/admhosto/delete_user/', methods=['POST'])
+def admin_delete_user(user_telegram_id_str):
+ if not is_telegram_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('root_path'))
if not HF_TOKEN_WRITE: flash('Удаление невозможно: токен не настроен.', 'error'); return redirect(url_for('admin_panel'))
-
data = load_data()
- if telegram_user_id not in data['users']: flash('Пользователь не найден!', 'error'); return redirect(url_for('admin_panel'))
-
- logging.warning(f"ADMIN ACTION: Attempting to delete user TGID {telegram_user_id} and all their data.")
+ if user_telegram_id_str not in data['users']: flash('Пользователь не найден!', 'error'); return redirect(url_for('admin_panel'))
+ logging.warning(f"ADMIN ACTION: Deleting user {user_telegram_id_str}.")
try:
- api = HfApi(); user_folder_path_on_hf = f"cloud_files/{telegram_user_id}"
- logging.info(f"Attempting to delete HF Hub folder: {user_folder_path_on_hf} for user TGID {telegram_user_id}")
- api.delete_folder(folder_path=user_folder_path_on_hf, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE,
- commit_message=f"ADMIN ACTION: Deleted all files/folders for user TGID {telegram_user_id}")
- logging.info(f"Successfully initiated deletion of folder {user_folder_path_on_hf} on HF Hub.")
+ api = HfApi(); user_folder_path_on_hf = f"cloud_files/{user_telegram_id_str}"
+ api.delete_folder(folder_path=user_folder_path_on_hf, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"ADMIN: Deleted all files for user {user_telegram_id_str}")
except hf_utils.HfHubHTTPError as e:
- if e.response.status_code == 404: logging.warning(f"User folder {user_folder_path_on_hf} not found on HF Hub.")
- else: logging.error(f"Error deleting user folder {user_folder_path_on_hf} from HF Hub: {e}"); flash(f'Ошибка удаления файлов с сервера: {e}. Пользователь НЕ удален из базы.', 'error'); return redirect(url_for('admin_panel'))
- except Exception as e: logging.error(f"Unexpected error during HF folder deletion: {e}"); flash(f'Неожиданная ошибка удаления файлов с сервера: {e}. Пользователь НЕ удален из базы.', 'error'); return redirect(url_for('admin_panel'))
-
+ if e.response.status_code == 404: logging.warning(f"User folder {user_folder_path_on_hf} not found on HF. Skipping HF deletion.")
+ else: logging.error(f"Error deleting user folder from HF for {user_telegram_id_str}: {e}"); flash(f'Ошибка удаления файлов {user_telegram_id_str} с сервера: {e}.', 'error'); return redirect(url_for('admin_panel'))
+ except Exception as e: logging.error(f"Unexpected error deleting HF folder for {user_telegram_id_str}: {e}"); flash(f'Ошибка удаления файлов {user_telegram_id_str} с сервера: {e}.', 'error'); return redirect(url_for('admin_panel'))
try:
- del data['users'][telegram_user_id]; save_data(data)
- flash(f'Пользователь TGID {telegram_user_id} и его файлы удалены/запрошены к удалению!')
- logging.info(f"ADMIN ACTION: Successfully deleted user TGID {telegram_user_id} from database.")
- except Exception as e: logging.error(f"Error saving data after deleting user {telegram_user_id}: {e}"); flash(f'Файлы удалены с сервера, но ошибка удаления из базы: {e}', 'error')
+ del data['users'][user_telegram_id_str]; save_data(data)
+ flash(f'Пользователь {user_telegram_id_str} и его файлы удалены!')
+ logging.info(f"ADMIN ACTION: Deleted user {user_telegram_id_str} from database.")
+ except Exception as e: logging.error(f"Error saving data after deleting user {user_telegram_id_str}: {e}"); flash(f'Файлы удалены, ошибка удаления из базы: {e}', 'error')
return redirect(url_for('admin_panel'))
-@app.route('/admhosto/delete_file//', methods=['POST'])
-def admin_delete_file(telegram_user_id, file_id):
- if not is_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('mini_app_entry'))
- if not HF_TOKEN_WRITE: flash('Удаление невозможно: токен не настроен.', 'error'); return redirect(url_for('admin_user_files', telegram_user_id=telegram_user_id))
-
- data = load_data()
- user_data = data.get('users', {}).get(telegram_user_id)
- if not user_data: flash(f'Пользователь {telegram_user_id} не найден.', 'error'); return redirect(url_for('admin_panel'))
-
- file_node, _ = find_node_by_id(user_data.get('filesystem',{}), file_id)
- if not file_node or file_node.get('type') != 'file': flash('Файл не найден.', 'error'); return redirect(url_for('admin_user_files', telegram_user_id=telegram_user_id))
-
- hf_path = file_node.get('path')
- original_filename = file_node.get('original_filename', 'файл')
+@app.route('/admhosto/delete_file//', methods=['POST'])
+def admin_delete_file(user_telegram_id_str, file_id):
+ if not is_telegram_admin(): flash('Доступ запрещен.', 'error'); return redirect(url_for('root_path'))
+ if not HF_TOKEN_WRITE: flash('Удаление невозможно: токен не настроен.', 'error'); return redirect(url_for('admin_user_files', user_telegram_id_str=user_telegram_id_str))
+ data = load_data(); user_data = data.get('users', {}).get(user_telegram_id_str)
+ if not user_data: flash(f'Пользователь {user_telegram_id_str} не найден.', 'error'); return redirect(url_for('admin_panel'))
+ file_node, parent_node = find_node_by_id(user_data['filesystem'], file_id)
+ if not file_node or file_node.get('type') != 'file' or not parent_node: flash('Файл не найден.', 'error'); return redirect(url_for('admin_user_files', user_telegram_id_str=user_telegram_id_str))
+ hf_path = file_node.get('path'); original_filename = file_node.get('original_filename', 'файл')
+ if not hf_path:
+ if remove_node(user_data['filesystem'], file_id):
+ try: save_data(data); flash(f'Метаданные {original_filename} удалены (путь отсутствовал).')
+ except Exception as e: flash('Ошибка сохранения (путь отсутствовал).', 'error'); logging.error(f"Admin delete file metadata (no path): {e}")
+ return redirect(url_for('admin_user_files', user_telegram_id_str=user_telegram_id_str))
try:
- if hf_path:
- api = HfApi()
- api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE,
- commit_message=f"ADMIN ACTION: Deleted file {original_filename} for user TGID {telegram_user_id}")
- if remove_node(user_data['filesystem'], file_id): save_data(data)
- flash(f'Файл {original_filename} удален!')
+ api = HfApi(); api.delete_file(path_in_repo=hf_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"ADMIN: Deleted {file_id} for {user_telegram_id_str}")
+ if remove_node(user_data['filesystem'], file_id):
+ try: save_data(data); flash(f'Файл {original_filename} удален!')
+ except Exception as e: flash('Файл удален, ошибка обновления базы.', 'error'); logging.error(f"Admin delete file DB update error: {e}")
+ else: flash('Файл удален, но не найден в базе.', 'error')
except hf_utils.EntryNotFoundError:
- if remove_node(user_data['filesystem'], file_id): save_data(data)
- flash(f'Файл {original_filename} не найден на сервере, удален из базы.')
- except Exception as e:
- logging.error(f"ADMIN ACTION: Error deleting file {hf_path} for TGID {telegram_user_id}: {e}")
- flash(f'Ошибка удаления файла {original_filename}: {e}', 'error')
- return redirect(url_for('admin_user_files', telegram_user_id=telegram_user_id))
+ if remove_node(user_data['filesystem'], file_id):
+ try: save_data(data); flash(f'Файл {original_filename} не найден на сервере, удален из базы.')
+ except Exception as e: flash('Ошибка сохранения (файл не на сервере).', 'error'); logging.error(f"Admin delete metadata (HF not found): {e}")
+ else: flash('Файл не найден нигде.', 'error')
+ except Exception as e: logging.error(f"ADMIN: Error deleting {hf_path} for {user_telegram_id_str}: {e}"); flash(f'Ошибка удаления {original_filename}: {e}', 'error')
+ return redirect(url_for('admin_user_files', user_telegram_id_str=user_telegram_id_str))
+
+async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ if update.effective_chat is None: return
+ keyboard = [[InlineKeyboardButton("Открыть Zeus Cloud", web_app=WebAppInfo(url=flask.url_for('root_path', _external=True)))]]
+ reply_markup = InlineKeyboardMarkup(keyboard)
+ await update.message.reply_text('Нажмите кнопку ниже, чтобы запустить Zeus Cloud:', reply_markup=reply_markup)
+
+async def plain_message_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ if update.message and update.message.web_app_data:
+ logging.info(f"Received web_app_data: {update.message.web_app_data.data}")
+ await update.message.reply_text(f"Получены данные из Web App: {update.message.web_app_data.data}")
if __name__ == '__main__':
- if not BOT_TOKEN: logging.critical("BOT_TOKEN is not set. Telegram authentication will FAIL.")
- if not ADMIN_TELEGRAM_USER_ID: logging.warning("ADMIN_TELEGRAM_USER_ID is not set. Admin panel will be inaccessible.")
- if not HF_TOKEN_WRITE: logging.warning("HF_TOKEN (write access) is not set. File uploads, deletions, and backups will fail.")
- if not HF_TOKEN_READ: logging.warning("HF_TOKEN_READ is not set (or HF_TOKEN). Downloads/previews might fail for private repos.")
-
+ if not HF_TOKEN_WRITE: logging.warning("HF_TOKEN (write) not set. Uploads/deletions/backups fail.")
+ if not HF_TOKEN_READ: logging.warning("HF_TOKEN_READ not set. Using HF_TOKEN. Downloads/previews might fail.")
if HF_TOKEN_WRITE:
- logging.info("Performing initial database download before starting background backup.")
- download_db_from_hf() # Download once at start
+ download_db_from_hf()
threading.Thread(target=periodic_backup, daemon=True).start()
- logging.info("Periodic backup thread started.")
else:
- logging.warning("Periodic backup disabled (HF_TOKEN_WRITE not set).")
if HF_TOKEN_READ: download_db_from_hf()
- elif not os.path.exists(DATA_FILE): # No tokens and no local file
- with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f)
- logging.info(f"Created empty local database file: {DATA_FILE}")
-
- app.run(debug=False, host='0.0.0.0', port=int(os.getenv("PORT", 7860)))
\ No newline at end of file
+ else:
+ if not os.path.exists(DATA_FILE):
+ with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump({'users': {}}, f)
+
+ bot_app = Application.builder().token(BOT_TOKEN).build()
+ bot_app.add_handler(CommandHandler("start", start_command))
+ bot_app.add_handler(MessageHandler(filters.StatusUpdate.WEB_APP_DATA, plain_message_handler))
+
+ flask_thread = threading.Thread(target=lambda: app.run(debug=False, host='0.0.0.0', port=7860, use_reloader=False))
+ flask_thread.start()
+
+ logging.info("Flask app started. Starting Telegram bot polling.")
+ bot_app.run_polling()
\ No newline at end of file