diff --git "a/Soola.txt" "b/Soola.txt"
new file mode 100644--- /dev/null
+++ "b/Soola.txt"
@@ -0,0 +1,3057 @@
+
+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, timedelta
+from huggingface_hub import HfApi, hf_hub_download
+from werkzeug.utils import secure_filename
+import random
+import html
+import uuid
+from collections import defaultdict
+
+app = Flask(__name__)
+app.secret_key = os.getenv("FLASK_SECRET_KEY", "verysecretkey")
+app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=30)
+DATA_FILE = 'data_adusis.json'
+REPO_ID = "Eluza133/A12d12s12"
+HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
+HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN_WRITE
+UPLOAD_FOLDER = 'uploads'
+os.makedirs(UPLOAD_FOLDER, exist_ok=True)
+
+cache = Cache(app, config={'CACHE_TYPE': 'simple'})
+logging.basicConfig(level=logging.INFO)
+
+USER_ROLES = ["white girl", "white boy", "couple", "BBC alpha bull"]
+REFERRAL_REWARDS = {
+ "white girl": 100,
+ "couple": 100,
+ "BBC alpha bull": 100,
+ "white boy": 50,
+}
+DEFAULT_REWARD = 0
+UPLOAD_REWARD = 10
+VIEW_REWARD_AMOUNT = 50
+VIEW_REWARD_THRESHOLD = 200
+PROMOTE_COST = 50
+PROMOTE_DURATION_DAYS = 7
+LOGO_URL = "https://cdn-avatars.huggingface.co/v1/production/uploads/673b00f35373479538ac373c/W_dumUND8K6IlMxVmpUgS.jpeg"
+
+def generate_unique_referral_code(data):
+ while True:
+ code = str(uuid.uuid4())[:8]
+ if not any(u.get('referral_code') == code for u in data.get('users', {}).values()):
+ return code
+
+def log_transaction(data, user, type, amount, description, related_user=None, related_story_id=None):
+ if user not in data['users']:
+ logging.warning(f"Attempted to log transaction for non-existent user: {user}")
+ return
+
+ balance_after = data['users'][user].get('aducoin', 0)
+
+ transaction = {
+ 'id': str(uuid.uuid4()),
+ 'user': user,
+ 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+ 'type': type,
+ 'amount': amount,
+ 'description': description,
+ 'related_user': related_user,
+ 'related_story_id': related_story_id,
+ 'balance_after': balance_after
+ }
+ data.setdefault('transactions', []).append(transaction)
+ logging.info(f"Transaction logged: {transaction}")
+
+
+def initialize_data_structure(data):
+ if not isinstance(data, dict):
+ logging.warning("Data is not a dict, initializing empty database")
+ data = {'posts': [], 'users': {}, 'transactions': [], 'direct_messages': {}}
+
+ data.setdefault('posts', [])
+ data.setdefault('users', {})
+ data.setdefault('transactions', [])
+ data.setdefault('direct_messages', {})
+
+ for post in data['posts']:
+ post.setdefault('likes', [])
+ post.setdefault('views', 0)
+ post.setdefault('comments', [])
+ post.setdefault('jerked_off_count', 0)
+ post.setdefault('views_reward_milestone', 0)
+ post.setdefault('promoted_until', None)
+ if 'id' not in post:
+ post['id'] = str(random.randint(100000, 999999))
+
+ all_codes = {u.get('referral_code') for u in data['users'].values() if u.get('referral_code')}
+
+ for username, user_data in data['users'].items():
+ user_data.setdefault('last_seen', '1970-01-01 00:00:00')
+ user_data.setdefault('bio', '')
+ user_data.setdefault('link', '')
+ user_data.setdefault('avatar', None)
+ user_data.setdefault('aducoin', 0)
+ user_data.setdefault('role', None)
+ user_data.setdefault('referred_by', None)
+ if 'referral_code' not in user_data or not user_data['referral_code'] or user_data['referral_code'] in all_codes:
+ new_code = generate_unique_referral_code(data)
+ user_data['referral_code'] = new_code
+ all_codes.add(new_code)
+
+ if isinstance(data['direct_messages'], dict):
+ for conv_key, messages in data['direct_messages'].items():
+ if not isinstance(messages, list):
+ logging.warning(f"Conversation {conv_key} is not a list, resetting.")
+ data['direct_messages'][conv_key] = []
+ else:
+ for msg in messages:
+ if not isinstance(msg, dict):
+ logging.warning(f"Found non-dict message in {conv_key}, removing.")
+ data['direct_messages'][conv_key] = [m for m in messages if isinstance(m, dict)]
+ else:
+ msg.setdefault('sender', 'unknown')
+ msg.setdefault('timestamp', datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
+ msg.setdefault('text', '')
+ msg.setdefault('message_id', str(uuid.uuid4()))
+ msg.setdefault('read', False)
+
+ return data
+
+@cache.memoize(timeout=120)
+def load_data():
+ try:
+ download_db_from_hf()
+ if os.path.exists(DATA_FILE) and os.path.getsize(DATA_FILE) > 0:
+ with open(DATA_FILE, 'r', encoding='utf-8') as file:
+ data = json.load(file)
+ else:
+ data = {'posts': [], 'users': {}, 'transactions': [], 'direct_messages': {}}
+
+ data = initialize_data_structure(data)
+ logging.info("Data loaded successfully")
+ return data
+ except json.JSONDecodeError:
+ logging.error(f"Error decoding JSON from {DATA_FILE}. Initializing empty data.")
+ return initialize_data_structure({})
+ except Exception as e:
+ logging.error(f"Error loading data: {e}")
+ return initialize_data_structure({})
+
+def save_data(data):
+ try:
+ temp_file = DATA_FILE + '.tmp'
+ with open(temp_file, 'w', encoding='utf-8') as file:
+ json.dump(data, file, ensure_ascii=False, indent=4)
+ os.replace(temp_file, DATA_FILE)
+ upload_db_to_hf()
+ cache.clear()
+ logging.info("Data saved and uploaded to HF")
+ except Exception as e:
+ logging.error(f"Error saving data: {e}")
+ if os.path.exists(temp_file):
+ os.remove(temp_file)
+ raise
+
+def upload_db_to_hf():
+ if not HF_TOKEN_WRITE:
+ logging.warning("HF_TOKEN_WRITE not set. Skipping upload.")
+ return
+ try:
+ api = HfApi()
+ api.upload_file(
+ path_or_fileobj=DATA_FILE,
+ path_in_repo=DATA_FILE,
+ repo_id=REPO_ID,
+ repo_type="dataset",
+ token=HF_TOKEN_WRITE,
+ commit_message=f"Backup {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
+ )
+ logging.info("Database uploaded to Hugging Face")
+ except Exception as e:
+ logging.error(f"Error uploading database: {e}")
+
+def download_db_from_hf():
+ if not HF_TOKEN_READ:
+ logging.warning("HF_TOKEN_READ not set. Skipping download.")
+ if not os.path.exists(DATA_FILE):
+ with open(DATA_FILE, 'w', encoding='utf-8') as f:
+ json.dump(initialize_data_structure({}), f)
+ return
+ try:
+ hf_hub_download(
+ repo_id=REPO_ID,
+ filename=DATA_FILE,
+ repo_type="dataset",
+ token=HF_TOKEN_READ,
+ local_dir=".",
+ local_dir_use_symlinks=False,
+ force_download=True
+ )
+ logging.info("Database downloaded from Hugging Face")
+ except Exception as e:
+ logging.error(f"Error downloading database: {e}")
+ if not os.path.exists(DATA_FILE):
+ logging.info("Creating empty database file as download failed and file doesn't exist.")
+ with open(DATA_FILE, 'w', encoding='utf-8') as f:
+ json.dump(initialize_data_structure({}), f)
+
+def periodic_backup():
+ while True:
+ time.sleep(1800)
+ logging.info("Initiating periodic backup.")
+ try:
+ data = load_data()
+ save_data(data)
+ except Exception as e:
+ logging.error(f"Error during periodic backup: {e}")
+
+def is_user_online(data, username):
+ if username not in data.get('users', {}):
+ return False
+ last_seen_str = data['users'][username].get('last_seen', '1970-01-01 00:00:00')
+ try:
+ last_seen = datetime.strptime(last_seen_str, '%Y-%m-%d %H:%M:%S')
+ return (datetime.now() - last_seen).total_seconds() < 300
+ except ValueError:
+ logging.error(f"Invalid last_seen format for user {username}: {last_seen_str}")
+ return False
+
+def update_last_seen(username):
+ if username:
+ try:
+ data = cache.get('data') or load_data()
+ if username in data.get('users', {}):
+ now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ if data['users'][username].get('last_seen') != now_str:
+ data['users'][username]['last_seen'] = now_str
+ save_data(data)
+ cache.set('data', data)
+ else:
+ logging.warning(f"Attempted to update last_seen for non-existent user: {username}")
+ except Exception as e:
+ logging.error(f"Error updating last_seen for {username}: {e}")
+
+def get_conversation_key(user1, user2):
+ return tuple(sorted((user1, user2)))
+
+def count_unread_messages(data, current_user):
+ count = 0
+ if current_user:
+ for conv_key, messages in data.get('direct_messages', {}).items():
+ if current_user in conv_key:
+ for msg in messages:
+ if msg.get('sender') != current_user and not msg.get('read'):
+ count += 1
+ return count
+
+
+@app.before_request
+def before_request_func():
+ if 'username' in session:
+ username = session['username']
+ last_update_key = f"last_seen_update_{username}"
+ last_update_time = cache.get(last_update_key)
+ if not last_update_time or (datetime.now() - last_update_time).total_seconds() > 60:
+ update_last_seen(username)
+ cache.set(last_update_key, datetime.now(), timeout=60)
+
+
+BASE_STYLE = '''
+:root {
+ --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+ --primary-hue: 330;
+ --secondary-hue: 270;
+ --accent-hue: 50;
+
+ --primary-color: hsl(var(--primary-hue), 100%, 55%);
+ --secondary-color: hsl(var(--secondary-hue), 85%, 60%);
+ --accent-color: hsl(var(--accent-hue), 100%, 50%);
+
+ --text-light: #f0f0f5;
+ --text-dark: #a0a0b0;
+ --bg-dark: #0D0C13;
+ --bg-light: #161422;
+
+ --glass-bg: rgba(22, 20, 34, 0.6);
+ --glass-border: rgba(255, 255, 255, 0.1);
+
+ --glow-primary: 0 0 20px 0px hsla(var(--primary-hue), 100%, 55%, 0.5);
+ --glow-secondary: 0 0 20px 0px hsla(var(--secondary-hue), 85%, 60%, 0.5);
+ --glow-accent: 0 0 20px 0px hsla(var(--accent-hue), 100%, 50%, 0.4);
+
+ --online-color: #00FF7F;
+ --offline-color: #FF4500;
+ --transition-fast: all 0.2s ease-in-out;
+ --transition-medium: all 0.4s ease-in-out;
+}
+@keyframes fadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
+@keyframes modalZoomIn { from { opacity: 0; transform: scale(0.8); } to { opacity: 1; transform: scale(1); } }
+
+* { margin: 0; padding: 0; box-sizing: border-box; }
+html { scroll-behavior: smooth; }
+body {
+ font-family: var(--font-sans);
+ background-color: var(--bg-dark);
+ color: var(--text-light);
+ line-height: 1.6;
+ overflow-x: hidden;
+ background-image:
+ radial-gradient(circle at 10% 15%, hsla(var(--primary-hue), 80%, 30%, 0.3) 0%, transparent 30%),
+ radial-gradient(circle at 90% 85%, hsla(var(--secondary-hue), 80%, 30%, 0.35) 0%, transparent 40%);
+ background-attachment: fixed;
+}
+.sidebar {
+ position: fixed; top: 0; left: 0; width: 280px; height: 100%;
+ background: var(--glass-bg);
+ backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
+ border-right: 1px solid var(--glass-border);
+ padding: 25px;
+ z-index: 1000;
+ transition: transform 0.4s ease;
+ display: flex; flex-direction: column;
+}
+.sidebar.hidden { transform: translateX(-100%); }
+.sidebar-header {
+ display: flex; align-items: center; gap: 15px; margin-bottom: 40px;
+ padding-bottom: 20px; border-bottom: 1px solid var(--glass-border);
+}
+.logo { width: 50px; height: 50px; border-radius: 14px; border: 2px solid var(--primary-color); box-shadow: var(--glow-primary); }
+.nav-brand { font-size: 1.9em; font-weight: 700; color: var(--primary-color); text-shadow: 0 0 10px var(--primary-color); }
+.nav-links { display: flex; flex-direction: column; gap: 10px; flex-grow: 1; }
+.nav-link {
+ display: flex; align-items: center; gap: 15px; padding: 14px 20px;
+ color: var(--text-dark); text-decoration: none; border-radius: 10px;
+ font-size: 1.1em; font-weight: 500;
+ transition: var(--transition-fast);
+ position: relative;
+ border: 1px solid transparent;
+}
+.nav-link:hover { color: var(--text-light); background: rgba(255,255,255,0.05); }
+.nav-link.active {
+ color: var(--primary-color);
+ background: hsla(var(--primary-hue), 100%, 55%, 0.1);
+ border: 1px solid hsla(var(--primary-hue), 100%, 55%, 0.3);
+ font-weight: 600;
+}
+.nav-link span:first-child { font-size: 1.3em; }
+.logout-btn:hover { color: var(--offline-color); }
+.menu-btn {
+ display: none; font-size: 28px; background: var(--glass-bg);
+ border: 1px solid var(--glass-border); color: var(--primary-color); cursor: pointer;
+ position: fixed; top: 20px; left: 20px; z-index: 1001; padding: 12px;
+ border-radius: 50%; box-shadow: 0 0 15px rgba(0,0,0,0.5);
+ transition: var(--transition-medium); backdrop-filter: blur(8px);
+}
+.menu-btn:hover { background: var(--primary-color); color: white; box-shadow: var(--glow-primary); }
+.container {
+ margin-left: 300px; padding: 40px;
+ transition: margin-left var(--transition-medium);
+ max-width: 1200px;
+}
+.container.animated { animation: fadeIn 0.6s ease forwards; }
+body:not(.sidebar-active-on-pc) .container { margin-left: auto; margin-right: auto; }
+
+.btn {
+ padding: 12px 28px;
+ background: var(--primary-color);
+ color: white; border: none; border-radius: 10px; cursor: pointer;
+ font-size: 1.05em; font-weight: 600;
+ transition: var(--transition-fast);
+ display: inline-flex; align-items: center; justify-content: center; gap: 10px;
+ box-shadow: 0 4px 15px -5px hsla(var(--primary-hue), 100%, 55%, 0.6);
+ text-decoration: none; position: relative; overflow: hidden;
+}
+.btn:hover { transform: translateY(-3px); box-shadow: 0 7px 20px -5px hsla(var(--primary-hue), 100%, 55%, 0.8); }
+.btn-secondary { background: var(--secondary-color); box-shadow: 0 4px 15px -5px hsla(var(--secondary-hue), 85%, 60%, 0.6); }
+.btn-secondary:hover { box-shadow: 0 7px 20px -5px hsla(var(--secondary-hue), 85%, 60%, 0.8); }
+.btn-accent { background: var(--accent-color); color: var(--bg-dark); box-shadow: 0 4px 15px -5px hsla(var(--accent-hue), 100%, 50%, 0.6); }
+.btn-accent:hover { box-shadow: 0 7px 20px -5px hsla(var(--accent-hue), 100%, 50%, 0.8); }
+
+input, textarea, select {
+ width: 100%; padding: 14px 18px; margin: 10px 0;
+ border: 1px solid var(--glass-border); border-radius: 10px;
+ background: var(--glass-bg); color: var(--text-light);
+ font-size: 1.05em; transition: var(--transition-fast); font-family: inherit;
+}
+input:focus, textarea:focus, select:focus {
+ outline: none;
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 3px hsla(var(--primary-hue), 100%, 55%, 0.3);
+}
+textarea { min-height: 120px; resize: vertical; }
+
+.modal {
+ display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
+ background: rgba(0, 0, 0, 0.9); z-index: 2000;
+ justify-content: center; align-items: center;
+ backdrop-filter: blur(5px);
+}
+.modal img, .modal video {
+ max-width: 90vw; max-height: 90vh; object-fit: contain;
+ border-radius: 15px; box-shadow: 0 0 40px rgba(0,0,0,0.8);
+ animation: modalZoomIn 0.4s ease;
+}
+
+.status-dot {
+ width: 10px; height: 10px; border-radius: 50%; display: inline-block;
+ vertical-align: middle; border: 1px solid rgba(0,0,0,0.2);
+}
+.online { background: var(--online-color); box-shadow: 0 0 8px var(--online-color); }
+.offline { background: var(--offline-color); }
+.badge {
+ background-color: var(--accent-color); color: var(--bg-dark);
+ padding: 2px 9px; border-radius: 20px; font-size: 0.8em; font-weight: bold;
+}
+.nav-link .badge { position: absolute; right: 20px; top: 50%; transform: translateY(-50%); }
+
+.flash {
+ padding: 18px; margin-bottom: 25px; border-radius: 10px;
+ font-weight: 600; text-align: center;
+ border: 1px solid;
+}
+.flash.success { background-color: hsla(150, 100%, 40%, 0.2); color: hsl(150, 100%, 70%); border-color: hsl(150, 100%, 40%); }
+.flash.error { background-color: hsla(var(--primary-hue), 100%, 55%, 0.2); color: hsl(var(--primary-hue), 100%, 80%); border-color: hsl(var(--primary-hue), 100%, 55%); }
+.flash.warning { background-color: hsla(var(--accent-hue), 100%, 50%, 0.2); color: hsl(var(--accent-hue), 100%, 70%); border-color: hsl(var(--accent-hue), 100%, 50%); }
+
+.story-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 30px; }
+.story-item {
+ background: var(--glass-bg); border-radius: 15px;
+ box-shadow: 0 8px 30px rgba(0,0,0,0.3);
+ transition: var(--transition-medium);
+ border: 1px solid var(--glass-border);
+ overflow: hidden; display: flex; flex-direction: column;
+}
+.story-item:hover { transform: translateY(-8px); box-shadow: var(--glow-primary); border-color: var(--primary-color); }
+.story-preview-link {
+ display: block; text-decoration: none; color: inherit; position: relative;
+ background-color: #000; height: 200px; overflow: hidden;
+}
+.story-preview { width: 100%; height: 100%; object-fit: cover; transition: transform 0.5s ease; }
+.story-item:hover .story-preview { transform: scale(1.1); }
+.story-item-info { padding: 20px; flex-grow: 1; display: flex; flex-direction: column; justify-content: space-between; }
+.story-item h3 { font-size: 1.25em; font-weight: 600; margin-bottom: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.story-item p.uploader { font-size: 0.95em; color: var(--text-dark); margin-bottom: 12px; display: flex; align-items: center; }
+.uploader-link { color: var(--secondary-color); font-weight: 500; text-decoration: none; margin-right: 5px; transition: color var(--transition-fast); }
+.uploader-link:hover { color: var(--primary-color); text-decoration: underline; }
+.story-item p.stats { font-size: 0.9em; color: var(--text-dark); margin-top: 15px; display: flex; gap: 15px; align-items: center; }
+.story-item p.stats span { display: inline-flex; align-items: center; gap: 5px; }
+
+.aducoin-display {
+ display: inline-flex; align-items: center; gap: 8px;
+ background-color: hsla(var(--accent-hue), 100%, 50%, 0.1);
+ padding: 6px 12px; border-radius: 20px;
+ border: 1px solid hsl(var(--accent-hue), 100%, 50%);
+ color: hsl(var(--accent-hue), 100%, 65%);
+ font-weight: bold; font-size: 1em; text-decoration: none;
+ cursor: pointer; transition: var(--transition-fast);
+}
+.aducoin-display:hover { background-color: hsla(var(--accent-hue), 100%, 50%, 0.2); box-shadow: var(--glow-accent); }
+.aducoin-icon { width: 20px; height: 20px; vertical-align: middle; border-radius: 50%; }
+
+.form-section {
+ margin-top: 30px; padding: 25px;
+ background: var(--glass-bg); border-radius: 12px;
+ border: 1px solid var(--glass-border);
+}
+.form-section h3 { font-size: 1.3em; color: var(--secondary-color); margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid var(--glass-border); }
+.form-input-container { display: flex; gap: 12px; align-items: center; margin-top: 15px; }
+.form-section input { flex-grow: 1; margin: 0; }
+.form-section .btn { flex-shrink: 0; }
+.form-section .info-text { font-size: 0.9em; color: var(--text-dark); margin-top: 8px; }
+
+.transaction-table { width: 100%; border-collapse: collapse; margin-top: 25px; }
+.transaction-table th, .transaction-table td { border-bottom: 1px solid var(--glass-border); padding: 15px; text-align: left; font-size: 0.95em; }
+.transaction-table th { background-color: rgba(255,255,255, 0.05); color: var(--text-light); font-weight: 600; }
+.transaction-table td { color: var(--text-dark); }
+.transaction-table tr:hover { background-color: rgba(255, 255, 255, 0.03); }
+.transaction-table td.amount-positive { color: var(--online-color); font-weight: bold; }
+.transaction-table td.amount-negative { color: var(--offline-color); font-weight: bold; }
+
+.conversation-item {
+ display: flex; align-items: center; gap: 18px; padding: 18px;
+ background: var(--glass-bg); border-radius: 12px; margin-bottom: 15px;
+ transition: var(--transition-medium); text-decoration: none; color: inherit;
+ border: 1px solid var(--glass-border);
+}
+.conversation-item:hover { transform: translateX(5px); box-shadow: var(--glow-secondary); border-color: var(--secondary-color); }
+.conversation-item.unread { border-left: 4px solid var(--accent-color); background: hsla(var(--accent-hue), 100%, 50%, 0.1); }
+.conversation-avatar { width: 50px; height: 50px; border-radius: 50%; object-fit: cover; flex-shrink: 0; border: 3px solid var(--secondary-color); }
+.conversation-avatar-placeholder {
+ width: 50px; height: 50px; border-radius: 50%;
+ background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));
+ display: flex; align-items: center; justify-content: center;
+ font-size: 24px; color: white; font-weight: bold; flex-shrink: 0;
+}
+.conversation-details { flex-grow: 1; overflow: hidden; }
+.conversation-username { font-weight: 600; color: var(--text-light); font-size: 1.1em; }
+.conversation-last-msg { font-size: 0.95em; color: var(--text-dark); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.conversation-timestamp { font-size: 0.85em; color: var(--text-dark); flex-shrink: 0; margin-left: auto; text-align: right; }
+
+.message-area { max-height: 65vh; overflow-y: auto; margin-bottom: 25px; padding: 15px; background: rgba(0,0,0,0.2); border-radius: 12px; display: flex; flex-direction: column-reverse; scroll-behavior: smooth; }
+.message-bubble { max-width: 80%; padding: 12px 18px; border-radius: 20px; margin-bottom: 12px; word-wrap: break-word; font-size: 1em; }
+.message-bubble.sent { background: hsl(var(--secondary-hue), 85%, 60%); border-bottom-right-radius: 8px; align-self: flex-end; color: white; }
+.message-bubble.received { background: var(--bg-light); border-bottom-left-radius: 8px; align-self: flex-start; color: var(--text-light); }
+.message-bubble .timestamp { font-size: 0.78em; color: var(--text-dark); display: block; margin-top: 8px; text-align: right; }
+.message-input-area { display: flex; gap: 12px; margin-top: 20px; }
+.message-input-area textarea { margin: 0; min-height: 50px; max-height: 180px; resize: vertical; height: 50px; flex-grow: 1; padding: 12px 18px; }
+.message-input-area button { flex-shrink: 0; height: 50px; align-self: flex-end; }
+
+@media (max-width: 900px) {
+ .sidebar { transform: translateX(-100%); }
+ .sidebar.active { transform: translateX(0); box-shadow: 10px 0 40px rgba(0,0,0,0.5); }
+ .menu-btn { display: block; }
+ .container { margin: 80px 20px 20px 20px; padding: 20px; max-width: none; }
+ body:not(.sidebar-active-on-pc) .container,
+ body.sidebar-active-on-pc .container { margin-left: auto; margin-right: auto; }
+}
+@media (max-width: 480px) {
+ .story-grid { grid-template-columns: 1fr; }
+ .form-input-container { flex-direction: column; align-items: stretch; }
+}
+'''
+
+NAV_HTML = '''
+
+'''
+
+@app.route('/register', methods=['GET', 'POST'])
+def register():
+ referral_code = request.args.get('ref')
+
+ if request.method == 'POST':
+ username = request.form.get('username')
+ password = request.form.get('password')
+ role = request.form.get('role')
+ ref_code_used = request.form.get('referral_code')
+
+ if not username or not password or not role:
+ flash('Username, password, and role are required.', 'error')
+ return redirect(url_for('register', ref=ref_code_used))
+ if len(username) < 3:
+ flash('Username must be at least 3 characters long.', 'error')
+ return redirect(url_for('register', ref=ref_code_used))
+ if len(password) < 6:
+ flash('Password must be at least 6 characters long.', 'error')
+ return redirect(url_for('register', ref=ref_code_used))
+ if role not in USER_ROLES:
+ flash('Invalid role selected.', 'error')
+ return redirect(url_for('register', ref=ref_code_used))
+
+ data = load_data()
+ if username in data['users']:
+ flash('Username already exists! Choose another.', 'error')
+ return redirect(url_for('register', ref=ref_code_used))
+
+ referrer_username = None
+ reward = 0
+ if ref_code_used:
+ for r_user, r_data in data['users'].items():
+ if r_data.get('referral_code') == ref_code_used:
+ referrer_username = r_user
+ break
+ if not referrer_username:
+ flash('Invalid referral code provided.', 'warning')
+ else:
+ reward = REFERRAL_REWARDS.get(role, DEFAULT_REWARD)
+
+
+ new_user_referral_code = generate_unique_referral_code(data)
+
+ data['users'][username] = {
+ 'password': password,
+ 'role': role,
+ 'bio': '',
+ 'link': '',
+ 'avatar': None,
+ 'aducoin': 0,
+ 'referral_code': new_user_referral_code,
+ 'referred_by': referrer_username,
+ 'last_seen': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ }
+
+ if referrer_username and reward > 0:
+ data['users'][referrer_username]['aducoin'] = data['users'][referrer_username].get('aducoin', 0) + reward
+ log_transaction(data, referrer_username, 'earn_referral', reward, f"Received for referring user '{username}' (Role: {role})", related_user=username)
+ logging.info(f"Awarded {reward} AduCoin to {referrer_username} for referring {username} (Role: {role})")
+
+
+ try:
+ save_data(data)
+ flash('Registration successful! Please login.', 'success')
+ if referrer_username and reward > 0:
+ flash(f'Your referrer {referrer_username} received {reward} AduCoin!', 'success')
+ return redirect(url_for('login'))
+ except Exception as e:
+ flash('Registration failed. Please try again.', 'error')
+ logging.error(f"Failed to save data during registration: {e}")
+ if username in data['users']:
+ del data['users'][username]
+ if referrer_username and reward > 0:
+ data['users'][referrer_username]['aducoin'] = data['users'][referrer_username].get('aducoin', 0) - reward
+
+ return redirect(url_for('register', ref=ref_code_used))
+
+ is_authenticated = 'username' in session
+ username_session = session.get('username')
+ data = load_data()
+ user_count = len(data.get('users', {}))
+ is_online = is_user_online(data, username_session) if username_session else False
+ unread_count = count_unread_messages(data, username_session)
+
+ html_content = '''
+
+
+
+
+
+ Register - Adusis
+
+
+
+
+
+
+
+ ''' + NAV_HTML + '''
+
+
+
+
+'''
+ return render_template_string(html_content,
+ is_authenticated=is_authenticated,
+ username=username_session,
+ user_count=user_count,
+ is_online=is_online,
+ unread_count=unread_count,
+ user_roles=USER_ROLES,
+ referral_code=referral_code,
+ logo_url=LOGO_URL)
+
+@app.route('/login', methods=['GET', 'POST'])
+def login():
+ if request.method == 'POST':
+ username = request.form.get('username')
+ password = request.form.get('password')
+ data = load_data()
+ if username in data.get('users', {}) and data['users'][username].get('password') == password:
+ session['username'] = username
+ session.permanent = True
+ update_last_seen(username)
+ flash('Login successful!', 'success')
+ return redirect(url_for('feed'))
+ else:
+ flash('Invalid username or password.', 'error')
+ return redirect(url_for('login'))
+
+ is_authenticated = 'username' in session
+ username_session = session.get('username')
+ data = load_data()
+ user_count = len(data.get('users', {}))
+ is_online = is_user_online(data, username_session) if username_session else False
+ unread_count = count_unread_messages(data, username_session)
+
+ html_content = '''
+
+
+
+
+
+ Login - Adusis
+
+
+
+
+
+
+
+ ''' + NAV_HTML + '''
+
+
+
+
+'''
+ return render_template_string(html_content,
+ is_authenticated=is_authenticated,
+ username=username_session,
+ user_count=user_count,
+ is_online=is_online,
+ unread_count=unread_count,
+ logo_url=LOGO_URL)
+
+
+@app.route('/logout')
+def logout():
+ username = session.get('username')
+ if username:
+ update_last_seen(username)
+ session.pop('username', None)
+ flash('You have been logged out.', 'success')
+ return redirect(url_for('login'))
+
+@app.route('/', defaults={'mode': 'latest'})
+@app.route('/feed/')
+def feed(mode='latest'):
+ data = load_data()
+ username = session.get('username')
+ posts_list = data.get('posts', [])
+ if not isinstance(posts_list, list):
+ logging.error("Posts data is not a list, resetting.")
+ posts_list = []
+ data['posts'] = []
+
+ now = datetime.now()
+ processed_stories = []
+
+ for post in posts_list:
+ post.setdefault('likes', [])
+ post.setdefault('views', 0)
+ post.setdefault('comments', [])
+ post.setdefault('jerked_off_count', 0)
+ post.setdefault('views_reward_milestone', 0)
+ post.setdefault('promoted_until', None)
+ if 'id' not in post:
+ post['id'] = str(random.randint(100000, 999999))
+ post.setdefault('upload_date', '1970-01-01 00:00:00')
+ post.setdefault('uploader', 'unknown')
+ post.setdefault('title', 'Untitled')
+ post.setdefault('type', 'photo')
+ post.setdefault('filename', '')
+
+ is_promoted = False
+ if post.get('promoted_until'):
+ try:
+ expiry_date = datetime.strptime(post['promoted_until'], '%Y-%m-%d %H:%M:%S')
+ if expiry_date > now:
+ is_promoted = True
+ except (ValueError, TypeError):
+ post['promoted_until'] = None
+
+ post['is_currently_promoted'] = is_promoted
+
+ if mode == 'hot':
+ if is_promoted:
+ processed_stories.append(post)
+ else:
+ processed_stories.append(post)
+
+
+ if mode == 'hot':
+ stories = sorted(processed_stories, key=lambda x: datetime.strptime(x.get('promoted_until', '1970-01-01 00:00:00'), '%Y-%m-%d %H:%M:%S'), reverse=True)
+ page_title = "Hot Stories"
+ else:
+ stories = sorted(processed_stories, key=lambda x: datetime.strptime(x.get('upload_date', '1970-01-01 00:00:00'), '%Y-%m-%d %H:%M:%S'), reverse=True)
+ page_title = "Latest Stories"
+
+ is_authenticated = 'username' in session
+ user_count = len(data.get('users', {}))
+ is_online = is_user_online(data, username) if username else False
+ unread_count = count_unread_messages(data, username)
+
+ html_content = '''
+
+
+
+
+
+ {{ page_title }} - Adusis
+
+
+
+
+
+
+
+ ''' + NAV_HTML + '''
+
+
{{ page_title }}
+ {% with messages = get_flashed_messages(with_categories=true) %}
+ {% if messages %}
+ {% for category, message in messages %}
+
{{ message | escape }}
+ {% endfor %}
+ {% endif %}
+ {% endwith %}
+
+ {% if not stories %}
+
+
{% if mode == 'hot' %}No hot stories right now!{% else %}No stories yet!{% endif %}
+
{% if mode == 'hot' %}Promote a story from your profile to feature it here.{% else %}Be the first one to upload something.{% endif %}
+
+ {% else %}
+
+ {% for story in stories %}
+
+ {% endfor %}
+
+ {% endif %}
+
+
+
+
+
+'''
+ return render_template_string(html_content,
+ page_title=page_title,
+ mode=mode,
+ stories=stories,
+ is_authenticated=is_authenticated,
+ username=username,
+ repo_id=REPO_ID,
+ user_count=user_count,
+ is_online=is_online,
+ unread_count=unread_count,
+ is_user_online=lambda u: is_user_online(data, u),
+ logo_url=LOGO_URL,
+ html=html)
+
+
+@app.route('/profile', methods=['GET', 'POST'])
+def profile():
+ if 'username' not in session:
+ flash('Login to view your profile!', 'error')
+ return redirect(url_for('login'))
+
+ username = session['username']
+ data = load_data()
+
+ if request.method == 'POST':
+ current_data = load_data()
+
+ if 'delete_story' in request.form:
+ story_id = request.form.get('story_id')
+ if story_id:
+ original_length = len(current_data['posts'])
+ story_to_delete = next((p for p in current_data['posts'] if p.get('id') == story_id and p.get('uploader') == username), None)
+
+ if story_to_delete:
+ current_data['posts'] = [p for p in current_data['posts'] if not (p.get('id') == story_id and p.get('uploader') == username)]
+
+ if len(current_data['posts']) < original_length:
+ try:
+ save_data(current_data)
+ flash('Story deleted.', 'success')
+ except Exception as e:
+ flash('Failed to delete story.', 'error')
+ logging.error(f"Error saving data after deleting story {story_id}: {e}")
+ current_data['posts'].append(story_to_delete)
+ else:
+ flash('Story not found or deletion failed unexpectedly.', 'error')
+ else:
+ flash('Story not found or you do not have permission.', 'error')
+ else:
+ flash('Missing story ID for deletion.', 'error')
+ return redirect(url_for('profile'))
+
+ elif 'update_profile' in request.form:
+ bio = request.form.get('bio', '').strip()[:500]
+ link = request.form.get('link', '').strip()[:200]
+ role = request.form.get('role')
+ avatar_file = request.files.get('avatar')
+ profile_updated = False
+
+ if username in current_data['users']:
+ user_profile_data = current_data['users'][username]
+
+ if user_profile_data.get('bio') != bio:
+ user_profile_data['bio'] = bio
+ profile_updated = True
+ if user_profile_data.get('link') != link:
+ user_profile_data['link'] = link
+ profile_updated = True
+ if role and role in USER_ROLES and user_profile_data.get('role') != role:
+ user_profile_data['role'] = role
+ profile_updated = True
+ elif role and role not in USER_ROLES:
+ flash('Invalid role selected.', 'error')
+ return redirect(url_for('profile'))
+
+
+ if avatar_file and avatar_file.filename:
+ if not allowed_file(avatar_file.filename, {'png', 'jpg', 'jpeg', 'gif'}):
+ flash('Invalid avatar file type. Use png, jpg, jpeg, gif.', 'error')
+ return redirect(url_for('profile'))
+
+ filename = secure_filename(f"{username}_avatar_{random.randint(100,999)}{os.path.splitext(avatar_file.filename)[1]}")
+ temp_path = os.path.join(UPLOAD_FOLDER, filename)
+
+ try:
+ avatar_file.save(temp_path)
+ api = HfApi()
+ avatar_path = f"avatars/{filename}"
+
+ old_avatar = user_profile_data.get('avatar')
+ if old_avatar:
+ try:
+ api.delete_file(path_in_repo=old_avatar, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, ignore_patterns=["*.md"])
+ logging.info(f"Deleted old avatar {old_avatar} from HF for user {username}")
+ except Exception as hf_del_e:
+ logging.warning(f"Could not delete old avatar {old_avatar} from HF (maybe already deleted?): {hf_del_e}")
+
+
+ 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 update for {username}"
+ )
+ user_profile_data['avatar'] = avatar_path
+ profile_updated = True
+ if os.path.exists(temp_path):
+ os.remove(temp_path)
+ except Exception as e:
+ flash('Failed to upload avatar.', 'error')
+ logging.error(f"Error uploading avatar for {username}: {e}")
+ if os.path.exists(temp_path):
+ os.remove(temp_path)
+
+
+ if profile_updated:
+ try:
+ save_data(current_data)
+ flash('Profile updated!', 'success')
+ except Exception as e:
+ flash('Failed to update profile.', 'error')
+ logging.error(f"Error saving profile data for {username}: {e}")
+ else:
+ flash('No changes detected.', 'warning')
+
+ else:
+ flash('User not found.', 'error')
+
+ return redirect(url_for('profile'))
+
+ user_data = data.get('users', {}).get(username, {})
+ all_posts = data.get('posts', [])
+ if not isinstance(all_posts, list): all_posts = []
+
+ now = datetime.now()
+ user_stories_processed = []
+ for p in all_posts:
+ if p.get('uploader') == username and isinstance(p.get('upload_date'), str):
+ p_copy = p.copy()
+ is_promoted = False
+ promoted_until_str = p_copy.get('promoted_until')
+ if promoted_until_str:
+ try:
+ expiry_date = datetime.strptime(promoted_until_str, '%Y-%m-%d %H:%M:%S')
+ if expiry_date > now:
+ is_promoted = True
+ p_copy['promotion_expiry_formatted'] = expiry_date.strftime('%Y-%m-%d %H:%M')
+ else:
+ pass
+ except (ValueError, TypeError):
+ p_copy['promotion_expiry_formatted'] = "Invalid Date"
+ p_copy['is_currently_promoted'] = is_promoted
+ user_stories_processed.append(p_copy)
+
+ user_stories = sorted(user_stories_processed,
+ key=lambda x: datetime.strptime(x.get('upload_date', '1970-01-01 00:00:00'), '%Y-%m-%d %H:%M:%S'), reverse=True)
+
+
+ is_authenticated = True
+ user_count = len(data.get('users', {}))
+ is_online = is_user_online(data, username)
+ unread_count = count_unread_messages(data, username)
+ bio = user_data.get('bio', '')
+ link = user_data.get('link', '')
+ avatar = user_data.get('avatar', None)
+ aducoin = user_data.get('aducoin', 0)
+ user_role = user_data.get('role', 'Not Set')
+ referral_code = user_data.get('referral_code', 'N/A')
+ referral_link = url_for('register', ref=referral_code, _external=True) if referral_code != 'N/A' else 'N/A'
+
+ last_seen_str = user_data.get('last_seen', 'Never')
+ try:
+ if last_seen_str != 'Never':
+ last_seen_dt = datetime.strptime(last_seen_str, '%Y-%m-%d %H:%M:%S')
+ last_seen = last_seen_dt.strftime('%Y-%m-%d %H:%M')
+ else:
+ last_seen = 'Never'
+ except ValueError:
+ last_seen = 'Invalid Date'
+
+
+ html_content = '''
+
+
+
+
+
+ Profile - {{ username | escape }} - Adusis
+
+
+
+
+
+
+
+ ''' + NAV_HTML + '''
+
+ {% with messages = get_flashed_messages(with_categories=true) %}
+ {% if messages %}
+ {% for category, message in messages %}
+
{{ message | escape }}
+ {% endfor %}
+ {% endif %}
+ {% endwith %}
+
+
+
+
+
Edit Profile
+
+
+
+
+
+
+
Your Stories
+
+ {% for story in user_stories %}
+
+
+ {% set media_url = "https://huggingface.co/datasets/" + repo_id + "/resolve/main/" + story['type'] + "s/" + story['filename'] %}
+
+
+
+
+
+
+ ❤️ {{ story.get('likes', [])|length }}
+ 👁️ {{ story.get('views', 0) }}
+ 🍆 {{ story.get('jerked_off_count', 0) }}
+ 💬 {{ story.get('comments', [])|length }}
+
+ {% if story.is_currently_promoted %}
+
🔥 Promoted until {{ story.promotion_expiry_formatted }}
+ {% else %}
+
+ {% endif %}
+
+
+
+
+ {% endfor %}
+ {% if not user_stories %}
+
You haven't uploaded any stories yet. Upload one now!
+ {% endif %}
+
+
+
+ ![]()
+
+
+
+
+'''
+ return render_template_string(html_content,
+ username=username,
+ user_stories=user_stories,
+ bio=bio,
+ link=link,
+ avatar=avatar,
+ aducoin=aducoin,
+ user_role=user_role,
+ user_roles=USER_ROLES,
+ referral_link=referral_link,
+ last_seen=last_seen,
+ is_authenticated=is_authenticated,
+ repo_id=REPO_ID,
+ user_count=user_count,
+ is_online=is_online,
+ unread_count=unread_count,
+ random=random,
+ promote_cost=PROMOTE_COST,
+ promote_duration_days=PROMOTE_DURATION_DAYS,
+ logo_url=LOGO_URL,
+ html=html
+ )
+
+@app.route('/promote_story/', methods=['POST'])
+def promote_story(story_id):
+ if 'username' not in session:
+ flash('Login to promote a story!', 'error')
+ return redirect(url_for('profile'))
+
+ username = session['username']
+ data = load_data()
+
+ if username not in data['users']:
+ flash('User not found.', 'error')
+ return redirect(url_for('profile'))
+
+ story_index = next((index for (index, d) in enumerate(data.get('posts',[])) if d.get('id') == story_id), None)
+ if story_index is None:
+ flash('Story not found.', 'error')
+ return redirect(url_for('profile'))
+
+ story = data['posts'][story_index]
+
+ if story.get('uploader') != username:
+ flash('You can only promote your own stories.', 'error')
+ return redirect(url_for('profile'))
+
+ now = datetime.now()
+ if story.get('promoted_until'):
+ try:
+ expiry_date = datetime.strptime(story['promoted_until'], '%Y-%m-%d %H:%M:%S')
+ if expiry_date > now:
+ flash('This story is already promoted.', 'warning')
+ return redirect(url_for('profile'))
+ except (ValueError, TypeError):
+ pass
+
+ user_balance = data['users'][username].get('aducoin', 0)
+ if user_balance < PROMOTE_COST:
+ flash(f'Insufficient AduCoin to promote. You need {PROMOTE_COST}, but only have {user_balance}.', 'error')
+ return redirect(url_for('profile'))
+
+ data['users'][username]['aducoin'] -= PROMOTE_COST
+ expiry_time = now + timedelta(days=PROMOTE_DURATION_DAYS)
+ story['promoted_until'] = expiry_time.strftime('%Y-%m-%d %H:%M:%S')
+
+ log_transaction(data, username, 'promote_post', -PROMOTE_COST, f"Promoted story '{story.get('title', 'Untitled')}' (ID: {story_id})", related_story_id=story_id)
+
+ try:
+ save_data(data)
+ flash(f'Story promoted to Hot for {PROMOTE_DURATION_DAYS} days! (-{PROMOTE_COST} AduCoin)', 'success')
+ except Exception as e:
+ flash('Failed to promote story due to a saving error.', 'error')
+ logging.error(f"Error saving data after promoting story {story_id}: {e}")
+ data['users'][username]['aducoin'] += PROMOTE_COST
+ story['promoted_until'] = None
+
+ return redirect(url_for('profile'))
+
+
+def allowed_file(filename, allowed_extensions):
+ return '.' in filename and \
+ filename.rsplit('.', 1)[1].lower() in allowed_extensions
+
+@app.route('/story/', methods=['GET', 'POST'])
+def story_page(story_id):
+ data = load_data()
+ username = session.get('username')
+ sender_aducoin = data['users'].get(username, {}).get('aducoin', 0) if username else 0
+
+ story_index = next((index for (index, d) in enumerate(data.get('posts',[])) if d.get('id') == story_id), None)
+
+ if story_index is None:
+ flash('Story not found.', 'error')
+ return redirect(url_for('feed'))
+
+ story = data['posts'][story_index].copy()
+
+ if request.method == 'POST':
+ if 'add_comment' in request.form:
+ if not username:
+ flash('You must be logged in to comment.', 'error')
+ return redirect(url_for('story_page', story_id=story_id))
+
+ comment_text = request.form.get('comment_text', '').strip()
+ if not comment_text:
+ flash('Comment cannot be empty.', 'error')
+ return redirect(url_for('story_page', story_id=story_id))
+ if len(comment_text) > 1000:
+ flash('Comment too long (max 1000 characters).', 'error')
+ return redirect(url_for('story_page', story_id=story_id))
+
+ new_comment = {
+ 'username': username,
+ 'text': comment_text,
+ 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ }
+
+ current_data = load_data()
+ current_story_index = next((index for (index, d) in enumerate(current_data.get('posts',[])) if d.get('id') == story_id), None)
+ if current_story_index is None:
+ flash('Story disappeared while commenting.', 'error')
+ return redirect(url_for('feed'))
+
+ story_to_update = current_data['posts'][current_story_index]
+ if not isinstance(story_to_update.get('comments'), list):
+ story_to_update['comments'] = []
+
+ story_to_update['comments'].append(new_comment)
+
+ try:
+ save_data(current_data)
+ flash('Comment added!', 'success')
+ return redirect(url_for('story_page', story_id=story_id) + '#comments')
+ except Exception as e:
+ flash('Failed to add comment.', 'error')
+ logging.error(f"Error saving comment for story {story_id}: {e}")
+ if new_comment in story_to_update.get('comments', []):
+ story_to_update['comments'].remove(new_comment)
+ return redirect(url_for('story_page', story_id=story_id))
+
+ story.setdefault('title', 'Untitled')
+ story.setdefault('description', '')
+ story.setdefault('type', 'photo')
+ story.setdefault('filename', '')
+ story.setdefault('uploader', 'unknown')
+ story.setdefault('upload_date', 'Unknown date')
+ story.setdefault('likes', [])
+ story.setdefault('views', 0)
+ story.setdefault('jerked_off_count', 0)
+ story.setdefault('comments', [])
+ story.setdefault('views_reward_milestone', 0)
+ story.setdefault('promoted_until', None)
+
+ is_promoted = False
+ if story.get('promoted_until'):
+ try:
+ if datetime.strptime(story['promoted_until'], '%Y-%m-%d %H:%M:%S') > datetime.now():
+ is_promoted = True
+ except (ValueError, TypeError): pass
+ story['is_currently_promoted'] = is_promoted
+
+
+ is_authenticated = 'username' in session
+ user_count = len(data.get('users', {}))
+ current_user_is_online = is_user_online(data, username) if username else False
+ unread_count = count_unread_messages(data, username)
+ uploader_online = is_user_online(data, story.get('uploader'))
+ can_donate = is_authenticated and username != story.get('uploader')
+
+
+ html_content = '''
+
+
+
+
+
+ {{ story['title'] | escape }} - Adusis
+
+
+
+
+
+
+
+ ''' + NAV_HTML + '''
+
+
+ {% with messages = get_flashed_messages(with_categories=true) %}
+ {% if messages %}
+ {% for category, message in messages %}
+
{{ message | escape }}
+ {% endfor %}
+ {% endif %}
+ {% endwith %}
+
+
+ {{ story['title'] | escape }}
+ {% if story.is_currently_promoted %}🔥 HOT{% endif %}
+
+
+
+
+
+
{{ story['description'] | escape if story['description'] else 'No description provided.'}}
+
By: {{ story['uploader'] | escape }}
+
Uploaded on: {{ story['upload_date'] }}
+
+
+
+
+
+
+ 👁️ Views {{ story.get('views', 0) }}
+
+
+
+ {% if can_donate %}
+
+ {% endif %}
+
+
+
Back to Feed
+
+
+ ![]()
+
+
+
+
+'''
+ return render_template_string(html_content,
+ story=story,
+ repo_id=REPO_ID,
+ is_authenticated=is_authenticated,
+ username=username,
+ user_count=user_count,
+ is_online=current_user_is_online,
+ unread_count=unread_count,
+ uploader_online=uploader_online,
+ can_donate=can_donate,
+ sender_aducoin=sender_aducoin,
+ html=html,
+ logo_url=LOGO_URL
+ )
+
+@app.route('/donate/', methods=['POST'])
+def donate_story(story_id):
+ if 'username' not in session:
+ flash('You must be logged in to donate AduCoin.', 'error')
+ return redirect(url_for('story_page', story_id=story_id))
+
+ sender_username = session['username']
+ amount_str = request.form.get('amount')
+
+ data = load_data()
+
+ story_index = next((index for (index, d) in enumerate(data.get('posts',[])) if d.get('id') == story_id), None)
+ if story_index is None:
+ flash('Story not found.', 'error')
+ return redirect(url_for('feed'))
+
+ story = data['posts'][story_index]
+ recipient_username = story.get('uploader')
+
+ if not recipient_username or recipient_username not in data.get('users', {}):
+ flash('Story uploader not found or invalid.', 'error')
+ return redirect(url_for('story_page', story_id=story_id))
+
+ if sender_username == recipient_username:
+ flash('You cannot donate AduCoin to your own post.', 'error')
+ return redirect(url_for('story_page', story_id=story_id))
+
+ try:
+ amount = int(amount_str)
+ if amount <= 0:
+ raise ValueError("Amount must be positive.")
+ except (ValueError, TypeError):
+ flash('Invalid amount specified.', 'error')
+ return redirect(url_for('story_page', story_id=story_id))
+
+ if sender_username not in data.get('users', {}):
+ flash('Sender account not found. Please re-login.', 'error')
+ session.pop('username', None)
+ return redirect(url_for('login'))
+
+ sender_balance = data['users'][sender_username].get('aducoin', 0)
+
+ if sender_balance < amount:
+ flash(f'Insufficient funds. You only have {sender_balance} AduCoin.', 'error')
+ return redirect(url_for('story_page', story_id=story_id))
+
+ data['users'][sender_username]['aducoin'] -= amount
+ data['users'][recipient_username]['aducoin'] = data['users'][recipient_username].get('aducoin', 0) + amount
+
+ log_transaction(data, sender_username, 'donation_sent', -amount, f"Donated to '{recipient_username}' for post '{story.get('title', 'Untitled')}'", related_user=recipient_username, related_story_id=story_id)
+ log_transaction(data, recipient_username, 'donation_received', amount, f"Received from '{sender_username}' for post '{story.get('title', 'Untitled')}'", related_user=sender_username, related_story_id=story_id)
+
+ try:
+ save_data(data)
+ flash(f'Successfully donated {amount} AduCoin to {html.escape(recipient_username)}!', 'success')
+ logging.info(f"User {sender_username} donated {amount} AduCoin to {recipient_username} for story {story_id}")
+ except Exception as e:
+ flash('An error occurred during the donation. Please try again.', 'error')
+ logging.error(f"Error saving AduCoin donation from {sender_username} to {recipient_username} for story {story_id}: {e}")
+ try:
+ data['users'][sender_username]['aducoin'] += amount
+ data['users'][recipient_username]['aducoin'] -= amount
+ except Exception as rollback_e:
+ logging.error(f"Error rolling back failed donation: {rollback_e}")
+
+ return redirect(url_for('story_page', story_id=story_id))
+
+
+@app.route('/upload', methods=['GET', 'POST'])
+def upload():
+ if 'username' not in session:
+ flash('Login to upload a story!', 'error')
+ return redirect(url_for('login'))
+
+ username = session['username']
+
+ if request.method == 'POST':
+ title = request.form.get('title', '').strip()[:150]
+ description = request.form.get('description', '').strip()[:2000]
+ file = request.files.get('file')
+
+ if not file or not file.filename:
+ flash('File is required!', 'error')
+ return redirect(url_for('upload'))
+ if not title:
+ flash('Title is required!', 'error')
+ return redirect(url_for('upload'))
+
+ if not allowed_file(file.filename, {'png', 'jpg', 'jpeg', 'gif', 'mp4', 'mov', 'avi', 'webm'}):
+ flash('Invalid file type. Allowed: Images (png, jpg, gif), Videos (mp4, mov, avi, webm)', 'error')
+ return redirect(url_for('upload'))
+
+ file_ext = os.path.splitext(file.filename)[1]
+ base_filename = secure_filename(f"{username}_{int(time.time())}_{random.randint(100,999)}{file_ext}")
+ temp_path = os.path.join(UPLOAD_FOLDER, base_filename)
+
+ try:
+ file.save(temp_path)
+
+ if os.path.getsize(temp_path) > 100 * 1024 * 1024:
+ os.remove(temp_path)
+ flash('File is too large (max 100MB).', 'error')
+ return redirect(url_for('upload'))
+
+ file_type = 'video' if base_filename.lower().endswith(('.mp4', '.mov', 'avi', '.webm')) else 'photo'
+
+ data = load_data()
+ story_id = str(random.randint(100000, 999999))
+ while any(p.get('id') == story_id for p in data.get('posts', [])):
+ story_id = str(random.randint(100000, 999999))
+
+ api = HfApi()
+ hf_filename = base_filename
+ file_path_in_repo = f"{file_type}s/{hf_filename}"
+
+ logging.info(f"Uploading {temp_path} to {file_path_in_repo}...")
+ api.upload_file(
+ path_or_fileobj=temp_path,
+ path_in_repo=file_path_in_repo,
+ repo_id=REPO_ID,
+ repo_type="dataset",
+ token=HF_TOKEN_WRITE,
+ commit_message=f"Upload {file_type} by {username} (ID: {story_id})"
+ )
+ logging.info("Upload to HF successful.")
+
+ new_story = {
+ 'id': story_id,
+ 'title': title,
+ 'description': description,
+ 'filename': hf_filename,
+ 'type': file_type,
+ 'uploader': username,
+ 'upload_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+ 'likes': [],
+ 'views': 0,
+ 'comments': [],
+ 'jerked_off_count': 0,
+ 'views_reward_milestone': 0,
+ 'promoted_until': None
+ }
+
+ data.setdefault('posts', []).append(new_story)
+
+ if username in data['users']:
+ data['users'][username]['aducoin'] = data['users'][username].get('aducoin', 0) + UPLOAD_REWARD
+ log_transaction(data, username, 'earn_upload', UPLOAD_REWARD, f"Earned for uploading story '{title}' (ID: {story_id})", related_story_id=story_id)
+ logging.info(f"Awarded {UPLOAD_REWARD} AduCoin to {username} for uploading story {story_id}")
+ else:
+ logging.warning(f"Uploader {username} not found in users data, cannot award AduCoin.")
+
+ save_data(data)
+ flash(f'Story uploaded successfully! (+{UPLOAD_REWARD} AduCoin)', 'success')
+
+ if os.path.exists(temp_path):
+ os.remove(temp_path)
+ return redirect(url_for('story_page', story_id=story_id))
+
+ except Exception as e:
+ flash(f'Upload failed: {html.escape(str(e))}', 'error')
+ logging.error(f"Error during upload for {username}: {e}", exc_info=True)
+ if os.path.exists(temp_path):
+ os.remove(temp_path)
+ return redirect(url_for('upload'))
+
+ data = load_data()
+ is_authenticated = True
+ user_count = len(data.get('users', {}))
+ is_online = is_user_online(data, username)
+ unread_count = count_unread_messages(data, username)
+
+ html_content = '''
+
+
+
+
+
+ Upload Story - Adusis
+
+
+
+
+
+
+
+ ''' + NAV_HTML + '''
+
+
+
+
+'''
+ return render_template_string(html_content,
+ username=username,
+ is_authenticated=is_authenticated,
+ repo_id=REPO_ID,
+ user_count=user_count,
+ is_online=is_online,
+ unread_count=unread_count,
+ upload_reward=UPLOAD_REWARD,
+ logo_url=LOGO_URL)
+
+@app.route('/users', methods=['GET', 'POST'])
+def users():
+ data = load_data()
+ current_user = session.get('username')
+
+ is_authenticated = 'username' in session
+ user_count = len(data.get('users', {}))
+ current_user_is_online = is_user_online(data, current_user) if current_user else False
+ unread_count = count_unread_messages(data, current_user)
+
+ search_query = request.form.get('search', '').strip().lower() if request.method == 'POST' else ''
+
+ user_list = []
+ for username_loop, user_data in data.get('users', {}).items():
+ if not search_query or search_query in username_loop.lower():
+ user_list.append({
+ 'name': username_loop,
+ 'avatar': user_data.get('avatar'),
+ 'online': is_user_online(data, username_loop),
+ 'role': user_data.get('role', 'N/A')
+ })
+
+ user_list.sort(key=lambda u: (not u['online'], u['name'].lower()))
+
+ html_content = '''
+
+
+
+
+
+ Users - Adusis
+
+
+
+
+
+
+
+ ''' + NAV_HTML + '''
+
+
Explore Users ({{ user_count }})
+
+
+
+
+
+
+
+
+'''
+ return render_template_string(html_content,
+ user_list=user_list,
+ username=current_user,
+ is_authenticated=is_authenticated,
+ repo_id=REPO_ID,
+ user_count=user_count,
+ is_online=current_user_is_online,
+ unread_count=unread_count,
+ search_query=search_query,
+ random=random,
+ logo_url=LOGO_URL,
+ html=html)
+
+@app.route('/user/', methods=['GET'])
+def user_profile(username):
+ data = load_data()
+ current_user = session.get('username')
+
+ if username == current_user:
+ return redirect(url_for('profile'))
+
+ if username not in data.get('users', {}):
+ flash(f'User "{html.escape(username)}" not found.', 'error')
+ return redirect(url_for('users'))
+
+ user_data = data['users'][username]
+ all_posts = data.get('posts', [])
+ if not isinstance(all_posts, list): all_posts = []
+
+ now = datetime.now()
+ user_stories_processed = []
+ for p in all_posts:
+ if p.get('uploader') == username and isinstance(p.get('upload_date'), str):
+ p_copy = p.copy()
+ is_promoted = False
+ promoted_until_str = p_copy.get('promoted_until')
+ if promoted_until_str:
+ try:
+ expiry_date = datetime.strptime(promoted_until_str, '%Y-%m-%d %H:%M:%S')
+ if expiry_date > now:
+ is_promoted = True
+ except (ValueError, TypeError): pass
+ p_copy['is_currently_promoted'] = is_promoted
+ user_stories_processed.append(p_copy)
+
+ user_stories = sorted(user_stories_processed,
+ key=lambda x: datetime.strptime(x.get('upload_date','1970-01-01 00:00:00'), '%Y-%m-%d %H:%M:%S'), reverse=True)
+
+
+ is_authenticated = 'username' in session
+ user_count = len(data.get('users', {}))
+ current_user_is_online = is_user_online(data, current_user) if current_user else False
+ unread_count = count_unread_messages(data, current_user)
+ profile_user_online = is_user_online(data, username)
+ sender_aducoin = data['users'].get(current_user, {}).get('aducoin', 0) if current_user else 0
+
+ bio = user_data.get('bio', '')
+ link = user_data.get('link', '')
+ avatar = user_data.get('avatar', None)
+ aducoin = user_data.get('aducoin', 0)
+ user_role = user_data.get('role', 'Not Set')
+
+ last_seen_str = user_data.get('last_seen', 'Never')
+ last_seen = 'Unknown'
+ try:
+ if last_seen_str != 'Never':
+ last_seen_dt = datetime.strptime(last_seen_str, '%Y-%m-%d %H:%M:%S')
+ time_diff = datetime.now() - last_seen_dt
+
+ if profile_user_online: last_seen = 'Online Now'
+ elif time_diff < timedelta(hours=1): last_seen = f"Seen {int(time_diff.total_seconds() / 60)}m ago"
+ elif time_diff < timedelta(days=1): last_seen = last_seen_dt.strftime('Today at %H:%M')
+ else: last_seen = last_seen_dt.strftime('%b %d, %Y')
+ else: last_seen = 'Never'
+ except ValueError: last_seen = 'Invalid Date'
+
+ html_content = '''
+
+
+
+
+
+ Profile - {{ profile_username | escape }} - Adusis
+
+
+
+
+
+
+
+ ''' + NAV_HTML + '''
+
+
+
+
+
+
+'''
+ return render_template_string(html_content,
+ profile_username=username,
+ user_stories=user_stories,
+ bio=bio,
+ link=link,
+ avatar=avatar,
+ aducoin=aducoin,
+ user_role=user_role,
+ last_seen=last_seen,
+ is_authenticated=is_authenticated,
+ username=current_user,
+ sender_aducoin=sender_aducoin,
+ repo_id=REPO_ID,
+ user_count=user_count,
+ is_online=current_user_is_online,
+ unread_count=unread_count,
+ profile_user_online=profile_user_online,
+ random=random,
+ logo_url=LOGO_URL,
+ html=html)
+
+@app.route('/transactions')
+def transactions():
+ if 'username' not in session:
+ flash('Login to view your transaction history!', 'error')
+ return redirect(url_for('login'))
+
+ username = session['username']
+ data = load_data()
+
+ user_transactions = sorted(
+ [t for t in data.get('transactions', []) if t.get('user') == username],
+ key=lambda x: x.get('timestamp', '0'),
+ reverse=True
+ )
+
+ user_data = data.get('users', {}).get(username, {})
+ current_aducoin = user_data.get('aducoin', 0)
+ user_count = len(data.get('users', {}))
+ is_online = is_user_online(data, username)
+ unread_count = count_unread_messages(data, username)
+
+ html_content = '''
+
+
+
+
+
+ AduCoin History - Adusis
+
+
+
+
+
+
+
+ ''' + NAV_HTML + '''
+
+
+
+ AduCoin History
+
+
+ Your current balance: {{ current_aducoin }} 🪙
+
+
+ {% if transactions %}
+
+
+ | Date | Type | Amount | Description | Balance After |
+
+ {% for tx in transactions %}
+
+ | {{ tx.timestamp }} |
+ {{ tx.type.replace('_', ' ')|title }} |
+ {{ '%+d'|format(tx.amount) if tx.amount != 0 else tx.amount }} |
+ {{ tx.description | escape }} |
+ {{ tx.balance_after }} |
+
+ {% endfor %}
+
+
+
+ {% else %}
+
No transaction history yet.
+ {% endif %}
+
+
+
+
+'''
+ return render_template_string(html_content,
+ transactions=user_transactions,
+ username=username,
+ current_aducoin=current_aducoin,
+ is_authenticated=True,
+ user_count=user_count,
+ is_online=is_online,
+ unread_count=unread_count,
+ logo_url=LOGO_URL,
+ html=html)
+
+
+@app.route('/transfer_aducoin/', methods=['POST'])
+def transfer_aducoin(recipient_username):
+ if 'username' not in session:
+ flash('You must be logged in to transfer AduCoin.', 'error')
+ return redirect(url_for('user_profile', username=recipient_username))
+
+ sender_username = session['username']
+ amount_str = request.form.get('amount')
+
+ if sender_username == recipient_username:
+ flash('You cannot transfer AduCoin to yourself.', 'error')
+ return redirect(url_for('user_profile', username=recipient_username))
+
+ try:
+ amount = int(amount_str)
+ if amount <= 0:
+ raise ValueError("Amount must be positive.")
+ except (ValueError, TypeError):
+ flash('Invalid amount specified.', 'error')
+ return redirect(url_for('user_profile', username=recipient_username))
+
+ data = load_data()
+
+ if sender_username not in data.get('users', {}):
+ flash('Sender account not found. Please re-login.', 'error')
+ session.pop('username', None)
+ return redirect(url_for('login'))
+
+ if recipient_username not in data.get('users', {}):
+ flash(f'Recipient user "{html.escape(recipient_username)}" not found.', 'error')
+ return redirect(url_for('user_profile', username=recipient_username))
+
+ sender_balance = data['users'][sender_username].get('aducoin', 0)
+
+ if sender_balance < amount:
+ flash(f'Insufficient funds. You only have {sender_balance} AduCoin.', 'error')
+ return redirect(url_for('user_profile', username=recipient_username))
+
+ data['users'][sender_username]['aducoin'] -= amount
+ data['users'][recipient_username]['aducoin'] = data['users'][recipient_username].get('aducoin', 0) + amount
+
+ log_transaction(data, sender_username, 'transfer_sent', -amount, f"Sent to user '{recipient_username}'", related_user=recipient_username)
+ log_transaction(data, recipient_username, 'transfer_received', amount, f"Received from user '{sender_username}'", related_user=sender_username)
+
+ try:
+ save_data(data)
+ flash(f'Successfully transferred {amount} AduCoin to {html.escape(recipient_username)}!', 'success')
+ logging.info(f"User {sender_username} transferred {amount} AduCoin to {recipient_username}")
+ except Exception as e:
+ flash('An error occurred during the transfer. Please try again.', 'error')
+ logging.error(f"Error saving AduCoin transfer from {sender_username} to {recipient_username}: {e}")
+ try:
+ data['users'][sender_username]['aducoin'] += amount
+ data['users'][recipient_username]['aducoin'] -= amount
+ except Exception as rollback_e:
+ logging.error(f"Error rolling back failed transfer: {rollback_e}")
+
+
+ return redirect(url_for('user_profile', username=recipient_username))
+
+
+@app.route('/like/', methods=['POST'])
+def like_story(story_id):
+ if 'username' not in session:
+ return jsonify({'message': 'Login required'}), 401
+
+ username = session['username']
+ data = load_data()
+
+ story_index = next((index for (index, d) in enumerate(data.get('posts',[])) if d.get('id') == story_id), None)
+ if story_index is None:
+ return jsonify({'message': 'Story not found'}), 404
+
+ story = data['posts'][story_index]
+
+ if not isinstance(story.get('likes'), list):
+ story['likes'] = []
+
+ liked = False
+ if username in story['likes']:
+ story['likes'].remove(username)
+ liked = False
+ else:
+ story['likes'].append(username)
+ liked = True
+
+ try:
+ save_data(data)
+ return jsonify({'message': 'Success', 'likes': len(story['likes']), 'liked': liked}), 200
+ except Exception as e:
+ logging.error(f"Error saving like for story {story_id} by {username}: {e}")
+ if liked:
+ if username in story['likes']: story['likes'].remove(username)
+ else:
+ if username not in story['likes']: story['likes'].append(username)
+ return jsonify({'message': 'Failed to save like'}), 500
+
+
+@app.route('/jerk_off/', methods=['POST'])
+def jerk_off_story(story_id):
+ data = load_data()
+
+ story_index = next((index for (index, d) in enumerate(data.get('posts',[])) if d.get('id') == story_id), None)
+ if story_index is None:
+ return jsonify({'message': 'Story not found'}), 404
+
+ story = data['posts'][story_index]
+
+ if not isinstance(story.get('jerked_off_count'), int):
+ story['jerked_off_count'] = 0
+
+ story['jerked_off_count'] += 1
+
+ try:
+ save_data(data)
+ return jsonify({'message': 'Success', 'jerked_off_count': story['jerked_off_count']}), 200
+ except Exception as e:
+ logging.error(f"Error saving jerk_off count for story {story_id}: {e}")
+ story['jerked_off_count'] -= 1
+ return jsonify({'message': 'Failed to save count'}), 500
+
+@app.route('/view/', methods=['POST'])
+def increment_view(story_id):
+ data = load_data()
+ story_index = next((index for (index, d) in enumerate(data.get('posts',[])) if d.get('id') == story_id), None)
+ if story_index is None:
+ return jsonify({'message': 'Story not found'}), 404
+
+ story = data['posts'][story_index]
+ uploader = story.get('uploader')
+
+ if not isinstance(story.get('views'), int):
+ story['views'] = 0
+ if not isinstance(story.get('views_reward_milestone'), int):
+ story['views_reward_milestone'] = 0
+
+ story['views'] += 1
+ current_views = story['views']
+ last_milestone_views = story['views_reward_milestone']
+ reward_message = None
+ reward_to_give = 0
+
+ if uploader and uploader in data.get('users', {}):
+ current_milestone = current_views // VIEW_REWARD_THRESHOLD
+ last_milestone = last_milestone_views // VIEW_REWARD_THRESHOLD
+
+ if current_milestone > last_milestone:
+ milestones_to_reward = current_milestone - last_milestone
+ reward_to_give = milestones_to_reward * VIEW_REWARD_AMOUNT
+
+ if reward_to_give > 0:
+ data['users'][uploader]['aducoin'] = data['users'][uploader].get('aducoin', 0) + reward_to_give
+ story['views_reward_milestone'] = current_views
+
+ milestone_desc = f"{current_milestone * VIEW_REWARD_THRESHOLD} views"
+ log_transaction(data, uploader, 'earn_views', reward_to_give, f"Reached {milestone_desc} on story '{story.get('title', 'Untitled')}' (ID: {story_id})", related_story_id=story_id)
+
+ reward_message = f"Awarded {reward_to_give} AduCoin to {uploader} for reaching {milestone_desc} views on story {story_id}"
+ logging.info(reward_message)
+
+
+ try:
+ save_data(data)
+ response_data = {'message': 'Success', 'views': story['views']}
+ if reward_message:
+ response_data['reward_message'] = reward_message
+ return jsonify(response_data), 200
+ except Exception as e:
+ logging.error(f"Error saving view count or reward for story {story_id}: {e}")
+ story['views'] -= 1
+ if reward_message and uploader and uploader in data.get('users', {}):
+ data['users'][uploader]['aducoin'] -= reward_to_give
+ story['views_reward_milestone'] = last_milestone_views
+
+ return jsonify({'message': 'Failed to save view count', 'views': story['views']}), 503
+
+@app.route('/messages')
+def inbox():
+ if 'username' not in session:
+ flash('Login to view your messages!', 'error')
+ return redirect(url_for('login'))
+
+ username = session['username']
+ data = load_data()
+ users_data = data.get('users', {})
+ messages_data = data.get('direct_messages', {})
+
+ conversations = []
+ for conv_key_str, messages in messages_data.items():
+ try:
+ conv_key = eval(conv_key_str)
+ if not isinstance(conv_key, tuple) or len(conv_key) != 2:
+ logging.warning(f"Skipping invalid conversation key format: {conv_key_str}")
+ continue
+
+ if username in conv_key:
+ other_user = next(user for user in conv_key if user != username)
+ if not messages:
+ last_message_text = "No messages yet"
+ last_message_time = ""
+ is_unread = False
+ else:
+ last_message = sorted(messages, key=lambda x: x['timestamp'])[-1]
+ last_message_text = last_message['text']
+ last_message_time = last_message['timestamp']
+ is_unread = any(msg['sender'] != username and not msg.get('read') for msg in messages)
+
+ other_user_data = users_data.get(other_user, {})
+ conversations.append({
+ 'other_user': other_user,
+ 'avatar': other_user_data.get('avatar'),
+ 'last_message_text': last_message_text,
+ 'last_message_time': last_message_time,
+ 'is_unread': is_unread
+ })
+ except Exception as e:
+ logging.error(f"Error processing conversation key {conv_key_str}: {e}")
+ continue
+
+ conversations.sort(key=lambda x: (not x['is_unread'], x['last_message_time']), reverse=True)
+
+ is_authenticated = True
+ user_count = len(users_data)
+ is_online = is_user_online(data, username)
+ unread_count = count_unread_messages(data, username)
+
+ html_content = '''
+
+
+
+
+
+ Inbox - Adusis
+
+
+
+
+
+
+
+ ''' + NAV_HTML + '''
+
+
Messages
+ {% with messages = get_flashed_messages(with_categories=true) %}
+ {% if messages %}
+ {% for category, message in messages %}
+
{{ message | escape }}
+ {% endfor %}
+ {% endif %}
+ {% endwith %}
+
+ {% if conversations %}
+
+ {% else %}
+
You have no conversations yet. Find someone on the Users page to start chatting!
+ {% endif %}
+
+
+
+
+'''
+ return render_template_string(html_content,
+ conversations=conversations,
+ is_authenticated=is_authenticated,
+ username=username,
+ user_count=user_count,
+ is_online=is_online,
+ unread_count=unread_count,
+ repo_id=REPO_ID,
+ random=random,
+ logo_url=LOGO_URL,
+ html=html)
+
+@app.route('/messages/', methods=['GET', 'POST'])
+def conversation(other_username):
+ if 'username' not in session:
+ flash('Login to view messages!', 'error')
+ return redirect(url_for('login'))
+
+ username = session['username']
+ data = load_data()
+ users_data = data.get('users', {})
+ messages_data = data.get('direct_messages', {})
+
+ if other_username not in users_data:
+ flash(f"User '{html.escape(other_username)}' not found.", 'error')
+ return redirect(url_for('inbox'))
+
+ if username == other_username:
+ flash("You cannot message yourself.", 'warning')
+ return redirect(url_for('inbox'))
+
+ conv_key_tuple = get_conversation_key(username, other_username)
+ conv_key = str(conv_key_tuple)
+
+ if request.method == 'POST':
+ message_text = request.form.get('message_text', '').strip()
+ if not message_text:
+ flash("Message cannot be empty.", 'error')
+ return redirect(url_for('conversation', other_username=other_username))
+ if len(message_text) > 2000:
+ flash('Message too long (max 2000 characters).', 'error')
+ return redirect(url_for('conversation', other_username=other_username))
+
+ new_message = {
+ 'message_id': str(uuid.uuid4()),
+ 'sender': username,
+ 'text': message_text,
+ 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+ 'read': False
+ }
+
+ current_data = load_data()
+ current_messages_data = current_data.setdefault('direct_messages', {})
+
+ if conv_key not in current_messages_data:
+ current_messages_data[conv_key] = []
+ elif not isinstance(current_messages_data[conv_key], list):
+ logging.warning(f"Correcting non-list conversation data for key {conv_key}")
+ current_messages_data[conv_key] = []
+
+ current_messages_data[conv_key].append(new_message)
+
+ try:
+ save_data(current_data)
+ return redirect(url_for('conversation', other_username=other_username))
+ except Exception as e:
+ flash("Failed to send message.", 'error')
+ logging.error(f"Error saving message from {username} to {other_username}: {e}")
+ if conv_key in current_messages_data and new_message in current_messages_data[conv_key]:
+ current_messages_data[conv_key].remove(new_message)
+ return redirect(url_for('conversation', other_username=other_username))
+
+ data = load_data()
+ messages_data = data.get('direct_messages', {})
+ messages = messages_data.get(conv_key, [])
+ if not isinstance(messages, list): messages = []
+
+ marked_read = False
+ for msg in messages:
+ if isinstance(msg, dict) and msg.get('sender') == other_username and not msg.get('read'):
+ msg['read'] = True
+ marked_read = True
+
+ if marked_read:
+ try:
+ save_data(data)
+ logging.info(f"Marked messages as read in conversation {conv_key} for user {username}")
+ except Exception as e:
+ logging.error(f"Failed to save read status for conversation {conv_key}: {e}")
+
+ messages.sort(key=lambda x: x.get('timestamp', '0'))
+
+ is_authenticated = True
+ user_count = len(users_data)
+ is_online = is_user_online(data, username)
+ other_user_is_online = is_user_online(data, other_username)
+ unread_count = count_unread_messages(data, username)
+ other_user_data = users_data.get(other_username, {})
+
+
+ html_content = '''
+
+
+
+
+
+ Chat with {{ other_username | escape }} - Adusis
+
+
+
+
+
+
+
+ ''' + NAV_HTML + '''
+
+
+
+ {% with messages_flash = get_flashed_messages(with_categories=true) %}
+ {% if messages_flash %}
+ {% for category, message in messages_flash %}
+
{{ message | escape }}
+ {% endfor %}
+ {% endif %}
+ {% endwith %}
+
+
+ {% if messages %}
+ {% for msg in messages %}
+ {% if msg is mapping %}
+
+
{{ msg.text | escape }}
+
{{ msg.timestamp }}
+
+ {% endif %}
+ {% endfor %}
+ {% else %}
+
Start the conversation!
+ {% endif %}
+
+
+
+
+
+
+
+'''
+ return render_template_string(html_content,
+ messages=messages,
+ other_username=other_username,
+ other_user_data=other_user_data,
+ other_user_is_online=other_user_is_online,
+ is_authenticated=is_authenticated,
+ username=username,
+ user_count=user_count,
+ is_online=is_online,
+ unread_count=unread_count,
+ repo_id=REPO_ID,
+ random=random,
+ logo_url=LOGO_URL,
+ html=html)
+
+if __name__ == '__main__':
+ if not os.path.exists(DATA_FILE):
+ logging.info(f"{DATA_FILE} not found locally, attempting download from HF.")
+ download_db_from_hf()
+ if not os.path.exists(DATA_FILE):
+ logging.info(f"Data file {DATA_FILE} still not found after download attempt, creating empty file.")
+ with open(DATA_FILE, 'w', encoding='utf-8') as f:
+ json.dump(initialize_data_structure({}), f)
+ else:
+ logging.info(f"Successfully downloaded {DATA_FILE}.")
+ else:
+ logging.info(f"Using existing local file {DATA_FILE}.")
+
+ try:
+ current_data = load_data()
+ initialized_data = initialize_data_structure(current_data)
+ needs_save = False
+ if 'direct_messages' not in current_data: needs_save = True
+ if 'transactions' not in current_data: needs_save = True
+
+ if needs_save:
+ logging.info("Applying initial data structure updates...")
+ save_data(initialized_data)
+ else:
+ if json.dumps(initialized_data, sort_keys=True) != json.dumps(current_data, sort_keys=True):
+ logging.info("Ensuring data structure consistency...")
+ save_data(initialized_data)
+
+ except Exception as e:
+ logging.error(f"Error during initial data structure check/update: {e}")
+
+ backup_thread = threading.Thread(target=periodic_backup, daemon=True)
+ backup_thread.start()
+ logging.info("Starting Flask application...")
+ app.run(debug=False, host='0.0.0.0', port=7860)
Comments ({{ story.get('comments', []) | length }})
+ {% if is_authenticated %} + + {% else %} +Login to comment.
+ {% endif %} +