diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,54 +1,60 @@ -from flask import Flask, render_template_string, request, redirect, url_for, session, flash, send_file, jsonify +from flask import Flask, render_template_string, request, redirect, url_for, session, flash, jsonify from flask_caching import Cache import json import os import logging import threading import time -from datetime import datetime +from datetime import datetime, timedelta from huggingface_hub import HfApi, hf_hub_download from werkzeug.utils import secure_filename -import requests -from io import BytesIO +import random app = Flask(__name__) app.secret_key = os.getenv("FLASK_SECRET_KEY", "supersecretkey") -DATA_FILE = 'cloud_data.json' -REPO_ID = "Eluza133/Z1e1u" +DATA_FILE = 'data_adusis.json' +REPO_ID = "Eluza133/A12d12s12" # Replace with your actual repo ID HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE -REGISTRATION_CODE = "morshenalphacl" -cache = Cache(app, config={'CACHE_TYPE': 'simple'}) +cache = Cache(app, config={'CACHE_TYPE': 'simple'}) # Consider 'filesystem' for larger apps logging.basicConfig(level=logging.INFO) -# Функции для работы с базой данных и Hugging Face -@cache.memoize(timeout=300) +# --- Helper Functions --- + +@cache.memoize(timeout=600) # Cache for longer, 10 minutes def load_data(): try: download_db_from_hf() with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file) + # Data validation and defaults (more robust) if not isinstance(data, dict): - logging.warning("Данные не в формате dict, инициализация пустой базы") - return {'users': {}, 'files': {}} + logging.warning("Data is not a dictionary. Initializing empty database.") + return {'posts': [], 'users': {}, 'general_chat': [], 'private_chats': {}} + data.setdefault('posts', []) data.setdefault('users', {}) - data.setdefault('files', {}) - logging.info("Данные успешно загружены") + data.setdefault('general_chat', []) + data.setdefault('private_chats', {}) + for user in data['users']: + data['users'][user].setdefault('last_chat_visit', '1970-01-01T00:00:00') # ISO 8601 + data['users'][user].setdefault('last_private_visit', '1970-01-01T00:00:00') + data['users'][user].setdefault('last_seen', '1970-01-01T00:00:00') + logging.info("Data loaded successfully.") return data except Exception as e: - logging.error(f"Ошибка при загрузке данных: {e}") - return {'users': {}, 'files': {}} + logging.error(f"Error loading data: {e}") + return {'posts': [], 'users': {}, 'general_chat': [], 'private_chats': {}} def save_data(data): try: with open(DATA_FILE, 'w', encoding='utf-8') as file: json.dump(data, file, ensure_ascii=False, indent=4) upload_db_to_hf() - cache.clear() - logging.info("Данные сохранены и загружены на HF") + cache.clear() # Clear cache on every save + logging.info("Data saved and uploaded to HF.") except Exception as e: - logging.error(f"Ошибка при сохранении данных: {e}") + logging.error(f"Error saving data: {e}") raise def upload_db_to_hf(): @@ -60,11 +66,11 @@ def upload_db_to_hf(): repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, - commit_message=f"Бэкап {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + commit_message=f"Backup {datetime.now().isoformat()}" # ISO 8601 ) - logging.info("База данных загружена на Hugging Face") + logging.info("Database uploaded to Hugging Face.") except Exception as e: - logging.error(f"Ошибка при загрузке базы данных: {e}") + logging.error(f"Error uploading database: {e}") def download_db_from_hf(): try: @@ -76,786 +82,2484 @@ def download_db_from_hf(): local_dir=".", local_dir_use_symlinks=False ) - logging.info("База данных скачана с Hugging Face") + logging.info("Database downloaded from Hugging Face.") except Exception as e: - logging.error(f"Ошибка при скачивании базы данных: {e}") + logging.error(f"Error downloading database: {e}") if not os.path.exists(DATA_FILE): + logging.info("Creating initial data file.") with open(DATA_FILE, 'w', encoding='utf-8') as f: - json.dump({'users': {}, 'files': {}}, f) + json.dump({'posts': [], 'users': {}, 'general_chat': [], 'private_chats': {}}, f, indent=4) + def periodic_backup(): while True: upload_db_to_hf() - time.sleep(1800) + time.sleep(1800) # 30 minutes + +def get_unread_count(data, username): + if username not in data['users']: + return 0 + last_visit = datetime.fromisoformat(data['users'][username]['last_chat_visit']) + return sum(1 for msg in data['general_chat'] if datetime.fromisoformat(msg['time']) > last_visit) + +def get_private_unread_count(data, username): + if username not in data['users']: + return 0 + last_visit = datetime.fromisoformat(data['users'][username]['last_private_visit']) + unread = 0 + for chat_key, messages in data['private_chats'].items(): + users = chat_key.split('_') + if username in users: + unread += sum(1 for msg in messages if datetime.fromisoformat(msg['time']) > last_visit and msg['sender'] != username) + return unread + +def is_user_online(data, username): + if username not in data['users']: + return False + last_seen = datetime.fromisoformat(data['users'][username]['last_seen']) + return (datetime.now() - last_seen).total_seconds() < 300 # 5 minutes -def get_file_type(filename): - video_extensions = ('.mp4', '.mov', '.avi') - image_extensions = ('.jpg', '.jpeg', '.png', '.gif') - if filename.lower().endswith(video_extensions): - return 'video' - elif filename.lower().endswith(image_extensions): - return 'image' - return 'other' +def update_last_seen(data, username): + if username in data['users']: + data['users'][username]['last_seen'] = datetime.now().isoformat() # Use ISO 8601 + save_data(data) + + + +# --- Styling and HTML Templates --- 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); + /* 2025 Modern UI Colors */ + --primary: #6A1B9A; /* Deep Purple */ + --secondary: #00838F; /* Cyan */ + --accent: #FFCA28; /* Amber */ + --background-light: #FAFAFA; /* Very Light Gray */ + --background-dark: #212121; /* Dark Gray */ + --card-bg: rgba(255, 255, 255, 0.9); + --card-bg-dark: rgba(50, 50, 50, 0.9); + --text-light: #424242; /* Dark Gray */ + --text-dark: #EEEEEE; /* Light Gray */ + --shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + --glass-bg: rgba(255, 255, 255, 0.2); --transition: all 0.3s ease; - --delete-color: #ff4444; + --online: #4CAF50; /* Green */ + --offline: #F44336; /* Red */ + --border-radius: 12px; } +/* Reset & Basic Styles */ * { margin: 0; padding: 0; box-sizing: border-box; } body { - font-family: 'Inter', sans-serif; + font-family: 'Inter', sans-serif; /* Modern Font */ background: var(--background-light); color: var(--text-light); line-height: 1.6; + overflow-x: hidden; } -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; +body.dark { background: var(--background-dark); color: var(--text-dark); } + +/* Sidebar */ +.sidebar { + position: fixed; + top: 0; + left: 0; + width: 280px; + height: 100%; + background: var(--glass-bg); + backdrop-filter: blur(15px); + padding: 20px; box-shadow: var(--shadow); + z-index: 1000; + transition: transform var(--transition); + border-right: 1px solid rgba(0, 0, 0, 0.1); } -body.dark .container { - background: var(--card-bg-dark); +.sidebar.hidden { transform: translateX(-100%); } +.sidebar-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 30px; } -h1 { - font-size: 2em; - font-weight: 800; - text-align: center; - margin-bottom: 25px; +.nav-brand { + font-size: 1.7em; + font-weight: 700; background: linear-gradient(135deg, var(--primary), var(--accent)); -webkit-background-clip: text; color: transparent; + letter-spacing: 1px; } -h2 { - font-size: 1.5em; - margin-top: 30px; +.logo { + width: 40px; height: 40px; + border-radius: var(--border-radius); + box-shadow: var(--shadow); +} +.nav-links { display: flex; flex-direction: column; gap: 10px; } +.nav-link { + display: flex; align-items: center; gap: 12px; + padding: 10px 15px; + background: var(--card-bg); color: var(--text-light); + text-decoration: none; + border-radius: var(--border-radius); + font-size: 1em; + font-weight: 500; + transition: var(--transition); + position: relative; + border: 1px solid rgba(0, 0, 0, 0.1); } -body.dark h2 { - color: var(--text-dark); +body.dark .nav-link { background: var(--card-bg-dark); color: var(--text-dark); border-color: rgba(255,255,255,0.1); } +.nav-link:hover { background: var(--primary); color: white; box-shadow: 0 4px 10px rgba(106, 27, 154, 0.4); } +.nav-link .badge { + position: absolute; + right: 15px; top: 50%; + transform: translateY(-50%); + background: var(--accent); + color: white; + padding: 3px 8px; + border-radius: 8px; + font-size: 0.75em; font-weight: 600; } -input, textarea { - width: 100%; - padding: 14px; - margin: 12px 0; - border: none; - border-radius: 14px; +.logout-btn { background: var(--secondary); color: white; } +.logout-btn:hover { background: #006064; /* Darker Cyan */ } + +/* Menu Button & Theme Toggle */ +.menu-btn, .theme-toggle { + display: none; /* Hidden by default, shown on smaller screens */ + font-size: 24px; 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); + border: none; + color: var(--primary); + cursor: pointer; + position: fixed; + top: 20px; + padding: 10px; + border-radius: 50%; + box-shadow: var(--shadow); + transition: var(--transition); + z-index: 1001; + border: 1px solid rgba(0, 0, 0, 0.1); } -input:focus, textarea:focus { - outline: none; - box-shadow: 0 0 0 4px var(--primary); +.menu-btn { left: 20px; } +.theme-toggle { right: 20px; } +.menu-btn:hover, .theme-toggle:hover { background: var(--primary); color: white; } + +/* Container & Buttons */ +.container { + margin: 20px auto 20px 300px; + max-width: 1100px; + padding: 25px; + transition: var(--transition); } .btn { - padding: 14px 28px; + padding: 10px 20px; background: var(--primary); color: white; border: none; - border-radius: 14px; + border-radius: var(--border-radius); cursor: pointer; - font-size: 1.1em; - font-weight: 600; + font-size: 1em; + font-weight: 500; transition: var(--transition); + display: inline-flex; + align-items: center; + gap: 8px; box-shadow: var(--shadow); - display: inline-block; - text-decoration: none; + border: 1px solid rgba(0, 0, 0, 0.1); } -.btn:hover { - transform: scale(1.05); - background: #e6415f; -} -.download-btn { - background: var(--secondary); - margin-top: 10px; -} -.download-btn:hover { - background: #00b8c5; -} -.delete-btn { - background: var(--delete-color); - margin-top: 10px; -} -.delete-btn:hover { - background: #cc3333; -} -.flash { - color: var(--secondary); - text-align: center; - margin-bottom: 15px; -} -.file-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); - gap: 20px; - margin-top: 20px; +.btn:hover { background: #880E4F; /* Darker Purple */ box-shadow: 0 6px 15px rgba(106, 27, 154, 0.5); } + +/* Inputs, Textareas, Selects */ +input, textarea, select { + width: 100%; + padding: 10px; + margin: 10px 0; + border: 1px solid rgba(0, 0, 0, 0.1); + border-radius: var(--border-radius); + background: var(--glass-bg); + color: var(--text-light); + font-size: 1em; + transition: var(--transition); + box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.1); } -.user-list { - margin-top: 20px; +body.dark input, body.dark textarea, body.dark select { color: var(--text-dark); border-color: rgba(255,255,255,0.1); } +input:focus, textarea:focus, select:focus { outline: none; background: rgba(255, 255, 255, 0.3); box-shadow: 0 0 0 3px var(--primary); } + +/* Modal */ +.modal { + display: none; /* Hidden by default */ + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background: rgba(0, 0, 0, 0.8); + z-index: 2000; + justify-content: center; + align-items: center; + transition: opacity var(--transition); } -.user-item { - padding: 15px; - background: var(--card-bg); - border-radius: 16px; - margin-bottom: 10px; +.modal img, .modal video { + max-width: 90%; max-height: 90%; + object-fit: contain; + border-radius: var(--border-radius); box-shadow: var(--shadow); - transition: var(--transition); + animation: zoomIn 0.3s ease; } -body.dark .user-item { - background: var(--card-bg-dark); + +/* Status Dot */ +.status-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-left: 5px; } +.online { background: var(--online); } +.offline { background: var(--offline); } + +/* Keyframes */ +@keyframes zoomIn { from { opacity: 0; transform: scale(0.8); } to { opacity: 1; transform: scale(1); } } + +/* Responsive Design */ +@media (max-width: 900px) { + .sidebar { width: 100%; max-width: 280px; transform: translateX(-100%); } + .sidebar.active { transform: translateX(0); } + .menu-btn { display: block; } + .container { margin: 80px 20px 20px 20px; max-width: calc(100% - 40px); } + .theme-toggle { top: 80px; } } -.user-item:hover { - transform: translateY(-5px); +@media (max-width: 480px) { + .nav-brand { font-size: 1.5em; } + .nav-link { font-size: 0.9em; padding: 8px 12px; } + .btn { padding: 8px 16px; font-size: 0.9em; } } -.user-item a { - color: var(--primary); - text-decoration: none; + +/* Additional Styles for Forms and Cards */ +h1, h2, h3 { font-weight: 600; + margin-bottom: 15px; + background: linear-gradient(135deg, var(--primary), var(--accent)); + -webkit-background-clip: text; + color: transparent; } -.user-item a:hover { - color: var(--accent); -} -@media (max-width: 768px) { - .file-grid { - grid-template-columns: repeat(2, 1fr); - } -} -@media (max-width: 480px) { - .file-grid { - grid-template-columns: 1fr; - } -} -.file-item { +h1 { font-size: 2em; } +h2 { font-size: 1.5em; } +h3 { font-size: 1.2em; } + +.form-group { margin-bottom: 20px; } +.form-group label { display: block; margin-bottom: 5px; font-weight: 500; color: var(--text-light);} +body.dark .form-group label { color: var(--text-dark); } +.form-control { width: 100%; } /* Use class for inputs, textareas, etc. */ + +.card { background: var(--card-bg); - padding: 15px; - border-radius: 16px; + border-radius: var(--border-radius); box-shadow: var(--shadow); - text-align: center; + padding: 20px; + margin-bottom: 20px; transition: var(--transition); } -body.dark .file-item { - background: var(--card-bg-dark); -} -.file-item:hover { - transform: translateY(-5px); -} -.file-preview { - max-width: 100%; - max-height: 200px; - object-fit: cover; - border-radius: 10px; - margin-bottom: 10px; - loading: lazy; -} -.file-item p { - font-size: 0.9em; - margin: 5px 0; +body.dark .card { background: var(--card-bg-dark); } +.card:hover { transform: translateY(-5px); } + +.flash { + color: var(--secondary); + text-align: center; + margin-bottom: 15px; + font-size: 1em; + font-weight: 500; } -.file-item a { + +.username-link { color: var(--primary); + font-weight: 600; text-decoration: none; } -.file-item a:hover { - color: var(--accent); -} -.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 img, .modal video { - max-width: 95%; - max-height: 95%; - object-fit: contain; - border-radius: 20px; - box-shadow: var(--shadow); -} -#progress-container { - width: 100%; - background: var(--glass-bg); - border-radius: 10px; - margin: 15px 0; - display: none; -} -#progress-bar { - width: 0%; - height: 20px; - background: var(--primary); - border-radius: 10px; - transition: width 0.3s ease; -} +.username-link:hover { color: var(--accent); } +/* Scrollbar Styling */ +::-webkit-scrollbar { width: 10px; } +::-webkit-scrollbar-track { background: var(--glass-bg); } +::-webkit-scrollbar-thumb { background: var(--primary); border-radius: 5px; } +::-webkit-scrollbar-thumb:hover { background: #880E4F; } +''' + +NAV_HTML = ''' + ''' + +# --- Route Handlers --- + @app.route('/register', methods=['GET', 'POST']) def register(): if request.method == 'POST': username = request.form.get('username') password = request.form.get('password') - code = request.form.get('code') - data = load_data() - + if username in data['users']: - flash('Пользователь с таким логином уже существует!') - return redirect(url_for('register')) - - if code != REGISTRATION_CODE: - flash('Неверный регистрационный код!') + flash('User already exists!') return redirect(url_for('register')) - - if not username or not password: - flash('Логин и пароль обязательны!') - return redirect(url_for('register')) - + data['users'][username] = { 'password': password, - 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - 'files': [] + 'bio': '', + 'link': '', + 'avatar': None, + 'last_chat_visit': '1970-01-01T00:00:00', # Use ISO 8601 + 'last_private_visit': '1970-01-01T00:00:00', + 'last_seen': datetime.now().isoformat() } save_data(data) - session['username'] = username - flash('Регистрация успешна!') - return redirect(url_for('dashboard')) - - html = ''' + flash('Registration successful! Please login.') + return redirect(url_for('login')) + + is_authenticated = 'username' in session + username = session.get('username', None) + data = load_data() + unread_count = get_unread_count(data, username) if is_authenticated else 0 + private_unread_count = get_private_unread_count(data, username) if is_authenticated else 0 + user_count = len(data['users']) + is_online = is_user_online(data, username) if is_authenticated else False + + html = f''' - Регистрация - Zeus Cloud + + + Register - Content Hub - + + + {NAV_HTML} +
-

Регистрация в Zeus Cloud

- {% with messages = get_flashed_messages() %} - {% if messages %} - {% for message in messages %} -
{{ message }}
- {% endfor %} - {% endif %} - {% endwith %} -
- - - - -
-

Уже есть аккаунт? Войдите

+
+

Register

+ {{% with messages = get_flashed_messages() %}} + {{% if messages %}} + {{% for message in messages %}} +
{{{{ message }}}}
+ {{% endfor %}} + {{% endif %}} + {{% endwith %}} +
+
+ +
+
+ +
+ +
+

Already have an account? Login

+
+ ''' - return render_template_string(html) + return render_template_string(html, is_authenticated=is_authenticated, username=username, unread_count=unread_count, user_count=user_count, private_unread_count=private_unread_count, is_online=is_online) -@app.route('/', methods=['GET', 'POST']) +@app.route('/login', methods=['GET', 'POST']) def login(): + data = load_data() if request.method == 'POST': username = request.form.get('username') password = request.form.get('password') - data = load_data() - + if username in data['users'] and data['users'][username]['password'] == password: session['username'] = username - return jsonify({'status': 'success', 'redirect': url_for('dashboard')}) - else: - return jsonify({'status': 'error', 'message': 'Неверный логин или пароль!'}) - - html = ''' + session.permanent = True + update_last_seen(data, username) + return redirect(url_for('feed')) + flash('Invalid username or password!') + return redirect(url_for('login')) + + is_authenticated = 'username' in session + username = session.get('username', None) + if is_authenticated: + update_last_seen(data, username) + unread_count = get_unread_count(data, username) if is_authenticated else 0 + private_unread_count = get_private_unread_count(data, username) if is_authenticated else 0 + user_count = len(data['users']) + is_online = is_user_online(data, username) if is_authenticated else False + + html = f''' - Zeus Cloud + + Login - Content Hub - + + + {NAV_HTML} +
-

Zeus Cloud

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

Login

+ {{% with messages = get_flashed_messages() %}} + {{% if messages %}} + {{% for message in messages %}} +
{{{{ message }}}}
+ {{% endfor %}} + {{% endif %}} + {{% endwith %}} +
+
+ +
+
+ +
+ +
+

Don't have an account? Register

-
- - - -
-

Нет аккаунта? Зарегистрируйтесь

''' - return render_template_string(html) + return render_template_string(html, is_authenticated=is_authenticated, username=username, unread_count=unread_count, user_count=user_count, private_unread_count=private_unread_count, is_online=is_online) + +@app.route('/logout') +def logout(): + data = load_data() + username = session.get('username', None) + if username: + update_last_seen(data, username) + session.pop('username', None) + return redirect(url_for('feed')) -@app.route('/dashboard', methods=['GET', 'POST']) -def dashboard(): +@app.route('/toggle_like/', methods=['POST']) +def toggle_like(post_id): if 'username' not in session: - flash('Пожалуйста, войдите!') - return redirect(url_for('login')) - + return jsonify({'error': 'Not authenticated'}), 401 + data = load_data() username = session['username'] + post = next((p for p in data['posts'] if p['id'] == post_id), None) + if not post: + return jsonify({'error': 'Post not found'}), 404 + + likes = post.get('likes', []) + if username in likes: + post['likes'] = [u for u in likes if u != username] + liked = False + else: + post['likes'] = likes + [username] + liked = True + save_data(data) + return jsonify({'liked': liked, 'likes_count': len(post['likes'])}) + +@app.route('/increment_view/', methods=['POST']) +def increment_view(post_id): data = load_data() - if username not in data['users']: - session.pop('username', None) - flash('Пользователь не найден!') - return redirect(url_for('login')) - - user_files = sorted(data['users'][username]['files'], key=lambda x: x['upload_date'], reverse=True) + post = next((p for p in data['posts'] if p['id'] == post_id), None) + if post: + post['views'] = post.get('views', 0) + 1 + save_data(data) + return jsonify({'views': post['views']}) + return jsonify({'error': 'Post not found'}), 404 +@app.route('/', methods=['GET', 'POST']) +def feed(): + data = load_data() + username = session.get('username', None) + if username: + update_last_seen(data, username) + posts = sorted(data.get('posts', []), key=lambda x: datetime.fromisoformat(x['upload_date']), reverse=True) + is_authenticated = 'username' in session + unread_count = get_unread_count(data, username) if is_authenticated else 0 + private_unread_count = get_private_unread_count(data, username) if is_authenticated else 0 + user_count = len(data['users']) + is_online = is_user_online(data, username) if is_authenticated else False - if request.method == 'POST': - files = request.files.getlist('files') - if files and len(files) > 20: - flash('Максимум 20 файлов за раз!') - return redirect(url_for('dashboard')) - - if files: - os.makedirs('uploads', exist_ok=True) - api = HfApi() - temp_files = [] - - for file in files: - if file and file.filename: - filename = secure_filename(file.filename) - temp_path = os.path.join('uploads', filename) - file.save(temp_path) - temp_files.append((temp_path, filename)) - - for temp_path, filename in temp_files: - file_path = f"cloud_files/{username}/{filename}" - api.upload_file( - path_or_fileobj=temp_path, - path_in_repo=file_path, - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=f"Загружен файл для {username}" - ) - - file_info = { - 'filename': filename, - 'path': file_path, - 'type': get_file_type(filename), - 'upload_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S') - } - data['users'][username]['files'].append(file_info) - - if os.path.exists(temp_path): - os.remove(temp_path) - - save_data(data) - flash('Файлы успешно загружены!') - - return redirect(url_for('dashboard')) + if request.method == 'POST' and is_authenticated: + post_id = request.form.get('post_id') + post = next((p for p in data['posts'] if p['id'] == post_id), None) + if not post: + return "Post not found", 404 + + if 'comment' in request.form: + comment_text = request.form.get('comment') + if comment_text: + post['comments'] = post.get('comments', []) + [{ + 'user': username, + 'text': comment_text, + 'date': datetime.now().isoformat() # ISO 8601 + }] + save_data(data) + return redirect(url_for('feed')) # Redirect after comment - html = ''' + elif 'share' in request.form: #For copy Link + # copy link logic will now be handled entirely on the client-side + return redirect(url_for('feed')) + + + html = f''' - Dashboard - Zeus Cloud + + + Adusis - QoS, BBC, BNWO HUB - + -
-

Zeus Cloud Dashboard

-

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

- {% with messages = get_flashed_messages() %} - {% if messages %} - {% for message in messages %} -
{{ message }}
- {% endfor %} - {% endif %} - {% endwith %} -
- - -
-
-
-
-

Ваши файлы

-
- {% for file in user_files %} -
- {% if file['type'] == 'video' %} - - {% elif file['type'] == 'image' %} - {{ file['filename'] }} - {% else %} -

{{ file['filename'] }}

- {% endif %} -

{{ file['upload_date'] }}

- Скачать - Удалить -
- {% endfor %} - {% if not user_files %} -

У вас пока нет загруженных файлов.

+ + {NAV_HTML} + +
+ {% for post in posts %} +
+ {% if post['type'] == 'video' %} + + + {% else %} + {{ post['title'] }} {% endif %} +
+

{{ post['title'] }}

+

{{ post['description']|truncate(150) }}

+

By: {{ post['uploader'] }}

+
+
+ + {{ post['likes']|length }} + + {{ post['comments']|length }} + + {{ post['views'] }} +
+
+ {% for comment in post.get('comments', []) %} +
+ {{ comment['user'] }}: {{ comment['text'] }} +
+ {% endfor %} + {% if is_authenticated %} +
+ + + +
+ {% endif %} +
+
- Выйти -
- + + +''' + return render_template_string(html, + posts=posts, + is_authenticated=is_authenticated, + username=username, + repo_id=REPO_ID, + unread_count=unread_count, + user_count=user_count, + private_unread_count=private_unread_count, + is_online=is_online, + is_user_online=lambda u: is_user_online(data, u), + data=data) - // Обработка выхода - document.getElementById('logout-btn').addEventListener('click', function(e) { - e.preventDefault(); - localStorage.removeItem('zeusCredentials'); - window.location.href = '/logout'; - }); +@app.route('/post/', methods=['GET', 'POST']) +def post_page(post_id): + data = load_data() + username = session.get('username', None) + if username: + update_last_seen(data, username) + post = next((p for p in data['posts'] if p['id'] == post_id), None) + if not post: + return "Post not found", 404 + + is_authenticated = 'username' in session + unread_count = get_unread_count(data, username) if is_authenticated else 0 + private_unread_count = get_private_unread_count(data, username) if is_authenticated else 0 + user_count = len(data['users']) + is_online = is_user_online(data, username) if is_authenticated else False + post['views'] = post.get('views', 0) + 1 #Increment views + save_data(data) + + if request.method == 'POST' and is_authenticated: + if 'comment' in request.form: + comment_text = request.form.get('comment') + if comment_text: + post['comments'] = post.get('comments', []) + [{ + 'user': username, + 'text': comment_text, + 'date': datetime.now().isoformat() # Use ISO 8601 format + }] + save_data(data) + # Redirect to the same page to prevent form resubmission + return redirect(url_for('post_page', post_id=post_id)) + + html = f''' + + + + + + + {{{{ post['title'] }}}} - Content Hub + + + + + + {NAV_HTML} + +
+
+

{{{{ post['title'] }}}}

+ {{% if post['type'] == 'video' %}} + + {{% else %}} + {{{{ post['title'] }}}} + {{% endif %}} +

{{{{ post['description'] }}}}

+

By: {{{{ post['uploader'] }}}} | {{{{ post['upload_date'] }}}}

+

Views: {{{{ post['views'] }}}} | Likes: {{{{ post['likes']|length }}}}

+ {{% if is_authenticated %}} + +
+
+ +
+ +
+ {{% endif %}} +

Comments

+ {{% for comment in post.get('comments', []) %}} +
+

{{{{ comment['user'] }}}} ({{{{ comment['date'] }}}}): {{{{ comment['text'] }}}}

+
+ {{% endfor %}} + Back to Feed + {{% if not is_authenticated %}} +

Login to like and comment.

+ {{% endif %}} +
+
+ + ''' - return render_template_string(html, username=username, user_files=user_files, repo_id=REPO_ID) + return render_template_string(html, post=post, repo_id=REPO_ID, is_authenticated=is_authenticated, username=username, unread_count=unread_count, user_count=user_count, private_unread_count=private_unread_count, is_online=is_online, is_user_online=lambda u: is_user_online(data, u),data=data) -@app.route('/download//') -def download_file(file_path, filename): +@app.route('/profile', methods=['GET', 'POST']) +def profile(): if 'username' not in session: - flash('Пожалуйста, войдите!') + flash('Login to view your profile!') return redirect(url_for('login')) - + + data = load_data() username = session['username'] + update_last_seen(data, username) + user_posts = sorted([p for p in data['posts'] if p['uploader'] == username], key=lambda x: datetime.fromisoformat(x['upload_date']), reverse=True) + is_authenticated = 'username' in session + unread_count = get_unread_count(data, username) + private_unread_count = get_private_unread_count(data, username) + user_count = len(data['users']) + is_online = is_user_online(data, username) + + total_views = sum(post.get('views', 0) for post in user_posts) + total_likes = sum(len(post.get('likes', [])) for post in user_posts) + user_data = data['users'].get(username, {}) + bio = user_data.get('bio', '') + link = user_data.get('link', '') + avatar = user_data.get('avatar', None) + last_seen = user_data.get('last_seen', 'Never') + + if request.method == 'POST': + if 'delete_post' in request.form: + post_id = request.form.get('post_id') + if post_id: + # Find and remove the post + data['posts'] = [p for p in data['posts'] if p['id'] != post_id or p['uploader'] != username] + save_data(data) + return redirect(url_for('profile')) # Redirect after deletion + + elif 'update_profile' in request.form: + bio = request.form.get('bio', '') + link = request.form.get('link', '') + avatar_file = request.files.get('avatar') + + if avatar_file and avatar_file.filename: + filename = secure_filename(avatar_file.filename) + temp_path = os.path.join('uploads', filename) + os.makedirs('uploads', exist_ok=True) + avatar_file.save(temp_path) + + api = HfApi() + avatar_path = f"avatars/{username}/{filename}" + api.upload_file( + path_or_fileobj=temp_path, + path_in_repo=avatar_path, + repo_id=REPO_ID, + repo_type="dataset", + token=HF_TOKEN_WRITE, + commit_message=f"Avatar uploaded for {username}" + ) + data['users'][username]['avatar'] = avatar_path + # Clean up the temporary file + if os.path.exists(temp_path): + os.remove(temp_path) + + data['users'][username]['bio'] = bio + data['users'][username]['link'] = link + save_data(data) + return redirect(url_for('profile')) # Redirect after update + + html = f''' + + + + + + + Profile - {{{{ username }}}} - Content Hub + + + + + + {NAV_HTML} + +
+
+ {{% if avatar %}} + Avatar + {{% else %}} +
+ {{% endif %}} +
+

{{{{ username }}}}

+

{{{{ bio }}}}

+ {{% if link %}} +

{{{{ link }}}}

+ {{% endif %}} +

Total Views: {{{{ total_views }}}} | Total Likes: {{{{ total_likes }}}}

+

Last Seen: {{{{ last_seen }}}}

+
+
+ +

Edit Profile

+
+
+ +
+ + +
+
+ + +
+ +
+ + +
+ + +
+
+ +

My Posts

+
+ {{% for post in user_posts %}} +
+ + {{% if post['type'] == 'video' %}} + + {{% else %}} + {{{{ post['title'] }}}} + {{% endif %}} +

{{{{ post['title'] }}}}

+
+

{{{{ post['description']|truncate(100) }}}}

+

{{{{ post['upload_date'] }}}} | Views: {{{{ post['views'] }}}} | Likes: {{{{ post['likes']|length }}}}

+
+ + +
+
+ {{% endfor %}} + {{% if not user_posts %}} +

You haven't uploaded any posts yet.

+ {{% endif %}} +
+
+ + + + +''' + return render_template_string(html, + username=username, + user_posts=user_posts, + bio=bio, + link=link, + avatar=avatar, + total_views=total_likes=total_likes, + last_seen=last_seen, + is_authenticated=is_authenticated, + repo_id=REPO_ID, + unread_count=unread_count, + user_count=user_count, + private_unread_count=private_unread_count, + is_online=is_online) + +@app.route('/user/', methods=['GET']) +def user_profile(username): data = load_data() + current_user = session.get('username', None) + if current_user: + update_last_seen(data, current_user) if username not in data['users']: - session.pop('username', None) - flash('Пользователь не найден!') - return redirect(url_for('login')) - - user_files = data['users'][username]['files'] - if not any(file['path'] == file_path for file in user_files): - flash('У вас нет доступа к этому файлу!') - return redirect(url_for('dashboard')) - - file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{file_path}" - response = requests.get(file_url) - - if response.status_code == 200: - file_content = BytesIO(response.content) - return send_file( - file_content, - as_attachment=True, - download_name=filename, - mimetype='application/octet-stream' - ) - else: - flash('Ошибка при скачивании файла!') - return redirect(url_for('dashboard')) + return "User not found", 404 + + user_posts = sorted([p for p in data['posts'] if p['uploader'] == username], key=lambda x: datetime.fromisoformat(x['upload_date']), reverse=True) + is_authenticated = 'username' in session + unread_count = get_unread_count(data, current_user) if is_authenticated else 0 + private_unread_count = get_private_unread_count(data, current_user) if is_authenticated else 0 + user_count = len(data['users']) + is_online = is_user_online(data, current_user) if is_authenticated else False + + total_views = sum(post.get('views', 0) for post in user_posts) + total_likes = sum(len(post.get('likes', [])) for post in user_posts) + user_data = data['users'].get(username, {}) + bio = user_data.get('bio', '') + link = user_data.get('link', '') + avatar = user_data.get('avatar', None) + last_seen = user_data.get('last_seen', 'Never') + user_online = is_user_online(data, username) + + html = f''' + + + + + + + {{{{ username }}}}'s Profile - Content Hub + + + + + + {NAV_HTML} + +
+
+ {{% if avatar %}} + Avatar + {{% else %}} +
+ {{% endif %}} +
+

{{{{ username }}}}

+

{{{{ bio }}}}

+ {{% if link %}} +

{{{{ link }}}}

+ {{% endif %}} +

Total Views: {{{{ total_views }}}} | Total Likes: {{{{ total_likes }}}}

+

Last Seen: {{{{ last_seen }}}}

+ {{% if is_authenticated and current_user != username %}} + Send Message + {{% endif %}} +
+
+

Posts

+
+ {{% for post in user_posts %}} +
+ + {{% if post['type'] == 'video' %}} + + {{% else %}} + {{{{ post['title'] }}}} + {{% endif %}} +

{{{{ post['title'] }}}}

+
+

{{{{ post['description']|truncate(100) }}}}

+

{{{{ post['upload_date'] }}}} | Views: {{{{ post['views'] }}}} | Likes: {{{{ post['likes']|length }}}}

+
+ {{% endfor %}} + {{% if not user_posts %}} +

This user hasn't uploaded any posts yet.

+ {{% endif %}} +
+
+ + + + +''' + return render_template_string(html, + username=username, + user_posts=user_posts, + bio=bio, + link=link, + avatar=avatar, + total_views=total_views, + total_likes=total_likes, + last_seen=last_seen, + is_authenticated=is_authenticated, + current_user=current_user, + repo_id=REPO_ID, + unread_count=unread_count, + user_count=user_count, + private_unread_count=private_unread_count, + is_online=is_online, + user_online=user_online) + +@app.route('/upload', methods=['GET', 'POST']) +def upload(): if 'username' not in session: - flash('Пожалуйста, войдите!') + flash('Login to upload content!') return redirect(url_for('login')) - - username = session['username'] + data = load_data() - if username not in data['users']: - session.pop('username', None) - flash('Пользователь не найден!') - return redirect(url_for('login')) - - user_files = data['users'][username]['files'] - file_to_delete = next((file for file in user_files if file['path'] == file_path), None) - - if not file_to_delete: - flash('Файл не найден!') - return redirect(url_for('dashboard')) - - try: + username = session['username'] + update_last_seen(data, username) + is_authenticated = 'username' in session + unread_count = get_unread_count(data, username) + private_unread_count = get_private_unread_count(data, username) + user_count = len(data['users']) + is_online = is_user_online(data, username) + + if request.method == 'POST': + title = request.form.get('title') + description = request.form.get('description') + file = request.files.get('file') + + if not file or not title: + flash('Title and file are required!') + return redirect(url_for('upload')) + + filename = secure_filename(file.filename) + temp_path = os.path.join('uploads', filename) + os.makedirs('uploads', exist_ok=True) + file.save(temp_path) + + file_type = 'video' if filename.lower().endswith(('.mp4', '.mov', '.avi')) else 'photo' api = HfApi() - api.delete_file( + file_path = f"{file_type}s/{username}/{filename}" # Include username in path + api.upload_file( + path_or_fileobj=temp_path, path_in_repo=file_path, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, - commit_message=f"Удален файл {file_path} для {username}" + commit_message=f"{file_type.capitalize()} uploaded by {username}" ) - data['users'][username]['files'] = [f for f in user_files if f['path'] != file_path] + # Generate unique post ID. Important for avoiding collisions + post_id = str(random.randint(100000, 999999)) + while any(p['id'] == post_id for p in data['posts']): + post_id = str(random.randint(100000, 999999)) + + data['posts'].append({ + 'id': post_id, + 'title': title, + 'description': description, + 'filename': f"{username}/{filename}", # Store relative path + 'type': file_type, + 'uploader': username, + 'upload_date': datetime.now().isoformat(), # Use ISO 8601 + 'views': 0, + 'likes': [], + 'comments': [] + }) save_data(data) - flash('Файл успешно удален!') - except Exception as e: - logging.error(f"Ошибка при удалении файла: {e}") - flash('Ошибка при удалении файла!') - - return redirect(url_for('dashboard')) -@app.route('/logout') -def logout(): - session.pop('username', None) - return redirect(url_for('login')) + if os.path.exists(temp_path): + os.remove(temp_path) + + return redirect(url_for('profile')) # Redirect to profile after upload + + + html = f''' + + + + + + + Upload Content - Content Hub + + + + + + {NAV_HTML} + +
+
+

Upload Content

+ {{% with messages = get_flashed_messages() %}} + {{% if messages %}} + {{% for message in messages %}} +
{{{{ message }}}}
+ {{% endfor %}} + {{% endif %}} + {{% endwith %}} +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+
+
+
+ + + +''' + return render_template_string(html, + username=username, + is_authenticated=is_authenticated, + repo_id=REPO_ID, + unread_count=unread_count, + user_count=user_count, + private_unread_count=private_unread_count, + is_online=is_online) +@app.route('/chat', methods=['GET', 'POST']) +def chat(): + data = load_data() + username = session.get('username', None) + if username: + update_last_seen(data, username) + is_authenticated = 'username' in session + chat_messages = data['general_chat'] + + if is_authenticated: + data['users'][username]['last_chat_visit'] = datetime.now().isoformat() # Update last visit + save_data(data) + + unread_count = get_unread_count(data, username) if is_authenticated else 0 + private_unread_count = get_private_unread_count(data, username) if is_authenticated else 0 + user_count = len(data['users']) + is_online = is_user_online(data, username) if is_authenticated else False + + if request.method == 'POST' and is_authenticated: + message = request.form.get('message') + file = request.files.get('file') + post_id = request.form.get('post_id') #Get post + + message_data = { + 'sender': username, + 'time': datetime.now().isoformat() # Use ISO 8601 format + } + + if message: + message_data['text'] = message + + if file and file.filename: + filename = secure_filename(file.filename) + temp_path = os.path.join('uploads', filename) + os.makedirs('uploads', exist_ok=True) + file.save(temp_path) + + file_type = 'video' if filename.lower().endswith(('.mp4', '.mov', '.avi')) else 'photo' + api = HfApi() + file_path = f"chat_files/general/{filename}" # Store in chat_files + api.upload_file( + path_or_fileobj=temp_path, + path_in_repo=file_path, + repo_id=REPO_ID, + repo_type="dataset", + token=HF_TOKEN_WRITE, + commit_message=f"Chat file uploaded by {username}" + ) + message_data['file'] = file_path + message_data['file_type'] = file_type + if os.path.exists(temp_path): + os.remove(temp_path) + + # Handle post sharing + if post_id: + post = next((p for p in data['posts'] if p['id'] == post_id and p['uploader'] == username), None) + if(post): + message_data['post_id'] = post_id + + if 'text' in message_data or 'file' in message_data or 'post_id' in message_data : + data['general_chat'].append(message_data) + save_data(data) + + return redirect(url_for('chat')) # Redirect after sending + + + html = f''' + + + + + + + General Chat - Content Hub + + + + + + {NAV_HTML} + +
+
+

General Chat

+
+ {{% for message in chat_messages %}} +
+ + {{% if message['sender'] == username %}} + {{% if avatar %}} + Your Avatar + {{% else %}} +
+ {{% endif %}} + {{% else %}} + {{% if data['users'][message['sender']].get('avatar') %}} + {{{{ message['sender'] }}}} Avatar + {{% else %}} +
+ {{% endif %}} + {{% endif %}} + +
+ {{{{ message['sender'] }}}} + + {{% if 'text' in message %}} +

{{{{ message['text'] }}}}

+ {{% endif %}} + {{% if 'file' in message %}} + {{% if message['file_type'] == 'video' %}} + + {{% else %}} + Chat file + {{% endif %}} + {{% endif %}} + + {{% if 'post_id' in message %}} + {{% with post = posts|selectattr('id', 'equalto', message['post_id'])|first %}} + {{% if post %}} +

Shared: {{{{ post['title'] }}}}

+ {{% endif %}} + {{% endwith %}} + {{% endif %}} + {{{{ message['time'] }}}} +
+
+ {{% endfor %}} +
+ {{% if is_authenticated %}} +
+
+ +
+
+ +
+ +
+ +
+ +
+ {{% else %}} +

Login to send messages.

+ {{% endif %}} +
+
+ + + + +''' + user_posts = [p for p in data['posts'] if p['uploader'] == username] if is_authenticated else [] # Get user's posts + avatar = data['users'][username].get('avatar') if is_authenticated else None + return render_template_string(html, + chat_messages=chat_messages, + username=username, + is_authenticated=is_authenticated, + repo_id=REPO_ID, + posts=data['posts'], + user_posts=user_posts, + unread_count=unread_count, + user_count=user_count, + private_unread_count=private_unread_count, + is_online=is_online, + is_user_online=lambda u: is_user_online(data, u), + data=data, + avatar=avatar) -# Новая админ-панель -@app.route('/admhosto') -def admin_panel(): +@app.route('/users', methods=['GET', 'POST']) +def users(): data = load_data() - users = data['users'] - - html = ''' + username = session.get('username', None) + if username: + update_last_seen(data, username) + + is_authenticated = 'username' in session + unread_count = get_unread_count(data, username) if is_authenticated else 0 + private_unread_count = get_private_unread_count(data, username) if is_authenticated else 0 + user_count = len(data['users']) + is_online = is_user_online(data, username) if is_authenticated else False + + search_query = request.form.get('search', '').strip().lower() if request.method == 'POST' else '' + user_list = [(user, data['users'][user].get('avatar'), is_user_online(data, user)) + for user in data['users'] if not search_query or search_query in user.lower()] + + html = f''' - Админ-панель - Zeus Cloud + + Users - Content Hub - + + + {NAV_HTML} +
-

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

-

Список пользователей

-
- {% for username, user_data in users.items() %} +
+

Users ({{{{ user_count }}}})

+
+
+ + +
+
+
+ {{% for user, avatar, online in user_list %}} + {{% if user != username %}}
- {{ username }} -

Дата регистрации: {{ user_data['created_at'] }}

-

Количество файлов: {{ user_data['files'] | length }}

+ {{% if avatar %}} + {{{{ user }}}} Avatar + {{% else %}} +
+ {{% endif %}} +
- {% endfor %} - {% if not users %} -

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

- {% endif %} + {{% endif %}} + {{% endfor %}}
+
+ ''' - return render_template_string(html, users=users) + return render_template_string(html, + user_list=user_list, + username=username, + is_authenticated=is_authenticated, + repo_id=REPO_ID, + unread_count=unread_count, + user_count=user_count, + private_unread_count=private_unread_count, + is_online=is_online, + search_query=search_query) + +@app.route('/messages', methods=['GET']) +def messages(): + if 'username' not in session: + flash('Login to view messages!') + return redirect(url_for('login')) -@app.route('/admhosto/user/') -def admin_user_files(username): data = load_data() - if username not in data['users']: - flash('Пользователь не найден!') - return redirect(url_for('admin_panel')) - - user_files = sorted(data['users'][username]['files'], key=lambda x: x['upload_date'], reverse=True) - - html = ''' + username = session['username'] + update_last_seen(data, username) + is_authenticated = 'username' in session + unread_count = get_unread_count(data, username) + private_unread_count = get_private_unread_count(data, username) + user_count = len(data['users']) + is_online = is_user_online(data, username) + + dialogs = {} + for chat_key, messages in data['private_chats'].items(): + user1, user2 = chat_key.split('_') + if username not in (user1, user2): + continue + other_user = user1 if user2 == username else user2 + last_message = messages[-1] if messages else None # Get the last message + # Count unread messages in this dialog + unread = sum(1 for msg in messages if datetime.fromisoformat(msg['time']) > datetime.fromisoformat(data['users'][username]['last_private_visit']) and msg['sender'] != username) + dialogs[other_user] = { + 'last_message': last_message, + 'unread': unread, + 'avatar': data['users'][other_user].get('avatar'), + 'online': is_user_online(data, other_user) # Check if the other user is online + } + + html = f''' - Файлы пользователя {{ username }} - Zeus Cloud + + Messages - Content Hub - + + + {NAV_HTML} +
-

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

- {% with messages = get_flashed_messages() %} - {% if messages %} - {% for message in messages %} -
{{ message }}
- {% endfor %} - {% endif %} - {% endwith %} -
- {% for file in user_files %} -
- {% if file['type'] == 'video' %} - - {% elif file['type'] == 'image' %} - {{ file['filename'] }} - {% else %} -

{{ file['filename'] }}

- {% endif %} -

{{ file['upload_date'] }}

- Скачать +
+

Messages

+
+ {{% for other_user, info in dialogs.items() %}} +
+ {{% if info.avatar %}} + {{{{ other_user }}}} Avatar + {{% else %}} +
+ {{% endif %}} +
+ {{{{ other_user }}}} + + {{% if info.last_message %}} +

{{{{ info.last_message['time'] }}}}: + {{% if 'text' in info.last_message %}}{{{{ info.last_message['text']|truncate(30) }}}}{{% elif 'file' in info.last_message %}}File{{% elif 'post_id' in info.last_message %}}Post{{% endif %}}

+ {{% endif %}} +
+ {{% if info.unread > 0 %}} + {{{{ info.unread }}}} + {{% endif %}}
- {% endfor %} - {% if not user_files %} -

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

- {% endif %} + {{% endfor %}} + {{% if not dialogs %}} +

No messages yet.

+ {{% endif %}} +
- Назад к списку пользователей
-