|
import sqlite3
|
|
import hashlib
|
|
import time as timelib
|
|
import secrets
|
|
from typing import Optional, List
|
|
from datetime import datetime, timedelta
|
|
|
|
from fastapi import FastAPI, Header, HTTPException, Query
|
|
from fastapi.responses import HTMLResponse, FileResponse
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from pydantic import BaseModel, Field, validator
|
|
|
|
class PronounceableIDGenerator:
|
|
syllables = [
|
|
"ba","be","bi","bo","bu","da","de","di","do","du",
|
|
"fa","fe","fi","fo","fu","ga","ge","gi","go","gu",
|
|
"ha","he","hi","ho","hu","ja","je","ji","jo","ju",
|
|
"ka","ke","ki","ko","ku","la","le","li","lo","lu",
|
|
"ma","me","mi","mo","mu","na","ne","ni","no","nu",
|
|
"pa","pe","pi","po","pu","ra","re","ri","ro","ru",
|
|
"sa","se","si","so","su","ta","te","ti","to","tu",
|
|
"va","ve","vi","vo","vu","wa","we","wi","wo","wu",
|
|
"ya","ye","yi","yo","yu","za","ze","zi","zo","zu",
|
|
"cha","che","chi","cho","chu","sha","she","shi","sho","shu"
|
|
]
|
|
base = len(syllables)
|
|
key = 982451653
|
|
|
|
def __init__(self, start=0):
|
|
self.counter = start
|
|
|
|
def scramble(self, n):
|
|
return (n * self.key) % 100_000_000
|
|
|
|
def encode(self, n):
|
|
s = []
|
|
for _ in range(4):
|
|
s.append(self.syllables[n % self.base])
|
|
n //= self.base
|
|
return ''.join(reversed(s))
|
|
|
|
def next_id(self):
|
|
n = self.scramble(self.counter)
|
|
self.counter += 1
|
|
return self.encode(n)
|
|
|
|
class Database:
|
|
def __init__(self, db_path="community.db"):
|
|
self.db_path = db_path
|
|
self._id = 0
|
|
self.init_db()
|
|
|
|
def get_conn(self):
|
|
conn = sqlite3.connect(self.db_path)
|
|
conn.row_factory = sqlite3.Row
|
|
return conn
|
|
|
|
def init_db(self):
|
|
conn = self.get_conn()
|
|
c = conn.cursor()
|
|
|
|
c.execute("""CREATE TABLE IF NOT EXISTS uid (
|
|
counter INTEGER DEFAULT 0
|
|
)
|
|
""")
|
|
count = c.execute("SELECT counter FROM uid").fetchone()
|
|
if not count:
|
|
c.execute("INSERT INTO uid (counter) VALUES (0)")
|
|
|
|
c.execute("""CREATE TABLE IF NOT EXISTS sessions (
|
|
token TEXT PRIMARY KEY,
|
|
user_id INTEGER,
|
|
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
|
expires_at INTEGER,
|
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
)""")
|
|
|
|
c.execute("""CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at)""")
|
|
c.execute("""CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id)""")
|
|
|
|
c.execute("""CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT UNIQUE NOT NULL,
|
|
password TEXT NOT NULL,
|
|
karma INTEGER DEFAULT 0,
|
|
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
|
)""")
|
|
|
|
c.execute("""CREATE TABLE IF NOT EXISTS communities (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT UNIQUE NOT NULL,
|
|
description TEXT,
|
|
rules TEXT,
|
|
wiki TEXT,
|
|
sidebar TEXT,
|
|
creator_id INTEGER,
|
|
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
|
FOREIGN KEY (creator_id) REFERENCES users(id)
|
|
)""")
|
|
|
|
c.execute("""CREATE TABLE IF NOT EXISTS moderators (
|
|
community_id INTEGER,
|
|
user_id INTEGER,
|
|
PRIMARY KEY (community_id, user_id),
|
|
FOREIGN KEY (community_id) REFERENCES communities(id),
|
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
)""")
|
|
|
|
c.execute("""CREATE TABLE IF NOT EXISTS posts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
community_id INTEGER,
|
|
user_id INTEGER,
|
|
title TEXT NOT NULL,
|
|
content TEXT,
|
|
url TEXT,
|
|
post_type TEXT DEFAULT 'text',
|
|
flair TEXT,
|
|
nsfw INTEGER DEFAULT 0,
|
|
spoiler INTEGER DEFAULT 0,
|
|
sticky INTEGER DEFAULT 0,
|
|
locked INTEGER DEFAULT 0,
|
|
score INTEGER DEFAULT 0,
|
|
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
|
FOREIGN KEY (community_id) REFERENCES communities(id),
|
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
)""")
|
|
|
|
c.execute("""CREATE INDEX IF NOT EXISTS idx_posts_community ON posts(community_id)""")
|
|
c.execute("""CREATE INDEX IF NOT EXISTS idx_posts_user ON posts(user_id)""")
|
|
c.execute("""CREATE INDEX IF NOT EXISTS idx_posts_created ON posts(created_at)""")
|
|
c.execute("""CREATE INDEX IF NOT EXISTS idx_posts_score ON posts(score)""")
|
|
|
|
c.execute("""CREATE TABLE IF NOT EXISTS comments (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
post_id INTEGER,
|
|
parent_id INTEGER,
|
|
user_id INTEGER,
|
|
content TEXT NOT NULL,
|
|
score INTEGER DEFAULT 0,
|
|
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
|
FOREIGN KEY (post_id) REFERENCES posts(id),
|
|
FOREIGN KEY (parent_id) REFERENCES comments(id),
|
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
)""")
|
|
|
|
c.execute("""CREATE INDEX IF NOT EXISTS idx_comments_post ON comments(post_id)""")
|
|
c.execute("""CREATE INDEX IF NOT EXISTS idx_comments_user ON comments(user_id)""")
|
|
c.execute("""CREATE INDEX IF NOT EXISTS idx_comments_parent ON comments(parent_id)""")
|
|
|
|
c.execute("""CREATE TABLE IF NOT EXISTS votes (
|
|
user_id INTEGER,
|
|
target_type TEXT,
|
|
target_id INTEGER,
|
|
vote INTEGER,
|
|
PRIMARY KEY (user_id, target_type, target_id),
|
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
)""")
|
|
|
|
c.execute("""CREATE INDEX IF NOT EXISTS idx_votes_target ON votes(target_type, target_id)""")
|
|
|
|
c.execute("""CREATE TABLE IF NOT EXISTS subscriptions (
|
|
user_id INTEGER,
|
|
community_id INTEGER,
|
|
PRIMARY KEY (user_id, community_id),
|
|
FOREIGN KEY (user_id) REFERENCES users(id),
|
|
FOREIGN KEY (community_id) REFERENCES communities(id)
|
|
)""")
|
|
|
|
c.execute("""CREATE TABLE IF NOT EXISTS saved (
|
|
user_id INTEGER,
|
|
target_type TEXT,
|
|
target_id INTEGER,
|
|
PRIMARY KEY (user_id, target_type, target_id),
|
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
)""")
|
|
|
|
c.execute("""CREATE TABLE IF NOT EXISTS hidden (
|
|
user_id INTEGER,
|
|
post_id INTEGER,
|
|
PRIMARY KEY (user_id, post_id),
|
|
FOREIGN KEY (user_id) REFERENCES users(id),
|
|
FOREIGN KEY (post_id) REFERENCES posts(id)
|
|
)""")
|
|
|
|
c.execute("""CREATE TABLE IF NOT EXISTS messages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
from_user_id INTEGER,
|
|
to_user_id INTEGER,
|
|
subject TEXT,
|
|
content TEXT,
|
|
read INTEGER DEFAULT 0,
|
|
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
|
FOREIGN KEY (from_user_id) REFERENCES users(id),
|
|
FOREIGN KEY (to_user_id) REFERENCES users(id)
|
|
)""")
|
|
|
|
c.execute("""CREATE TABLE IF NOT EXISTS user_flair (
|
|
user_id INTEGER,
|
|
community_id INTEGER,
|
|
flair TEXT,
|
|
PRIMARY KEY (user_id, community_id),
|
|
FOREIGN KEY (user_id) REFERENCES users(id),
|
|
FOREIGN KEY (community_id) REFERENCES communities(id)
|
|
)""")
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
def get_id(self):
|
|
if not self._id:
|
|
self._id = self._get_counter()
|
|
return self.update_id()
|
|
|
|
def _get_counter(self):
|
|
conn = self.get_conn()
|
|
c = conn.cursor()
|
|
c.execute("SELECT counter FROM uid")
|
|
return c.fetchone()[0]
|
|
|
|
def update_id(self):
|
|
conn = self.get_conn()
|
|
c = conn.cursor()
|
|
c.execute("UPDATE uid SET counter = counter + 1")
|
|
conn.commit()
|
|
conn.close()
|
|
self._id += 1
|
|
return self._id
|
|
|
|
def get_uid(self):
|
|
gen = PronounceableIDGenerator(self.get_id())
|
|
return gen.next_id()
|
|
|
|
# Pydantic Models
|
|
class RegisterRequest(BaseModel):
|
|
username: str = Field(..., min_length=3)
|
|
password: str = Field(..., min_length=6)
|
|
|
|
class LoginRequest(BaseModel):
|
|
username: str
|
|
password: str
|
|
|
|
class CreateCommunityRequest(BaseModel):
|
|
name: str = Field(..., min_length=3)
|
|
description: Optional[str] = ""
|
|
|
|
class CreatePostRequest(BaseModel):
|
|
community_id: int
|
|
title: str = Field(..., min_length=3)
|
|
content: Optional[str] = ""
|
|
url: Optional[str] = ""
|
|
type: Optional[str] = "text"
|
|
flair: Optional[str] = ""
|
|
nsfw: Optional[int] = 0
|
|
spoiler: Optional[int] = 0
|
|
|
|
class VoteRequest(BaseModel):
|
|
target_type: str
|
|
target_id: int
|
|
vote: int
|
|
|
|
class CreateCommentRequest(BaseModel):
|
|
post_id: int
|
|
parent_id: Optional[int] = None
|
|
content: str = Field(..., min_length=1)
|
|
|
|
class SubscribeRequest(BaseModel):
|
|
community_id: int
|
|
action: str
|
|
|
|
class SaveRequest(BaseModel):
|
|
target_type: str
|
|
target_id: int
|
|
action: str
|
|
|
|
class HideRequest(BaseModel):
|
|
post_id: int
|
|
action: str
|
|
|
|
class SendMessageRequest(BaseModel):
|
|
to_username: str
|
|
subject: str = Field(..., min_length=1)
|
|
content: str = Field(..., min_length=1)
|
|
|
|
class StickyRequest(BaseModel):
|
|
post_id: int
|
|
sticky: int
|
|
|
|
class LockRequest(BaseModel):
|
|
post_id: int
|
|
locked: int
|
|
|
|
class SetFlairRequest(BaseModel):
|
|
community_id: int
|
|
flair: Optional[str] = ""
|
|
|
|
class UpdateCommunityRequest(BaseModel):
|
|
community_id: int
|
|
rules: Optional[str] = None
|
|
wiki: Optional[str] = None
|
|
sidebar: Optional[str] = None
|
|
|
|
# Initialize FastAPI
|
|
app = FastAPI()
|
|
|
|
# CORS middleware
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# Initialize Database
|
|
db = Database()
|
|
|
|
# Helper functions
|
|
def hash_password(password: str) -> str:
|
|
return hashlib.sha256(password.encode()).hexdigest()
|
|
|
|
def get_session_user(x_session_token: Optional[str] = None, authorization: Optional[str] = None) -> Optional[int]:
|
|
"""Get user_id from session token"""
|
|
token = x_session_token or authorization
|
|
if not token:
|
|
return None
|
|
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
now = int(timelib.time())
|
|
c.execute("SELECT user_id FROM sessions WHERE token = ? AND expires_at > ?", (token, now))
|
|
result = c.fetchone()
|
|
conn.close()
|
|
|
|
return result["user_id"] if result else None
|
|
|
|
def create_session(user_id: int) -> str:
|
|
"""Create a new session for user"""
|
|
token = secrets.token_urlsafe(32)
|
|
expires_at = int(timelib.time()) + (30 * 24 * 60 * 60)
|
|
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
|
|
c.execute("DELETE FROM sessions WHERE expires_at < ?", (int(timelib.time()),))
|
|
|
|
c.execute("INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, ?)",
|
|
(token, user_id, expires_at))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return token
|
|
|
|
def calculate_hot_score(score: int, created_at: int) -> float:
|
|
now = timelib.time()
|
|
age_hours = (now - created_at) / 3600
|
|
return (score - 1) / pow(age_hours + 2, 1.5)
|
|
|
|
def calculate_controversial_score(upvotes: int, downvotes: int) -> float:
|
|
if upvotes == 0 and downvotes == 0:
|
|
return 0
|
|
total = upvotes + downvotes
|
|
balance = min(upvotes, downvotes)
|
|
return total * balance
|
|
|
|
# Routes
|
|
@app.get("/")
|
|
@app.get("/index.html")
|
|
async def serve_index():
|
|
return FileResponse("index.html")
|
|
|
|
@app.post("/api/register")
|
|
async def api_register(request: RegisterRequest):
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
|
|
try:
|
|
c.execute("INSERT INTO users (username, password) VALUES (?, ?)",
|
|
(request.username, hash_password(request.password)))
|
|
conn.commit()
|
|
user_id = c.lastrowid
|
|
|
|
c.execute("SELECT id, username, karma FROM users WHERE id = ?", (user_id,))
|
|
user = c.fetchone()
|
|
conn.close()
|
|
|
|
token = create_session(user_id)
|
|
return {"success": True, "token": token, "username": user["username"], "karma": user["karma"]}
|
|
except sqlite3.IntegrityError:
|
|
conn.close()
|
|
return {"success": False, "error": "Username already exists"}
|
|
|
|
@app.post("/api/login")
|
|
async def api_login(request: LoginRequest):
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
c.execute("SELECT * FROM users WHERE username = ? AND password = ?",
|
|
(request.username, hash_password(request.password)))
|
|
user = c.fetchone()
|
|
conn.close()
|
|
|
|
if user:
|
|
token = create_session(user["id"])
|
|
return {"success": True, "token": token, "user_id": user["id"], "username": user["username"], "karma": user["karma"]}
|
|
return {"success": False, "error": "Invalid credentials"}
|
|
|
|
@app.post("/api/logout")
|
|
async def api_logout(x_session_token: Optional[str] = Header(None), authorization: Optional[str] = Header(None)):
|
|
token = x_session_token or authorization
|
|
if token:
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
c.execute("DELETE FROM sessions WHERE token = ?", (token,))
|
|
conn.commit()
|
|
conn.close()
|
|
return {"success": True}
|
|
|
|
@app.get("/api/me")
|
|
async def api_me(x_session_token: Optional[str] = Header(None), authorization: Optional[str] = Header(None)):
|
|
user_id = get_session_user(x_session_token, authorization)
|
|
if not user_id:
|
|
return {"success": False, "error": "Not logged in"}
|
|
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
c.execute("SELECT id, username, karma FROM users WHERE id = ?", (user_id,))
|
|
user = c.fetchone()
|
|
conn.close()
|
|
|
|
if user:
|
|
return {"success": True, "user": dict(user)}
|
|
return {"success": False, "error": "User not found"}
|
|
|
|
@app.post("/api/create_community")
|
|
async def api_create_community(request: CreateCommunityRequest, x_session_token: Optional[str] = Header(None), authorization: Optional[str] = Header(None)):
|
|
user_id = get_session_user(x_session_token, authorization)
|
|
if not user_id:
|
|
return {"success": False, "error": "Not logged in"}
|
|
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
|
|
try:
|
|
c.execute("INSERT INTO communities (name, description, creator_id) VALUES (?, ?, ?)",
|
|
(request.name, request.description, user_id))
|
|
community_id = c.lastrowid
|
|
c.execute("INSERT INTO moderators (community_id, user_id) VALUES (?, ?)",
|
|
(community_id, user_id))
|
|
c.execute("INSERT INTO subscriptions (user_id, community_id) VALUES (?, ?)",
|
|
(user_id, community_id))
|
|
conn.commit()
|
|
conn.close()
|
|
return {"success": True, "community_id": community_id}
|
|
except sqlite3.IntegrityError:
|
|
conn.close()
|
|
return {"success": False, "error": "Community already exists"}
|
|
|
|
@app.get("/api/communities")
|
|
async def api_get_communities():
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
c.execute("""SELECT c.*, u.username as creator,
|
|
(SELECT COUNT(*) FROM subscriptions WHERE community_id = c.id) as subscribers
|
|
FROM communities c
|
|
LEFT JOIN users u ON c.creator_id = u.id
|
|
ORDER BY subscribers DESC""")
|
|
communities = [dict(row) for row in c.fetchall()]
|
|
conn.close()
|
|
return {"communities": communities}
|
|
|
|
@app.get("/api/community")
|
|
async def api_get_community(id: Optional[int] = None, name: Optional[str] = None):
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
|
|
if id:
|
|
c.execute("SELECT * FROM communities WHERE id = ?", (id,))
|
|
elif name:
|
|
c.execute("SELECT * FROM communities WHERE name = ?", (name,))
|
|
else:
|
|
conn.close()
|
|
return {"success": False, "error": "ID or name required"}
|
|
|
|
community = c.fetchone()
|
|
conn.close()
|
|
|
|
if community:
|
|
return {"success": True, "community": dict(community)}
|
|
return {"success": False, "error": "Community not found"}
|
|
|
|
@app.get("/api/posts")
|
|
async def api_get_posts(
|
|
community_id: Optional[int] = None,
|
|
sort: str = "hot",
|
|
time: str = "all",
|
|
feed: str = "all",
|
|
limit: int = 50,
|
|
offset: int = 0,
|
|
x_session_token: Optional[str] = Header(None),
|
|
authorization: Optional[str] = Header(None)
|
|
):
|
|
user_id = get_session_user(x_session_token, authorization)
|
|
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
|
|
time_cutoff = 0
|
|
now = timelib.time()
|
|
if time == "hour":
|
|
time_cutoff = now - 3600
|
|
elif time == "day":
|
|
time_cutoff = now - 86400
|
|
elif time == "week":
|
|
time_cutoff = now - 604800
|
|
elif time == "month":
|
|
time_cutoff = now - 2592000
|
|
elif time == "year":
|
|
time_cutoff = now - 31536000
|
|
|
|
base_query = """SELECT p.*, u.username, c.name as community_name,
|
|
(SELECT COUNT(*) FROM comments WHERE post_id = p.id) as comment_count,
|
|
(SELECT vote FROM votes WHERE user_id = ? AND target_type = 'post' AND target_id = p.id) as user_vote
|
|
FROM posts p
|
|
LEFT JOIN users u ON p.user_id = u.id
|
|
LEFT JOIN communities c ON p.community_id = c.id"""
|
|
|
|
where_clauses = []
|
|
params = [user_id if user_id else 0]
|
|
|
|
if community_id:
|
|
where_clauses.append("p.community_id = ?")
|
|
params.append(community_id)
|
|
|
|
if feed == "home" and user_id:
|
|
where_clauses.append("p.community_id IN (SELECT community_id FROM subscriptions WHERE user_id = ?)")
|
|
params.append(user_id)
|
|
|
|
is_subscribed = False
|
|
if user_id:
|
|
where_clauses.append("p.id NOT IN (SELECT post_id FROM hidden WHERE user_id = ?)")
|
|
params.append(user_id)
|
|
if community_id:
|
|
is_subscribed_query = "SELECT community_id FROM subscriptions WHERE user_id = ? AND community_id = ?"
|
|
c.execute(is_subscribed_query, (user_id, community_id))
|
|
result = c.fetchone()
|
|
is_subscribed = result and str(dict(result).get("community_id")) == str(community_id)
|
|
|
|
if time_cutoff > 0:
|
|
where_clauses.append("p.created_at >= ?")
|
|
params.append(time_cutoff)
|
|
|
|
if where_clauses:
|
|
base_query += " WHERE " + " AND ".join(where_clauses)
|
|
|
|
if sort == "new":
|
|
base_query += " ORDER BY p.sticky DESC, p.created_at DESC"
|
|
elif sort == "top":
|
|
base_query += " ORDER BY p.sticky DESC, p.score DESC"
|
|
elif sort == "old":
|
|
base_query += " ORDER BY p.sticky DESC, p.created_at ASC"
|
|
else:
|
|
base_query += " ORDER BY p.sticky DESC, p.score DESC"
|
|
|
|
base_query += f" LIMIT {limit} OFFSET {offset}"
|
|
|
|
c.execute(base_query, params)
|
|
posts = [dict(row) for row in c.fetchall()]
|
|
|
|
if sort == "hot":
|
|
for post in posts:
|
|
if post["sticky"]:
|
|
post["hot_score"] = float('inf')
|
|
else:
|
|
post["hot_score"] = calculate_hot_score(post["score"], post["created_at"])
|
|
posts.sort(key=lambda x: x["hot_score"], reverse=True)
|
|
elif sort == "controversial":
|
|
if posts:
|
|
post_ids = [str(p["id"]) for p in posts]
|
|
c.execute(f"""
|
|
SELECT target_id,
|
|
SUM(CASE WHEN vote = 1 THEN 1 ELSE 0 END) as upvotes,
|
|
SUM(CASE WHEN vote = -1 THEN 1 ELSE 0 END) as downvotes
|
|
FROM votes
|
|
WHERE target_type = 'post' AND target_id IN ({','.join(['?']*len(post_ids))})
|
|
GROUP BY target_id
|
|
""", post_ids)
|
|
vote_counts = {row["target_id"]: (row["upvotes"], row["downvotes"]) for row in c.fetchall()}
|
|
|
|
for post in posts:
|
|
upvotes, downvotes = vote_counts.get(post["id"], (0, 0))
|
|
post["controversial_score"] = calculate_controversial_score(upvotes, downvotes)
|
|
posts.sort(key=lambda x: x.get("controversial_score", 0), reverse=True)
|
|
elif sort == "rising":
|
|
recent_cutoff = now - 7200
|
|
rising_posts = [p for p in posts if p["created_at"] >= recent_cutoff]
|
|
for post in rising_posts:
|
|
age_hours = (now - post["created_at"]) / 3600
|
|
post["rising_score"] = post["score"] / max(age_hours, 0.1)
|
|
rising_posts.sort(key=lambda x: x["rising_score"], reverse=True)
|
|
posts = rising_posts
|
|
|
|
conn.close()
|
|
return {"posts": posts, "is_subscribed": is_subscribed}
|
|
|
|
@app.get("/api/post")
|
|
async def api_get_post(id: int, x_session_token: Optional[str] = Header(None), authorization: Optional[str] = Header(None)):
|
|
user_id = get_session_user(x_session_token, authorization)
|
|
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
c.execute("""SELECT p.*, u.username, c.name as community_name,
|
|
(SELECT COUNT(*) FROM comments WHERE post_id = p.id) as comment_count,
|
|
(SELECT vote FROM votes WHERE user_id = ? AND target_type = 'post' AND target_id = p.id) as user_vote
|
|
FROM posts p
|
|
LEFT JOIN users u ON p.user_id = u.id
|
|
LEFT JOIN communities c ON p.community_id = c.id
|
|
WHERE p.id = ?""", (user_id if user_id else 0, id))
|
|
post = c.fetchone()
|
|
conn.close()
|
|
|
|
if post:
|
|
return {"success": True, "post": dict(post)}
|
|
return {"success": False, "error": "Post not found"}
|
|
|
|
@app.post("/api/create_post")
|
|
async def api_create_post(request: CreatePostRequest, x_session_token: Optional[str] = Header(None), authorization: Optional[str] = Header(None)):
|
|
user_id = get_session_user(x_session_token, authorization)
|
|
if not user_id:
|
|
return {"success": False, "error": "Not logged in"}
|
|
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
c.execute("""INSERT INTO posts (community_id, user_id, title, content, url, post_type, flair, nsfw, spoiler)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
(request.community_id, user_id, request.title, request.content, request.url,
|
|
request.type, request.flair, request.nsfw, request.spoiler))
|
|
post_id = c.lastrowid
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return {"success": True, "post_id": post_id}
|
|
|
|
@app.post("/api/vote")
|
|
async def api_vote(request: VoteRequest, x_session_token: Optional[str] = Header(None), authorization: Optional[str] = Header(None)):
|
|
user_id = get_session_user(x_session_token, authorization)
|
|
if not user_id:
|
|
return {"success": False, "error": "Not logged in"}
|
|
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
|
|
c.execute("SELECT vote FROM votes WHERE user_id = ? AND target_type = ? AND target_id = ?",
|
|
(user_id, request.target_type, request.target_id))
|
|
existing = c.fetchone()
|
|
|
|
if existing:
|
|
old_vote = existing["vote"]
|
|
if request.vote == 0:
|
|
c.execute("DELETE FROM votes WHERE user_id = ? AND target_type = ? AND target_id = ?",
|
|
(user_id, request.target_type, request.target_id))
|
|
score_change = -old_vote
|
|
else:
|
|
c.execute("UPDATE votes SET vote = ? WHERE user_id = ? AND target_type = ? AND target_id = ?",
|
|
(request.vote, user_id, request.target_type, request.target_id))
|
|
score_change = request.vote - old_vote
|
|
else:
|
|
if request.vote != 0:
|
|
c.execute("INSERT INTO votes (user_id, target_type, target_id, vote) VALUES (?, ?, ?, ?)",
|
|
(user_id, request.target_type, request.target_id, request.vote))
|
|
score_change = request.vote
|
|
else:
|
|
score_change = 0
|
|
|
|
if score_change != 0:
|
|
if request.target_type == "post":
|
|
c.execute("UPDATE posts SET score = score + ? WHERE id = ?", (score_change, request.target_id))
|
|
c.execute("SELECT user_id FROM posts WHERE id = ?", (request.target_id,))
|
|
post = c.fetchone()
|
|
if post:
|
|
c.execute("UPDATE users SET karma = karma + ? WHERE id = ?", (score_change, post["user_id"]))
|
|
elif request.target_type == "comment":
|
|
c.execute("UPDATE comments SET score = score + ? WHERE id = ?", (score_change, request.target_id))
|
|
c.execute("SELECT user_id FROM comments WHERE id = ?", (request.target_id,))
|
|
comment = c.fetchone()
|
|
if comment:
|
|
c.execute("UPDATE users SET karma = karma + ? WHERE id = ?", (score_change, comment["user_id"]))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return {"success": True}
|
|
|
|
@app.post("/api/comment")
|
|
async def api_create_comment(request: CreateCommentRequest, x_session_token: Optional[str] = Header(None), authorization: Optional[str] = Header(None)):
|
|
user_id = get_session_user(x_session_token, authorization)
|
|
if not user_id:
|
|
return {"success": False, "error": "Not logged in"}
|
|
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
c.execute("INSERT INTO comments (post_id, parent_id, user_id, content) VALUES (?, ?, ?, ?)",
|
|
(request.post_id, request.parent_id, user_id, request.content))
|
|
comment_id = c.lastrowid
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return {"success": True, "comment_id": comment_id}
|
|
|
|
@app.get("/api/comments")
|
|
async def api_get_comments(post_id: int, sort: str = "best", x_session_token: Optional[str] = Header(None), authorization: Optional[str] = Header(None)):
|
|
user_id = get_session_user(x_session_token, authorization)
|
|
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
c.execute("""SELECT c.*, u.username,
|
|
(SELECT vote FROM votes WHERE user_id = ? AND target_type = 'comment' AND target_id = c.id) as user_vote
|
|
FROM comments c
|
|
LEFT JOIN users u ON c.user_id = u.id
|
|
WHERE c.post_id = ?""", (user_id if user_id else 0, post_id))
|
|
comments = [dict(row) for row in c.fetchall()]
|
|
|
|
if sort == "top":
|
|
comments.sort(key=lambda x: x["score"], reverse=True)
|
|
elif sort == "new":
|
|
comments.sort(key=lambda x: x["created_at"], reverse=True)
|
|
elif sort == "old":
|
|
comments.sort(key=lambda x: x["created_at"])
|
|
elif sort == "controversial":
|
|
if comments:
|
|
comment_ids = [str(cm["id"]) for cm in comments]
|
|
c.execute(f"""
|
|
SELECT target_id,
|
|
SUM(CASE WHEN vote = 1 THEN 1 ELSE 0 END) as upvotes,
|
|
SUM(CASE WHEN vote = -1 THEN 1 ELSE 0 END) as downvotes
|
|
FROM votes
|
|
WHERE target_type = 'comment' AND target_id IN ({','.join(['?']*len(comment_ids))})
|
|
GROUP BY target_id
|
|
""", comment_ids)
|
|
vote_counts = {row["target_id"]: (row["upvotes"], row["downvotes"]) for row in c.fetchall()}
|
|
|
|
for comment in comments:
|
|
upvotes, downvotes = vote_counts.get(comment["id"], (0, 0))
|
|
comment["controversial_score"] = calculate_controversial_score(upvotes, downvotes)
|
|
comments.sort(key=lambda x: x.get("controversial_score", 0), reverse=True)
|
|
else:
|
|
comments.sort(key=lambda x: (x["score"], -x["created_at"]), reverse=True)
|
|
|
|
conn.close()
|
|
return {"comments": comments}
|
|
|
|
@app.post("/api/subscribe")
|
|
async def api_subscribe(request: SubscribeRequest, x_session_token: Optional[str] = Header(None), authorization: Optional[str] = Header(None)):
|
|
user_id = get_session_user(x_session_token, authorization)
|
|
if not user_id:
|
|
return {"success": False, "error": "Not logged in"}
|
|
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
|
|
if request.action == "subscribe":
|
|
try:
|
|
c.execute("INSERT INTO subscriptions (user_id, community_id) VALUES (?, ?)",
|
|
(user_id, request.community_id))
|
|
conn.commit()
|
|
except sqlite3.IntegrityError:
|
|
pass
|
|
elif request.action == "unsubscribe":
|
|
c.execute("DELETE FROM subscriptions WHERE user_id = ? AND community_id = ?",
|
|
(user_id, request.community_id))
|
|
conn.commit()
|
|
|
|
conn.close()
|
|
return {"success": True}
|
|
|
|
@app.get("/api/subscriptions")
|
|
async def api_get_subscriptions(x_session_token: Optional[str] = Header(None), authorization: Optional[str] = Header(None)):
|
|
user_id = get_session_user(x_session_token, authorization)
|
|
if not user_id:
|
|
return {"success": False, "error": "Not logged in"}
|
|
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
c.execute("""SELECT c.* FROM communities c
|
|
JOIN subscriptions s ON c.id = s.community_id
|
|
WHERE s.user_id = ?""", (user_id,))
|
|
subscriptions = [dict(row) for row in c.fetchall()]
|
|
conn.close()
|
|
|
|
return {"subscriptions": subscriptions}
|
|
|
|
@app.get("/api/user")
|
|
async def api_get_user(id: Optional[int] = None, username: Optional[str] = None):
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
|
|
if id:
|
|
c.execute("SELECT id, username, karma, created_at FROM users WHERE id = ?", (id,))
|
|
elif username:
|
|
c.execute("SELECT id, username, karma, created_at FROM users WHERE username = ?", (username,))
|
|
else:
|
|
conn.close()
|
|
return {"success": False, "error": "ID or username required"}
|
|
|
|
user = c.fetchone()
|
|
|
|
if not user:
|
|
conn.close()
|
|
return {"success": False, "error": "User not found"}
|
|
|
|
user_dict = dict(user)
|
|
|
|
c.execute("""SELECT p.*,u.username, c.name as community_name FROM posts p
|
|
LEFT JOIN communities c ON p.community_id = c.id
|
|
INNER JOIN users u ON p.user_id = u.id
|
|
WHERE p.user_id = ? ORDER BY p.created_at DESC LIMIT 20""", (user_dict["id"],))
|
|
posts = [dict(row) for row in c.fetchall()]
|
|
|
|
c.execute("""SELECT c.*,u.username, p.title as post_title FROM comments c
|
|
LEFT JOIN posts p ON c.post_id = p.id
|
|
INNER JOIN users u ON c.user_id = u.id
|
|
WHERE c.user_id = ? ORDER BY c.created_at DESC LIMIT 20""", (user_dict["id"],))
|
|
comments = [dict(row) for row in c.fetchall()]
|
|
|
|
conn.close()
|
|
|
|
return {"success": True, "user": user_dict, "posts": posts, "comments": comments}
|
|
|
|
@app.post("/api/save")
|
|
async def api_save(request: SaveRequest, x_session_token: Optional[str] = Header(None), authorization: Optional[str] = Header(None)):
|
|
user_id = get_session_user(x_session_token, authorization)
|
|
if not user_id:
|
|
return {"success": False, "error": "Not logged in"}
|
|
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
|
|
if request.action == "save":
|
|
try:
|
|
c.execute("INSERT INTO saved (user_id, target_type, target_id) VALUES (?, ?, ?)",
|
|
(user_id, request.target_type, request.target_id))
|
|
conn.commit()
|
|
except sqlite3.IntegrityError:
|
|
pass
|
|
elif request.action == "unsave":
|
|
c.execute("DELETE FROM saved WHERE user_id = ? AND target_type = ? AND target_id = ?",
|
|
(user_id, request.target_type, request.target_id))
|
|
conn.commit()
|
|
|
|
conn.close()
|
|
return {"success": True}
|
|
|
|
@app.post("/api/hide")
|
|
async def api_hide(request: HideRequest, x_session_token: Optional[str] = Header(None), authorization: Optional[str] = Header(None)):
|
|
user_id = get_session_user(x_session_token, authorization)
|
|
if not user_id:
|
|
return {"success": False, "error": "Not logged in"}
|
|
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
|
|
if request.action == "hide":
|
|
try:
|
|
c.execute("INSERT INTO hidden (user_id, post_id) VALUES (?, ?)",
|
|
(user_id, request.post_id))
|
|
conn.commit()
|
|
except sqlite3.IntegrityError:
|
|
pass
|
|
elif request.action == "unhide":
|
|
c.execute("DELETE FROM hidden WHERE user_id = ? AND post_id = ?",
|
|
(user_id, request.post_id))
|
|
conn.commit()
|
|
|
|
conn.close()
|
|
return {"success": True}
|
|
|
|
@app.get("/api/saved")
|
|
async def api_get_saved(x_session_token: Optional[str] = Header(None), authorization: Optional[str] = Header(None)):
|
|
user_id = get_session_user(x_session_token, authorization)
|
|
if not user_id:
|
|
return {"success": False, "error": "Not logged in"}
|
|
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
|
|
c.execute("""SELECT p.*, u.username, c.name as community_name, 'post' as type
|
|
FROM posts p
|
|
LEFT JOIN users u ON p.user_id = u.id
|
|
LEFT JOIN communities c ON p.community_id = c.id
|
|
WHERE p.id IN (SELECT target_id FROM saved WHERE user_id = ? AND target_type = 'post')
|
|
ORDER BY p.created_at DESC""", (user_id,))
|
|
saved_posts = [dict(row) for row in c.fetchall()]
|
|
|
|
c.execute("""SELECT c.*, u.username, 'comment' as type
|
|
FROM comments c
|
|
LEFT JOIN users u ON c.user_id = u.id
|
|
WHERE c.id IN (SELECT target_id FROM saved WHERE user_id = ? AND target_type = 'comment')
|
|
ORDER BY c.created_at DESC""", (user_id,))
|
|
saved_comments = [dict(row) for row in c.fetchall()]
|
|
|
|
conn.close()
|
|
|
|
return {"posts": saved_posts, "comments": saved_comments}
|
|
|
|
@app.get("/api/search")
|
|
async def api_search(q: str = "", community_id: Optional[int] = None, type: str = "all"):
|
|
if not q:
|
|
return {"posts": [], "comments": [], "communities": []}
|
|
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
|
|
search_term = f"%{q}%"
|
|
posts = []
|
|
comments = []
|
|
communities = []
|
|
|
|
if type in ["all", "posts"]:
|
|
post_query = """SELECT p.*, u.username, c.name as community_name FROM posts p
|
|
LEFT JOIN users u ON p.user_id = u.id
|
|
LEFT JOIN communities c ON p.community_id = c.id
|
|
WHERE (p.title LIKE ? OR p.content LIKE ?)"""
|
|
params = [search_term, search_term]
|
|
|
|
if community_id:
|
|
post_query += " AND p.community_id = ?"
|
|
params.append(community_id)
|
|
|
|
post_query += " ORDER BY p.created_at DESC LIMIT 50"
|
|
c.execute(post_query, params)
|
|
posts = [dict(row) for row in c.fetchall()]
|
|
|
|
if type in ["all", "comments"]:
|
|
comment_query = """SELECT c.*, u.username FROM comments c
|
|
LEFT JOIN users u ON c.user_id = u.id
|
|
WHERE c.content LIKE ?"""
|
|
params = [search_term]
|
|
|
|
if community_id:
|
|
comment_query += " AND c.post_id IN (SELECT id FROM posts WHERE community_id = ?)"
|
|
params.append(community_id)
|
|
|
|
comment_query += " ORDER BY c.created_at DESC LIMIT 50"
|
|
c.execute(comment_query, params)
|
|
comments = [dict(row) for row in c.fetchall()]
|
|
|
|
if type in ["all", "communities"]:
|
|
c.execute("""SELECT * FROM communities
|
|
WHERE name LIKE ? OR description LIKE ?
|
|
ORDER BY created_at DESC LIMIT 20""", (search_term, search_term))
|
|
communities = [dict(row) for row in c.fetchall()]
|
|
|
|
conn.close()
|
|
|
|
return {"posts": posts, "comments": comments, "communities": communities}
|
|
|
|
@app.post("/api/message")
|
|
async def api_send_message(request: SendMessageRequest, x_session_token: Optional[str] = Header(None), authorization: Optional[str] = Header(None)):
|
|
user_id = get_session_user(x_session_token, authorization)
|
|
if not user_id:
|
|
return {"success": False, "error": "Not logged in"}
|
|
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
|
|
c.execute("SELECT id FROM users WHERE username = ?", (request.to_username,))
|
|
to_user = c.fetchone()
|
|
|
|
if not to_user:
|
|
conn.close()
|
|
return {"success": False, "error": "User not found"}
|
|
|
|
c.execute("INSERT INTO messages (from_user_id, to_user_id, subject, content) VALUES (?, ?, ?, ?)",
|
|
(user_id, to_user["id"], request.subject, request.content))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return {"success": True}
|
|
|
|
@app.get("/api/messages")
|
|
async def api_get_messages(x_session_token: Optional[str] = Header(None), authorization: Optional[str] = Header(None)):
|
|
user_id = get_session_user(x_session_token, authorization)
|
|
if not user_id:
|
|
return {"success": False, "error": "Not logged in"}
|
|
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
|
|
c.execute("""SELECT m.*, u.username as from_username FROM messages m
|
|
LEFT JOIN users u ON m.from_user_id = u.id
|
|
WHERE m.to_user_id = ?
|
|
ORDER BY m.created_at DESC""", (user_id,))
|
|
messages = [dict(row) for row in c.fetchall()]
|
|
|
|
c.execute("UPDATE messages SET read = 1 WHERE to_user_id = ?", (user_id,))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return {"messages": messages}
|
|
|
|
@app.post("/api/sticky")
|
|
async def api_sticky(request: StickyRequest, x_session_token: Optional[str] = Header(None), authorization: Optional[str] = Header(None)):
|
|
user_id = get_session_user(x_session_token, authorization)
|
|
if not user_id:
|
|
return {"success": False, "error": "Not logged in"}
|
|
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
|
|
c.execute("SELECT community_id FROM posts WHERE id = ?", (request.post_id,))
|
|
post = c.fetchone()
|
|
|
|
if not post:
|
|
conn.close()
|
|
return {"success": False, "error": "Post not found"}
|
|
|
|
c.execute("SELECT * FROM moderators WHERE community_id = ? AND user_id = ?",
|
|
(post["community_id"], user_id))
|
|
is_mod = c.fetchone()
|
|
|
|
if not is_mod:
|
|
conn.close()
|
|
return {"success": False, "error": "Not a moderator"}
|
|
|
|
c.execute("UPDATE posts SET sticky = ? WHERE id = ?", (request.sticky, request.post_id))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return {"success": True}
|
|
|
|
@app.post("/api/lock")
|
|
async def api_lock(request: LockRequest, x_session_token: Optional[str] = Header(None), authorization: Optional[str] = Header(None)):
|
|
user_id = get_session_user(x_session_token, authorization)
|
|
if not user_id:
|
|
return {"success": False, "error": "Not logged in"}
|
|
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
|
|
c.execute("SELECT community_id FROM posts WHERE id = ?", (request.post_id,))
|
|
post = c.fetchone()
|
|
|
|
if not post:
|
|
conn.close()
|
|
return {"success": False, "error": "Post not found"}
|
|
|
|
c.execute("SELECT * FROM moderators WHERE community_id = ? AND user_id = ?",
|
|
(post["community_id"], user_id))
|
|
is_mod = c.fetchone()
|
|
|
|
if not is_mod:
|
|
conn.close()
|
|
return {"success": False, "error": "Not a moderator"}
|
|
|
|
c.execute("UPDATE posts SET locked = ? WHERE id = ?", (request.locked, request.post_id))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return {"success": True}
|
|
|
|
@app.post("/api/set_flair")
|
|
async def api_set_flair(request: SetFlairRequest, x_session_token: Optional[str] = Header(None), authorization: Optional[str] = Header(None)):
|
|
user_id = get_session_user(x_session_token, authorization)
|
|
if not user_id:
|
|
return {"success": False, "error": "Not logged in"}
|
|
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
|
|
c.execute("INSERT OR REPLACE INTO user_flair (user_id, community_id, flair) VALUES (?, ?, ?)",
|
|
(user_id, request.community_id, request.flair))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return {"success": True}
|
|
|
|
@app.post("/api/update_community")
|
|
async def api_update_community(request: UpdateCommunityRequest, x_session_token: Optional[str] = Header(None), authorization: Optional[str] = Header(None)):
|
|
user_id = get_session_user(x_session_token, authorization)
|
|
if not user_id:
|
|
return {"success": False, "error": "Not logged in"}
|
|
|
|
conn = db.get_conn()
|
|
c = conn.cursor()
|
|
|
|
c.execute("SELECT * FROM moderators WHERE community_id = ? AND user_id = ?",
|
|
(request.community_id, user_id))
|
|
is_mod = c.fetchone()
|
|
|
|
if not is_mod:
|
|
conn.close()
|
|
return {"success": False, "error": "Not a moderator"}
|
|
|
|
updates = []
|
|
params = []
|
|
|
|
if request.rules is not None:
|
|
updates.append("rules = ?")
|
|
params.append(request.rules)
|
|
if request.wiki is not None:
|
|
updates.append("wiki = ?")
|
|
params.append(request.wiki)
|
|
if request.sidebar is not None:
|
|
updates.append("sidebar = ?")
|
|
params.append(request.sidebar)
|
|
|
|
if updates:
|
|
params.append(request.community_id)
|
|
c.execute(f"UPDATE communities SET {', '.join(updates)} WHERE id = ?", params)
|
|
conn.commit()
|
|
|
|
conn.close()
|
|
return {"success": True}
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(app, host="0.0.0.0", port=8589)
|