|
|
|
|
|
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 = ''' |
|
|
<aside class="sidebar" id="sidebar"> |
|
|
<div class="sidebar-header"> |
|
|
<img src="{{ logo_url }}" alt="Adusis Logo" class="logo"> |
|
|
<span class="nav-brand">Adusis</span> |
|
|
</div> |
|
|
<nav class="nav-links"> |
|
|
<a href="{{ url_for('feed', mode='latest') }}" class="nav-link {% if request.endpoint == 'feed' and request.view_args.get('mode') == 'latest' %}active{% endif %}"><span>✨</span> Latest</a> |
|
|
<a href="{{ url_for('feed', mode='hot') }}" class="nav-link {% if request.endpoint == 'feed' and request.view_args.get('mode') == 'hot' %}active{% endif %}"><span>🔥</span> Hot</a> |
|
|
{% if is_authenticated %} |
|
|
<a href="{{ url_for('profile') }}" class="nav-link {% if request.endpoint == 'profile' %}active{% endif %}"><span>💖</span> Profile</a> |
|
|
<a href="{{ url_for('upload') }}" class="nav-link {% if request.endpoint == 'upload' %}active{% endif %}"><span>📹</span> Upload Story</a> |
|
|
<a href="{{ url_for('inbox') }}" class="nav-link {% if request.endpoint == 'inbox' or request.endpoint == 'conversation' %}active{% endif %}"> |
|
|
<span>💌</span> Messages {% if unread_count > 0 %}<span class="badge">{{ unread_count }}</span>{% endif %} |
|
|
</a> |
|
|
<a href="{{ url_for('users') }}" class="nav-link {% if request.endpoint == 'users' %}active{% endif %}"><span>👯♀️</span> Users <span class="badge">{{ user_count }}</span></a> |
|
|
<a href="{{ url_for('transactions') }}" class="nav-link {% if request.endpoint == 'transactions' %}active{% endif %}"><span>💎</span> AduCoin</a> |
|
|
<a href="{{ url_for('logout') }}" class="nav-link logout-btn" style="margin-top: auto;"><span>🚪</span> Logout</a> |
|
|
{% else %} |
|
|
<a href="{{ url_for('login') }}" class="nav-link {% if request.endpoint == 'login' %}active{% endif %}"><span>🔑</span> Login</a> |
|
|
<a href="{{ url_for('register') }}" class="nav-link {% if request.endpoint == 'register' %}active{% endif %}"><span>📝</span> Register</a> |
|
|
<a href="{{ url_for('users') }}" class="nav-link {% if request.endpoint == 'users' %}active{% endif %}"><span>👯♀️</span> Users <span class="badge">{{ user_count }}</span></a> |
|
|
{% endif %} |
|
|
</nav> |
|
|
</aside> |
|
|
''' |
|
|
|
|
|
@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 = ''' |
|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Register - Adusis</title> |
|
|
<link rel="preconnect" href="https://fonts.googleapis.com"> |
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> |
|
|
<style> |
|
|
''' + BASE_STYLE + ''' |
|
|
.form-container { |
|
|
max-width: 480px; |
|
|
background: var(--glass-bg); |
|
|
padding: 40px; |
|
|
border-radius: 16px; |
|
|
border: 1px solid var(--glass-border); |
|
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); |
|
|
backdrop-filter: blur(10px); |
|
|
} |
|
|
h1 { |
|
|
font-size: 2.2em; font-weight: 700; text-align: center; |
|
|
margin-bottom: 30px; color: var(--primary-color); |
|
|
text-shadow: 0 0 10px var(--primary-color); |
|
|
} |
|
|
.link { |
|
|
display: block; text-align: center; margin-top: 25px; |
|
|
color: var(--secondary-color); font-size: 1em; text-decoration: none; |
|
|
font-weight: 500; transition: var(--transition-fast); |
|
|
} |
|
|
.link:hover { color: var(--primary-color); } |
|
|
label { display: block; margin-bottom: 5px; margin-top: 18px; font-weight: 500; color: var(--text-dark); } |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<button class="menu-btn" onclick="toggleSidebar()">☰</button> |
|
|
''' + NAV_HTML + ''' |
|
|
<div class="container animated" style="display: flex; justify-content: center; align-items: center; min-height: 100vh; padding: 20px;"> |
|
|
<div class="form-container"> |
|
|
<h1>Join Adusis</h1> |
|
|
{% with messages = get_flashed_messages(with_categories=true) %} |
|
|
{% if messages %} |
|
|
{% for category, message in messages %} |
|
|
<div class="flash {{ category }}">{{ message | escape }}</div> |
|
|
{% endfor %} |
|
|
{% endif %} |
|
|
{% endwith %} |
|
|
<form method="POST"> |
|
|
<input type="text" name="username" placeholder="Choose a Username" required minlength="3" value="{{ request.form.get('username', '') | escape }}"> |
|
|
<input type="password" name="password" placeholder="Create a Password" required minlength="6"> |
|
|
<label for="role">Select Your Role</label> |
|
|
<select id="role" name="role" required> |
|
|
<option value="" disabled selected>-- Who are you? --</option> |
|
|
{% for role_option in user_roles %} |
|
|
<option value="{{ role_option }}" {% if request.form.get('role') == role_option %}selected{% endif %}>{{ role_option|title }}</option> |
|
|
{% endfor %} |
|
|
</select> |
|
|
<input type="hidden" name="referral_code" value="{{ referral_code | escape }}"> |
|
|
{% if referral_code %} |
|
|
<p style="font-size: 0.9em; color: var(--text-dark); text-align: center; margin-top: 15px;">Using referral code: <strong>{{ referral_code | escape }}</strong></p> |
|
|
{% endif %} |
|
|
<button type="submit" class="btn" style="width: 100%; margin-top: 25px;">Register</button> |
|
|
</form> |
|
|
<a href="{{ url_for('login') }}" class="link">Already a member? Login</a> |
|
|
</div> |
|
|
</div> |
|
|
<script> |
|
|
function toggleSidebar() { |
|
|
const sidebar = document.getElementById('sidebar'); |
|
|
const isActive = sidebar.classList.toggle('active'); |
|
|
sidebar.classList.toggle('hidden', !isActive); |
|
|
if (window.innerWidth > 900) document.body.classList.toggle('sidebar-active-on-pc', isActive); |
|
|
} |
|
|
function initializeUIState() { |
|
|
const sidebar = document.getElementById('sidebar'); |
|
|
let isActive = window.innerWidth > 900; |
|
|
sidebar.classList.toggle('active', isActive); |
|
|
sidebar.classList.toggle('hidden', !isActive); |
|
|
document.body.classList.toggle('sidebar-active-on-pc', isActive); |
|
|
} |
|
|
window.onload = initializeUIState; |
|
|
window.onresize = initializeUIState; |
|
|
</script> |
|
|
</body> |
|
|
</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 = ''' |
|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Login - Adusis</title> |
|
|
<link rel="preconnect" href="https://fonts.googleapis.com"> |
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> |
|
|
<style> |
|
|
''' + BASE_STYLE + ''' |
|
|
.form-container { |
|
|
max-width: 480px; |
|
|
background: var(--glass-bg); |
|
|
padding: 40px; |
|
|
border-radius: 16px; |
|
|
border: 1px solid var(--glass-border); |
|
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); |
|
|
backdrop-filter: blur(10px); |
|
|
} |
|
|
h1 { |
|
|
font-size: 2.2em; font-weight: 700; text-align: center; |
|
|
margin-bottom: 30px; color: var(--primary-color); |
|
|
text-shadow: 0 0 10px var(--primary-color); |
|
|
} |
|
|
.link { |
|
|
display: block; text-align: center; margin-top: 25px; |
|
|
color: var(--secondary-color); font-size: 1em; text-decoration: none; |
|
|
font-weight: 500; transition: var(--transition-fast); |
|
|
} |
|
|
.link:hover { color: var(--primary-color); } |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<button class="menu-btn" onclick="toggleSidebar()">☰</button> |
|
|
''' + NAV_HTML + ''' |
|
|
<div class="container animated" style="display: flex; justify-content: center; align-items: center; min-height: 100vh; padding: 20px;"> |
|
|
<div class="form-container"> |
|
|
<h1>Welcome Back</h1> |
|
|
{% with messages = get_flashed_messages(with_categories=true) %} |
|
|
{% if messages %} |
|
|
{% for category, message in messages %} |
|
|
<div class="flash {{ category }}">{{ message | escape }}</div> |
|
|
{% endfor %} |
|
|
{% endif %} |
|
|
{% endwith %} |
|
|
<form method="POST"> |
|
|
<input type="text" name="username" placeholder="Username" required> |
|
|
<input type="password" name="password" placeholder="Password" required> |
|
|
<button type="submit" class="btn" style="width: 100%; margin-top: 15px;">Login</button> |
|
|
</form> |
|
|
<a href="{{ url_for('register') }}" class="link">New here? Create an account</a> |
|
|
</div> |
|
|
</div> |
|
|
<script> |
|
|
function toggleSidebar() { |
|
|
const sidebar = document.getElementById('sidebar'); |
|
|
const isActive = sidebar.classList.toggle('active'); |
|
|
sidebar.classList.toggle('hidden', !isActive); |
|
|
if (window.innerWidth > 900) document.body.classList.toggle('sidebar-active-on-pc', isActive); |
|
|
} |
|
|
function initializeUIState() { |
|
|
const sidebar = document.getElementById('sidebar'); |
|
|
let isActive = window.innerWidth > 900; |
|
|
sidebar.classList.toggle('active', isActive); |
|
|
sidebar.classList.toggle('hidden', !isActive); |
|
|
document.body.classList.toggle('sidebar-active-on-pc', isActive); |
|
|
} |
|
|
window.onload = initializeUIState; |
|
|
window.onresize = initializeUIState; |
|
|
</script> |
|
|
</body> |
|
|
</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/<mode>') |
|
|
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 = ''' |
|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>{{ page_title }} - Adusis</title> |
|
|
<link rel="preconnect" href="https://fonts.googleapis.com"> |
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> |
|
|
<style> |
|
|
''' + BASE_STYLE + ''' |
|
|
h1 { |
|
|
font-size: 2.5em; font-weight: 700; margin-bottom: 30px; |
|
|
color: var(--text-light); border-bottom: 1px solid var(--glass-border); |
|
|
padding-bottom: 15px; |
|
|
} |
|
|
.no-stories { |
|
|
text-align: center; color: var(--text-dark); padding: 50px; |
|
|
font-size: 1.1em; |
|
|
} |
|
|
.no-stories a { color: var(--primary-color); font-weight: bold; } |
|
|
.promoted-badge { |
|
|
position: absolute; top: 15px; right: 15px; |
|
|
background: hsla(var(--accent-hue), 100%, 50%, 0.8); |
|
|
color: var(--bg-dark); padding: 5px 12px; border-radius: 8px; |
|
|
font-size: 0.85em; font-weight: bold; backdrop-filter: blur(5px); |
|
|
z-index: 5; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<button class="menu-btn" onclick="toggleSidebar()">☰</button> |
|
|
''' + NAV_HTML + ''' |
|
|
<div class="container animated"> |
|
|
<h1>{{ page_title }}</h1> |
|
|
{% with messages = get_flashed_messages(with_categories=true) %} |
|
|
{% if messages %} |
|
|
{% for category, message in messages %} |
|
|
<div class="flash {{ category }}">{{ message | escape }}</div> |
|
|
{% endfor %} |
|
|
{% endif %} |
|
|
{% endwith %} |
|
|
|
|
|
{% if not stories %} |
|
|
<div class="no-stories"> |
|
|
<h2>{% if mode == 'hot' %}No hot stories right now!{% else %}No stories yet!{% endif %}</h2> |
|
|
<p>{% if mode == 'hot' %}Promote a story from your profile to feature it here.{% else %}Be the first one to <a href="{{ url_for('upload') }}">upload</a> something.{% endif %}</p> |
|
|
</div> |
|
|
{% else %} |
|
|
<div class="story-grid"> |
|
|
{% for story in stories %} |
|
|
<div class="story-item"> |
|
|
<a href="{{ url_for('story_page', story_id=story['id']) }}" class="story-preview-link"> |
|
|
{% if story.is_currently_promoted %} |
|
|
<span class="promoted-badge">🔥 HOT</span> |
|
|
{% endif %} |
|
|
{% set media_url = "https://huggingface.co/datasets/" + repo_id + "/resolve/main/" + story['type'] + "s/" + story['filename'] %} |
|
|
{% if story['type'] == 'video' %} |
|
|
<video class="story-preview" preload="metadata" muted loading="lazy"> |
|
|
<source src="{{ media_url }}" type="video/mp4"> |
|
|
</video> |
|
|
{% else %} |
|
|
<img class="story-preview" src="{{ media_url }}" alt="{{ story['title'] | escape }}" loading="lazy"> |
|
|
{% endif %} |
|
|
</a> |
|
|
<div class="story-item-info"> |
|
|
<div> |
|
|
<h3><a href="{{ url_for('story_page', story_id=story['id']) }}" style="color: inherit; text-decoration: none;">{{ story['title'] | escape }}</a></h3> |
|
|
<p class="uploader"> |
|
|
By: <a href="{{ url_for('user_profile', username=story['uploader']) }}" class="uploader-link">{{ story['uploader'] | escape }}</a> |
|
|
<span class="status-dot {{ 'online' if is_user_online(story['uploader']) else 'offline' }}"></span> |
|
|
</p> |
|
|
</div> |
|
|
<div> |
|
|
<p class="stats"> |
|
|
<span>❤️ {{ story.get('likes', [])|length }}</span> |
|
|
<span>👁️ {{ story.get('views', 0) }}</span> |
|
|
<span>🍆 {{ story.get('jerked_off_count', 0) }}</span> |
|
|
<span>💬 {{ story.get('comments', [])|length }}</span> |
|
|
</p> |
|
|
<p style="font-size: 0.8em; color: var(--text-dark); margin-top: 5px;">{{ story['upload_date'] }}</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
{% endfor %} |
|
|
</div> |
|
|
{% endif %} |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
function toggleSidebar() { |
|
|
const sidebar = document.getElementById('sidebar'); |
|
|
const isActive = sidebar.classList.toggle('active'); |
|
|
sidebar.classList.toggle('hidden', !isActive); |
|
|
if (window.innerWidth > 900) document.body.classList.toggle('sidebar-active-on-pc', isActive); |
|
|
} |
|
|
function initializeUIState() { |
|
|
const sidebar = document.getElementById('sidebar'); |
|
|
let isActive = window.innerWidth > 900; |
|
|
sidebar.classList.toggle('active', isActive); |
|
|
sidebar.classList.toggle('hidden', !isActive); |
|
|
document.body.classList.toggle('sidebar-active-on-pc', isActive); |
|
|
} |
|
|
function initializeVideoPreviews() { |
|
|
document.querySelectorAll('video.story-preview').forEach(video => { |
|
|
const item = video.closest('.story-item'); |
|
|
if (item) { |
|
|
item.addEventListener('mouseenter', () => video.play().catch(e => {})); |
|
|
item.addEventListener('mouseleave', () => { video.pause(); video.currentTime = 0; }); |
|
|
} |
|
|
}); |
|
|
} |
|
|
window.onload = () => { |
|
|
initializeUIState(); |
|
|
initializeVideoPreviews(); |
|
|
}; |
|
|
window.onresize = initializeUIState; |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
''' |
|
|
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 = ''' |
|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Profile - {{ username | escape }} - Adusis</title> |
|
|
<link rel="preconnect" href="https://fonts.googleapis.com"> |
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> |
|
|
<style> |
|
|
''' + BASE_STYLE + ''' |
|
|
.container { max-width: 1100px; } |
|
|
.profile-header { |
|
|
display: flex; flex-wrap: wrap; align-items: center; gap: 30px; |
|
|
background: var(--glass-bg); padding: 30px; border-radius: 16px; |
|
|
border: 1px solid var(--glass-border); margin-bottom: 30px; |
|
|
} |
|
|
.avatar { |
|
|
width: 140px; height: 140px; border-radius: 50%; object-fit: cover; |
|
|
border: 4px solid var(--primary-color); box-shadow: var(--glow-primary); |
|
|
transition: var(--transition-medium); cursor: pointer; |
|
|
} |
|
|
.avatar:hover { transform: scale(1.05); } |
|
|
.avatar-placeholder { |
|
|
width: 140px; height: 140px; border-radius: 50%; |
|
|
background: linear-gradient(45deg, var(--primary-color), var(--secondary-color)); |
|
|
display: flex; align-items: center; justify-content: center; |
|
|
font-size: 60px; color: white; font-weight: bold; |
|
|
} |
|
|
.profile-info { flex-grow: 1; } |
|
|
.profile-info h1 { |
|
|
font-size: 2.4em; font-weight: 700; color: var(--text-light); |
|
|
margin-bottom: 10px; display: flex; align-items: center; flex-wrap: wrap; gap: 15px; |
|
|
} |
|
|
.profile-info p { margin: 8px 0; font-size: 1.05em; color: var(--text-dark); word-wrap: break-word; } |
|
|
.profile-info p strong { color: var(--text-light); } |
|
|
.profile-info a { color: var(--secondary-color); text-decoration: none; font-weight: 500; } |
|
|
.profile-info a:hover { color: var(--primary-color); text-decoration: underline; } |
|
|
.section-title { |
|
|
font-size: 2em; color: var(--text-light); margin-top: 40px; margin-bottom: 25px; |
|
|
border-bottom: 1px solid var(--glass-border); padding-bottom: 15px; |
|
|
} |
|
|
.delete-btn { background: #d32f2f; box-shadow: 0 4px 15px -5px hsla(0, 70%, 50%, 0.6); } |
|
|
.delete-btn:hover { background: #b71c1c; box-shadow: 0 7px 20px -5px hsla(0, 70%, 50%, 0.8); } |
|
|
.promoted-info { font-size: 0.9em; color: var(--accent-color); margin-top: 15px; display: block; text-align: center; font-weight: bold; text-shadow: 0 0 5px var(--accent-color); } |
|
|
.promote-btn { margin-top: 15px; width: 100%; } |
|
|
.story-item .btn { font-size: 0.9em; padding: 10px 20px; } |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<button class="menu-btn" onclick="toggleSidebar()">☰</button> |
|
|
''' + NAV_HTML + ''' |
|
|
<div class="container animated"> |
|
|
{% with messages = get_flashed_messages(with_categories=true) %} |
|
|
{% if messages %} |
|
|
{% for category, message in messages %} |
|
|
<div class="flash {{ category }}">{{ message | escape }}</div> |
|
|
{% endfor %} |
|
|
{% endif %} |
|
|
{% endwith %} |
|
|
|
|
|
<div class="profile-header"> |
|
|
{% if avatar %} |
|
|
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ avatar }}?rand={{ random.randint(1,1000) }}" alt="Avatar" class="avatar" loading="lazy" onclick="openModal(this.src)"> |
|
|
{% else %} |
|
|
<div class="avatar-placeholder">{{ username[0]|upper }}</div> |
|
|
{% endif %} |
|
|
<div class="profile-info"> |
|
|
<h1> |
|
|
<span>{{ username | escape }}</span> |
|
|
<span class="status-dot {{ 'online' if is_online else 'offline' }}"></span> |
|
|
<a href="{{ url_for('transactions') }}" class="aducoin-display" title="View Transaction History"> |
|
|
<img src="{{ logo_url }}" alt="AduCoin" class="aducoin-icon"> |
|
|
<span class="aducoin-balance">{{ aducoin }}</span> |
|
|
</a> |
|
|
</h1> |
|
|
<p><strong>Role:</strong> {{ user_role|title if user_role else 'Not Set' }}</p> |
|
|
<p><strong>Bio:</strong> {{ bio | escape if bio else 'No bio yet.' }}</p> |
|
|
{% if link %} |
|
|
<p><strong>Link:</strong> <a href="{{ link if link.startswith('http') else '//' + link }}" target="_blank" rel="noopener noreferrer">{{ link | escape }}</a></p> |
|
|
{% endif %} |
|
|
<p><small>Last Seen: {{ last_seen }}</small></p> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="form-section"> |
|
|
<h3 style="margin-top:0; color: var(--primary-color);">Edit Profile</h3> |
|
|
<form method="POST" enctype="multipart/form-data"> |
|
|
<input type="hidden" name="update_profile" value="1"> |
|
|
<select name="role" required> |
|
|
<option value="" disabled {% if not user_role %}selected{% endif %}>-- Select Your Role --</option> |
|
|
{% for role_option in user_roles %} |
|
|
<option value="{{ role_option }}" {% if user_role == role_option %}selected{% endif %}>{{ role_option|title }}</option> |
|
|
{% endfor %} |
|
|
</select> |
|
|
<textarea name="bio" placeholder="Tell us about yourself (max 500 chars)..." maxlength="500">{{ bio | escape }}</textarea> |
|
|
<input type="text" name="link" placeholder="https://yourlink.com (max 200 chars)" value="{{ link | escape }}" maxlength="200"> |
|
|
<label for="avatar" style="color: var(--text-dark); font-size: 0.9em; margin-top: 15px;">Change Avatar (JPG, PNG, GIF):</label> |
|
|
<input id="avatar" type="file" name="avatar" accept="image/png, image/jpeg, image/gif"> |
|
|
<button type="submit" class="btn btn-secondary" style="margin-top: 20px;">Save Changes</button> |
|
|
</form> |
|
|
</div> |
|
|
|
|
|
<div class="form-section"> |
|
|
<h3 style="color: var(--accent-color);">Your Referral Link</h3> |
|
|
<p class="info-text">Share this link! Earn <strong>100 AduCoin</strong> for each Girl, Couple, or Bull, and <strong>50 AduCoin</strong> for each Boy.</p> |
|
|
<div class="form-input-container"> |
|
|
<input type="text" id="referralLinkInput" value="{{ referral_link | escape }}" readonly> |
|
|
<button class="btn copy-btn btn-accent" onclick="copyReferralLink()">Copy</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<h2 class="section-title">Your Stories</h2> |
|
|
<div class="story-grid"> |
|
|
{% for story in user_stories %} |
|
|
<div class="story-item"> |
|
|
<a href="{{ url_for('story_page', story_id=story['id']) }}" class="story-preview-link"> |
|
|
{% set media_url = "https://huggingface.co/datasets/" + repo_id + "/resolve/main/" + story['type'] + "s/" + story['filename'] %} |
|
|
<video class="story-preview" preload="metadata" muted loading="lazy" loop> <source src="{{ media_url }}"></video> |
|
|
</a> |
|
|
<div class="story-item-info"> |
|
|
<div> |
|
|
<h3><a href="{{ url_for('story_page', story_id=story['id']) }}" style="color: inherit; text-decoration: none;">{{ story['title'] | escape }}</a></h3> |
|
|
</div> |
|
|
<div> |
|
|
<p class="stats"> |
|
|
<span>❤️ {{ story.get('likes', [])|length }}</span> |
|
|
<span>👁️ {{ story.get('views', 0) }}</span> |
|
|
<span>🍆 {{ story.get('jerked_off_count', 0) }}</span> |
|
|
<span>💬 {{ story.get('comments', [])|length }}</span> |
|
|
</p> |
|
|
{% if story.is_currently_promoted %} |
|
|
<span class="promoted-info">🔥 Promoted until {{ story.promotion_expiry_formatted }}</span> |
|
|
{% else %} |
|
|
<form method="POST" action="{{ url_for('promote_story', story_id=story.id) }}" onsubmit="return confirm('Promote for {{ promote_cost }} AduCoin for {{ promote_duration_days }} days?');"> |
|
|
<button type="submit" class="btn promote-btn btn-accent">Promote ({{ promote_cost }} 🪙)</button> |
|
|
</form> |
|
|
{% endif %} |
|
|
<form method="POST" onsubmit="return confirm('Delete this story forever?');" style="margin-top:10px;"> |
|
|
<input type="hidden" name="story_id" value="{{ story['id'] }}"> |
|
|
<button type="submit" name="delete_story" class="btn delete-btn" style="width: 100%;">Delete</button> |
|
|
</form> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
{% endfor %} |
|
|
{% if not user_stories %} |
|
|
<p style="color: var(--text-dark); grid-column: 1 / -1; text-align: center; padding: 20px;">You haven't uploaded any stories yet. <a href="{{ url_for('upload') }}" style="color: var(--primary-color);">Upload one now!</a></p> |
|
|
{% endif %} |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="modal" id="imageModal" onclick="closeModal(event)"><img id="modalImage" src=""></div> |
|
|
|
|
|
<script> |
|
|
function toggleSidebar() { |
|
|
const sidebar = document.getElementById('sidebar'); |
|
|
const isActive = sidebar.classList.toggle('active'); |
|
|
sidebar.classList.toggle('hidden', !isActive); |
|
|
if (window.innerWidth > 900) document.body.classList.toggle('sidebar-active-on-pc', isActive); |
|
|
} |
|
|
function openModal(src) { |
|
|
document.getElementById('modalImage').src = src; |
|
|
document.getElementById('imageModal').style.display = 'flex'; |
|
|
} |
|
|
function closeModal(event) { |
|
|
if (event.target.id === 'imageModal') { |
|
|
document.getElementById('imageModal').style.display = 'none'; |
|
|
} |
|
|
} |
|
|
function copyReferralLink() { |
|
|
const linkInput = document.getElementById('referralLinkInput'); |
|
|
navigator.clipboard.writeText(linkInput.value).then(() => alert('Referral link copied!')); |
|
|
} |
|
|
function initializeUIState() { |
|
|
const sidebar = document.getElementById('sidebar'); |
|
|
let isActive = window.innerWidth > 900; |
|
|
sidebar.classList.toggle('active', isActive); |
|
|
sidebar.classList.toggle('hidden', !isActive); |
|
|
document.body.classList.toggle('sidebar-active-on-pc', isActive); |
|
|
} |
|
|
function initializeVideoPreviews() { |
|
|
document.querySelectorAll('video.story-preview').forEach(video => { |
|
|
const item = video.closest('.story-item'); |
|
|
if(item) { |
|
|
item.addEventListener('mouseenter', () => video.play().catch(e => {})); |
|
|
item.addEventListener('mouseleave', () => { video.pause(); video.currentTime = 0; }); |
|
|
} |
|
|
}); |
|
|
} |
|
|
window.onload = () => { |
|
|
initializeUIState(); |
|
|
initializeVideoPreviews(); |
|
|
}; |
|
|
window.onresize = initializeUIState; |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
''' |
|
|
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/<story_id>', 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/<story_id>', 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 = ''' |
|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>{{ story['title'] | escape }} - Adusis</title> |
|
|
<link rel="preconnect" href="https://fonts.googleapis.com"> |
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> |
|
|
<style> |
|
|
''' + BASE_STYLE + ''' |
|
|
.story-container { |
|
|
max-width: 950px; background: var(--glass-bg); padding: 30px; |
|
|
border-radius: 16px; border: 1px solid var(--glass-border); |
|
|
} |
|
|
h1.story-title { |
|
|
font-size: 2.2em; font-weight: 700; margin-bottom: 25px; |
|
|
color: var(--text-light); border-bottom: 1px solid var(--glass-border); |
|
|
padding-bottom: 15px; word-break: break-word; |
|
|
display: flex; align-items: center; gap: 15px; |
|
|
} |
|
|
.promoted-tag { |
|
|
background: var(--accent-color); color: var(--bg-dark); |
|
|
padding: 6px 15px; border-radius: 20px; font-size: 0.7em; |
|
|
font-weight: bold; text-transform: uppercase; |
|
|
} |
|
|
.media-container { margin-bottom: 30px; background-color: #000; border-radius: 15px; overflow: hidden; } |
|
|
.media-container video, .media-container img { width: 100%; max-height: 85vh; object-fit: contain; display: block; } |
|
|
.story-details p { font-size: 1.1em; margin-bottom: 15px; color: var(--text-dark); word-wrap: break-word; } |
|
|
.story-actions { margin: 30px 0; display: flex; gap: 15px; align-items: center; flex-wrap: wrap; padding-bottom: 25px; border-bottom: 1px solid var(--glass-border); } |
|
|
.action-btn { padding: 12px 20px; font-size: 1em; min-width: 130px; border-radius: 25px; } |
|
|
.action-btn.liked { background: var(--primary-color); box-shadow: var(--glow-primary); } |
|
|
@keyframes pulse { 50% { transform: scale(1.08); } } |
|
|
.action-btn.pulsing { animation: pulse 0.4s ease; } |
|
|
|
|
|
.comments-section h2 { font-size: 1.8em; color: var(--text-light); margin-bottom: 25px; } |
|
|
.comment { background: rgba(0,0,0,0.2); padding: 18px; border-radius: 10px; margin-bottom: 18px; border-left: 3px solid var(--secondary-color); } |
|
|
.comment-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; font-size: 0.95em; color: var(--text-dark); } |
|
|
.comment-user a { font-weight: bold; color: var(--secondary-color); text-decoration: none; } |
|
|
.comment-text { white-space: pre-wrap; word-wrap: break-word; } |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<button class="menu-btn" onclick="toggleSidebar()">☰</button> |
|
|
''' + NAV_HTML + ''' |
|
|
<div class="container animated"> |
|
|
<div class="story-container"> |
|
|
{% with messages = get_flashed_messages(with_categories=true) %} |
|
|
{% if messages %} |
|
|
{% for category, message in messages %} |
|
|
<div class="flash {{ category }}">{{ message | escape }}</div> |
|
|
{% endfor %} |
|
|
{% endif %} |
|
|
{% endwith %} |
|
|
|
|
|
<h1 class="story-title"> |
|
|
{{ story['title'] | escape }} |
|
|
{% if story.is_currently_promoted %}<span class="promoted-tag">🔥 HOT</span>{% endif %} |
|
|
</h1> |
|
|
|
|
|
<div class="media-container"> |
|
|
{% set media_url = "https://huggingface.co/datasets/" + repo_id + "/resolve/main/" + story['type'] + "s/" + story['filename'] %} |
|
|
{% if story['type'] == 'video' %} |
|
|
<video controls controlsList="nodownload" preload="metadata" loading="lazy"> |
|
|
<source src="{{ media_url }}" type="video/mp4"> |
|
|
</video> |
|
|
{% else %} |
|
|
<img src="{{ media_url }}" alt="{{ story['title'] | escape }}" loading="lazy" onclick="openModal(this.src)" style="cursor: zoom-in;"> |
|
|
{% endif %} |
|
|
</div> |
|
|
|
|
|
<div class="story-details"> |
|
|
<p>{{ story['description'] | escape if story['description'] else 'No description provided.'}}</p> |
|
|
<p>By: <a href="{{ url_for('user_profile', username=story['uploader']) }}" class="uploader-link">{{ story['uploader'] | escape }}</a> <span class="status-dot {{ 'online' if uploader_online else 'offline' }}"></span></p> |
|
|
<p><small>Uploaded on: {{ story['upload_date'] }}</small></p> |
|
|
</div> |
|
|
|
|
|
<div class="story-actions"> |
|
|
<button class="btn action-btn like-btn {% if username in story.get('likes', []) %}liked{% endif %}" onclick="likeStory('{{ story['id'] }}', this)"> |
|
|
❤️ Like <span class="like-count">{{ story.get('likes', []) | length }}</span> |
|
|
</button> |
|
|
<button class="btn action-btn btn-accent jerk-btn" onclick="jerkOff('{{ story['id'] }}', this)"> |
|
|
🍆 Jerked Off <span class="jerk-count">{{ story.get('jerked_off_count', 0) }}</span> |
|
|
</button> |
|
|
<div class="btn action-btn" style="background: var(--glass-bg); cursor: default; box-shadow: none;"> |
|
|
👁️ Views <span class="view-count">{{ story.get('views', 0) }}</span> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
{% if can_donate %} |
|
|
<div class="form-section" style="margin-top:0;"> |
|
|
<h3>Support {{ story.uploader | escape }}!</h3> |
|
|
<form method="POST" action="{{ url_for('donate_story', story_id=story.id) }}"> |
|
|
<div class="form-input-container"> |
|
|
<input type="number" name="amount" placeholder="Amount" min="1" required> |
|
|
<button type="submit" class="btn btn-accent">Donate 🪙</button> |
|
|
</div> |
|
|
<p class="info-text">Your balance: {{ sender_aducoin }} AduCoin</p> |
|
|
</form> |
|
|
</div> |
|
|
{% endif %} |
|
|
|
|
|
<div class="comments-section" id="comments"> |
|
|
<h2>Comments ({{ story.get('comments', []) | length }})</h2> |
|
|
{% if is_authenticated %} |
|
|
<form method="POST"> |
|
|
<input type="hidden" name="add_comment" value="1"> |
|
|
<textarea name="comment_text" placeholder="Add your comment..." required maxlength="1000" rows="3"></textarea> |
|
|
<button type="submit" class="btn btn-secondary">Post Comment</button> |
|
|
</form> |
|
|
{% else %} |
|
|
<p style="text-align: center; color: var(--text-dark);"><a href="{{ url_for('login') }}" style="color: var(--primary-color);">Login</a> to comment.</p> |
|
|
{% endif %} |
|
|
<div class="comments-list" style="margin-top: 30px;"> |
|
|
{% for comment in story['comments'] | reverse %} |
|
|
<div class="comment"> |
|
|
<div class="comment-header"> |
|
|
<span class="comment-user"><a href="{{ url_for('user_profile', username=comment['username']) }}">{{ comment['username'] | escape }}</a></span> |
|
|
<span><small>{{ comment['timestamp'] }}</small></span> |
|
|
</div> |
|
|
<div class="comment-text">{{ comment['text'] | escape }}</div> |
|
|
</div> |
|
|
{% endfor %} |
|
|
</div> |
|
|
</div> |
|
|
<a href="{{ url_for('feed') }}" class="btn" style="margin-top: 30px;">Back to Feed</a> |
|
|
</div> |
|
|
</div> |
|
|
<div class="modal" id="imageModal" onclick="closeModal(event)"><img id="modalImage" src=""></div> |
|
|
|
|
|
<script> |
|
|
function toggleSidebar() { |
|
|
const sidebar = document.getElementById('sidebar'); |
|
|
const isActive = sidebar.classList.toggle('active'); |
|
|
sidebar.classList.toggle('hidden', !isActive); |
|
|
if (window.innerWidth > 900) document.body.classList.toggle('sidebar-active-on-pc', isActive); |
|
|
} |
|
|
function openModal(src) { document.getElementById('modalImage').src = src; document.getElementById('imageModal').style.display = 'flex'; } |
|
|
function closeModal(event) { if (event.target.id === 'imageModal') document.getElementById('imageModal').style.display = 'none'; } |
|
|
|
|
|
async function handleAction(url, button, countSelector, newClass) { |
|
|
try { |
|
|
const response = await fetch(url, { method: 'POST' }); |
|
|
const data = await response.json(); |
|
|
if (response.ok) { |
|
|
const countSpan = button.querySelector(countSelector); |
|
|
const key = Object.keys(data).find(k => k.includes('count') || k === 'likes'); |
|
|
countSpan.textContent = data[key]; |
|
|
if (newClass) { |
|
|
if (data.liked !== undefined) button.classList.toggle(newClass, data.liked); |
|
|
else { |
|
|
button.classList.add(newClass); |
|
|
setTimeout(() => button.classList.remove(newClass), 400); |
|
|
} |
|
|
} |
|
|
} |
|
|
} catch (error) { console.error('Action failed:', error); } |
|
|
} |
|
|
function likeStory(storyId, btn) { if ({{ 'true' if is_authenticated else 'false' }}) handleAction(`/like/${storyId}`, btn, '.like-count', 'liked'); else window.location.href = '{{ url_for("login") }}'; } |
|
|
function jerkOff(storyId, btn) { handleAction(`/jerk_off/${storyId}`, btn, '.jerk-count', 'pulsing'); } |
|
|
|
|
|
async function incrementViewOnLoad(storyId) { |
|
|
if (!sessionStorage.getItem(`viewed_${storyId}`)) { |
|
|
try { |
|
|
const response = await fetch(`/view/${storyId}`, { method: 'POST' }); |
|
|
if (response.ok) { |
|
|
const data = await response.json(); |
|
|
const viewCountSpan = document.querySelector('.view-count'); |
|
|
if (viewCountSpan) viewCountSpan.textContent = data.views; |
|
|
sessionStorage.setItem(`viewed_${storyId}`, 'true'); |
|
|
} |
|
|
} catch (error) { console.error('View increment failed:', error); } |
|
|
} |
|
|
} |
|
|
function initializeUIState() { |
|
|
const sidebar = document.getElementById('sidebar'); |
|
|
let isActive = window.innerWidth > 900; |
|
|
sidebar.classList.toggle('active', isActive); |
|
|
sidebar.classList.toggle('hidden', !isActive); |
|
|
document.body.classList.toggle('sidebar-active-on-pc', isActive); |
|
|
} |
|
|
window.onload = () => { |
|
|
initializeUIState(); |
|
|
incrementViewOnLoad('{{ story['id'] }}'); |
|
|
}; |
|
|
window.onresize = initializeUIState; |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
''' |
|
|
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/<story_id>', 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 = ''' |
|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Upload Story - Adusis</title> |
|
|
<link rel="preconnect" href="https://fonts.googleapis.com"> |
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> |
|
|
<style> |
|
|
''' + BASE_STYLE + ''' |
|
|
.form-container { |
|
|
max-width: 700px; |
|
|
background: var(--glass-bg); padding: 30px; border-radius: 16px; |
|
|
border: 1px solid var(--glass-border); |
|
|
} |
|
|
h1 { font-size: 2em; font-weight: 700; margin-bottom: 25px; color: var(--primary-color); text-align: center; } |
|
|
label { display: block; margin: 18px 0 5px 0; font-weight: 500; color: var(--text-dark); } |
|
|
input[type="file"] { border: 2px dashed var(--glass-border); padding: 12px; cursor: pointer; } |
|
|
.upload-btn { width: 100%; margin-top: 30px; min-height: 50px; } |
|
|
.spinner { |
|
|
border: 4px solid rgba(255, 255, 255, 0.3); border-radius: 50%; |
|
|
border-top: 4px solid white; width: 25px; height: 25px; |
|
|
animation: spin 1s linear infinite; display: none; |
|
|
position: absolute; left: 50%; top: 50%; |
|
|
margin-left: -12.5px; margin-top: -12.5px; |
|
|
} |
|
|
.upload-btn.uploading { cursor: not-allowed; } |
|
|
.upload-btn.uploading .btn-text { opacity: 0; } |
|
|
.upload-btn.uploading .spinner { display: block; } |
|
|
@keyframes spin { to { transform: rotate(360deg); } } |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<button class="menu-btn" onclick="toggleSidebar()">☰</button> |
|
|
''' + NAV_HTML + ''' |
|
|
<div class="container animated"> |
|
|
<div class="form-container" style="margin: 0 auto;"> |
|
|
<h1>Upload Your Story</h1> |
|
|
{% with messages = get_flashed_messages(with_categories=true) %} |
|
|
{% if messages %} |
|
|
{% for category, message in messages %} |
|
|
<div class="flash {{ category }}">{{ message | safe }}</div> |
|
|
{% endfor %} |
|
|
{% endif %} |
|
|
{% endwith %} |
|
|
<form id="upload-form" method="POST" enctype="multipart/form-data"> |
|
|
<label for="title">Title* (max 150 chars)</label> |
|
|
<input type="text" id="title" name="title" required maxlength="150"> |
|
|
<label for="description">Description (optional, max 2000 chars)</label> |
|
|
<textarea id="description" name="description" rows="4" maxlength="2000"></textarea> |
|
|
<label for="file">Select Photo or Video*</label> |
|
|
<input type="file" id="file" name="file" accept="image/*,video/*" required> |
|
|
<small style="display: block; margin-top: 8px; color: var(--text-dark);">Max 100MB</small> |
|
|
<button type="submit" class="btn upload-btn"> |
|
|
<span class="btn-text">Upload (+{{ upload_reward }} 🪙)</span> |
|
|
<div class="spinner"></div> |
|
|
</button> |
|
|
</form> |
|
|
</div> |
|
|
</div> |
|
|
<script> |
|
|
function toggleSidebar() { |
|
|
const sidebar = document.getElementById('sidebar'); |
|
|
const isActive = sidebar.classList.toggle('active'); |
|
|
sidebar.classList.toggle('hidden', !isActive); |
|
|
if (window.innerWidth > 900) document.body.classList.toggle('sidebar-active-on-pc', isActive); |
|
|
} |
|
|
document.getElementById('upload-form').onsubmit = function(e) { |
|
|
const fileInput = document.getElementById('file'); |
|
|
if (fileInput.files.length > 0 && fileInput.files[0].size > 100 * 1024 * 1024) { |
|
|
alert('File is too large (max 100MB).'); |
|
|
e.preventDefault(); |
|
|
return; |
|
|
} |
|
|
const submitButton = this.querySelector('button[type="submit"]'); |
|
|
submitButton.disabled = true; |
|
|
submitButton.classList.add('uploading'); |
|
|
}; |
|
|
function initializeUIState() { |
|
|
const sidebar = document.getElementById('sidebar'); |
|
|
let isActive = window.innerWidth > 900; |
|
|
sidebar.classList.toggle('active', isActive); |
|
|
sidebar.classList.toggle('hidden', !isActive); |
|
|
document.body.classList.toggle('sidebar-active-on-pc', isActive); |
|
|
} |
|
|
window.onload = initializeUIState; |
|
|
window.onresize = initializeUIState; |
|
|
</script> |
|
|
</body> |
|
|
</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 = ''' |
|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Users - Adusis</title> |
|
|
<link rel="preconnect" href="https://fonts.googleapis.com"> |
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> |
|
|
<style> |
|
|
''' + BASE_STYLE + ''' |
|
|
h1 { font-size: 2.5em; font-weight: 700; margin-bottom: 30px; color: var(--text-light); } |
|
|
.search-container { max-width: 600px; margin: 0 auto 30px auto; } |
|
|
.user-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 25px; } |
|
|
.user-item { |
|
|
background: var(--glass-bg); padding: 18px; border-radius: 12px; |
|
|
transition: var(--transition-medium); display: flex; align-items: center; gap: 18px; |
|
|
border: 1px solid var(--glass-border); text-decoration: none; color: inherit; |
|
|
} |
|
|
.user-item:hover { transform: translateY(-5px); box-shadow: var(--glow-primary); border-color: var(--primary-color); } |
|
|
.user-avatar { width: 60px; height: 60px; border-radius: 50%; object-fit: cover; border: 3px solid var(--secondary-color); flex-shrink: 0; } |
|
|
.user-avatar-placeholder { |
|
|
width: 60px; height: 60px; border-radius: 50%; |
|
|
background: linear-gradient(45deg, var(--primary-color), var(--secondary-color)); |
|
|
display: flex; align-items: center; justify-content: center; |
|
|
font-size: 26px; color: white; font-weight: bold; flex-shrink: 0; |
|
|
} |
|
|
.user-info .username { color: var(--text-light); font-weight: 600; font-size: 1.2em; } |
|
|
.user-info .user-role { font-size: 0.9em; color: var(--text-dark); } |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<button class="menu-btn" onclick="toggleSidebar()">☰</button> |
|
|
''' + NAV_HTML + ''' |
|
|
<div class="container animated"> |
|
|
<h1>Explore Users ({{ user_count }})</h1> |
|
|
<div class="search-container"> |
|
|
<form method="POST"> |
|
|
<input type="text" name="search" placeholder="Find someone..." value="{{ search_query | escape }}" aria-label="Search users"> |
|
|
</form> |
|
|
</div> |
|
|
<div class="user-grid"> |
|
|
{% for user_info in user_list %} |
|
|
<a href="{{ url_for('user_profile', username=user_info['name']) }}" class="user-item"> |
|
|
{% if user_info['avatar'] %} |
|
|
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ user_info['avatar'] }}?rand={{ random.randint(1,1000) }}" alt="Avatar" class="user-avatar" loading="lazy"> |
|
|
{% else %} |
|
|
<div class="user-avatar-placeholder">{{ user_info['name'][0]|upper }}</div> |
|
|
{% endif %} |
|
|
<div class="user-info"> |
|
|
<span class="username">{{ user_info['name'] | escape }}</span> |
|
|
<div> |
|
|
<span class="status-dot {{ 'online' if user_info['online'] else 'offline' }}"></span> |
|
|
<span style="font-size: 0.9em; color: var(--text-dark);">{{ 'Online' if user_info['online'] else 'Offline' }}</span> |
|
|
</div> |
|
|
<span class="user-role">{{ user_info['role'] | title if user_info['role'] else 'N/A' }}</span> |
|
|
</div> |
|
|
</a> |
|
|
{% endfor %} |
|
|
{% if not user_list %} |
|
|
<p style="grid-column: 1 / -1; text-align: center; color: var(--text-dark); padding: 35px;">No users found.</p> |
|
|
{% endif %} |
|
|
</div> |
|
|
</div> |
|
|
<script> |
|
|
function toggleSidebar() { |
|
|
const sidebar = document.getElementById('sidebar'); |
|
|
const isActive = sidebar.classList.toggle('active'); |
|
|
sidebar.classList.toggle('hidden', !isActive); |
|
|
if (window.innerWidth > 900) document.body.classList.toggle('sidebar-active-on-pc', isActive); |
|
|
} |
|
|
function initializeUIState() { |
|
|
const sidebar = document.getElementById('sidebar'); |
|
|
let isActive = window.innerWidth > 900; |
|
|
sidebar.classList.toggle('active', isActive); |
|
|
sidebar.classList.toggle('hidden', !isActive); |
|
|
document.body.classList.toggle('sidebar-active-on-pc', isActive); |
|
|
} |
|
|
window.onload = initializeUIState; |
|
|
window.onresize = initializeUIState; |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
''' |
|
|
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/<username>', 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 = ''' |
|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Profile - {{ profile_username | escape }} - Adusis</title> |
|
|
<link rel="preconnect" href="https://fonts.googleapis.com"> |
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> |
|
|
<style> |
|
|
''' + BASE_STYLE + ''' |
|
|
.container { max-width: 1100px; } |
|
|
.profile-header { |
|
|
display: flex; flex-wrap: wrap; align-items: center; gap: 30px; |
|
|
background: var(--glass-bg); padding: 30px; border-radius: 16px; |
|
|
border: 1px solid var(--glass-border); margin-bottom: 30px; |
|
|
} |
|
|
.avatar { |
|
|
width: 140px; height: 140px; border-radius: 50%; object-fit: cover; |
|
|
border: 4px solid var(--primary-color); box-shadow: var(--glow-primary); |
|
|
transition: var(--transition-medium); cursor: pointer; |
|
|
} |
|
|
.avatar-placeholder { |
|
|
width: 140px; height: 140px; border-radius: 50%; |
|
|
background: linear-gradient(45deg, var(--primary-color), var(--secondary-color)); |
|
|
display: flex; align-items: center; justify-content: center; |
|
|
font-size: 60px; color: white; font-weight: bold; |
|
|
} |
|
|
.profile-info { flex-grow: 1; } |
|
|
.profile-info h1 { font-size: 2.4em; font-weight: 700; color: var(--text-light); margin-bottom: 10px; display: flex; align-items: center; gap: 15px; } |
|
|
.profile-info p { margin: 8px 0; color: var(--text-dark); } |
|
|
.profile-info a { color: var(--secondary-color); text-decoration: none; font-weight: 500; } |
|
|
.profile-actions { margin-top: 20px; display: flex; gap: 10px; flex-wrap: wrap; } |
|
|
.section-title { font-size: 2em; color: var(--text-light); margin: 40px 0 25px 0; border-bottom: 1px solid var(--glass-border); padding-bottom: 15px; } |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<button class="menu-btn" onclick="toggleSidebar()">☰</button> |
|
|
''' + NAV_HTML + ''' |
|
|
<div class="container animated"> |
|
|
<div class="profile-header"> |
|
|
{% if avatar %} |
|
|
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ avatar }}?rand={{ random.randint(1,1000) }}" alt="Avatar" class="avatar" loading="lazy" onclick="openModal(this.src)"> |
|
|
{% else %} |
|
|
<div class="avatar-placeholder">{{ profile_username[0]|upper }}</div> |
|
|
{% endif %} |
|
|
<div class="profile-info"> |
|
|
<h1> |
|
|
<span>{{ profile_username | escape }}</span> |
|
|
<span class="status-dot {{ 'online' if profile_user_online else 'offline' }}"></span> |
|
|
<span class="aducoin-display" title="{{ aducoin }} AduCoin"> |
|
|
<img src="{{ logo_url }}" alt="AduCoin" class="aducoin-icon"> |
|
|
<span>{{ aducoin }}</span> |
|
|
</span> |
|
|
</h1> |
|
|
<p><strong>Role:</strong> {{ user_role|title if user_role else 'Not Set' }}</p> |
|
|
<p><strong>Bio:</strong> {{ bio | escape if bio else 'No bio provided.' }}</p> |
|
|
{% if link %}<p><strong>Link:</strong> <a href="{{ link if link.startswith('http') else '//' + link }}" target="_blank" rel="noopener noreferrer">{{ link | escape }}</a></p>{% endif %} |
|
|
<p><small>Last Seen: {{ last_seen }}</small></p> |
|
|
{% if is_authenticated %} |
|
|
<div class="profile-actions"> |
|
|
<a href="{{ url_for('conversation', other_username=profile_username) }}" class="btn btn-secondary"><span>💬</span> Send Message</a> |
|
|
</div> |
|
|
{% endif %} |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
{% if is_authenticated %} |
|
|
<div class="form-section"> |
|
|
<h3 style="color: var(--accent-color);">Send AduCoin to {{ profile_username | escape }}</h3> |
|
|
<form method="POST" action="{{ url_for('transfer_aducoin', recipient_username=profile_username) }}"> |
|
|
<div class="form-input-container"> |
|
|
<input type="number" name="amount" placeholder="Amount" min="1" required> |
|
|
<button type="submit" class="btn btn-accent">Send 🪙</button> |
|
|
</div> |
|
|
<p class="info-text">Your balance: {{ sender_aducoin }} AduCoin</p> |
|
|
</form> |
|
|
</div> |
|
|
{% endif %} |
|
|
|
|
|
<h2 class="section-title">{{ profile_username | escape }}'s Stories</h2> |
|
|
<div class="story-grid"> |
|
|
{% for story in user_stories %} |
|
|
<div class="story-item"> |
|
|
<a href="{{ url_for('story_page', story_id=story['id']) }}" class="story-preview-link"> |
|
|
{% set media_url = "https://huggingface.co/datasets/" + repo_id + "/resolve/main/" + story['type'] + "s/" + story['filename'] %} |
|
|
<video class="story-preview" preload="metadata" muted loading="lazy" loop><source src="{{ media_url }}"></video> |
|
|
</a> |
|
|
<div class="story-item-info"> |
|
|
<h3><a href="{{ url_for('story_page', story_id=story['id']) }}" style="color: inherit; text-decoration: none;">{{ story['title'] | escape }}</a></h3> |
|
|
<p class="stats"> |
|
|
<span>❤️ {{ story.get('likes', [])|length }}</span> |
|
|
<span>👁️ {{ story.get('views', 0) }}</span> |
|
|
<span>🍆 {{ story.get('jerked_off_count', 0) }}</span> |
|
|
<span>💬 {{ story.get('comments', [])|length }}</span> |
|
|
</p> |
|
|
</div> |
|
|
</div> |
|
|
{% endfor %} |
|
|
{% if not user_stories %} |
|
|
<p style="color: var(--text-dark); grid-column: 1 / -1; text-align: center; padding: 20px;">This user has no stories yet.</p> |
|
|
{% endif %} |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="modal" id="imageModal" onclick="closeModal(event)"><img id="modalImage" src=""></div> |
|
|
<script> |
|
|
function toggleSidebar() { |
|
|
const sidebar = document.getElementById('sidebar'); |
|
|
const isActive = sidebar.classList.toggle('active'); |
|
|
sidebar.classList.toggle('hidden', !isActive); |
|
|
if (window.innerWidth > 900) document.body.classList.toggle('sidebar-active-on-pc', isActive); |
|
|
} |
|
|
function openModal(src) { document.getElementById('modalImage').src = src; document.getElementById('imageModal').style.display = 'flex'; } |
|
|
function closeModal(event) { if (event.target.id === 'imageModal') document.getElementById('imageModal').style.display = 'none'; } |
|
|
function initializeUIState() { |
|
|
const sidebar = document.getElementById('sidebar'); |
|
|
let isActive = window.innerWidth > 900; |
|
|
sidebar.classList.toggle('active', isActive); |
|
|
sidebar.classList.toggle('hidden', !isActive); |
|
|
document.body.classList.toggle('sidebar-active-on-pc', isActive); |
|
|
} |
|
|
function initializeVideoPreviews() { |
|
|
document.querySelectorAll('video.story-preview').forEach(video => { |
|
|
const item = video.closest('.story-item'); |
|
|
if(item) { |
|
|
item.addEventListener('mouseenter', () => video.play().catch(e => {})); |
|
|
item.addEventListener('mouseleave', () => { video.pause(); video.currentTime = 0; }); |
|
|
} |
|
|
}); |
|
|
} |
|
|
window.onload = () => { |
|
|
initializeUIState(); |
|
|
initializeVideoPreviews(); |
|
|
}; |
|
|
window.onresize = initializeUIState; |
|
|
</script> |
|
|
</body> |
|
|
</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 = ''' |
|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>AduCoin History - Adusis</title> |
|
|
<link rel="preconnect" href="https://fonts.googleapis.com"> |
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> |
|
|
<style> |
|
|
''' + BASE_STYLE + ''' |
|
|
h1 { font-size: 2.2em; display: flex; align-items: center; gap: 15px; } |
|
|
.balance-info { |
|
|
font-size: 1.15em; color: var(--text-dark); margin-bottom: 30px; |
|
|
padding: 20px; background: var(--glass-bg); border-radius: 12px; |
|
|
border: 1px solid var(--glass-border); |
|
|
} |
|
|
.balance-info strong { color: var(--accent-color); font-size: 1.5em; text-shadow: 0 0 8px var(--accent-color); } |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<button class="menu-btn" onclick="toggleSidebar()">☰</button> |
|
|
''' + NAV_HTML + ''' |
|
|
<div class="container animated"> |
|
|
<h1> |
|
|
<img src="{{ logo_url }}" alt="AduCoin" class="aducoin-icon" style="width:40px; height:40px;"> |
|
|
AduCoin History |
|
|
</h1> |
|
|
<div class="balance-info"> |
|
|
Your current balance: <strong>{{ current_aducoin }} 🪙</strong> |
|
|
</div> |
|
|
|
|
|
{% if transactions %} |
|
|
<div style="overflow-x: auto;"> |
|
|
<table class="transaction-table"> |
|
|
<thead><tr><th>Date</th><th>Type</th><th>Amount</th><th>Description</th><th>Balance After</th></tr></thead> |
|
|
<tbody> |
|
|
{% for tx in transactions %} |
|
|
<tr> |
|
|
<td>{{ tx.timestamp }}</td> |
|
|
<td>{{ tx.type.replace('_', ' ')|title }}</td> |
|
|
<td class="{{ 'amount-positive' if tx.amount > 0 else 'amount-negative' }}">{{ '%+d'|format(tx.amount) if tx.amount != 0 else tx.amount }}</td> |
|
|
<td>{{ tx.description | escape }}</td> |
|
|
<td>{{ tx.balance_after }}</td> |
|
|
</tr> |
|
|
{% endfor %} |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
{% else %} |
|
|
<p style="text-align: center; color: var(--text-dark); padding: 40px;">No transaction history yet.</p> |
|
|
{% endif %} |
|
|
</div> |
|
|
<script> |
|
|
function toggleSidebar() { |
|
|
const sidebar = document.getElementById('sidebar'); |
|
|
const isActive = sidebar.classList.toggle('active'); |
|
|
sidebar.classList.toggle('hidden', !isActive); |
|
|
if (window.innerWidth > 900) document.body.classList.toggle('sidebar-active-on-pc', isActive); |
|
|
} |
|
|
function initializeUIState() { |
|
|
const sidebar = document.getElementById('sidebar'); |
|
|
let isActive = window.innerWidth > 900; |
|
|
sidebar.classList.toggle('active', isActive); |
|
|
sidebar.classList.toggle('hidden', !isActive); |
|
|
document.body.classList.toggle('sidebar-active-on-pc', isActive); |
|
|
} |
|
|
window.onload = initializeUIState; |
|
|
window.onresize = initializeUIState; |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
''' |
|
|
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/<recipient_username>', 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/<story_id>', 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/<story_id>', 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/<story_id>', 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 = ''' |
|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Inbox - Adusis</title> |
|
|
<link rel="preconnect" href="https://fonts.googleapis.com"> |
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> |
|
|
<style> |
|
|
''' + BASE_STYLE + ''' |
|
|
.container { max-width: 800px; } |
|
|
h1 { font-size: 2.2em; color: var(--text-light); margin-bottom: 25px; border-bottom: 1px solid var(--glass-border); padding-bottom: 15px;} |
|
|
.no-conversations { text-align: center; color: var(--text-dark); padding: 40px; font-size: 1.1em; } |
|
|
.no-conversations a { color: var(--primary-color); font-weight: bold; } |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<button class="menu-btn" onclick="toggleSidebar()">☰</button> |
|
|
''' + NAV_HTML + ''' |
|
|
<div class="container animated"> |
|
|
<h1>Messages</h1> |
|
|
{% with messages = get_flashed_messages(with_categories=true) %} |
|
|
{% if messages %} |
|
|
{% for category, message in messages %} |
|
|
<div class="flash {{ category }}">{{ message | escape }}</div> |
|
|
{% endfor %} |
|
|
{% endif %} |
|
|
{% endwith %} |
|
|
|
|
|
{% if conversations %} |
|
|
<div class="conversation-list"> |
|
|
{% for conv in conversations %} |
|
|
<a href="{{ url_for('conversation', other_username=conv.other_user) }}" class="conversation-item {% if conv.is_unread %}unread{% endif %}"> |
|
|
{% if conv.avatar %} |
|
|
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ conv.avatar }}?rand={{ random.randint(1,1000) }}" alt="Avatar" class="conversation-avatar" loading="lazy"> |
|
|
{% else %} |
|
|
<div class="conversation-avatar-placeholder">{{ conv.other_user[0]|upper }}</div> |
|
|
{% endif %} |
|
|
<div class="conversation-details"> |
|
|
<span class="conversation-username">{{ conv.other_user | escape }}</span> |
|
|
<p class="conversation-last-msg">{{ conv.last_message_text | escape | truncate(50) }}</p> |
|
|
</div> |
|
|
<span class="conversation-timestamp"><small>{{ conv.last_message_time }}</small></span> |
|
|
</a> |
|
|
{% endfor %} |
|
|
</div> |
|
|
{% else %} |
|
|
<p class="no-conversations">You have no conversations yet. Find someone on the <a href="{{ url_for('users') }}">Users</a> page to start chatting!</p> |
|
|
{% endif %} |
|
|
</div> |
|
|
<script> |
|
|
function toggleSidebar() { |
|
|
const sidebar = document.getElementById('sidebar'); |
|
|
const isActive = sidebar.classList.toggle('active'); |
|
|
sidebar.classList.toggle('hidden', !isActive); |
|
|
if (window.innerWidth > 900) document.body.classList.toggle('sidebar-active-on-pc', isActive); |
|
|
} |
|
|
function initializeUIState() { |
|
|
const sidebar = document.getElementById('sidebar'); |
|
|
let isActive = window.innerWidth > 900; |
|
|
sidebar.classList.toggle('active', isActive); |
|
|
sidebar.classList.toggle('hidden', !isActive); |
|
|
document.body.classList.toggle('sidebar-active-on-pc', isActive); |
|
|
} |
|
|
window.onload = initializeUIState; |
|
|
window.onresize = initializeUIState; |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
''' |
|
|
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/<other_username>', 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 = ''' |
|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Chat with {{ other_username | escape }} - Adusis</title> |
|
|
<link rel="preconnect" href="https://fonts.googleapis.com"> |
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> |
|
|
<style> |
|
|
''' + BASE_STYLE + ''' |
|
|
.container { max-width: 850px; } |
|
|
.chat-header { |
|
|
display: flex; align-items: center; gap: 18px; margin-bottom: 25px; |
|
|
padding: 20px; background: var(--glass-bg); border-radius: 12px; |
|
|
border: 1px solid var(--glass-border); |
|
|
} |
|
|
.chat-avatar { width: 55px; height: 55px; border-radius: 50%; object-fit: cover; flex-shrink: 0; border: 3px solid var(--secondary-color); } |
|
|
.chat-avatar-placeholder { width: 55px; height: 55px; border-radius: 50%; background: linear-gradient(45deg, var(--primary-color), var(--secondary-color)); display: flex; align-items: center; justify-content: center; font-size: 26px; color: white; font-weight: bold; flex-shrink: 0;} |
|
|
.chat-username { font-size: 1.8em; font-weight: bold; color: var(--text-light); } |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<button class="menu-btn" onclick="toggleSidebar()">☰</button> |
|
|
''' + NAV_HTML + ''' |
|
|
<div class="container animated"> |
|
|
<div class="chat-header"> |
|
|
{% if other_user_data.avatar %} |
|
|
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/{{ other_user_data.avatar }}?rand={{ random.randint(1,1000) }}" alt="Avatar" class="chat-avatar" loading="lazy"> |
|
|
{% else %} |
|
|
<div class="chat-avatar-placeholder">{{ other_username[0]|upper }}</div> |
|
|
{% endif %} |
|
|
<div> |
|
|
<h1 class="chat-username">{{ other_username | escape }}</h1> |
|
|
<div> |
|
|
<span class="status-dot {{ 'online' if other_user_is_online else 'offline' }}"></span> |
|
|
<span>{{ 'Online' if other_user_is_online else 'Offline' }}</span> |
|
|
</div> |
|
|
</div> |
|
|
<a href="{{ url_for('user_profile', username=other_username) }}" class="btn btn-secondary" style="margin-left: auto; padding: 10px 18px; font-size: 0.95em;">View Profile</a> |
|
|
</div> |
|
|
|
|
|
{% with messages_flash = get_flashed_messages(with_categories=true) %} |
|
|
{% if messages_flash %} |
|
|
{% for category, message in messages_flash %} |
|
|
<div class="flash {{ category }}">{{ message | escape }}</div> |
|
|
{% endfor %} |
|
|
{% endif %} |
|
|
{% endwith %} |
|
|
|
|
|
<div class="message-area" id="message-area"> |
|
|
{% if messages %} |
|
|
{% for msg in messages %} |
|
|
{% if msg is mapping %} |
|
|
<div class="message-bubble {{ 'sent' if msg.sender == username else 'received' }}"> |
|
|
<div class="message-text">{{ msg.text | escape }}</div> |
|
|
<span class="timestamp">{{ msg.timestamp }}</span> |
|
|
</div> |
|
|
{% endif %} |
|
|
{% endfor %} |
|
|
{% else %} |
|
|
<p style="text-align: center; color: var(--text-dark); padding: 40px;">Start the conversation!</p> |
|
|
{% endif %} |
|
|
</div> |
|
|
|
|
|
<form method="POST" class="message-input-area"> |
|
|
<textarea name="message_text" placeholder="Type your message..." required rows="1" maxlength="2000" oninput='this.style.height = "";this.style.height = this.scrollHeight + "px"'></textarea> |
|
|
<button type="submit" class="btn">Send</button> |
|
|
</form> |
|
|
</div> |
|
|
<script> |
|
|
function toggleSidebar() { |
|
|
const sidebar = document.getElementById('sidebar'); |
|
|
const isActive = sidebar.classList.toggle('active'); |
|
|
sidebar.classList.toggle('hidden', !isActive); |
|
|
if (window.innerWidth > 900) document.body.classList.toggle('sidebar-active-on-pc', isActive); |
|
|
} |
|
|
function initializeUIState() { |
|
|
const sidebar = document.getElementById('sidebar'); |
|
|
let isActive = window.innerWidth > 900; |
|
|
sidebar.classList.toggle('active', isActive); |
|
|
sidebar.classList.toggle('hidden', !isActive); |
|
|
document.body.classList.toggle('sidebar-active-on-pc', isActive); |
|
|
|
|
|
const messageArea = document.getElementById('message-area'); |
|
|
if(messageArea) messageArea.scrollTop = 0; |
|
|
const messageInput = document.querySelector('.message-input-area textarea'); |
|
|
if(messageInput) setTimeout(() => messageInput.focus(), 100); |
|
|
} |
|
|
window.onload = initializeUIState; |
|
|
window.onresize = initializeUIState; |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
''' |
|
|
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) |
|
|
|