Homepage (site/index.html): integration-v14 promoted, Writings section integrated with 33 pieces clustered by type (stories/essays/miscellany), Writings welcome lightbox, content frame at 98% opacity. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1416 lines
51 KiB
Python
1416 lines
51 KiB
Python
import os
|
|
import io
|
|
import re
|
|
import socket
|
|
import sqlite3
|
|
import subprocess
|
|
import functools
|
|
from flask import Flask, request, session, redirect, url_for, Response
|
|
from markupsafe import escape
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. EMOJI POOL + KEY GENERATOR
|
|
# ---------------------------------------------------------------------------
|
|
|
|
EMOJI_POOL = [
|
|
"😀","😂","😍","😎","😭","😡","🥰","🤔","😴","🤯",
|
|
"🎉","🔥","💀","👻","🌙","⭐","🌈","🍎","🍕","🎸",
|
|
"🐶","🐱","🐸","🦊","🐺","🐻","🦁","🐯","🦋","🐙",
|
|
"🌺","🍀","🌊","⚡","🏆","🎯","🔑","💎","🛸","🎭",
|
|
"🌋","🧊","🍄","🦄","🔮","🗝️","🧲","🎪",
|
|
]
|
|
|
|
def generate_emoji_key(length):
|
|
return "".join(EMOJI_POOL[b % len(EMOJI_POOL)] for b in os.urandom(length))
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2. MODULE-LEVEL STATE
|
|
# ---------------------------------------------------------------------------
|
|
|
|
ADMIN_POST_KEY = generate_emoji_key(4)
|
|
TRUSTED_POST_KEY = generate_emoji_key(2)
|
|
DEFAULT_POST_KEY = generate_emoji_key(2)
|
|
QR_VISIBLE = True
|
|
|
|
def get_all_local_ips():
|
|
"""Return list of (ip, iface_name) for all non-loopback IPv4 interfaces."""
|
|
results = []
|
|
|
|
# Method 1: ifconfig (net-tools) — uses socket ioctl, no netlink needed.
|
|
# Works in Termux. Install with: pkg install net-tools
|
|
try:
|
|
out = subprocess.run(
|
|
['ifconfig'], capture_output=True, text=True, timeout=3
|
|
).stdout
|
|
current_iface = None
|
|
for line in out.splitlines():
|
|
# New-style: "wlan0: flags=..." Old-style: "wlan0 Link encap:..."
|
|
m = re.match(r'^(\S+?)(?::\s+flags=|\s+Link)', line)
|
|
if m:
|
|
current_iface = m.group(1).rstrip(':')
|
|
# New-style: "inet 192.x.x.x" Old-style: "inet addr:192.x.x.x"
|
|
m = re.search(r'inet\s+(?:addr:)?(\d+\.\d+\.\d+\.\d+)', line)
|
|
if m and current_iface:
|
|
ip = m.group(1)
|
|
if not ip.startswith('127.'):
|
|
results.append((ip, current_iface))
|
|
if results:
|
|
return results
|
|
except Exception:
|
|
pass
|
|
|
|
# Method 2: ip addr (iproute2) — works on desktop Linux, but requires
|
|
# netlink socket permissions not available in Termux by default.
|
|
try:
|
|
out = subprocess.run(
|
|
['ip', 'addr'], capture_output=True, text=True, timeout=3
|
|
).stdout
|
|
current_iface = None
|
|
for line in out.splitlines():
|
|
m = re.match(r'^\d+:\s+(\S+?)[@:]', line)
|
|
if m:
|
|
current_iface = m.group(1)
|
|
m = re.match(r'^\s+inet\s+(\d+\.\d+\.\d+\.\d+)/', line)
|
|
if m and current_iface:
|
|
ip = m.group(1)
|
|
if not ip.startswith('127.'):
|
|
results.append((ip, current_iface))
|
|
except Exception:
|
|
pass
|
|
|
|
return results
|
|
|
|
def get_hotspot_ip():
|
|
ips = get_all_local_ips()
|
|
for ip, _ in ips:
|
|
if ip.startswith('192.168.') or ip.startswith('10.42.'):
|
|
return ip
|
|
return ips[0][0] if ips else '127.0.0.1'
|
|
|
|
def generate_qr_bytes(url):
|
|
try:
|
|
import qrcode
|
|
img = qrcode.make(url)
|
|
buf = io.BytesIO()
|
|
img.save(buf, format="PNG")
|
|
return buf.getvalue()
|
|
except Exception:
|
|
return b""
|
|
|
|
HOTSPOT_IP = get_hotspot_ip()
|
|
JOIN_URL = f"http://{HOTSPOT_IP}:5000/join"
|
|
QR_IMAGE_BYTES = generate_qr_bytes(JOIN_URL)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. FLASK APP
|
|
# ---------------------------------------------------------------------------
|
|
|
|
app = Flask(__name__)
|
|
app.secret_key = os.urandom(32)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 4. SQLITE
|
|
# ---------------------------------------------------------------------------
|
|
|
|
DB_CONN = sqlite3.connect(":memory:", check_same_thread=False)
|
|
DB_CONN.row_factory = sqlite3.Row
|
|
|
|
def init_db():
|
|
cur = DB_CONN.cursor()
|
|
cur.executescript("""
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
session_label TEXT NOT NULL,
|
|
username TEXT NOT NULL,
|
|
profile_notes TEXT DEFAULT '',
|
|
notes_visible INTEGER DEFAULT 1,
|
|
post_tier TEXT DEFAULT 'DEFAULT',
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE TABLE IF NOT EXISTS keys (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
key_type TEXT NOT NULL,
|
|
emoji_code TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS handshakes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
from_user_id INTEGER NOT NULL REFERENCES users(id),
|
|
to_user_id INTEGER NOT NULL REFERENCES users(id),
|
|
submitted_code TEXT NOT NULL,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE TABLE IF NOT EXISTS posts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
author_id INTEGER NOT NULL REFERENCES users(id),
|
|
content TEXT NOT NULL,
|
|
score INTEGER DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE TABLE IF NOT EXISTS votes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
post_id INTEGER NOT NULL REFERENCES posts(id),
|
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
direction INTEGER NOT NULL,
|
|
weight INTEGER NOT NULL,
|
|
UNIQUE(post_id, user_id)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS reports (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
post_id INTEGER NOT NULL REFERENCES posts(id),
|
|
reporter_id INTEGER NOT NULL REFERENCES users(id),
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(post_id, reporter_id)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS notifications (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
message TEXT NOT NULL,
|
|
read INTEGER DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
""")
|
|
DB_CONN.commit()
|
|
|
|
init_db()
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 5. CSS
|
|
# ---------------------------------------------------------------------------
|
|
|
|
BASE_CSS = """
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body {
|
|
background: #0a0a0f;
|
|
color: #c8c8d8;
|
|
font-family: 'Courier New', Courier, monospace;
|
|
font-size: 15px;
|
|
min-height: 100vh;
|
|
padding: 8px;
|
|
}
|
|
a { color: #cc44ff; text-decoration: none; }
|
|
a:hover { text-decoration: underline; }
|
|
h1, h2, h3 { color: #ff44cc; letter-spacing: 1px; margin-bottom: 8px; }
|
|
h1 { font-size: 1.3em; border-bottom: 2px solid #cc44ff; padding-bottom: 4px; margin-bottom: 12px; }
|
|
h2 { font-size: 1.1em; }
|
|
h3 { font-size: 1em; color: #cc44ff; }
|
|
.container { max-width: 600px; margin: 0 auto; }
|
|
.card {
|
|
border: 1px solid #442255;
|
|
background: #0f0f1a;
|
|
padding: 12px;
|
|
margin-bottom: 10px;
|
|
}
|
|
.card.pinned-top { border-color: #ff44cc; border-width: 2px; }
|
|
.card.pinned-bottom { border-color: #334; border-width: 2px; }
|
|
.post-meta { font-size: 0.8em; color: #886699; margin-bottom: 6px; }
|
|
.post-content { margin-bottom: 10px; word-break: break-word; line-height: 1.5; }
|
|
.spoiler {
|
|
background: #330033; color: #330033; cursor: pointer;
|
|
padding: 4px; border: 1px solid #660066; user-select: none;
|
|
}
|
|
.spoiler.revealed { background: transparent; color: inherit; }
|
|
.btn {
|
|
display: inline-block;
|
|
min-height: 48px;
|
|
min-width: 48px;
|
|
padding: 10px 16px;
|
|
border: 1px solid #cc44ff;
|
|
background: #1a0a2e;
|
|
color: #cc44ff;
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
font-size: 0.9em;
|
|
vertical-align: middle;
|
|
text-align: center;
|
|
line-height: 1.2;
|
|
}
|
|
.btn:hover { background: #2a1040; }
|
|
.btn.active { background: #440066; color: #ff88ff; }
|
|
.btn.danger { border-color: #ff4444; color: #ff4444; background: #1a0a0a; }
|
|
.btn.danger:hover { background: #2a0a0a; }
|
|
.btn.small { min-height: 36px; padding: 6px 10px; font-size: 0.8em; }
|
|
input[type=text], input[type=password], textarea {
|
|
background: #0a0a1a;
|
|
border: 1px solid #442255;
|
|
color: #c8c8d8;
|
|
font-family: inherit;
|
|
font-size: 0.95em;
|
|
padding: 10px;
|
|
width: 100%;
|
|
min-height: 48px;
|
|
margin-bottom: 8px;
|
|
}
|
|
textarea { min-height: 80px; resize: vertical; }
|
|
input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
|
outline: none;
|
|
border-color: #cc44ff;
|
|
}
|
|
.form-row { margin-bottom: 10px; }
|
|
label { display: block; color: #886699; margin-bottom: 4px; font-size: 0.85em; }
|
|
.banner {
|
|
border: 1px solid #ff44cc;
|
|
background: #1a0020;
|
|
padding: 12px;
|
|
margin-bottom: 12px;
|
|
color: #ff88ff;
|
|
}
|
|
.banner.error { border-color: #ff4444; background: #1a0000; color: #ff8888; }
|
|
.banner.info { border-color: #4488ff; background: #00001a; color: #88aaff; }
|
|
.nav {
|
|
display: flex;
|
|
gap: 4px;
|
|
flex-wrap: wrap;
|
|
margin-bottom: 16px;
|
|
border-bottom: 1px solid #332244;
|
|
padding-bottom: 8px;
|
|
}
|
|
.score { color: #ffcc44; font-weight: bold; }
|
|
.score.neg { color: #ff4444; }
|
|
.emoji-picker { margin-bottom: 8px; }
|
|
.emoji-grid {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 2px;
|
|
background: #0f0f1a;
|
|
border: 1px solid #332244;
|
|
padding: 6px;
|
|
max-height: 160px;
|
|
overflow-y: auto;
|
|
}
|
|
.emoji-btn {
|
|
font-size: 1.4em;
|
|
min-width: 44px;
|
|
min-height: 44px;
|
|
background: #1a0a2e;
|
|
border: 1px solid #332244;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.emoji-btn:hover { background: #2a1040; }
|
|
.emoji-display {
|
|
font-size: 1.4em;
|
|
background: #0a0a1a;
|
|
border: 1px solid #442255;
|
|
padding: 6px 10px;
|
|
min-height: 48px;
|
|
display: inline-block;
|
|
min-width: 80px;
|
|
vertical-align: middle;
|
|
letter-spacing: 2px;
|
|
}
|
|
.tier-admin { color: #ff44cc; }
|
|
.tier-trusted { color: #44ccff; }
|
|
.tier-default { color: #c8c8d8; }
|
|
.tier-restricted { color: #ff4444; }
|
|
table { width: 100%; border-collapse: collapse; margin-bottom: 12px; font-size: 0.85em; }
|
|
th { background: #1a0a2e; color: #cc44ff; padding: 6px; border: 1px solid #332244; text-align: left; }
|
|
td { padding: 6px; border: 1px solid #221133; vertical-align: top; word-break: break-word; }
|
|
tr:nth-child(even) td { background: #0d0d18; }
|
|
.section { margin-bottom: 20px; }
|
|
.friends-toggle { margin-bottom: 12px; }
|
|
"""
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 6. BASE PAGE
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def base_page(title, content, current_user=None):
|
|
notif_html = ""
|
|
if current_user:
|
|
notifs = DB_CONN.execute(
|
|
"SELECT message FROM notifications WHERE user_id=? AND read=0 ORDER BY created_at",
|
|
(current_user["id"],)
|
|
).fetchall()
|
|
if notifs:
|
|
msgs = "".join(f"<div>{escape(n['message'])}</div>" for n in notifs)
|
|
notif_html = f'<div class="banner">{msgs}</div>'
|
|
DB_CONN.execute("UPDATE notifications SET read=1 WHERE user_id=? AND read=0", (current_user["id"],))
|
|
DB_CONN.commit()
|
|
|
|
user_info = ""
|
|
if current_user:
|
|
tier = current_user["post_tier"]
|
|
tier_class = f"tier-{tier.lower()}"
|
|
label = escape(current_user["session_label"])
|
|
user_info = f'<span class="{tier_class}">[{label} · {tier}]</span>'
|
|
|
|
return f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
|
<title>{escape(title)} — MESHAGORA</title>
|
|
<style>{BASE_CSS}</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>◆ MESHAGORA</h1>
|
|
<div class="nav">
|
|
<a class="btn small" href="/">The Square</a>
|
|
<a class="btn small" href="/profile">Profile</a>
|
|
<a class="btn small" href="/admin">Admin</a>
|
|
{user_info}
|
|
</div>
|
|
{notif_html}
|
|
{content}
|
|
</div>
|
|
<script>
|
|
function appendEmoji(fieldId, displayId, emoji, maxLen) {{
|
|
var hidden = document.getElementById(fieldId);
|
|
var display = document.getElementById(displayId);
|
|
var arr = Array.from(hidden.value);
|
|
if (arr.length < maxLen) {{
|
|
hidden.value += emoji;
|
|
display.textContent = hidden.value;
|
|
}}
|
|
}}
|
|
function clearEmoji(fieldId, displayId) {{
|
|
document.getElementById(fieldId).value = '';
|
|
document.getElementById(displayId).textContent = '';
|
|
}}
|
|
</script>
|
|
</body>
|
|
</html>"""
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 7. EMOJI PICKER WIDGET
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def emoji_picker(field_name, field_id, max_len=2, current_val=""):
|
|
display_id = field_id + "_display"
|
|
grid = ""
|
|
for em in EMOJI_POOL:
|
|
grid += f'<button type="button" class="emoji-btn" onclick="appendEmoji(\'{field_id}\',\'{display_id}\',\'{em}\',{max_len})">{em}</button>'
|
|
return f"""
|
|
<div class="emoji-picker">
|
|
<input type="hidden" name="{field_name}" id="{field_id}" value="{escape(current_val)}">
|
|
<div style="margin-bottom:4px;display:flex;align-items:center;gap:8px;">
|
|
<span class="emoji-display" id="{display_id}">{escape(current_val)}</span>
|
|
<button type="button" class="btn small danger" onclick="clearEmoji('{field_id}','{display_id}')">CLEAR</button>
|
|
</div>
|
|
<div class="emoji-grid">{grid}</div>
|
|
</div>"""
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 8. HELPER FUNCTIONS
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def get_current_user():
|
|
uid = session.get("user_id")
|
|
if not uid:
|
|
return None
|
|
return DB_CONN.execute("SELECT * FROM users WHERE id=?", (uid,)).fetchone()
|
|
|
|
def get_friend_key(user_id):
|
|
row = DB_CONN.execute(
|
|
"SELECT emoji_code FROM keys WHERE user_id=? AND key_type='FRIEND'", (user_id,)
|
|
).fetchone()
|
|
return row["emoji_code"] if row else ""
|
|
|
|
def get_profile_key(user_id):
|
|
row = DB_CONN.execute(
|
|
"SELECT emoji_code FROM keys WHERE user_id=? AND key_type='PROFILE'", (user_id,)
|
|
).fetchone()
|
|
return row["emoji_code"] if row else ""
|
|
|
|
def are_friends(a_id, b_id):
|
|
a_key = get_friend_key(a_id)
|
|
b_key = get_friend_key(b_id)
|
|
code_ab = a_key + b_key
|
|
code_ba = b_key + a_key
|
|
ab = DB_CONN.execute(
|
|
"SELECT id FROM handshakes WHERE from_user_id=? AND to_user_id=? AND submitted_code=?",
|
|
(a_id, b_id, code_ab)
|
|
).fetchone()
|
|
ba = DB_CONN.execute(
|
|
"SELECT id FROM handshakes WHERE from_user_id=? AND to_user_id=? AND submitted_code=?",
|
|
(b_id, a_id, code_ba)
|
|
).fetchone()
|
|
return ab is not None and ba is not None
|
|
|
|
def get_friends(user_id):
|
|
all_users = DB_CONN.execute("SELECT id FROM users WHERE id != ?", (user_id,)).fetchall()
|
|
return [u["id"] for u in all_users if are_friends(user_id, u["id"])]
|
|
|
|
def restrict_user(user_id):
|
|
global DEFAULT_POST_KEY
|
|
DB_CONN.execute("UPDATE users SET post_tier='RESTRICTED' WHERE id=?", (user_id,))
|
|
DB_CONN.commit()
|
|
DEFAULT_POST_KEY = generate_emoji_key(2)
|
|
|
|
def check_auto_restrict(author_id):
|
|
author = DB_CONN.execute("SELECT * FROM users WHERE id=?", (author_id,)).fetchone()
|
|
if not author or author["post_tier"] == "RESTRICTED":
|
|
return
|
|
total_users = DB_CONN.execute("SELECT COUNT(*) FROM users").fetchone()[0]
|
|
if total_users <= 1:
|
|
return
|
|
unique_reporters = DB_CONN.execute(
|
|
"""SELECT COUNT(DISTINCT r.reporter_id) FROM reports r
|
|
JOIN posts p ON r.post_id = p.id
|
|
WHERE p.author_id = ? AND r.reporter_id != ?""",
|
|
(author_id, author_id)
|
|
).fetchone()[0]
|
|
threshold = (total_users - 1) * 0.51
|
|
if unique_reporters >= threshold:
|
|
restrict_user(author_id)
|
|
|
|
def order_feed(posts):
|
|
if not posts:
|
|
return []
|
|
if len(posts) == 1:
|
|
return list(posts)
|
|
posts = list(posts)
|
|
max_score = max(p["score"] for p in posts)
|
|
min_score = min(p["score"] for p in posts)
|
|
# Find top pinned (highest score, most recent tiebreak)
|
|
top_candidates = [p for p in posts if p["score"] == max_score]
|
|
top_pin = max(top_candidates, key=lambda p: p["created_at"])
|
|
# Find bottom pinned (lowest score, most recent tiebreak) — must be different from top
|
|
remaining = [p for p in posts if p["id"] != top_pin["id"]]
|
|
if not remaining:
|
|
return [top_pin]
|
|
bot_min = min(p["score"] for p in remaining)
|
|
# Only pin bottom separately if it's actually different score from top or there are multiple posts
|
|
bot_candidates = [p for p in remaining if p["score"] == bot_min]
|
|
bot_pin = max(bot_candidates, key=lambda p: p["created_at"])
|
|
middle = [p for p in remaining if p["id"] != bot_pin["id"]]
|
|
middle.sort(key=lambda p: p["created_at"], reverse=True)
|
|
if len(posts) == 2:
|
|
return [top_pin, bot_pin]
|
|
return [top_pin] + middle + [bot_pin]
|
|
|
|
def require_admin(f):
|
|
@functools.wraps(f)
|
|
def decorated(*args, **kwargs):
|
|
if not session.get("admin_authed"):
|
|
return redirect(url_for("admin"))
|
|
return f(*args, **kwargs)
|
|
return decorated
|
|
|
|
def vote_weight(tier):
|
|
return {"ADMIN": 1, "TRUSTED": 2, "DEFAULT": 1, "RESTRICTED": 0}.get(tier, 0)
|
|
|
|
def tier_badge(tier):
|
|
cls = f"tier-{tier.lower()}"
|
|
return f'<span class="{cls}">[{escape(tier)}]</span>'
|
|
|
|
def has_reported(post_id, user_id):
|
|
return DB_CONN.execute(
|
|
"SELECT id FROM reports WHERE post_id=? AND reporter_id=?", (post_id, user_id)
|
|
).fetchone() is not None
|
|
|
|
def report_count(post_id):
|
|
return DB_CONN.execute(
|
|
"SELECT COUNT(*) FROM reports WHERE post_id=?", (post_id,)
|
|
).fetchone()[0]
|
|
|
|
def user_vote(post_id, user_id):
|
|
row = DB_CONN.execute(
|
|
"SELECT direction FROM votes WHERE post_id=? AND user_id=?", (post_id, user_id)
|
|
).fetchone()
|
|
return row["direction"] if row else None
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 9. ROUTES
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@app.route("/join", methods=["GET", "POST"])
|
|
def join():
|
|
if session.get("user_id"):
|
|
return redirect(url_for("square"))
|
|
|
|
error = ""
|
|
if request.method == "POST":
|
|
username = request.form.get("username", "").strip()
|
|
profile_key = request.form.get("profile_key", "")
|
|
friend_key = request.form.get("friend_key", "")
|
|
|
|
if not username:
|
|
error = "Username required."
|
|
elif len(profile_key) < 2:
|
|
error = "Profile Key must be 2 emoji."
|
|
elif len(friend_key) < 2:
|
|
error = "Friend Key must be 2 emoji."
|
|
else:
|
|
existing = DB_CONN.execute("SELECT id FROM users WHERE username=?", (username,)).fetchone()
|
|
if existing:
|
|
error = "Username taken."
|
|
else:
|
|
cur = DB_CONN.execute(
|
|
"INSERT INTO users (session_label, username) VALUES (?, ?)",
|
|
("?", username)
|
|
)
|
|
DB_CONN.commit()
|
|
uid = cur.lastrowid
|
|
label = f"Anon #{uid}"
|
|
DB_CONN.execute("UPDATE users SET session_label=? WHERE id=?", (label, uid))
|
|
DB_CONN.execute(
|
|
"INSERT INTO keys (user_id, key_type, emoji_code) VALUES (?, 'PROFILE', ?)",
|
|
(uid, profile_key)
|
|
)
|
|
DB_CONN.execute(
|
|
"INSERT INTO keys (user_id, key_type, emoji_code) VALUES (?, 'FRIEND', ?)",
|
|
(uid, friend_key)
|
|
)
|
|
DB_CONN.commit()
|
|
session["user_id"] = uid
|
|
session["show_default_key"] = True
|
|
return redirect(url_for("square"))
|
|
|
|
error_html = f'<div class="banner error">{escape(error)}</div>' if error else ""
|
|
content = f"""
|
|
{error_html}
|
|
<div class="card">
|
|
<h2>JOIN THE SQUARE</h2>
|
|
<p style="color:#886699;font-size:0.85em;margin-bottom:12px;">Choose a secret username. Pick two emoji keys.</p>
|
|
<form method="POST">
|
|
<div class="form-row">
|
|
<label>USERNAME (secret — share only with friends)</label>
|
|
<input type="text" name="username" placeholder="e.g. CoolPenguin42" autocomplete="off" autocorrect="off" autocapitalize="off">
|
|
</div>
|
|
<div class="form-row">
|
|
<label>PROFILE KEY (2 emoji — protects your notes)</label>
|
|
{emoji_picker("profile_key", "pk_field", max_len=2)}
|
|
</div>
|
|
<div class="form-row">
|
|
<label>FRIEND KEY (2 emoji — for handshakes)</label>
|
|
{emoji_picker("friend_key", "fk_field", max_len=2)}
|
|
</div>
|
|
<button type="submit" class="btn" style="width:100%;min-height:56px;">JOIN</button>
|
|
</form>
|
|
</div>
|
|
"""
|
|
return base_page("Join", content)
|
|
|
|
|
|
@app.route("/", methods=["GET", "POST"])
|
|
def square():
|
|
user = get_current_user()
|
|
if not user:
|
|
return redirect(url_for("join"))
|
|
|
|
error = ""
|
|
success = ""
|
|
|
|
# Show default key on first join
|
|
if session.pop("show_default_key", False):
|
|
success = f"Welcome, {escape(user['session_label'])}! Default Post Key: {DEFAULT_POST_KEY}"
|
|
|
|
if request.method == "POST":
|
|
content = request.form.get("content", "").strip()
|
|
post_key = request.form.get("post_key", "")
|
|
tier = user["post_tier"]
|
|
|
|
valid_key = False
|
|
if tier == "ADMIN":
|
|
valid_key = (post_key == ADMIN_POST_KEY)
|
|
elif tier == "TRUSTED":
|
|
valid_key = (post_key == TRUSTED_POST_KEY)
|
|
elif tier == "DEFAULT":
|
|
valid_key = (post_key == DEFAULT_POST_KEY)
|
|
elif tier == "RESTRICTED":
|
|
admin_key = request.form.get("admin_key", "")
|
|
valid_key = (admin_key == ADMIN_POST_KEY)
|
|
|
|
if not content:
|
|
error = "Post content required."
|
|
elif not valid_key:
|
|
error = "Post key incorrect."
|
|
else:
|
|
DB_CONN.execute(
|
|
"INSERT INTO posts (author_id, content) VALUES (?, ?)",
|
|
(user["id"], content)
|
|
)
|
|
DB_CONN.commit()
|
|
return redirect(url_for("square"))
|
|
|
|
friends_only = request.args.get("friends_only") == "1"
|
|
friend_ids = get_friends(user["id"])
|
|
|
|
if friends_only:
|
|
if not friend_ids:
|
|
raw_posts = []
|
|
else:
|
|
placeholders = ",".join("?" * len(friend_ids))
|
|
raw_posts = DB_CONN.execute(
|
|
f"SELECT * FROM posts WHERE author_id IN ({placeholders}) ORDER BY created_at DESC",
|
|
friend_ids
|
|
).fetchall()
|
|
else:
|
|
raw_posts = DB_CONN.execute(
|
|
"SELECT * FROM posts ORDER BY created_at DESC"
|
|
).fetchall()
|
|
|
|
ordered = order_feed(raw_posts)
|
|
|
|
posts_html = ""
|
|
for i, post in enumerate(ordered):
|
|
author = DB_CONN.execute("SELECT * FROM users WHERE id=?", (post["author_id"],)).fetchone()
|
|
is_friend = post["author_id"] in friend_ids
|
|
is_own = post["author_id"] == user["id"]
|
|
display_name = escape(author["username"]) if (is_friend or is_own) else escape(author["session_label"])
|
|
|
|
pin_class = ""
|
|
if i == 0 and len(ordered) > 1:
|
|
pin_class = " pinned-top"
|
|
elif i == len(ordered) - 1 and len(ordered) > 1:
|
|
pin_class = " pinned-bottom"
|
|
|
|
rcount = report_count(post["id"])
|
|
already_reported = has_reported(post["id"], user["id"])
|
|
my_vote = user_vote(post["id"], user["id"])
|
|
|
|
score_val = post["score"]
|
|
score_cls = "score neg" if score_val < 0 else "score"
|
|
|
|
content_escaped = escape(post["content"])
|
|
if rcount > 0:
|
|
content_html = f'<span class="spoiler" onclick="this.classList.toggle(\'revealed\')">[REPORTED — click to reveal] {content_escaped}</span>'
|
|
else:
|
|
content_html = str(content_escaped)
|
|
|
|
up_active = "active" if my_vote == 1 else ""
|
|
down_active = "active" if my_vote == -1 else ""
|
|
|
|
report_btn = ""
|
|
if not is_own:
|
|
if already_reported:
|
|
report_btn = '<span class="btn small" style="opacity:0.4;cursor:default;">reported</span>'
|
|
else:
|
|
report_btn = f'''<form method="POST" action="/report" style="display:inline">
|
|
<input type="hidden" name="post_id" value="{post['id']}">
|
|
<input type="hidden" name="friends_only" value="{'1' if friends_only else '0'}">
|
|
<button type="submit" class="btn small danger">report</button>
|
|
</form>'''
|
|
|
|
posts_html += f"""
|
|
<div class="card{pin_class}">
|
|
<div class="post-meta">{display_name} · <span class="{score_cls}">{score_val:+d}</span></div>
|
|
<div class="post-content">{content_html}</div>
|
|
<div style="display:flex;gap:4px;flex-wrap:wrap;align-items:center;">
|
|
<form method="POST" action="/vote" style="display:inline">
|
|
<input type="hidden" name="post_id" value="{post['id']}">
|
|
<input type="hidden" name="direction" value="1">
|
|
<input type="hidden" name="friends_only" value="{'1' if friends_only else '0'}">
|
|
<button type="submit" class="btn small {up_active}">▲ up</button>
|
|
</form>
|
|
<form method="POST" action="/vote" style="display:inline">
|
|
<input type="hidden" name="post_id" value="{post['id']}">
|
|
<input type="hidden" name="direction" value="-1">
|
|
<input type="hidden" name="friends_only" value="{'1' if friends_only else '0'}">
|
|
<button type="submit" class="btn small {down_active}">▼ down</button>
|
|
</form>
|
|
{report_btn}
|
|
</div>
|
|
</div>"""
|
|
|
|
if not posts_html:
|
|
posts_html = '<div class="card"><p style="color:#886699;">No posts yet.</p></div>'
|
|
|
|
tier = user["post_tier"]
|
|
if tier == "ADMIN":
|
|
key_label = "Admin Post Key (4 emoji)"
|
|
key_picker = emoji_picker("post_key", "postkey_field", max_len=4)
|
|
extra_field = ""
|
|
elif tier == "TRUSTED":
|
|
key_label = "Trusted Post Key (2 emoji)"
|
|
key_picker = emoji_picker("post_key", "postkey_field", max_len=2)
|
|
extra_field = ""
|
|
elif tier == "DEFAULT":
|
|
key_label = "Default Post Key (2 emoji)"
|
|
key_picker = emoji_picker("post_key", "postkey_field", max_len=2)
|
|
extra_field = ""
|
|
else: # RESTRICTED
|
|
key_label = "Admin Co-Sign Key (4 emoji — admin must approve)"
|
|
key_picker = emoji_picker("admin_key", "postkey_field", max_len=4)
|
|
extra_field = '<input type="hidden" name="post_key" value="">'
|
|
|
|
fo_toggle_url = url_for("square") + ("" if friends_only else "?friends_only=1")
|
|
fo_label = "★ Friends Only (ON)" if friends_only else "★ Friends Only (OFF)"
|
|
fo_active = "active" if friends_only else ""
|
|
|
|
success_html = f'<div class="banner">{escape(success)}</div>' if success else ""
|
|
error_html = f'<div class="banner error">{escape(error)}</div>' if error else ""
|
|
|
|
content = f"""
|
|
{success_html}{error_html}
|
|
<div class="friends-toggle">
|
|
<a href="{fo_toggle_url}" class="btn {fo_active}">{fo_label}</a>
|
|
</div>
|
|
<div class="card">
|
|
<h2>POST TO THE SQUARE</h2>
|
|
<form method="POST">
|
|
{extra_field}
|
|
<div class="form-row">
|
|
<textarea name="content" placeholder="What's on your mind?" rows="3"></textarea>
|
|
</div>
|
|
<div class="form-row">
|
|
<label>{escape(key_label)}</label>
|
|
{key_picker}
|
|
</div>
|
|
<button type="submit" class="btn" style="width:100%;min-height:56px;">POST</button>
|
|
</form>
|
|
</div>
|
|
<div class="section">
|
|
<h2>{'FRIENDS FEED' if friends_only else 'THE SQUARE'}</h2>
|
|
{posts_html}
|
|
</div>
|
|
"""
|
|
return base_page("The Square", content, current_user=user)
|
|
|
|
|
|
@app.route("/vote", methods=["POST"])
|
|
def vote():
|
|
user = get_current_user()
|
|
if not user:
|
|
return redirect(url_for("join"))
|
|
|
|
post_id = int(request.form.get("post_id", 0))
|
|
direction = int(request.form.get("direction", 1))
|
|
friends_only = request.form.get("friends_only", "0")
|
|
if direction not in (1, -1):
|
|
return redirect(url_for("square"))
|
|
|
|
weight = vote_weight(user["post_tier"])
|
|
existing = DB_CONN.execute(
|
|
"SELECT id, direction FROM votes WHERE post_id=? AND user_id=?",
|
|
(post_id, user["id"])
|
|
).fetchone()
|
|
|
|
if existing:
|
|
if existing["direction"] == direction:
|
|
DB_CONN.execute("DELETE FROM votes WHERE post_id=? AND user_id=?", (post_id, user["id"]))
|
|
else:
|
|
DB_CONN.execute(
|
|
"UPDATE votes SET direction=?, weight=? WHERE post_id=? AND user_id=?",
|
|
(direction, weight, post_id, user["id"])
|
|
)
|
|
else:
|
|
DB_CONN.execute(
|
|
"INSERT INTO votes (post_id, user_id, direction, weight) VALUES (?, ?, ?, ?)",
|
|
(post_id, user["id"], direction, weight)
|
|
)
|
|
|
|
DB_CONN.execute(
|
|
"UPDATE posts SET score=(SELECT COALESCE(SUM(direction*weight),0) FROM votes WHERE post_id=?) WHERE id=?",
|
|
(post_id, post_id)
|
|
)
|
|
DB_CONN.commit()
|
|
|
|
redirect_url = url_for("square") + ("?friends_only=1" if friends_only == "1" else "")
|
|
return redirect(redirect_url)
|
|
|
|
|
|
@app.route("/report", methods=["POST"])
|
|
def report():
|
|
user = get_current_user()
|
|
if not user:
|
|
return redirect(url_for("join"))
|
|
|
|
post_id = int(request.form.get("post_id", 0))
|
|
post = DB_CONN.execute("SELECT * FROM posts WHERE id=?", (post_id,)).fetchone()
|
|
if not post:
|
|
return redirect(url_for("square"))
|
|
if post["author_id"] == user["id"]:
|
|
return redirect(url_for("square"))
|
|
|
|
try:
|
|
DB_CONN.execute(
|
|
"INSERT INTO reports (post_id, reporter_id) VALUES (?, ?)",
|
|
(post_id, user["id"])
|
|
)
|
|
DB_CONN.execute(
|
|
"INSERT INTO notifications (user_id, message) VALUES (?, ?)",
|
|
(post["author_id"], "One of your posts has been reported.")
|
|
)
|
|
DB_CONN.commit()
|
|
except sqlite3.IntegrityError:
|
|
pass
|
|
|
|
check_auto_restrict(post["author_id"])
|
|
return redirect(url_for("square"))
|
|
|
|
|
|
@app.route("/profile", methods=["GET", "POST"])
|
|
def profile():
|
|
user = get_current_user()
|
|
if not user:
|
|
return redirect(url_for("join"))
|
|
|
|
message = ""
|
|
error = ""
|
|
|
|
# Consume handshake message from redirect
|
|
hs_msg = session.pop("handshake_msg", None)
|
|
if hs_msg:
|
|
kind, text = hs_msg
|
|
if kind == "error":
|
|
error = text
|
|
else:
|
|
message = text
|
|
|
|
if request.method == "POST":
|
|
action = request.form.get("action", "")
|
|
prof_key_submitted = request.form.get("profile_key_input", "")
|
|
stored_key = get_profile_key(user["id"])
|
|
|
|
if action == "edit_notes":
|
|
notes = request.form.get("notes", "")
|
|
if prof_key_submitted != stored_key:
|
|
error = "Profile Key incorrect."
|
|
else:
|
|
DB_CONN.execute("UPDATE users SET profile_notes=? WHERE id=?", (notes, user["id"]))
|
|
DB_CONN.commit()
|
|
message = "Notes saved."
|
|
user = get_current_user()
|
|
|
|
elif action == "toggle_visibility":
|
|
if prof_key_submitted != stored_key:
|
|
error = "Profile Key incorrect."
|
|
else:
|
|
new_vis = 0 if user["notes_visible"] else 1
|
|
DB_CONN.execute("UPDATE users SET notes_visible=? WHERE id=?", (new_vis, user["id"]))
|
|
DB_CONN.commit()
|
|
message = "Visibility updated."
|
|
user = get_current_user()
|
|
|
|
# Reload user
|
|
user = get_current_user()
|
|
friend_key_val = get_friend_key(user["id"])
|
|
profile_key_val = get_profile_key(user["id"])
|
|
|
|
# Confirmed friends
|
|
friend_ids = get_friends(user["id"])
|
|
friends_html = ""
|
|
if friend_ids:
|
|
for fid in friend_ids:
|
|
fr = DB_CONN.execute("SELECT session_label, username FROM users WHERE id=?", (fid,)).fetchone()
|
|
friends_html += f'<div class="card" style="padding:8px;">{escape(fr["username"])} ({escape(fr["session_label"])})</div>'
|
|
else:
|
|
friends_html = '<p style="color:#886699;">No confirmed friends yet.</p>'
|
|
|
|
# All users for handshake dropdown (excluding self)
|
|
all_users = DB_CONN.execute(
|
|
"SELECT id, username, session_label FROM users WHERE id != ? ORDER BY session_label",
|
|
(user["id"],)
|
|
).fetchall()
|
|
user_options = "".join(
|
|
f'<option value="{escape(u["username"])}">{escape(u["username"])}</option>'
|
|
for u in all_users
|
|
)
|
|
|
|
vis_label = "PUBLIC" if user["notes_visible"] else "PRIVATE"
|
|
message_html = f'<div class="banner">{escape(message)}</div>' if message else ""
|
|
error_html = f'<div class="banner error">{escape(error)}</div>' if error else ""
|
|
|
|
content = f"""
|
|
{message_html}{error_html}
|
|
<div class="card section">
|
|
<h2>YOUR IDENTITY</h2>
|
|
<table>
|
|
<tr><th>Session Label</th><td>{escape(user['session_label'])}</td></tr>
|
|
<tr><th>Username</th><td>{escape(user['username'])}</td></tr>
|
|
<tr><th>Tier</th><td>{tier_badge(user['post_tier'])}</td></tr>
|
|
<tr><th>Friend Key</th><td style="font-size:1.4em;letter-spacing:2px;">{escape(friend_key_val)}</td></tr>
|
|
<tr><th>Profile Key</th><td style="font-size:1.4em;letter-spacing:2px;">{escape(profile_key_val)}</td></tr>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="card section">
|
|
<h2>YOUR NOTES <span style="font-size:0.8em;color:#886699;">[{vis_label}]</span></h2>
|
|
<form method="POST">
|
|
<input type="hidden" name="action" value="edit_notes">
|
|
<textarea name="notes" rows="4">{escape(user['profile_notes'])}</textarea>
|
|
<label>Profile Key (to save)</label>
|
|
{emoji_picker("profile_key_input", "pk_edit_field", max_len=2)}
|
|
<button type="submit" class="btn" style="width:100%;">SAVE NOTES</button>
|
|
</form>
|
|
<form method="POST" style="margin-top:8px;">
|
|
<input type="hidden" name="action" value="toggle_visibility">
|
|
<label>Profile Key (to toggle visibility)</label>
|
|
{emoji_picker("profile_key_input", "pk_toggle_field", max_len=2)}
|
|
<button type="submit" class="btn" style="width:100%;">TOGGLE VISIBILITY (now: {vis_label})</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="card section">
|
|
<h2>CONFIRMED FRIENDS</h2>
|
|
{friends_html}
|
|
</div>
|
|
|
|
<div class="card section">
|
|
<h2>HANDSHAKE</h2>
|
|
<p style="color:#886699;font-size:0.85em;margin-bottom:10px;">Exchange usernames and Friend Keys in person, then submit here.</p>
|
|
<form method="POST" action="/handshake">
|
|
<div class="form-row">
|
|
<label>THEIR USERNAME</label>
|
|
<select name="target_username" style="background:#0a0a1a;border:1px solid #442255;color:#c8c8d8;font-family:inherit;font-size:0.95em;padding:10px;width:100%;min-height:48px;">
|
|
<option value="">-- select --</option>
|
|
{user_options}
|
|
</select>
|
|
</div>
|
|
<div class="form-row">
|
|
<label>YOUR FRIEND KEY (2 emoji)</label>
|
|
{emoji_picker("my_friend_key", "hs_my_field", max_len=2)}
|
|
</div>
|
|
<div class="form-row">
|
|
<label>THEIR FRIEND KEY (2 emoji)</label>
|
|
{emoji_picker("their_friend_key", "hs_their_field", max_len=2)}
|
|
</div>
|
|
<button type="submit" class="btn" style="width:100%;min-height:56px;">SUBMIT HANDSHAKE</button>
|
|
</form>
|
|
</div>
|
|
"""
|
|
return base_page("Profile", content, current_user=user)
|
|
|
|
|
|
@app.route("/handshake", methods=["POST"])
|
|
def handshake():
|
|
user = get_current_user()
|
|
if not user:
|
|
return redirect(url_for("join"))
|
|
|
|
target_username = request.form.get("target_username", "")
|
|
my_friend_key = request.form.get("my_friend_key", "")
|
|
their_friend_key = request.form.get("their_friend_key", "")
|
|
|
|
target = DB_CONN.execute("SELECT * FROM users WHERE username=?", (target_username,)).fetchone()
|
|
valid = (
|
|
target is not None
|
|
and target["id"] != user["id"]
|
|
and my_friend_key == get_friend_key(user["id"])
|
|
and their_friend_key == get_friend_key(target["id"])
|
|
)
|
|
|
|
if not valid:
|
|
session["handshake_msg"] = ("error", "One or both keys are incorrect.")
|
|
return redirect(url_for("profile"))
|
|
|
|
submitted_code = my_friend_key + their_friend_key
|
|
try:
|
|
DB_CONN.execute(
|
|
"INSERT INTO handshakes (from_user_id, to_user_id, submitted_code) VALUES (?, ?, ?)",
|
|
(user["id"], target["id"], submitted_code)
|
|
)
|
|
DB_CONN.commit()
|
|
except sqlite3.IntegrityError:
|
|
pass
|
|
|
|
if are_friends(user["id"], target["id"]):
|
|
session["handshake_msg"] = ("success", f"Friendship confirmed with {target['username']}!")
|
|
else:
|
|
session["handshake_msg"] = ("info", f"Your side submitted. Waiting for {escape(target['session_label'])} to complete the handshake.")
|
|
|
|
return redirect(url_for("profile"))
|
|
|
|
|
|
@app.route("/admin", methods=["GET", "POST"])
|
|
def admin():
|
|
error = ""
|
|
if request.method == "POST" and not session.get("admin_authed"):
|
|
submitted = request.form.get("admin_key", "")
|
|
if submitted == ADMIN_POST_KEY:
|
|
session["admin_authed"] = True
|
|
else:
|
|
error = "Incorrect Admin Key."
|
|
|
|
if not session.get("admin_authed"):
|
|
error_html = f'<div class="banner error">{escape(error)}</div>' if error else ""
|
|
content = f"""
|
|
{error_html}
|
|
<div class="card">
|
|
<h2>ADMIN LOGIN</h2>
|
|
<form method="POST">
|
|
<div class="form-row">
|
|
<label>Admin Post Key (4 emoji)</label>
|
|
{emoji_picker("admin_key", "admin_login_key", max_len=4)}
|
|
</div>
|
|
<button type="submit" class="btn" style="width:100%;min-height:56px;">LOGIN</button>
|
|
</form>
|
|
</div>"""
|
|
return base_page("Admin Login", content)
|
|
|
|
# Full admin panel
|
|
users = DB_CONN.execute("SELECT * FROM users ORDER BY id").fetchall()
|
|
posts = DB_CONN.execute("SELECT * FROM posts ORDER BY id DESC").fetchall()
|
|
votes = DB_CONN.execute("SELECT * FROM votes ORDER BY id DESC LIMIT 50").fetchall()
|
|
reports_all = DB_CONN.execute("SELECT * FROM reports ORDER BY id DESC LIMIT 50").fetchall()
|
|
keys_all = DB_CONN.execute("SELECT * FROM keys ORDER BY id").fetchall()
|
|
|
|
users_html = ""
|
|
for u in users:
|
|
tier = u["post_tier"]
|
|
users_html += f"""
|
|
<tr>
|
|
<td>{escape(u['session_label'])}</td>
|
|
<td>{escape(u['username'])}</td>
|
|
<td>{tier_badge(tier)}</td>
|
|
<td style="white-space:nowrap;">
|
|
<form method="POST" action="/admin/action" style="display:inline">
|
|
<input type="hidden" name="target_id" value="{u['id']}">
|
|
<input type="hidden" name="action" value="restrict">
|
|
<button class="btn small danger">Restrict</button>
|
|
</form>
|
|
<form method="POST" action="/admin/action" style="display:inline">
|
|
<input type="hidden" name="target_id" value="{u['id']}">
|
|
<input type="hidden" name="action" value="unrestrict">
|
|
<button class="btn small">Unrestrict</button>
|
|
</form>
|
|
<form method="POST" action="/admin/action" style="display:inline">
|
|
<input type="hidden" name="target_id" value="{u['id']}">
|
|
<input type="hidden" name="action" value="promote_trusted">
|
|
<button class="btn small">Promote</button>
|
|
</form>
|
|
<form method="POST" action="/admin/action" style="display:inline">
|
|
<input type="hidden" name="target_id" value="{u['id']}">
|
|
<input type="hidden" name="action" value="demote_default">
|
|
<button class="btn small">Demote</button>
|
|
</form>
|
|
</td>
|
|
</tr>"""
|
|
|
|
qr_btn_label = "HIDE QR" if QR_VISIBLE else "SHOW QR"
|
|
qr_img_html = f'<img src="/qr" style="border:2px solid #cc44ff;max-width:200px;display:block;margin:8px 0;" alt="QR">' if QR_VISIBLE else ""
|
|
|
|
posts_html = "".join(
|
|
f"<tr><td>{p['id']}</td><td>{escape(p['content'][:60])}</td><td>{p['author_id']}</td><td>{p['score']}</td></tr>"
|
|
for p in posts
|
|
)
|
|
votes_html = "".join(
|
|
f"<tr><td>{v['id']}</td><td>{v['post_id']}</td><td>{v['user_id']}</td><td>{v['direction']}</td><td>{v['weight']}</td></tr>"
|
|
for v in votes
|
|
)
|
|
reports_html = "".join(
|
|
f"<tr><td>{r['id']}</td><td>{r['post_id']}</td><td>{r['reporter_id']}</td></tr>"
|
|
for r in reports_all
|
|
)
|
|
keys_html = "".join(
|
|
f"<tr><td>{k['id']}</td><td>{k['user_id']}</td><td>{k['key_type']}</td><td style='font-size:1.3em;letter-spacing:2px;'>{escape(k['emoji_code'])}</td></tr>"
|
|
for k in keys_all
|
|
)
|
|
|
|
content = f"""
|
|
<div class="card section">
|
|
<h2>CURRENT KEYS</h2>
|
|
<table>
|
|
<tr><th>Admin Post Key</th><td style="font-size:1.5em;letter-spacing:3px;">{escape(ADMIN_POST_KEY)}</td></tr>
|
|
<tr><th>Trusted Post Key</th><td style="font-size:1.5em;letter-spacing:3px;">{escape(TRUSTED_POST_KEY)}</td></tr>
|
|
<tr><th>Default Post Key</th><td style="font-size:1.5em;letter-spacing:3px;color:#ff44cc;">{escape(DEFAULT_POST_KEY)}</td></tr>
|
|
</table>
|
|
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:8px;">
|
|
<form method="POST" action="/admin/action">
|
|
<input type="hidden" name="action" value="rotate_default_key">
|
|
<button class="btn danger">ROTATE DEFAULT KEY</button>
|
|
</form>
|
|
<form method="POST" action="/admin/action">
|
|
<input type="hidden" name="action" value="toggle_qr">
|
|
<button class="btn">{qr_btn_label}</button>
|
|
</form>
|
|
<form method="POST" action="/admin/action">
|
|
<input type="hidden" name="action" value="flood_attack">
|
|
<button class="btn danger">FLOOD ATTACK</button>
|
|
</form>
|
|
</div>
|
|
{qr_img_html}
|
|
</div>
|
|
|
|
<div class="card section">
|
|
<h2>USERS</h2>
|
|
<table>
|
|
<tr><th>Label</th><th>Username</th><th>Tier</th><th>Actions</th></tr>
|
|
{users_html}
|
|
</table>
|
|
</div>
|
|
|
|
<div class="card section">
|
|
<h2>RAW DB — POSTS</h2>
|
|
<table>
|
|
<tr><th>id</th><th>content</th><th>author</th><th>score</th></tr>
|
|
{posts_html}
|
|
</table>
|
|
</div>
|
|
|
|
<div class="card section">
|
|
<h2>RAW DB — VOTES (last 50)</h2>
|
|
<table>
|
|
<tr><th>id</th><th>post</th><th>user</th><th>dir</th><th>wt</th></tr>
|
|
{votes_html}
|
|
</table>
|
|
</div>
|
|
|
|
<div class="card section">
|
|
<h2>RAW DB — REPORTS (last 50)</h2>
|
|
<table>
|
|
<tr><th>id</th><th>post</th><th>reporter</th></tr>
|
|
{reports_html}
|
|
</table>
|
|
</div>
|
|
|
|
<div class="card section">
|
|
<h2>RAW DB — KEYS</h2>
|
|
<table>
|
|
<tr><th>id</th><th>user</th><th>type</th><th>code</th></tr>
|
|
{keys_html}
|
|
</table>
|
|
</div>
|
|
"""
|
|
|
|
# ── Platform View ────────────────────────────────────────────────────────
|
|
doxx_rows = DB_CONN.execute("""
|
|
SELECT u.id, u.session_label, u.username, u.post_tier,
|
|
COUNT(DISTINCT p.id) AS post_count,
|
|
COALESCE(SUM(p.score), 0) AS net_score
|
|
FROM users u
|
|
LEFT JOIN posts p ON p.author_id = u.id
|
|
GROUP BY u.id
|
|
ORDER BY u.id
|
|
""").fetchall()
|
|
|
|
hs_rows = DB_CONN.execute("""
|
|
SELECT h.from_user_id, h.to_user_id,
|
|
u1.session_label AS from_label,
|
|
u2.session_label AS to_label,
|
|
u1.username AS from_name,
|
|
u2.username AS to_name
|
|
FROM handshakes h
|
|
JOIN users u1 ON h.from_user_id = u1.id
|
|
JOIN users u2 ON h.to_user_id = u2.id
|
|
ORDER BY h.id
|
|
""").fetchall()
|
|
|
|
# Build directed-pair set once; check both directions for CONFIRMED
|
|
hs_pairs = {(r["from_user_id"], r["to_user_id"]) for r in hs_rows}
|
|
seen_pairs = set()
|
|
graph_rows = []
|
|
for r in hs_rows:
|
|
a, b = r["from_user_id"], r["to_user_id"]
|
|
key = (min(a, b), max(a, b))
|
|
if key in seen_pairs:
|
|
continue
|
|
seen_pairs.add(key)
|
|
confirmed = (a, b) in hs_pairs and (b, a) in hs_pairs
|
|
graph_rows.append((r, confirmed))
|
|
|
|
def is_friends_pair(id_a, id_b):
|
|
return (id_a, id_b) in hs_pairs and (id_b, id_a) in hs_pairs
|
|
|
|
vote_corr = DB_CONN.execute("""
|
|
SELECT v.user_id AS voter_id,
|
|
uv.session_label AS voter_label,
|
|
uv.username AS voter_name,
|
|
p.author_id,
|
|
ua.session_label AS author_label,
|
|
ua.username AS author_name,
|
|
SUM(v.direction * v.weight) AS net
|
|
FROM votes v
|
|
JOIN posts p ON v.post_id = p.id
|
|
JOIN users uv ON v.user_id = uv.id
|
|
JOIN users ua ON p.author_id = ua.id
|
|
WHERE v.user_id != p.author_id
|
|
GROUP BY v.user_id, p.author_id
|
|
ORDER BY ABS(SUM(v.direction * v.weight)) DESC
|
|
""").fetchall()
|
|
|
|
report_corr = DB_CONN.execute("""
|
|
SELECT r.reporter_id,
|
|
ur.session_label AS reporter_label,
|
|
ur.username AS reporter_name,
|
|
p.author_id,
|
|
ua.session_label AS author_label,
|
|
ua.username AS author_name,
|
|
COUNT(*) AS count
|
|
FROM reports r
|
|
JOIN posts p ON r.post_id = p.id
|
|
JOIN users ur ON r.reporter_id = ur.id
|
|
JOIN users ua ON p.author_id = ua.id
|
|
WHERE r.reporter_id != p.author_id
|
|
GROUP BY r.reporter_id, p.author_id
|
|
ORDER BY COUNT(*) DESC
|
|
""").fetchall()
|
|
|
|
# Build HTML for Panel 1
|
|
doxx_html = ""
|
|
for r in doxx_rows:
|
|
net = r["net_score"]
|
|
net_cls = "neg" if net < 0 else ""
|
|
doxx_html += f"""<tr>
|
|
<td>{escape(r['session_label'])}</td>
|
|
<td>{escape(r['username'])}</td>
|
|
<td>{tier_badge(r['post_tier'])}</td>
|
|
<td>{r['post_count']}</td>
|
|
<td class="score {net_cls}">{net:+d}</td>
|
|
</tr>"""
|
|
|
|
# Build HTML for Panel 2
|
|
graph_html = ""
|
|
for r, confirmed in graph_rows:
|
|
status_color = "#44ccff" if confirmed else "#886699"
|
|
status_text = "CONFIRMED" if confirmed else "PENDING"
|
|
graph_html += f"""<tr>
|
|
<td>{escape(r['from_label'])} · {escape(r['from_name'])}</td>
|
|
<td>{escape(r['to_label'])} · {escape(r['to_name'])}</td>
|
|
<td style="color:{status_color};font-weight:bold;">{status_text}</td>
|
|
</tr>"""
|
|
|
|
# Build HTML for Panel 3 — votes
|
|
vote_corr_html = ""
|
|
for r in vote_corr:
|
|
net = r["net"]
|
|
net_cls = "neg" if net < 0 else ""
|
|
friends_badge = ' <span style="color:#44ccff;font-size:0.8em;">[FRIENDS]</span>' \
|
|
if is_friends_pair(r["voter_id"], r["author_id"]) else ""
|
|
vote_corr_html += f"""<tr>
|
|
<td>{escape(r['voter_label'])} · {escape(r['voter_name'])}</td>
|
|
<td>{escape(r['author_label'])} · {escape(r['author_name'])}</td>
|
|
<td class="score {net_cls}">{net:+d}</td>
|
|
<td>{friends_badge}</td>
|
|
</tr>"""
|
|
|
|
# Build HTML for Panel 3 — reports
|
|
report_corr_html = ""
|
|
for r in report_corr:
|
|
friends_badge = ' <span style="color:#44ccff;font-size:0.8em;">[FRIENDS]</span>' \
|
|
if is_friends_pair(r["reporter_id"], r["author_id"]) else ""
|
|
count_cls = "tier-restricted" if r["count"] >= 3 else "tier-default"
|
|
report_corr_html += f"""<tr>
|
|
<td>{escape(r['reporter_label'])} · {escape(r['reporter_name'])}</td>
|
|
<td>{escape(r['author_label'])} · {escape(r['author_name'])}</td>
|
|
<td class="{count_cls}">{r['count']}</td>
|
|
<td>{friends_badge}</td>
|
|
</tr>"""
|
|
|
|
platform_view_html = f"""
|
|
<h2 style="color:#ff44cc;border-top:2px solid #cc44ff;padding-top:12px;margin-top:8px;">
|
|
◈ WHAT THE PLATFORM SEES
|
|
</h2>
|
|
|
|
<div class="card section">
|
|
<h2>DEANONYMIZATION — WHO IS WHO</h2>
|
|
<table>
|
|
<tr><th>Session Label</th><th>Username</th><th>Tier</th><th>Posts</th><th>Net Score</th></tr>
|
|
{doxx_html}
|
|
</table>
|
|
</div>
|
|
|
|
<div class="card section">
|
|
<h2>SOCIAL GRAPH — ALL CONNECTIONS</h2>
|
|
<table>
|
|
<tr><th>User A</th><th>User B</th><th>Status</th></tr>
|
|
{graph_html}
|
|
</table>
|
|
</div>
|
|
|
|
<div class="card section">
|
|
<h2>BEHAVIORAL CORRELATIONS — WHAT THE PLATFORM INFERS</h2>
|
|
<h3 style="color:#c8c8d8;margin-top:8px;">Votes</h3>
|
|
<table>
|
|
<tr><th>Voter</th><th>Author</th><th>Net Vote</th><th>Friends?</th></tr>
|
|
{vote_corr_html}
|
|
</table>
|
|
<h3 style="color:#c8c8d8;margin-top:16px;">Reports</h3>
|
|
<table>
|
|
<tr><th>Reporter</th><th>Author</th><th>Count</th><th>Friends?</th></tr>
|
|
{report_corr_html}
|
|
</table>
|
|
</div>
|
|
"""
|
|
content += platform_view_html
|
|
return base_page("Admin Panel", content)
|
|
|
|
|
|
@app.route("/admin/action", methods=["POST"])
|
|
@require_admin
|
|
def admin_action():
|
|
global DEFAULT_POST_KEY, QR_VISIBLE, QR_IMAGE_BYTES
|
|
action = request.form.get("action", "")
|
|
target_id = request.form.get("target_id", None)
|
|
|
|
if action == "restrict" and target_id:
|
|
restrict_user(int(target_id))
|
|
|
|
elif action == "unrestrict" and target_id:
|
|
DB_CONN.execute("UPDATE users SET post_tier='DEFAULT' WHERE id=?", (int(target_id),))
|
|
DB_CONN.commit()
|
|
|
|
elif action == "promote_trusted" and target_id:
|
|
DB_CONN.execute("UPDATE users SET post_tier='TRUSTED' WHERE id=?", (int(target_id),))
|
|
DB_CONN.commit()
|
|
|
|
elif action == "demote_default" and target_id:
|
|
DB_CONN.execute("UPDATE users SET post_tier='DEFAULT' WHERE id=?", (int(target_id),))
|
|
DB_CONN.commit()
|
|
|
|
elif action == "rotate_default_key":
|
|
DEFAULT_POST_KEY = generate_emoji_key(2)
|
|
|
|
elif action == "toggle_qr":
|
|
QR_VISIBLE = not QR_VISIBLE
|
|
|
|
elif action == "flood_attack":
|
|
for i in range(5):
|
|
bot_name = f"Bot_{os.urandom(2).hex()}"
|
|
cur = DB_CONN.execute(
|
|
"INSERT INTO users (session_label, username, post_tier) VALUES (?, ?, 'DEFAULT')",
|
|
("?", bot_name)
|
|
)
|
|
DB_CONN.commit()
|
|
bot_id = cur.lastrowid
|
|
bot_label = f"Anon #{bot_id}"
|
|
DB_CONN.execute("UPDATE users SET session_label=? WHERE id=?", (bot_label, bot_id))
|
|
flood_msgs = [
|
|
"SYSTEM NOTICE: All users must re-register.",
|
|
"FREE COINS click here [link removed by admin]",
|
|
"Did you know? Your data is being collected.",
|
|
"ALERT: Someone knows your real name.",
|
|
"Join the resistance. Ask me how.",
|
|
]
|
|
DB_CONN.execute(
|
|
"INSERT INTO posts (author_id, content) VALUES (?, ?)",
|
|
(bot_id, flood_msgs[i % len(flood_msgs)])
|
|
)
|
|
pk = generate_emoji_key(2)
|
|
fk = generate_emoji_key(2)
|
|
DB_CONN.execute("INSERT INTO keys (user_id, key_type, emoji_code) VALUES (?, 'PROFILE', ?)", (bot_id, pk))
|
|
DB_CONN.execute("INSERT INTO keys (user_id, key_type, emoji_code) VALUES (?, 'FRIEND', ?)", (bot_id, fk))
|
|
DB_CONN.commit()
|
|
|
|
return redirect(url_for("admin"))
|
|
|
|
|
|
@app.route("/qr")
|
|
def qr():
|
|
if not QR_VISIBLE:
|
|
return Response("Not found", status=404)
|
|
return Response(QR_IMAGE_BYTES, mimetype="image/png")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 10. ENTRY POINT
|
|
# ---------------------------------------------------------------------------
|
|
|
|
if __name__ == "__main__":
|
|
all_ips = get_all_local_ips()
|
|
print("=" * 50)
|
|
print("MESHAGORA v2.3 — SOCIAL SANDBOX")
|
|
print(f"Admin Post Key : {ADMIN_POST_KEY}")
|
|
print(f"Trusted Post Key : {TRUSTED_POST_KEY}")
|
|
print(f"Default Post Key : {DEFAULT_POST_KEY}")
|
|
print("-" * 50)
|
|
if all_ips:
|
|
first = True
|
|
for ip, iface in all_ips:
|
|
label = "Join URL :" if first else "Also try :"
|
|
first = False
|
|
print(f"{label} http://{ip}:5000/join ({iface})")
|
|
else:
|
|
print(f"Join URL : {JOIN_URL} (detection failed)")
|
|
print(f"QR code : http://{HOTSPOT_IP}:5000/qr")
|
|
print("If no URL works, run: ifconfig | grep inet")
|
|
print("=" * 50)
|
|
|
|
app.run(host="0.0.0.0", port=5000, debug=False, threaded=False)
|