diff --git "a/app (38).py" "b/app (38).py" new file mode 100644--- /dev/null +++ "b/app (38).py" @@ -0,0 +1,4518 @@ + +# --- START OF FILE app.py --- +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 { + --primary: #FF007F; /* Deep Pink */ + --secondary: #8A2BE2; /* Blue Violet */ + --accent: #FFD700; /* Gold */ + --background-light: #140A1A; + --background-dark: #0A050F; + --card-bg: rgba(20, 10, 25, 0.9); + --card-bg-dark: rgba(10, 5, 15, 0.95); + --text-light: #F8F8F8; + --text-dark: #C0C0C0; + --shadow: 0 10px 30px rgba(255, 0, 127, 0.5); + --glass-bg: rgba(30, 15, 40, 0.6); + --transition: all 0.3s ease-in-out; + --online: #00FF7F; /* Neon Green */ + --offline: #FF4500; /* Orange Red */ + --like-color: #FF007F; + --jerk-color: #FFD700; + --aducoin-color: #FFD700; + --message-sent-bg: rgba(138, 43, 226, 0.3); + --message-received-bg: rgba(40, 20, 50, 0.7); +} +* { margin: 0; padding: 0; box-sizing: border-box; } +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: radial-gradient(circle at top left, var(--background-light) 0%, var(--background-dark) 70%); + color: var(--text-light); + line-height: 1.6; + overflow-x: hidden; +} +body.dark { + background: radial-gradient(circle at top left, var(--background-light) 0%, var(--background-dark) 70%); + color: var(--text-light); +} +.sidebar { + position: fixed; + top: 0; + left: 0; + width: 280px; + height: 100%; + background: var(--card-bg-dark); + backdrop-filter: blur(15px); + padding: 20px; + box-shadow: 5px 0 20px rgba(0, 0, 0, 0.7); + z-index: 1000; + transition: transform var(--transition); + border-right: 1px solid var(--primary); + overflow-y: auto; +} +.sidebar.hidden { + transform: translateX(-100%); +} +.sidebar-header { + display: flex; + align-items: center; + gap: 15px; + margin-bottom: 30px; + border-bottom: 2px solid var(--primary); + padding-bottom: 15px; + background: linear-gradient(90deg, transparent, rgba(255, 0, 127, 0.1)); + border-radius: 5px; + padding-left: 10px; +} +.nav-brand { + font-size: 1.8em; + font-weight: 700; + color: var(--primary); + text-shadow: 0 0 15px var(--primary); + letter-spacing: 1px; +} +.logo { + width: 50px; + height: 50px; + border-radius: 12px; + border: 2px solid var(--primary); + box-shadow: 0 0 10px var(--primary); +} +.nav-links { + display: flex; + flex-direction: column; + gap: 15px; +} +.nav-link { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 22px; + background: transparent; + color: var(--text-light); + text-decoration: none; + border-radius: 10px; + font-size: 1.1em; + font-weight: 500; + transition: var(--transition); + border-left: 4px solid transparent; + position: relative; + overflow: hidden; +} +body.dark .nav-link { + color: var(--text-light); +} +.nav-link::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(90deg, rgba(255, 0, 127, 0.1), transparent); + transform: translateX(-100%); + transition: transform var(--transition); + z-index: -1; +} +.nav-link:hover::before { + transform: translateX(0); +} +.nav-link:hover, .nav-link.active { + color: var(--primary); + border-left: 4px solid var(--primary); + transform: translateX(8px); + background: transparent; +} +.nav-link.active::before { + transform: translateX(0); + background: linear-gradient(90deg, rgba(255, 0, 127, 0.2), transparent); +} +.nav-link span:first-child { + font-size: 1.3em; +} +.logout-btn { + color: var(--secondary); +} +.logout-btn:hover { + background: var(--secondary); + color: white; + border-left: 4px solid var(--secondary); +} +.menu-btn { + display: none; + font-size: 28px; + background: var(--card-bg-dark); + border: 1px solid var(--primary); + color: var(--primary); + cursor: pointer; + position: fixed; + top: 20px; + left: 20px; + z-index: 1001; + padding: 12px; + border-radius: 50%; + box-shadow: 0 0 15px rgba(255, 0, 127, 0.4); + transition: var(--transition); +} +.menu-btn:hover { + background: var(--primary); + color: var(--background-dark); + box-shadow: 0 0 20px var(--primary); +} +.container { + margin: 20px auto 20px 300px; + max-width: 1100px; + padding: 20px; + transition: margin-left var(--transition); +} +body:not(.sidebar-active-on-pc) .container { + margin-left: auto; + margin-right: auto; +} + +.btn { + padding: 14px 30px; + background: var(--primary); + color: white; + border: none; + border-radius: 10px; + cursor: pointer; + font-size: 1.05em; + font-weight: 600; + transition: var(--transition); + display: inline-flex; + align-items: center; + gap: 10px; + box-shadow: 0 5px 20px rgba(255, 0, 127, 0.4); + text-transform: uppercase; + letter-spacing: 1.5px; + position: relative; + overflow: hidden; +} +.btn::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: radial-gradient(circle, rgba(255,255,255,0.2) 0%, transparent 70%); + transform: scale(0); + transition: transform 0.5s ease; + border-radius: 50%; + opacity: 0; +} +.btn:hover::before { + transform: scale(2); + opacity: 1; +} +.btn:hover { + transform: translateY(-3px); + background: #e00070; + box-shadow: 0 8px 25px rgba(255, 0, 127, 0.6); +} +.btn-secondary { + background: var(--secondary); + box-shadow: 0 5px 20px rgba(138, 43, 226, 0.4); +} +.btn-secondary:hover { + background: #7a20c9; + box-shadow: 0 8px 25px rgba(138, 43, 226, 0.6); +} +.btn-accent { + background: var(--accent); + color: var(--background-dark); + box-shadow: 0 5px 20px rgba(255, 215, 0, 0.4); +} +.btn-accent:hover { + background: #e6c200; + box-shadow: 0 8px 25px rgba(255, 215, 0, 0.6); +} +input, textarea, select { + width: 100%; + padding: 14px 18px; + margin: 12px 0; + border: 1px solid var(--glass-bg); + border-radius: 10px; + background: var(--glass-bg); + color: var(--text-light); + font-size: 1.05em; + transition: var(--transition); +} +body.dark input, body.dark textarea, body.dark select { + color: var(--text-light); +} +input:focus, textarea:focus, select:focus { + outline: none; + border-color: var(--primary); + background: rgba(40, 20, 50, 0.7); + box-shadow: 0 0 0 4px rgba(255, 0, 127, 0.3); +} +textarea { + min-height: 120px; + resize: vertical; +} +input[type=number]::-webkit-inner-spin-button, +input[type=number]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} +input[type=number] { + -moz-appearance: textfield; +} +.modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.95); + z-index: 2000; + justify-content: center; + align-items: center; + transition: opacity var(--transition); +} +.modal img, .modal video { + max-width: 90%; + max-height: 90%; + object-fit: contain; + border-radius: 15px; + box-shadow: 0 0 40px rgba(255, 0, 127, 0.6); + animation: zoomIn 0.4s ease; +} +.theme-toggle { + position: fixed; + top: 20px; + right: 20px; + background: var(--glass-bg); + border: 1px solid var(--secondary); + padding: 12px; + border-radius: 50%; + cursor: pointer; + font-size: 22px; + color: var(--secondary); + box-shadow: 0 0 15px rgba(138, 43, 226, 0.4); + transition: var(--transition); + z-index: 1001; +} +.theme-toggle:hover { + transform: rotate(360deg); + background: var(--secondary); + color: white; + box-shadow: 0 0 20px var(--secondary); +} +.status-dot { + width: 12px; + height: 12px; + border-radius: 50%; + display: inline-block; + margin-left: 8px; + vertical-align: middle; + box-shadow: 0 0 7px currentColor; + border: 1px solid rgba(255,255,255,0.2); +} +.online { background: var(--online); } +.offline { background: var(--offline); } +.badge { + background-color: var(--primary); + color: white; + padding: 3px 8px; + border-radius: 6px; + font-size: 0.85em; + margin-left: 10px; + font-weight: bold; + box-shadow: 0 2px 5px rgba(0,0,0,0.3); +} +.nav-link .badge { + position: absolute; + right: 20px; + top: 50%; + transform: translateY(-50%); + background-color: var(--accent); + color: var(--background-dark); +} + +.flash { + padding: 18px; + margin-bottom: 25px; + border-radius: 10px; + font-weight: 600; + text-align: center; + box-shadow: 0 5px 15px rgba(0,0,0,0.3); + border: 1px solid transparent; +} +.flash.success { + background-color: rgba(0, 255, 127, 0.2); + color: var(--online); + border-color: var(--online); +} +.flash.error { + background-color: rgba(255, 0, 127, 0.2); + color: var(--primary); + border-color: var(--primary); +} +.flash.warning { + background-color: rgba(255, 215, 0, 0.2); + color: var(--accent); + border-color: var(--accent); +} +.story-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 30px; +} +.story-item { + background: var(--card-bg); + border-radius: 15px; + box-shadow: 0 6px 15px rgba(0,0,0,0.4); + transition: var(--transition); + overflow: hidden; + border: 1px solid var(--glass-bg); + display: flex; + flex-direction: column; +} +body.dark .story-item { background: var(--card-bg); } +.story-item:hover { + transform: translateY(-8px) scale(1.02); + box-shadow: 0 12px 25px rgba(255, 0, 127, 0.4); + border-color: var(--primary); +} +.story-preview-link { + display: block; + text-decoration: none; + color: inherit; + position: relative; + background-color: #000; + height: 200px; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; +} +.story-preview { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + border-bottom: 1px solid var(--glass-bg); + loading: lazy; + 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: 700; + margin-bottom: 10px; + color: var(--text-light); + 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); + font-weight: 500; + text-decoration: none; + margin-right: 5px; + transition: color var(--transition); +} +.uploader-link:hover { + color: var(--primary); + text-decoration: underline; +} +.story-item p.stats { + font-size: 0.85em; + 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: 4px; +} +.aducoin-display { + display: inline-flex; + align-items: center; + gap: 7px; + background-color: rgba(255, 215, 0, 0.15); + padding: 5px 10px; + border-radius: 20px; + border: 1px solid var(--aducoin-color); + color: var(--aducoin-color); + font-weight: bold; + font-size: 1em; + margin-left: 15px; + vertical-align: middle; + text-decoration: none; + cursor: pointer; + transition: var(--transition); + box-shadow: 0 0 5px rgba(255, 215, 0, 0.2); +} +.aducoin-display:hover { + background-color: rgba(255, 215, 0, 0.3); + box-shadow: 0 0 12px var(--aducoin-color); + transform: translateY(-2px); +} +.aducoin-icon { + width: 20px; + height: 20px; + vertical-align: middle; + border-radius: 50%; +} +.aducoin-balance { + vertical-align: middle; +} +label { + display: block; + margin-bottom: 5px; + margin-top: 18px; + font-weight: 500; + color: var(--text-dark); +} +.referral-section, .transfer-section, .donation-section { + margin-top: 30px; + padding: 20px; + background: var(--glass-bg); + border-radius: 12px; + border: 1px solid var(--secondary); + box-shadow: 0 5px 15px rgba(0,0,0,0.3); +} +.referral-section h3, .transfer-section h3, .donation-section h3 { + font-size: 1.2em; + color: var(--secondary); + margin-bottom: 15px; + border-bottom: 1px dashed rgba(138, 43, 226, 0.5); + padding-bottom: 8px; +} +.referral-link-container, .transfer-input-container, .donation-input-container { + display: flex; + gap: 12px; + align-items: center; + margin-top: 15px; +} +.referral-link-input { + flex-grow: 1; + background: rgba(0,0,0,0.3); + color: var(--text-light); + border: 1px solid var(--glass-bg); + padding: 10px 15px; + border-radius: 8px; + font-size: 0.95em; + cursor: text; +} +.copy-btn, .transfer-btn, .donate-btn { + padding: 10px 20px; + font-size: 0.9em; + text-transform: none; + letter-spacing: normal; + flex-shrink: 0; +} +.transfer-input, .donate-input { + flex-grow: 1; + margin: 0; + padding: 10px 15px; + font-size: 0.95em; +} +.transfer-info, .donation-info { + font-size: 0.9em; + color: var(--text-dark); + margin-top: 8px; +} +.promote-btn { + background: var(--accent); + color: var(--background-dark); + font-size: 0.9em; + padding: 8px 15px; + margin-top: 12px; + width: 100%; + text-transform: none; + letter-spacing: normal; + box-shadow: 0 3px 10px rgba(255, 215, 0, 0.3); +} +.promote-btn:hover { + background: #e6b800; +} +.promoted-info { + font-size: 0.85em; + color: var(--accent); + margin-top: 10px; + display: block; + text-align: center; + font-style: italic; + font-weight: bold; + text-shadow: 0 0 5px rgba(255, 215, 0, 0.3); +} +.transaction-table { + width: 100%; + border-collapse: collapse; + margin-top: 25px; +} +.transaction-table th, .transaction-table td { + border: 1px solid var(--glass-bg); + padding: 12px 15px; + text-align: left; + font-size: 0.95em; +} +.transaction-table th { + background-color: rgba(255, 0, 127, 0.2); + color: var(--primary); + font-weight: 600; +} +.transaction-table td { color: var(--text-dark); } +.transaction-table td.amount-positive { color: var(--online); font-weight: bold;} +.transaction-table td.amount-negative { color: var(--offline); font-weight: bold;} +.transaction-table td.desc { white-space: normal; word-break: break-word; } +.transaction-table tr:nth-child(even) { background-color: rgba(0,0,0,0.1); } +.transaction-table tr:hover { background-color: rgba(255, 255, 255, 0.07); } + +.conversation-list { list-style: none; padding: 0; } +.conversation-item { display: flex; align-items: center; gap: 18px; padding: 18px; background: var(--card-bg); border-radius: 12px; margin-bottom: 15px; transition: var(--transition); border: 1px solid transparent; text-decoration: none; color: inherit; box-shadow: 0 4px 10px rgba(0,0,0,0.3);} +.conversation-item:hover { background: var(--glass-bg); border-color: var(--secondary); transform: translateX(5px); box-shadow: 0 6px 15px rgba(138, 43, 226, 0.3); } +.conversation-item.unread { border-left: 5px solid var(--accent); font-weight: bold; background: rgba(255, 215, 0, 0.1); box-shadow: 0 4px 15px rgba(255, 215, 0, 0.2);} +.conversation-avatar { width: 50px; height: 50px; border-radius: 50%; object-fit: cover; flex-shrink: 0; border: 3px solid var(--secondary); } +.conversation-avatar-placeholder { width: 50px; height: 50px; border-radius: 50%; background: linear-gradient(45deg, var(--primary), var(--secondary)); display: flex; align-items: center; justify-content: center; font-size: 24px; color: white; font-weight: bold; flex-shrink: 0;box-shadow: 0 0 10px rgba(138, 43, 226, 0.5);} +.conversation-details { flex-grow: 1; overflow: hidden; } +.conversation-username { font-weight: 600; color: var(--primary); display: block; margin-bottom: 5px; 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: var(--card-bg-dark); 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; line-height: 1.4; word-wrap: break-word; position: relative; font-size: 1em; } +.message-bubble.sent { background: var(--message-sent-bg); border-bottom-right-radius: 8px; align-self: flex-end; color: var(--text-light); box-shadow: 0 3px 8px rgba(138, 43, 226, 0.2);} +.message-bubble.received { background: var(--message-received-bg); border-bottom-left-radius: 8px; align-self: flex-start; color: var(--text-light); box-shadow: 0 3px 8px rgba(0,0,0,0.4);} +.message-bubble .sender-name { font-size: 0.8em; font-weight: bold; color: var(--primary); display: block; margin-bottom: 5px; } +.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; } + + +@keyframes zoomIn { + from { opacity: 0; transform: scale(0.7); } + to { opacity: 1; transform: scale(1); } +} +@media (max-width: 900px) { + .sidebar { + transform: translateX(-100%); + width: 260px; + } + .sidebar.active { + transform: translateX(0); + box-shadow: 5px 0 20px rgba(0, 0, 0, 0.8); + } + .menu-btn { + display: block; + } + .container { + margin: 80px 18px 18px 18px; + max-width: calc(100% - 36px); + padding: 18px; + } + body:not(.sidebar-active-on-pc) .container, + body.sidebar-active-on-pc .container { + margin-left: auto; + margin-right: auto; + } + + .theme-toggle { + top: 80px; + } + .story-grid { + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 20px; + } + .story-preview-link { height: 180px; } + .transaction-table th, .transaction-table td { font-size: 0.9em; padding: 10px;} + .user-grid { grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); } +} +@media (max-width: 480px) { + .nav-brand { font-size: 1.6em; } + .nav-link { font-size: 1.05em; padding: 12px 18px; } + .btn { padding: 12px 25px; font-size: 0.95em; } + input, textarea, select { padding: 12px 15px; font-size: 1em;} + .story-grid { + grid-template-columns: 1fr; + gap: 20px; + } + .story-item h3 { font-size: 1.1em; } + .story-preview-link { height: 220px; } + .aducoin-display { font-size: 0.95em; padding: 4px 8px;} + .aducoin-icon { width: 18px; height: 18px; } + .referral-link-container, .transfer-input-container, .donation-input-container { flex-direction: column; align-items: stretch;} + .copy-btn, .transfer-btn, .donate-btn { width: 100%; } + .transfer-input, .donate-input { width: 100%; margin-bottom: 12px; } + .transaction-table { font-size: 0.85em; } + .transaction-table th, .transaction-table td { font-size: 0.85em; padding: 8px;} + .user-grid { grid-template-columns: 1fr; } + .conversation-item { flex-direction: column; align-items: flex-start; text-align: left; gap: 10px; padding: 15px;} + .conversation-timestamp { align-self: flex-end; margin-left: 0; } + .message-bubble { max-width: 95%; padding: 10px 15px; } + .message-bubble .timestamp { margin-top: 5px; } +} +''' + +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 - BBC QoS hub + + + + + + ''' + NAV_HTML + ''' + +
+

Join the Adusis Family

+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message | escape }}
+ {% endfor %} + {% endif %} + {% endwith %} +
+ + + + + + + + {% if referral_code %} +

Registering with referral code: {{ referral_code | escape }}

+ {% endif %} + + +
+ Already joined? Login here +
+ + + +''' + 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 - BBC QoS hub + + + + + + ''' + NAV_HTML + ''' + +
+

Welcome Back

+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message | escape }}
+ {% endfor %} + {% endif %} + {% endwith %} +
+ + + +
+ New here? Register an account +
+ + + +''' + 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 - BBC QoS hub + + + + + + ''' + 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!

+

Promote your own stories from your profile to feature them here.

+
+ {% else %} +
+

No stories yet!

+

Be the first one to upload something hot.

+
+ {% endif %} + {% else %} +
+ {% for story in stories %} +
+ + {% if story.is_currently_promoted %} + + {% endif %} + {% set media_url = "https://huggingface.co/datasets/" + repo_id + "/resolve/main/" + story['type'] + "s/" + story['filename'] %} + {% if story['type'] == 'video' %} + + {% else %} + {{ story['title'] | escape }} + {% endif %} + +
+ +
+

+ ❤️ {{ story.get('likes', [])|length }} + 👁️ {{ story.get('views', 0) }} + 🍆 {{ story.get('jerked_off_count', 0) }} + 💬 {{ story.get('comments', [])|length }} +

+

{{ story['upload_date'] }}

+
+
+
+ {% 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 - BBC QoS hub + + + + + + ''' + NAV_HTML + ''' + +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message | escape }}
+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+ {% if avatar %} + Avatar + {% else %} +
{{ username[0]|upper }}
+ {% endif %} +
+
+

+ {{ username | escape }} + + + AduCoin + {{ aducoin }} + +

+

Role: {{ user_role|title if user_role else 'Not Set' }}

+

Bio: {{ bio | escape if bio else 'No bio yet.' }}

+ {% if link %} +

Link: {{ link | escape }}

+ {% else %} +

Link: None

+ {% endif %} +

Last Seen: {{ last_seen }}

+
+
+ +
+

Edit Profile

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

Your Referral Link

+

Share this link! Earn 100 AduCoin for each referred Girl, Couple, or Bull, and 50 AduCoin for each Boy.

+ +
+ + +

Your Stories

+
+ {% for story in user_stories %} +
+ + {% set media_url = "https://huggingface.co/datasets/" + repo_id + "/resolve/main/" + story['type'] + "s/" + story['filename'] %} + {% if story['type'] == 'video' %} + + {% else %} + {{ story['title'] | escape }} + {% endif %} + +
+
+

{{ story['title'] | escape }}

+ {% if story.description %} +

{{ story.description | escape }}

+ {% endif %} +
+
+

+ ❤️ {{ story.get('likes', [])|length }} + 👁️ {{ story.get('views', 0) }} + 🍆 {{ story.get('jerked_off_count', 0) }} + 💬 {{ story.get('comments', [])|length }} +

+

{{ story['upload_date'] }}

+ + {% if story.is_currently_promoted %} + + {% 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 - BBC QoS hub + + + + + + ''' + 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 %} + + {% endif %} +

+ +
+ {% set media_url = "https://huggingface.co/datasets/" + repo_id + "/resolve/main/" + story['type'] + "s/" + story['filename'] %} + {% if story['type'] == 'video' %} + + {% else %} + {{ story['title'] | escape }} + {% endif %} +
+ +
+

Description: {{ story['description'] | escape if story['description'] else 'No description provided.'}}

+

Uploaded by: {{ story['uploader'] | escape }}

+

Uploaded on: {{ story['upload_date'] }}

+
+ +
+ {% if is_authenticated %} + + {% else %} +
+ ❤️ Likes {{ story.get('likes', []) | length }} +
+ {% endif %} + +
+ 👁️ Views {{ story.get('views', 0) }} +
+
+ + {% if can_donate %} +
+

Support {{ story.uploader | escape }}! Send AduCoin

+
+
+ + +
+

Your balance: {{ sender_aducoin }} AduCoin

+
+
+ {% endif %} + + +
+

Comments ({{ story.get('comments', []) | length }})

+ {% if is_authenticated %} +
+
+ + + +
+
+
+ {% else %} +

Login to post a comment.

+ {% endif %} + +
+ {% if story.get('comments') %} + {% for comment in story['comments'] | reverse %} +
+
+ {{ comment['username'] | escape }} + {{ comment['timestamp'] }} +
+
+ {{ comment['text'] | escape }} +
+
+ {% endfor %} + {% else %} +

Be the first to comment!

+ {% 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 - BBC QoS hub + + + + + + ''' + NAV_HTML + ''' + +
+

Upload Your Story

+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message | safe }}
+ {% endfor %} + {% endif %} + {% endwith %} +
+ + + + + + + + + Allowed: JPG, PNG, GIF, MP4, MOV, AVI, WEBM (Max 100MB) + + +
+
+ + + +''' + 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 - BBC QoS hub + + + + + + ''' + NAV_HTML + ''' + +
+

Explore Users ({{ user_count }})

+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message | escape }}
+ {% endfor %} + {% endif %} + {% endwith %} +
+
+ + +
+
+
+ {% for user_info in user_list %} + + {% if user_info['avatar'] %} + {{ user_info['name'] | escape }} Avatar + {% else %} +
{{ user_info['name'][0]|upper }}
+ {% endif %} + +
+ {% endfor %} + {% if not user_list and search_query %} +

No users found matching "{{ search_query | escape }}".

+ {% elif not user_list %} +

Looks like it's just you (and maybe the admin)!

+ {% endif %} +
+
+ + + +''' + 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(minutes=10): + last_seen = 'Seen recently' + 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 %H:%M') + elif time_diff < timedelta(days=2): + last_seen = last_seen_dt.strftime('Yesterday %H:%M') + elif time_diff < timedelta(days=7): + last_seen = last_seen_dt.strftime('%a %H:%M') + else: + last_seen = last_seen_dt.strftime('%Y-%m-%d') + else: + last_seen = 'Never' + except ValueError: + last_seen = 'Invalid Date' + + html_content = ''' + + + + + + Profile - {{ profile_username | escape }} - Adusis - BBC QoS hub + + + + + + ''' + NAV_HTML + ''' + +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message | escape }}
+ {% endfor %} + {% endif %} + {% endwith %} + +
+
+ {% if avatar %} + {{ profile_username | escape }} Avatar + {% else %} +
{{ profile_username[0]|upper }}
+ {% endif %} +
+
+

+ {{ profile_username | escape }} + + + AduCoin + {{ aducoin }} + +

+

Role: {{ user_role|title if user_role else 'Not Set' }}

+

Bio: {{ bio | escape if bio else 'No bio provided.' }}

+ {% if link %} +

Link: {{ link | escape }}

+ {% else %} +

Link: None

+ {% endif %} +

Last Seen: {{ last_seen }}

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

Send AduCoin to {{ profile_username | escape }}

+
+
+ + +
+

Your balance: {{ sender_aducoin }} AduCoin

+
+
+ {% endif %} + +

{{ profile_username | escape }}'s Stories

+
+ {% for story in user_stories %} +
+ + {% if story.is_currently_promoted %} + + {% endif %} + {% set media_url = "https://huggingface.co/datasets/" + repo_id + "/resolve/main/" + story['type'] + "s/" + story['filename'] %} + {% if story['type'] == 'video' %} + + {% else %} + {{ story['title'] | escape }} + {% endif %} + +
+
+

{{ story['title'] | escape }}

+ {% if story.description %} +

{{ story.description | escape }}

+ {% endif %} +
+
+

+ ❤️ {{ story.get('likes', [])|length }} + 👁️ {{ story.get('views', 0) }} + 🍆 {{ story.get('jerked_off_count', 0) }} + 💬 {{ story.get('comments', [])|length }} +

+

{{ story['upload_date'] }}

+
+
+
+ {% endfor %} + {% if not user_stories %} +

This user hasn't uploaded any stories yet.

+ {% endif %} +
+
+ + + + + + +''' + 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 - {{ username | escape }} - Adusis - BBC QoS hub + + + + + + ''' + NAV_HTML + ''' + +
+

+ AduCoin AduCoin History +

+

Your current balance: {{ current_aducoin }} AduCoin

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message | escape }}
+ {% endfor %} + {% endif %} + {% endwith %} + + {% if transactions %} +
+ + + + + + + + + + + + {% for tx in transactions %} + + + + + + + + {% endfor %} + +
DateTypeAmountDescriptionBalance After
{{ tx.timestamp }}{{ tx.type.replace('_', ' ')|title }}{{ '%+d'|format(tx.amount) if tx.amount != 0 else tx.amount }}{{ tx.description | escape }}{{ tx.balance_after }}
+
+ {% else %} +

You have no transaction history yet.

+ {% endif %} + + Back to Profile +
+ + + +''' + 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 a user 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 + ''' + +
+
+ {% if other_user_data.avatar %} + {{ other_username | escape }} + {% else %} +
{{ other_username[0]|upper }}
+ {% endif %} +
+

{{ other_username | escape }}

+
+ + {{ 'Online' if other_user_is_online else 'Offline' }} +
+
+ View Profile +
+ + {% 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 %} +
+ +
+ + +
+ + Back to Inbox + +
+ + + +''' + 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) +# --- END OF FILE app.py ---