1328 lines
40 KiB
Python
Raw Normal View History

2025-08-03 20:36:36 +02:00
from fastapi import FastAPI, HTTPException, Depends, Form, File, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from pydantic import BaseModel, EmailStr
from typing import Optional, List, Dict, Any, Literal
from datetime import datetime, timedelta
import hashlib
import secrets
import json
import os
from pathlib import Path
2025-08-04 00:04:08 +02:00
import asyncio
from ads import AsyncDataSet
2025-08-03 20:36:36 +02:00
2025-08-04 16:53:26 +02:00
app = FastAPI(title="Rant Community API")
2025-08-03 20:36:36 +02:00
# Enable CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Database setup
2025-08-04 00:44:29 +02:00
DB_PATH = "rant_community.db"
2025-08-03 20:36:36 +02:00
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)
2025-08-04 00:04:08 +02:00
# Initialize AsyncDataSet
db = AsyncDataSet(DB_PATH)
async def init_db():
2025-08-03 20:36:36 +02:00
# Users table
2025-08-04 00:04:08 +02:00
await db.create_table("users", {
"id": "INTEGER PRIMARY KEY AUTOINCREMENT",
"username": "TEXT NOT NULL",
"email": "TEXT NOT NULL",
"password_hash": "TEXT NOT NULL",
"score": "INTEGER DEFAULT 0",
"about": "TEXT DEFAULT ''",
"location": "TEXT DEFAULT ''",
"skills": "TEXT DEFAULT ''",
"github": "TEXT DEFAULT ''",
"website": "TEXT DEFAULT ''",
"created_time": "INTEGER NOT NULL",
"avatar_b": "TEXT DEFAULT '7bc8a4'",
"avatar_i": "TEXT"
}, ["UNIQUE(username)", "UNIQUE(email)"])
2025-08-03 20:36:36 +02:00
# Auth tokens table
2025-08-04 00:04:08 +02:00
await db.create_table("auth_tokens", {
"id": "INTEGER PRIMARY KEY AUTOINCREMENT",
"user_id": "INTEGER NOT NULL",
"token_key": "TEXT NOT NULL",
"expire_time": "INTEGER NOT NULL"
}, ["UNIQUE(token_key)", "FOREIGN KEY (user_id) REFERENCES users (id)"])
2025-08-03 20:36:36 +02:00
# Rants table
2025-08-04 00:04:08 +02:00
await db.create_table("rants", {
"id": "INTEGER PRIMARY KEY AUTOINCREMENT",
"user_id": "INTEGER NOT NULL",
"text": "TEXT NOT NULL",
"score": "INTEGER DEFAULT 0",
"created_time": "INTEGER NOT NULL",
"attached_image": "TEXT DEFAULT ''",
"tags": "TEXT DEFAULT ''",
"edited": "INTEGER DEFAULT 0",
"type": "INTEGER DEFAULT 1"
}, ["FOREIGN KEY (user_id) REFERENCES users (id)"])
2025-08-03 20:36:36 +02:00
# Comments table
2025-08-04 00:04:08 +02:00
await db.create_table("comments", {
"id": "INTEGER PRIMARY KEY AUTOINCREMENT",
"rant_id": "INTEGER NOT NULL",
"user_id": "INTEGER NOT NULL",
"body": "TEXT NOT NULL",
"score": "INTEGER DEFAULT 0",
"created_time": "INTEGER NOT NULL",
"attached_image": "TEXT DEFAULT ''",
"edited": "INTEGER DEFAULT 0"
}, ["FOREIGN KEY (rant_id) REFERENCES rants (id)", "FOREIGN KEY (user_id) REFERENCES users (id)"])
2025-08-03 20:36:36 +02:00
# Votes table
2025-08-04 00:04:08 +02:00
await db.create_table("votes", {
"id": "INTEGER PRIMARY KEY AUTOINCREMENT",
"user_id": "INTEGER NOT NULL",
"target_id": "INTEGER NOT NULL",
"target_type": "TEXT NOT NULL",
"vote": "INTEGER NOT NULL",
"reason": "INTEGER"
}, ["UNIQUE(user_id, target_id, target_type)", "FOREIGN KEY (user_id) REFERENCES users (id)"])
2025-08-03 20:36:36 +02:00
# Favorites table
2025-08-04 00:04:08 +02:00
await db.create_table("favorites", {
"id": "INTEGER PRIMARY KEY AUTOINCREMENT",
"user_id": "INTEGER NOT NULL",
"rant_id": "INTEGER NOT NULL"
}, ["UNIQUE(user_id, rant_id)", "FOREIGN KEY (user_id) REFERENCES users (id)", "FOREIGN KEY (rant_id) REFERENCES rants (id)"])
2025-08-03 20:36:36 +02:00
# Notifications table
2025-08-04 00:04:08 +02:00
await db.create_table("notifications", {
"id": "INTEGER PRIMARY KEY AUTOINCREMENT",
"user_id": "INTEGER NOT NULL",
"type": "TEXT NOT NULL",
"rant_id": "INTEGER",
"comment_id": "INTEGER",
"from_user_id": "INTEGER",
"created_time": "INTEGER NOT NULL",
"read": "INTEGER DEFAULT 0"
}, ["FOREIGN KEY (user_id) REFERENCES users (id)"])
2025-08-03 20:36:36 +02:00
2025-08-04 00:04:08 +02:00
# Run init_db on startup
@app.on_event("startup")
async def startup_event():
await init_db()
2025-08-03 20:36:36 +02:00
# Pydantic models
class UserRegister(BaseModel):
email: EmailStr
username: str
password: str
class UserLogin(BaseModel):
username: str
password: str
class RantCreate(BaseModel):
rant: str
tags: str
type: int = 1
class CommentCreate(BaseModel):
comment: str
class VoteRequest(BaseModel):
vote: Literal[-1, 0, 1]
reason: Optional[int] = None
# Helper functions
def hash_password(password: str) -> str:
return hashlib.sha256(password.encode()).hexdigest()
def generate_token() -> str:
return secrets.token_urlsafe(32)
2025-08-04 00:40:37 +02:00
async def DELETE_get_current_user(token_id: Optional[int] = Form(None),
2025-08-04 00:04:08 +02:00
token_key: Optional[str] = Form(None),
user_id: Optional[int] = Form(None)):
2025-08-03 20:36:36 +02:00
if not all([token_id, token_key, user_id]):
return None
2025-08-04 00:04:08 +02:00
token = await db.get("auth_tokens", {
"id": token_id,
"token_key": token_key,
"user_id": user_id
})
2025-08-03 20:36:36 +02:00
2025-08-04 00:04:08 +02:00
if not token or token['expire_time'] <= int(datetime.now().timestamp()):
2025-08-03 20:36:36 +02:00
return None
return user_id
2025-08-04 00:04:08 +02:00
async def format_rant(rant_row, user_row, current_user_id=None):
2025-08-03 20:36:36 +02:00
# Get comment count
2025-08-04 00:04:08 +02:00
comment_count = await db.count("comments", {"rant_id": rant_row['id']})
2025-08-03 20:36:36 +02:00
# Get vote state for current user
vote_state = 0
if current_user_id:
2025-08-04 00:04:08 +02:00
vote = await db.get("votes", {
"user_id": current_user_id,
"target_id": rant_row['id'],
"target_type": "rant"
})
2025-08-03 20:36:36 +02:00
if vote:
vote_state = vote['vote']
tags = json.loads(rant_row['tags']) if rant_row['tags'] else []
return {
"id": rant_row['id'],
"text": rant_row['text'],
"score": rant_row['score'],
"created_time": rant_row['created_time'],
"attached_image": rant_row['attached_image'],
"num_comments": comment_count,
"tags": tags,
"vote_state": vote_state,
"edited": bool(rant_row['edited']),
"rt": rant_row['type'],
"rc": 1,
"user_id": user_row['id'],
"user_username": user_row['username'],
"user_score": user_row['score'],
"user_avatar": {
"b": user_row['avatar_b'],
"i": user_row['avatar_i'] or ""
},
"user_avatar_lg": {
"b": user_row['avatar_b'],
"i": user_row['avatar_i'] or ""
}
}
2025-08-04 00:04:08 +02:00
async def format_comment(comment_row, user_row, current_user_id=None):
2025-08-03 20:36:36 +02:00
# Get vote state for current user
vote_state = 0
if current_user_id:
2025-08-04 00:04:08 +02:00
vote = await db.get("votes", {
"user_id": current_user_id,
"target_id": comment_row['id'],
"target_type": "comment"
})
2025-08-03 20:36:36 +02:00
if vote:
vote_state = vote['vote']
return {
"id": comment_row['id'],
"rant_id": comment_row['rant_id'],
"body": comment_row['body'],
"score": comment_row['score'],
"created_time": comment_row['created_time'],
"vote_state": vote_state,
"user_id": user_row['id'],
"user_username": user_row['username'],
"user_score": user_row['score'],
"user_avatar": {
"b": user_row['avatar_b'],
"i": user_row['avatar_i'] or ""
}
}
# Endpoints
@app.post("/api/users")
async def register_user(
email: str = Form(...),
username: str = Form(...),
password: str = Form(...),
type: int = Form(1),
app: int = Form(3)
):
# Validate username length
if len(username) < 4 or len(username) > 15:
return {
"success": False,
"error": "Your username must be between 4 and 15 characters.",
"error_field": "username"
}
# Check if username exists
2025-08-04 00:04:08 +02:00
if await db.exists("users", {"username": username}):
2025-08-03 20:36:36 +02:00
return {
"success": False,
"error": "Username already taken.",
"error_field": "username"
}
# Check if email exists
2025-08-04 00:04:08 +02:00
if await db.exists("users", {"email": email}):
2025-08-03 20:36:36 +02:00
return {
"success": False,
"error": "Email already registered.",
"error_field": "email"
}
# Create user
password_hash = hash_password(password)
created_time = int(datetime.now().timestamp())
try:
2025-08-04 00:04:08 +02:00
await db.insert("users", {
"username": username,
"email": email,
"password_hash": password_hash,
"created_time": created_time
}, return_id=True)
2025-08-03 20:36:36 +02:00
return {"success": True}
except Exception as e:
return {"success": False, "error": str(e)}
@app.post("/api/users/auth-token")
async def login(
username: str = Form(...),
password: str = Form(...),
app: int = Form(3)
):
2025-08-04 00:04:08 +02:00
# Find user by username or email
user = await db.query_one(
"SELECT * FROM users WHERE username = ? OR email = ?",
(username, username)
)
2025-08-03 20:36:36 +02:00
if not user or user['password_hash'] != hash_password(password):
return {
"success": False,
"error": "Invalid login credentials entered. Please try again."
}
# Create auth token
token_key = generate_token()
expire_time = int((datetime.now() + timedelta(days=30)).timestamp())
2025-08-04 00:04:08 +02:00
token_id = await db.insert("auth_tokens", {
"user_id": user['id'],
"token_key": token_key,
"expire_time": expire_time
}, return_id=True)
2025-08-03 20:36:36 +02:00
return {
"success": True,
"auth_token": {
"id": token_id,
"key": token_key,
"expire_time": expire_time,
"user_id": user['id']
}
}
2025-08-04 00:44:29 +02:00
@app.get("/api/rant/rants")
2025-08-03 20:36:36 +02:00
async def get_rants(
sort: str = "recent",
limit: int = 20,
skip: int = 0,
app: int = 3,
token_id: Optional[int] = None,
token_key: Optional[str] = None,
user_id: Optional[int] = None
):
2025-08-04 00:40:37 +02:00
current_user_id = await authenticate_user(token_id, token_key, user_id) if token_id else None
2025-08-04 00:04:08 +02:00
# Get rants with user info
order_by = "r.created_time DESC" if sort == "recent" else "r.score DESC"
rows = await db.query_raw(
f"""SELECT r.*, u.id as user_id, u.username, u.score as user_score,
u.avatar_b, u.avatar_i
FROM rants r
JOIN users u ON r.user_id = u.id
ORDER BY {order_by}
LIMIT ? OFFSET ?""",
(limit, skip)
)
2025-08-03 20:36:36 +02:00
rants = []
2025-08-04 00:04:08 +02:00
for row in rows:
rant_data = {
'id': row['id'],
'text': row['text'],
'score': row['score'],
'created_time': row['created_time'],
'attached_image': row['attached_image'],
'tags': row['tags'],
'edited': row['edited'],
'type': row['type']
}
user_data = {
'id': row['user_id'],
'username': row['username'],
'score': row['user_score'],
'avatar_b': row['avatar_b'],
'avatar_i': row['avatar_i']
}
2025-08-03 20:36:36 +02:00
2025-08-04 00:04:08 +02:00
rants.append(await format_rant(rant_data, user_data, current_user_id))
2025-08-03 20:36:36 +02:00
return {
"success": True,
"rants": rants,
"settings": {"notif_state": -1, "notif_token": ""},
"set": secrets.token_hex(7),
"wrw": 385,
"dpp": 0,
"num_notifs": 0,
"unread": {"total": 0},
"news": {
"id": 356,
"type": "intlink",
"headline": "Weekly Group Rant",
"body": "Share your thoughts!",
"footer": "Add tag 'wk247' to your rant",
"height": 100,
"action": "grouprant"
}
}
2025-08-04 00:44:29 +02:00
@app.get("/api/rant/rants/{rant_id}")
2025-08-03 20:36:36 +02:00
async def get_rant(
rant_id: int,
app: int = 3,
last_comment_id: Optional[int] = None,
token_id: Optional[int] = None,
token_key: Optional[str] = None,
user_id: Optional[int] = None
):
2025-08-04 00:40:37 +02:00
current_user_id = await authenticate_user(token_id, token_key, user_id) if token_id else None
2025-08-04 00:04:08 +02:00
# Get rant with user info
rant_row = await db.query_one(
"""SELECT r.*, u.id as user_id, u.username, u.score as user_score,
u.avatar_b, u.avatar_i
FROM rants r
JOIN users u ON r.user_id = u.id
WHERE r.id = ?""",
(rant_id,)
)
2025-08-03 20:36:36 +02:00
if not rant_row:
raise HTTPException(status_code=404, detail="Rant not found")
2025-08-04 00:04:08 +02:00
rant_data = {
'id': rant_row['id'],
'text': rant_row['text'],
'score': rant_row['score'],
'created_time': rant_row['created_time'],
'attached_image': rant_row['attached_image'],
'tags': rant_row['tags'],
'edited': rant_row['edited'],
'type': rant_row['type']
}
user_data = {
'id': rant_row['user_id'],
'username': rant_row['username'],
'score': rant_row['user_score'],
'avatar_b': rant_row['avatar_b'],
'avatar_i': rant_row['avatar_i']
}
2025-08-03 20:36:36 +02:00
2025-08-04 00:04:08 +02:00
rant = await format_rant(rant_data, user_data, current_user_id)
2025-08-03 20:36:36 +02:00
# Get comments
2025-08-04 00:04:08 +02:00
comment_rows = await db.query_raw(
"""SELECT c.*, u.id as user_id, u.username, u.score as user_score,
u.avatar_b, u.avatar_i
FROM comments c
JOIN users u ON c.user_id = u.id
WHERE c.rant_id = ?
ORDER BY c.created_time ASC""",
(rant_id,)
)
2025-08-03 20:36:36 +02:00
comments = []
2025-08-04 00:04:08 +02:00
for row in comment_rows:
comment_data = {
'id': row['id'],
'rant_id': row['rant_id'],
'body': row['body'],
'score': row['score'],
'created_time': row['created_time']
}
user_data = {
'id': row['user_id'],
'username': row['username'],
'score': row['user_score'],
'avatar_b': row['avatar_b'],
'avatar_i': row['avatar_i']
}
2025-08-03 20:36:36 +02:00
2025-08-04 00:04:08 +02:00
comments.append(await format_comment(comment_data, user_data, current_user_id))
2025-08-03 20:36:36 +02:00
# Check if subscribed (favorited)
subscribed = 0
if current_user_id:
2025-08-04 00:04:08 +02:00
if await db.exists("favorites", {"user_id": current_user_id, "rant_id": rant_id}):
2025-08-03 20:36:36 +02:00
subscribed = 1
# Add link to rant
rant['link'] = f"rants/{rant_id}/{rant['text'][:50].replace(' ', '-')}"
return {
"rant": rant,
"comments": comments,
"success": True,
"subscribed": subscribed
}
2025-08-04 00:44:29 +02:00
@app.post("/api/rant/rants")
2025-08-03 20:36:36 +02:00
async def create_rant(
rant: str = Form(...),
tags: str = Form(...),
type: int = Form(1),
app: int = Form(3),
token_id: int = Form(...),
token_key: str = Form(...),
user_id: int = Form(...),
image: Optional[UploadFile] = File(None)
):
2025-08-04 00:40:37 +02:00
current_user_id = await authenticate_user(token_id, token_key, user_id)
2025-08-03 20:36:36 +02:00
if not current_user_id:
return {"success": False, "error": "Authentication required"}
# Check for duplicate rant
2025-08-04 00:04:08 +02:00
recent_time = int(datetime.now().timestamp()) - 300
duplicate = await db.query_one(
"SELECT id FROM rants WHERE user_id = ? AND text = ? AND created_time > ?",
(current_user_id, rant, recent_time)
)
2025-08-03 20:36:36 +02:00
2025-08-04 00:04:08 +02:00
if duplicate:
2025-08-03 20:36:36 +02:00
return {
"success": False,
2025-08-04 00:44:29 +02:00
"error": "It looks like you just posted this same rant! Your connection might have timed out while posting so you might have seen an error, but sometimes the rant still gets posted and in this case it seems it did, so please check :) If this was not the case please contact info@rant.io. Thanks!"
2025-08-03 20:36:36 +02:00
}
# Handle image upload
image_path = ""
if image:
file_ext = os.path.splitext(image.filename)[1]
file_name = f"{secrets.token_hex(16)}{file_ext}"
file_path = UPLOAD_DIR / file_name
with open(file_path, "wb") as f:
f.write(await image.read())
image_path = f"/uploads/{file_name}"
# Create rant
created_time = int(datetime.now().timestamp())
tags_list = [tag.strip() for tag in tags.split(',') if tag.strip()]
2025-08-04 00:04:08 +02:00
rant_id = await db.insert("rants", {
"user_id": current_user_id,
"text": rant,
"created_time": created_time,
"attached_image": image_path,
"tags": json.dumps(tags_list),
"type": type
}, return_id=True)
2025-08-03 20:36:36 +02:00
return {"success": True, "rant_id": rant_id}
2025-08-04 00:44:29 +02:00
@app.post("/api/rant/rants/{rant_id}")
2025-08-03 20:36:36 +02:00
async def update_rant(
rant_id: int,
rant: str = Form(...),
tags: str = Form(...),
app: int = Form(3),
token_id: int = Form(...),
token_key: str = Form(...),
user_id: int = Form(...)
):
2025-08-04 00:40:37 +02:00
current_user_id = await authenticate_user(token_id, token_key, user_id)
2025-08-03 20:36:36 +02:00
if not current_user_id:
return {"success": False, "error": "Authentication required"}
# Check ownership
2025-08-04 00:04:08 +02:00
rant_row = await db.get("rants", {"id": rant_id})
2025-08-03 20:36:36 +02:00
if not rant_row or rant_row['user_id'] != current_user_id:
return {"success": False, "fail_reason": "Unauthorized"}
# Update rant
tags_list = [tag.strip() for tag in tags.split(',') if tag.strip()]
2025-08-04 00:04:08 +02:00
await db.update("rants", {
"text": rant,
"tags": json.dumps(tags_list),
"edited": 1
}, {"id": rant_id})
2025-08-03 20:36:36 +02:00
return {"success": True}
2025-08-04 00:44:29 +02:00
@app.delete("/api/rant/rants/{rant_id}")
2025-08-03 20:36:36 +02:00
async def delete_rant(
rant_id: int,
app: int = 3,
token_id: int = None,
token_key: str = None,
user_id: int = None
):
2025-08-04 00:40:37 +02:00
current_user_id = await authenticate_user(token_id, token_key, user_id)
2025-08-03 20:36:36 +02:00
if not current_user_id:
return {"success": False, "error": "Authentication required"}
# Check ownership
2025-08-04 00:04:08 +02:00
rant_row = await db.get("rants", {"id": rant_id})
2025-08-03 20:36:36 +02:00
if not rant_row:
return {"success": False, "error": "Rant not found"}
if rant_row['user_id'] != current_user_id:
return {"success": False, "error": "Unauthorized"}
# Delete rant and related data
2025-08-04 00:04:08 +02:00
await db.delete("comments", {"rant_id": rant_id})
await db.delete("votes", {"target_id": rant_id, "target_type": "rant"})
await db.delete("favorites", {"rant_id": rant_id})
await db.delete("rants", {"id": rant_id})
2025-08-03 20:36:36 +02:00
return {"success": True}
2025-08-04 00:44:29 +02:00
@app.post("/api/rant/rants/{rant_id}/vote")
2025-08-03 20:36:36 +02:00
async def vote_rant(
rant_id: int,
vote: int = Form(...),
reason: Optional[int] = Form(None),
app: int = Form(3),
token_id: int = Form(...),
token_key: str = Form(...),
user_id: int = Form(...)
):
2025-08-04 00:40:37 +02:00
current_user_id = await authenticate_user(token_id, token_key, user_id)
2025-08-03 20:36:36 +02:00
if not current_user_id:
return {"success": False, "error": "Authentication required"}
# Get rant
2025-08-04 00:04:08 +02:00
rant = await db.get("rants", {"id": rant_id})
2025-08-03 20:36:36 +02:00
if not rant:
return {"success": False, "error": "Rant not found"}
# Check for existing vote
2025-08-04 00:04:08 +02:00
existing_vote = await db.get("votes", {
"user_id": current_user_id,
"target_id": rant_id,
"target_type": "rant"
})
2025-08-03 20:36:36 +02:00
if vote == 0:
# Remove vote
if existing_vote:
2025-08-04 00:04:08 +02:00
await db.delete("votes", {"id": existing_vote['id']})
2025-08-03 20:36:36 +02:00
# Update score
2025-08-04 00:04:08 +02:00
await db.update("rants", {
"score": rant['score'] - existing_vote['vote']
}, {"id": rant_id})
2025-08-03 20:36:36 +02:00
else:
if existing_vote:
# Update vote
score_diff = vote - existing_vote['vote']
2025-08-04 00:04:08 +02:00
await db.update("votes", {
"vote": vote,
"reason": reason
}, {"id": existing_vote['id']})
await db.update("rants", {
"score": rant['score'] + score_diff
}, {"id": rant_id})
2025-08-03 20:36:36 +02:00
else:
# New vote
2025-08-04 00:04:08 +02:00
await db.insert("votes", {
"user_id": current_user_id,
"target_id": rant_id,
"target_type": "rant",
"vote": vote,
"reason": reason
}, return_id=True)
await db.update("rants", {
"score": rant['score'] + vote
}, {"id": rant_id})
2025-08-03 20:36:36 +02:00
# Update user score
2025-08-04 00:04:08 +02:00
user = await db.get("users", {"id": rant['user_id']})
score_change = vote if vote != 0 else -existing_vote['vote'] if existing_vote else 0
await db.update("users", {
"score": user['score'] + score_change
}, {"id": rant['user_id']})
# Get updated rant with user info
updated_rant = await db.query_one(
"""SELECT r.*, u.id as user_id, u.username, u.score as user_score,
u.avatar_b, u.avatar_i
FROM rants r
JOIN users u ON r.user_id = u.id
WHERE r.id = ?""",
(rant_id,)
)
rant_data = {
'id': updated_rant['id'],
'text': updated_rant['text'],
'score': updated_rant['score'],
'created_time': updated_rant['created_time'],
'attached_image': updated_rant['attached_image'],
'tags': updated_rant['tags'],
'edited': updated_rant['edited'],
'type': updated_rant['type']
}
user_data = {
'id': updated_rant['user_id'],
'username': updated_rant['username'],
'score': updated_rant['user_score'],
'avatar_b': updated_rant['avatar_b'],
'avatar_i': updated_rant['avatar_i']
}
2025-08-03 20:36:36 +02:00
return {
"success": True,
2025-08-04 00:04:08 +02:00
"rant": await format_rant(rant_data, user_data, current_user_id)
2025-08-03 20:36:36 +02:00
}
2025-08-04 00:44:29 +02:00
@app.post("/api/rant/rants/{rant_id}/favorite")
2025-08-03 20:36:36 +02:00
async def favorite_rant(
rant_id: int,
app: int = Form(3),
token_id: int = Form(...),
token_key: str = Form(...),
user_id: int = Form(...)
):
2025-08-04 00:40:37 +02:00
current_user_id = await authenticate_user(token_id, token_key, user_id)
2025-08-03 20:36:36 +02:00
if not current_user_id:
return {"success": False, "error": "Authentication required"}
try:
2025-08-04 00:04:08 +02:00
await db.insert("favorites", {
"user_id": current_user_id,
"rant_id": rant_id
}, return_id=True)
2025-08-03 20:36:36 +02:00
return {"success": True}
2025-08-04 00:04:08 +02:00
except Exception:
2025-08-03 20:36:36 +02:00
return {"success": False, "error": "Already favorited"}
2025-08-04 00:44:29 +02:00
@app.post("/api/rant/rants/{rant_id}/unfavorite")
2025-08-03 20:36:36 +02:00
async def unfavorite_rant(
rant_id: int,
app: int = Form(3),
token_id: int = Form(...),
token_key: str = Form(...),
user_id: int = Form(...)
):
2025-08-04 00:40:37 +02:00
current_user_id = await authenticate_user(token_id, token_key, user_id)
2025-08-03 20:36:36 +02:00
if not current_user_id:
return {"success": False, "error": "Authentication required"}
2025-08-04 00:04:08 +02:00
await db.delete("favorites", {
"user_id": current_user_id,
"rant_id": rant_id
})
2025-08-03 20:36:36 +02:00
return {"success": True}
2025-08-04 00:44:29 +02:00
@app.post("/api/rant/rants/{rant_id}/comments")
2025-08-03 20:36:36 +02:00
async def create_comment(
rant_id: int,
comment: str = Form(...),
app: int = Form(3),
token_id: int = Form(...),
token_key: str = Form(...),
user_id: int = Form(...),
image: Optional[UploadFile] = File(None)
):
2025-08-04 00:40:37 +02:00
current_user_id = await authenticate_user(token_id, token_key, user_id)
2025-08-03 20:36:36 +02:00
if not current_user_id:
return {"success": False, "confirmed": False}
# Check if rant exists
2025-08-04 00:04:08 +02:00
if not await db.exists("rants", {"id": rant_id}):
2025-08-03 20:36:36 +02:00
return {"success": False, "error": "Rant not found"}
# Handle image upload
image_path = ""
if image:
file_ext = os.path.splitext(image.filename)[1]
file_name = f"{secrets.token_hex(16)}{file_ext}"
file_path = UPLOAD_DIR / file_name
with open(file_path, "wb") as f:
f.write(await image.read())
image_path = f"/uploads/{file_name}"
# Create comment
created_time = int(datetime.now().timestamp())
2025-08-04 00:04:08 +02:00
comment_id = await db.insert("comments", {
"rant_id": rant_id,
"user_id": current_user_id,
"body": comment,
"created_time": created_time,
"attached_image": image_path
}, return_id=True)
2025-08-03 20:36:36 +02:00
# Create notification for rant owner
2025-08-04 00:04:08 +02:00
rant = await db.get("rants", {"id": rant_id})
if rant and rant['user_id'] != current_user_id:
await db.insert("notifications", {
"user_id": rant['user_id'],
"type": "comment",
"rant_id": rant_id,
"comment_id": comment_id,
"from_user_id": current_user_id,
"created_time": created_time
}, return_id=True)
2025-08-03 20:36:36 +02:00
return {"success": True}
@app.get("/api/comments/{comment_id}")
async def get_comment(
comment_id: int,
app: int = 3,
token_id: Optional[int] = None,
token_key: Optional[str] = None,
user_id: Optional[int] = None
):
2025-08-04 00:40:37 +02:00
current_user_id = await authenticate_user(token_id, token_key, user_id) if token_id else None
2025-08-03 20:36:36 +02:00
2025-08-04 00:04:08 +02:00
row = await db.query_one(
"""SELECT c.*, u.id as user_id, u.username, u.score as user_score,
u.avatar_b, u.avatar_i
FROM comments c
JOIN users u ON c.user_id = u.id
WHERE c.id = ?""",
(comment_id,)
)
2025-08-03 20:36:36 +02:00
if not row:
return {"success": False, "error": "Invalid comment specified in path."}
2025-08-04 00:04:08 +02:00
comment_data = {
'id': row['id'],
'rant_id': row['rant_id'],
'body': row['body'],
'score': row['score'],
'created_time': row['created_time']
}
user_data = {
'id': row['user_id'],
'username': row['username'],
'score': row['user_score'],
'avatar_b': row['avatar_b'],
'avatar_i': row['avatar_i']
}
2025-08-03 20:36:36 +02:00
return {
"success": True,
2025-08-04 00:04:08 +02:00
"comment": await format_comment(comment_data, user_data, current_user_id)
2025-08-03 20:36:36 +02:00
}
@app.post("/api/comments/{comment_id}")
async def update_comment(
comment_id: int,
comment: str = Form(...),
app: int = Form(3),
token_id: int = Form(...),
token_key: str = Form(...),
user_id: int = Form(...)
):
2025-08-04 00:40:37 +02:00
current_user_id = await authenticate_user(token_id, token_key, user_id)
2025-08-03 20:36:36 +02:00
if not current_user_id:
return {"success": False, "error": "Authentication required"}
# Check ownership
2025-08-04 00:04:08 +02:00
comment_row = await db.get("comments", {"id": comment_id})
2025-08-03 20:36:36 +02:00
if not comment_row:
return {"success": False, "error": "Invalid comment specified in path."}
if comment_row['user_id'] != current_user_id:
return {"success": False, "fail_reason": "Unauthorized"}
# Update comment
2025-08-04 00:04:08 +02:00
await db.update("comments", {
"body": comment,
"edited": 1
}, {"id": comment_id})
2025-08-03 20:36:36 +02:00
return {"success": True}
@app.delete("/api/comments/{comment_id}")
async def delete_comment(
comment_id: int,
app: int = 3,
token_id: int = None,
token_key: str = None,
user_id: int = None
):
2025-08-04 00:40:37 +02:00
current_user_id = await authenticate_user(token_id, token_key, user_id)
2025-08-03 20:36:36 +02:00
if not current_user_id:
return {"success": False, "error": "Authentication required"}
# Check ownership
2025-08-04 00:04:08 +02:00
comment_row = await db.get("comments", {"id": comment_id})
2025-08-03 20:36:36 +02:00
if not comment_row:
return {"success": False, "error": "Invalid comment specified in path."}
if comment_row['user_id'] != current_user_id:
return {"success": False, "error": "Unauthorized"}
# Delete comment and related data
2025-08-04 00:04:08 +02:00
await db.delete("votes", {"target_id": comment_id, "target_type": "comment"})
await db.delete("comments", {"id": comment_id})
2025-08-03 20:36:36 +02:00
return {"success": True}
@app.post("/api/comments/{comment_id}/vote")
async def vote_comment(
comment_id: int,
vote: int = Form(...),
reason: Optional[int] = Form(None),
app: int = Form(3),
token_id: int = Form(...),
token_key: str = Form(...),
user_id: int = Form(...)
):
2025-08-04 00:40:37 +02:00
current_user_id = await authenticate_user(token_id, token_key, user_id)
2025-08-03 20:36:36 +02:00
if not current_user_id:
return {"success": False, "error": "Authentication required"}
# Get comment
2025-08-04 00:04:08 +02:00
comment = await db.get("comments", {"id": comment_id})
2025-08-03 20:36:36 +02:00
if not comment:
return {"success": False, "error": "Invalid comment specified in path."}
# Check for existing vote
2025-08-04 00:04:08 +02:00
existing_vote = await db.get("votes", {
"user_id": current_user_id,
"target_id": comment_id,
"target_type": "comment"
})
2025-08-03 20:36:36 +02:00
if vote == 0:
# Remove vote
if existing_vote:
2025-08-04 00:04:08 +02:00
await db.delete("votes", {"id": existing_vote['id']})
2025-08-03 20:36:36 +02:00
# Update score
2025-08-04 00:04:08 +02:00
await db.update("comments", {
"score": comment['score'] - existing_vote['vote']
}, {"id": comment_id})
2025-08-03 20:36:36 +02:00
else:
if existing_vote:
# Update vote
score_diff = vote - existing_vote['vote']
2025-08-04 00:04:08 +02:00
await db.update("votes", {
"vote": vote,
"reason": reason
}, {"id": existing_vote['id']})
await db.update("comments", {
"score": comment['score'] + score_diff
}, {"id": comment_id})
2025-08-03 20:36:36 +02:00
else:
# New vote
2025-08-04 00:04:08 +02:00
await db.insert("votes", {
"user_id": current_user_id,
"target_id": comment_id,
"target_type": "comment",
"vote": vote,
"reason": reason
}, return_id=True)
await db.update("comments", {
"score": comment['score'] + vote
}, {"id": comment_id})
2025-08-03 20:36:36 +02:00
return {"success": True}
@app.get("/api/users/{user_id}")
async def get_profile(
user_id: int,
app: int = 3,
token_id: Optional[int] = None,
token_key: Optional[str] = None,
auth_user_id: Optional[int] = None
):
2025-08-04 00:40:37 +02:00
current_user_id = await authenticate_user(token_id, token_key, auth_user_id) if token_id else None
2025-08-03 20:36:36 +02:00
# Get user
2025-08-04 00:04:08 +02:00
user = await db.get("users", {"id": user_id})
2025-08-03 20:36:36 +02:00
if not user:
return {"success": False, "error": "User not found"}
# Get user's rants
2025-08-04 00:04:08 +02:00
rant_rows = await db.query_raw(
"""SELECT r.*, u.id as user_id, u.username, u.score as user_score,
u.avatar_b, u.avatar_i
FROM rants r
JOIN users u ON r.user_id = u.id
WHERE r.user_id = ?
ORDER BY r.created_time DESC
LIMIT 50""",
(user_id,)
)
2025-08-03 20:36:36 +02:00
rants = []
2025-08-04 00:04:08 +02:00
for row in rant_rows:
rant_data = {
'id': row['id'],
'text': row['text'],
'score': row['score'],
'created_time': row['created_time'],
'attached_image': row['attached_image'],
'tags': row['tags'],
'edited': row['edited'],
'type': row['type']
}
user_data = {
'id': row['user_id'],
'username': row['username'],
'score': row['user_score'],
'avatar_b': row['avatar_b'],
'avatar_i': row['avatar_i']
}
2025-08-03 20:36:36 +02:00
2025-08-04 00:04:08 +02:00
rants.append(await format_rant(rant_data, user_data, current_user_id))
2025-08-03 20:36:36 +02:00
# Get user's comments
2025-08-04 00:04:08 +02:00
comment_rows = await db.query_raw(
"""SELECT c.*, u.id as user_id, u.username, u.score as user_score,
u.avatar_b, u.avatar_i
FROM comments c
JOIN users u ON c.user_id = u.id
WHERE c.user_id = ?
ORDER BY c.created_time DESC
LIMIT 50""",
(user_id,)
)
2025-08-03 20:36:36 +02:00
comments = []
2025-08-04 00:04:08 +02:00
for row in comment_rows:
comment_data = {
'id': row['id'],
'rant_id': row['rant_id'],
'body': row['body'],
'score': row['score'],
'created_time': row['created_time']
}
user_data = {
'id': row['user_id'],
'username': row['username'],
'score': row['user_score'],
'avatar_b': row['avatar_b'],
'avatar_i': row['avatar_i']
}
2025-08-03 20:36:36 +02:00
2025-08-04 00:04:08 +02:00
comments.append(await format_comment(comment_data, user_data, current_user_id))
2025-08-03 20:36:36 +02:00
# Get favorited rants
2025-08-04 00:04:08 +02:00
favorite_rows = await db.query_raw(
"""SELECT r.*, u.id as user_id, u.username, u.score as user_score,
u.avatar_b, u.avatar_i
FROM rants r
JOIN users u ON r.user_id = u.id
JOIN favorites f ON f.rant_id = r.id
WHERE f.user_id = ?
ORDER BY f.id DESC
LIMIT 50""",
(user_id,)
)
2025-08-03 20:36:36 +02:00
favorites = []
2025-08-04 00:04:08 +02:00
for row in favorite_rows:
rant_data = {
'id': row['id'],
'text': row['text'],
'score': row['score'],
'created_time': row['created_time'],
'attached_image': row['attached_image'],
'tags': row['tags'],
'edited': row['edited'],
'type': row['type']
}
user_data = {
'id': row['user_id'],
'username': row['username'],
'score': row['user_score'],
'avatar_b': row['avatar_b'],
'avatar_i': row['avatar_i']
}
2025-08-03 20:36:36 +02:00
2025-08-04 00:04:08 +02:00
favorites.append(await format_rant(rant_data, user_data, current_user_id))
2025-08-03 20:36:36 +02:00
return {
"success": True,
"profile": {
"username": user['username'],
"score": user['score'],
"about": user['about'],
"location": user['location'],
"created_time": user['created_time'],
"skills": user['skills'],
"github": user['github'],
"website": user['website'],
"avatar": {
"b": user['avatar_b'],
"i": user['avatar_i'] or ""
},
"content": {
"content": {
"rants": rants,
"comments": comments,
"favorites": favorites
}
}
}
}
@app.get("/api/get-user-id")
async def get_user_id(
username: str,
app: int = 3
):
2025-08-04 00:04:08 +02:00
user = await db.get("users", {"username": username})
2025-08-03 20:36:36 +02:00
if not user:
return {"success": False, "error": "User not found"}
return {"success": True, "user_id": user['id']}
2025-08-04 00:44:29 +02:00
@app.get("/api/rant/search")
2025-08-03 20:36:36 +02:00
async def search(
term: str,
app: int = 3,
token_id: Optional[int] = None,
token_key: Optional[str] = None,
user_id: Optional[int] = None
):
2025-08-04 00:40:37 +02:00
current_user_id = await authenticate_user(token_id, token_key, user_id) if token_id else None
2025-08-03 20:36:36 +02:00
# Search rants
2025-08-04 00:04:08 +02:00
rows = await db.query_raw(
"""SELECT r.*, u.id as user_id, u.username, u.score as user_score,
u.avatar_b, u.avatar_i
FROM rants r
JOIN users u ON r.user_id = u.id
WHERE r.text LIKE ? OR r.tags LIKE ?
ORDER BY r.score DESC
LIMIT 50""",
(f'%{term}%', f'%{term}%')
)
2025-08-03 20:36:36 +02:00
results = []
2025-08-04 00:04:08 +02:00
for row in rows:
rant_data = {
'id': row['id'],
'text': row['text'],
'score': row['score'],
'created_time': row['created_time'],
'attached_image': row['attached_image'],
'tags': row['tags'],
'edited': row['edited'],
'type': row['type']
}
user_data = {
'id': row['user_id'],
'username': row['username'],
'score': row['user_score'],
'avatar_b': row['avatar_b'],
'avatar_i': row['avatar_i']
}
2025-08-03 20:36:36 +02:00
2025-08-04 00:04:08 +02:00
results.append(await format_rant(rant_data, user_data, current_user_id))
2025-08-03 20:36:36 +02:00
return {"success": True, "results": results}
@app.get("/api/users/me/notif-feed")
async def get_notifications(
ext_prof: int = 1,
last_time: Optional[int] = None,
app: int = 3,
2025-08-04 00:40:37 +02:00
token_id: Optional[int] = None,
token_key: Optional[str] = None,
user_id: Optional[int] = None
2025-08-03 20:36:36 +02:00
):
2025-08-04 00:40:37 +02:00
# Use the generic authenticate_user function
current_user_id = await authenticate_user(token_id, token_key, user_id)
2025-08-03 20:36:36 +02:00
if not current_user_id:
return {"success": False, "error": "Authentication required"}
# Get notifications
2025-08-04 00:04:08 +02:00
rows = await db.query_raw(
"""SELECT n.*, u.username
FROM notifications n
LEFT JOIN users u ON n.from_user_id = u.id
WHERE n.user_id = ?
ORDER BY n.created_time DESC
LIMIT 50""",
(current_user_id,)
)
2025-08-03 20:36:36 +02:00
items = []
unread_count = 0
2025-08-04 00:04:08 +02:00
for row in rows:
2025-08-03 20:36:36 +02:00
item = {
"type": row['type'],
"rant_id": row['rant_id'],
"comment_id": row['comment_id'],
"created_time": row['created_time'],
"read": row['read'],
"uid": row['from_user_id'],
"username": row['username'] or ""
}
items.append(item)
if not row['read']:
unread_count += 1
2025-08-04 00:40:37 +02:00
# Mark notifications as read
if rows: # Only update if there are notifications
await db.update("notifications", {"read": 1}, {"user_id": current_user_id})
2025-08-03 20:36:36 +02:00
return {
"success": True,
"data": {
"items": items,
"check_time": int(datetime.now().timestamp()),
"username_map": [],
"unread": {
"all": unread_count,
"upvotes": 0,
"mentions": 0,
"comments": unread_count,
"subs": 0,
"total": unread_count
},
"num_unread": unread_count
}
}
2025-08-04 00:40:37 +02:00
async def authenticate_user(token_id: Optional[int] = None,
token_key: Optional[str] = None,
user_id: Optional[int] = None):
"""Generic authentication function that works with any parameter source"""
if not all([token_id, token_key, user_id]):
return None
token = await db.get("auth_tokens", {
"id": token_id,
"token_key": token_key,
"user_id": user_id
})
if not token or token['expire_time'] <= int(datetime.now().timestamp()):
return None
return user_id
2025-08-03 20:36:36 +02:00
@app.delete("/api/users/me/notif-feed")
async def clear_notifications(
app: int = Form(3),
token_id: int = Form(...),
token_key: str = Form(...),
user_id: int = Form(...)
):
2025-08-04 00:40:37 +02:00
current_user_id = await authenticate_user(token_id, token_key, user_id)
2025-08-03 20:36:36 +02:00
if not current_user_id:
return {"success": False, "error": "Authentication required"}
2025-08-04 00:04:08 +02:00
await db.delete("notifications", {"user_id": current_user_id})
2025-08-03 20:36:36 +02:00
return {"success": True}
@app.post("/api/users/me/edit-profile")
async def edit_profile(
profile_about: str = Form(""),
profile_skills: str = Form(""),
profile_location: str = Form(""),
profile_website: str = Form(""),
profile_github: str = Form(""),
app: int = Form(3),
token_id: int = Form(...),
token_key: str = Form(...),
user_id: int = Form(...)
):
2025-08-04 00:40:37 +02:00
current_user_id = await authenticate_user(token_id, token_key, user_id)
2025-08-03 20:36:36 +02:00
if not current_user_id:
return {"success": False, "error": "Authentication required"}
2025-08-04 00:04:08 +02:00
await db.update("users", {
"about": profile_about,
"skills": profile_skills,
"location": profile_location,
"website": profile_website,
"github": profile_github
}, {"id": current_user_id})
2025-08-03 20:36:36 +02:00
return {"success": True}
@app.post("/api/users/forgot-password")
async def forgot_password(
username: str = Form(...),
app: int = Form(3)
):
# In a real implementation, this would send an email
return {"success": True}
@app.post("/api/users/me/resend-confirm")
async def resend_confirmation(
app: int = Form(3),
token_id: int = Form(...),
token_key: str = Form(...),
user_id: int = Form(...)
):
2025-08-04 00:40:37 +02:00
current_user_id = await authenticate_user(token_id, token_key, user_id)
2025-08-03 20:36:36 +02:00
if not current_user_id:
return {"success": False, "error": "Authentication required"}
# In a real implementation, this would send an email
return {"success": True}
@app.post("/api/users/me/mark-news-read")
async def mark_news_read(
news_id: str = Form(...),
app: int = Form(3),
token_id: int = Form(...),
token_key: str = Form(...),
user_id: int = Form(...)
):
2025-08-04 00:40:37 +02:00
current_user_id = await authenticate_user(token_id, token_key, user_id)
2025-08-03 20:36:36 +02:00
if not current_user_id:
return {"success": False, "error": "Authentication required"}
# In a real implementation, this would mark news as read
return {"success": True}
# Serve uploaded files
@app.get("/uploads/{filename}")
async def get_upload(filename: str):
file_path = UPLOAD_DIR / filename
if file_path.exists():
return FileResponse(file_path)
raise HTTPException(status_code=404, detail="File not found")
# Serve static files (for frontend)
from fastapi.staticfiles import StaticFiles
app.mount("/static", StaticFiles(directory="static"), name="static")
# Root endpoint serves the main HTML
@app.get("/")
async def root():
return FileResponse("static/index.html")
if __name__ == "__main__":
import uvicorn
2025-08-03 20:37:53 +02:00
uvicorn.run(app, host="0.0.0.0", port=8111)