|
"""
|
|
Merged Rant Community and Chat Application
|
|
Environment variables (from .env):
|
|
- DB_DSN: Database connection string
|
|
- SECRET_KEY: Secret key for security
|
|
- SESSION_COOKIE_NAME: Name of session cookie
|
|
- PWA_SCOPE: PWA scope path
|
|
- UPLOAD_DIR: Upload directory path
|
|
- APP_ID: Application ID
|
|
"""
|
|
|
|
from fastapi import FastAPI, HTTPException, Depends, Form, File, UploadFile, Request, Response, WebSocket, WebSocketDisconnect
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
from fastapi.templating import Jinja2Templates
|
|
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
|
|
import asyncio
|
|
from ads import AsyncDataSet
|
|
from dotenv import load_dotenv
|
|
from contextlib import asynccontextmanager
|
|
|
|
# Load environment variables
|
|
load_dotenv()
|
|
|
|
# Configuration from environment
|
|
DB_DSN = os.getenv('DB_DSN', 'rant_community.db')
|
|
SECRET_KEY = os.getenv('SECRET_KEY', secrets.token_urlsafe(32))
|
|
SESSION_COOKIE_NAME = os.getenv('SESSION_COOKIE_NAME', 'auth_token')
|
|
PWA_SCOPE = os.getenv('PWA_SCOPE', '/')
|
|
UPLOAD_DIR = Path(os.getenv('UPLOAD_DIR', 'uploads'))
|
|
APP_ID = int(os.getenv('APP_ID', '3'))
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
await init_db()
|
|
Path("templates").mkdir(exist_ok=True)
|
|
Path("static").mkdir(exist_ok=True)
|
|
yield
|
|
|
|
app = FastAPI(title="Rant Community API", lifespan=lifespan)
|
|
|
|
# Enable CORS
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# Database setup
|
|
UPLOAD_DIR.mkdir(exist_ok=True)
|
|
|
|
# Template setup
|
|
templates = Jinja2Templates(directory="templates")
|
|
|
|
# Initialize AsyncDataSet
|
|
db = AsyncDataSet(DB_DSN)
|
|
|
|
# WebSocket connection manager
|
|
class ConnectionManager:
|
|
def __init__(self):
|
|
self.active_connections: Dict[str, List[WebSocket]] = {}
|
|
self.user_sockets: Dict[str, str] = {}
|
|
|
|
async def connect(self, websocket: WebSocket, user_id: str):
|
|
await websocket.accept()
|
|
if user_id not in self.active_connections:
|
|
self.active_connections[user_id] = []
|
|
self.active_connections[user_id].append(websocket)
|
|
socket_id = str(id(websocket))
|
|
self.user_sockets[socket_id] = user_id
|
|
|
|
def disconnect(self, websocket: WebSocket, user_id: str):
|
|
if user_id in self.active_connections:
|
|
self.active_connections[user_id].remove(websocket)
|
|
if not self.active_connections[user_id]:
|
|
del self.active_connections[user_id]
|
|
socket_id = str(id(websocket))
|
|
if socket_id in self.user_sockets:
|
|
del self.user_sockets[socket_id]
|
|
|
|
async def send_personal_message(self, message: str, user_id: str):
|
|
if user_id in self.active_connections:
|
|
for connection in self.active_connections[user_id]:
|
|
await connection.send_text(message)
|
|
|
|
async def broadcast_to_channel(self, message: str, channel_id: str, exclude_user: str = None):
|
|
members = await db.find("channel_members", {"channel_id": channel_id})
|
|
for member in members:
|
|
if member["user_id"] != exclude_user:
|
|
await self.send_personal_message(message, member["user_id"])
|
|
|
|
|
|
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
|
|
|
|
|
|
manager = ConnectionManager()
|
|
|
|
async def init_db():
|
|
# Main app tables (from main.py)
|
|
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",
|
|
"status": "TEXT DEFAULT 'offline'",
|
|
"last_seen": "TEXT"
|
|
}, ["UNIQUE(username)", "UNIQUE(email)"])
|
|
|
|
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)"])
|
|
|
|
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)"])
|
|
|
|
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)"])
|
|
|
|
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)"])
|
|
|
|
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)"])
|
|
|
|
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)"])
|
|
|
|
# Chat tables (merged from chat.py)
|
|
await db.create_table("channels", {
|
|
"name": "TEXT UNIQUE NOT NULL",
|
|
"description": "TEXT",
|
|
"created_by": "TEXT NOT NULL",
|
|
"is_dm": "INTEGER DEFAULT 0"
|
|
})
|
|
|
|
await db.create_table("channel_members", {
|
|
"channel_id": "TEXT NOT NULL",
|
|
"user_id": "TEXT NOT NULL",
|
|
"joined_at": "TEXT",
|
|
"last_read_message_id": "TEXT"
|
|
}, ["UNIQUE(channel_id, user_id)"])
|
|
|
|
await db.create_table("messages", {
|
|
"channel_id": "TEXT NOT NULL",
|
|
"user_id": "TEXT NOT NULL",
|
|
"content": "TEXT NOT NULL",
|
|
"edited": "INTEGER DEFAULT 0",
|
|
"edited_at": "TEXT"
|
|
})
|
|
|
|
await db.create_table("dm_channels", {
|
|
"user_id_1": "TEXT NOT NULL",
|
|
"user_id_2": "TEXT NOT NULL",
|
|
"channel_id": "TEXT NOT NULL"
|
|
}, ["UNIQUE(user_id_1, user_id_2)"])
|
|
|
|
# Create default channels
|
|
default_channels = ["general", "random", "development", "design"]
|
|
for channel_name in default_channels:
|
|
existing = await db.get("channels", {"name": channel_name})
|
|
if not existing:
|
|
await db.insert("channels", {
|
|
"name": channel_name,
|
|
"description": f"#{channel_name} channel",
|
|
"created_by": "system"
|
|
})
|
|
|
|
# Pydantic models (combined from both apps)
|
|
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
|
|
|
|
class ChannelCreate(BaseModel):
|
|
name: str
|
|
description: Optional[str] = None
|
|
|
|
class MessageCreate(BaseModel):
|
|
content: str
|
|
channel_id: Optional[str] = None
|
|
dm_user_id: Optional[str] = None
|
|
|
|
# Helper functions
|
|
def hash_password(password: str) -> str:
|
|
return hashlib.sha256(password.encode()).hexdigest()
|
|
|
|
def generate_token() -> str:
|
|
return secrets.token_urlsafe(32)
|
|
|
|
def format_time(timestamp):
|
|
date = datetime.fromtimestamp(timestamp)
|
|
now = datetime.now()
|
|
diff = (now - date).total_seconds()
|
|
|
|
if diff < 60: return 'just now'
|
|
if diff < 3600: return f'{int(diff / 60)}m ago'
|
|
if diff < 86400: return f'{int(diff / 3600)}h ago'
|
|
if diff < 604800: return f'{int(diff / 86400)}d ago'
|
|
|
|
return date.strftime('%Y-%m-%d')
|
|
|
|
templates.env.globals['format_time'] = format_time
|
|
|
|
def escape_html(text):
|
|
return text.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''')
|
|
|
|
templates.env.globals['escape_html'] = escape_html
|
|
|
|
# Authentication helpers
|
|
async def get_current_user_from_cookie(request: Request):
|
|
auth_token = request.cookies.get(SESSION_COOKIE_NAME)
|
|
if not auth_token:
|
|
return None
|
|
|
|
try:
|
|
token_data = json.loads(auth_token)
|
|
token = await db.get("auth_tokens", {
|
|
"id": token_data['id'],
|
|
"token_key": token_data['key'],
|
|
"user_id": token_data['user_id']
|
|
})
|
|
|
|
if not token or token['expire_time'] <= int(datetime.now().timestamp()):
|
|
return None
|
|
|
|
user = await db.get("users", {"id": token_data['user_id']})
|
|
if user:
|
|
user['token_id'] = token_data['id']
|
|
user['token_key'] = token_data['key']
|
|
return user
|
|
except:
|
|
return None
|
|
|
|
async def format_rant(rant_row, user_row, current_user_id=None):
|
|
comment_count = await db.count("comments", {"rant_id": rant_row['id']})
|
|
|
|
vote_state = 0
|
|
if current_user_id:
|
|
vote = await db.get("votes", {
|
|
"user_id": current_user_id,
|
|
"target_id": rant_row['id'],
|
|
"target_type": "rant"
|
|
})
|
|
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 ""
|
|
}
|
|
}
|
|
|
|
async def format_comment(comment_row, user_row, current_user_id=None):
|
|
vote_state = 0
|
|
if current_user_id:
|
|
vote = await db.get("votes", {
|
|
"user_id": current_user_id,
|
|
"target_id": comment_row['id'],
|
|
"target_type": "comment"
|
|
})
|
|
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 ""
|
|
}
|
|
}
|
|
|
|
# SSR Routes from main.py
|
|
@app.get("/", response_class=HTMLResponse)
|
|
async def home(request: Request, sort: str = "recent"):
|
|
current_user = await get_current_user_from_cookie(request)
|
|
current_user_id = current_user['id'] if current_user else None
|
|
|
|
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 50 OFFSET 0""",
|
|
()
|
|
)
|
|
|
|
rants = []
|
|
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']
|
|
}
|
|
|
|
rants.append(await format_rant(rant_data, user_data, current_user_id))
|
|
|
|
notif_count = 0
|
|
if current_user:
|
|
notif_count = await db.count("notifications", {"user_id": current_user['id'], "read": 0})
|
|
|
|
return templates.TemplateResponse("index.html", {
|
|
"request": request,
|
|
"current_user": current_user,
|
|
"notif_count": notif_count,
|
|
"rants": rants,
|
|
"current_sort": sort,
|
|
"escape_html": escape_html,
|
|
"format_time": format_time
|
|
})
|
|
|
|
# Chat route - ONLY /chat is valid, NOT /chat/
|
|
@app.get("/chat", response_class=HTMLResponse)
|
|
async def chat_page(request: Request):
|
|
current_user = await get_current_user_from_cookie(request)
|
|
if not current_user:
|
|
return RedirectResponse("/login", status_code=302)
|
|
|
|
# Calculate notification count
|
|
notif_count = await db.count("notifications", {"user_id": current_user['id'], "read": 0})
|
|
|
|
return templates.TemplateResponse("chat.html", {
|
|
"request": request,
|
|
"current_user": current_user,
|
|
"notif_count": notif_count,
|
|
"escape_html": escape_html,
|
|
"format_time": format_time
|
|
})
|
|
|
|
|
|
@app.get("/rant/{rant_id}", response_class=HTMLResponse)
|
|
async def rant_detail(request: Request, rant_id: int):
|
|
current_user = await get_current_user_from_cookie(request)
|
|
current_user_id = current_user['id'] if current_user else None
|
|
|
|
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,)
|
|
)
|
|
|
|
if not rant_row:
|
|
raise HTTPException(status_code=404, detail="Rant not found")
|
|
|
|
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']
|
|
}
|
|
|
|
rant = await format_rant(rant_data, user_data, current_user_id)
|
|
|
|
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,)
|
|
)
|
|
|
|
comments = []
|
|
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']
|
|
}
|
|
|
|
comments.append(await format_comment(comment_data, user_data, current_user_id))
|
|
|
|
subscribed = 0
|
|
if current_user_id:
|
|
if await db.exists("favorites", {"user_id": current_user_id, "rant_id": rant_id}):
|
|
subscribed = 1
|
|
|
|
notif_count = 0
|
|
if current_user:
|
|
notif_count = await db.count("notifications", {"user_id": current_user['id'], "read": 0})
|
|
|
|
return templates.TemplateResponse("rant_detail.html", {
|
|
"request": request,
|
|
"current_user": current_user,
|
|
"notif_count": notif_count,
|
|
"rant": rant,
|
|
"comments": comments,
|
|
"subscribed": subscribed,
|
|
"escape_html": escape_html,
|
|
"format_time": format_time
|
|
})
|
|
|
|
@app.get("/profile/{user_id}", response_class=HTMLResponse)
|
|
async def profile(request: Request, user_id: int, tab: str = "rants"):
|
|
current_user = await get_current_user_from_cookie(request)
|
|
current_user_id = current_user['id'] if current_user else None
|
|
|
|
user = await db.get("users", {"id": user_id})
|
|
|
|
if not user:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
|
|
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,)
|
|
)
|
|
|
|
rants = []
|
|
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']
|
|
}
|
|
|
|
rants.append(await format_rant(rant_data, user_data, current_user_id))
|
|
|
|
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,)
|
|
)
|
|
|
|
comments = []
|
|
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']
|
|
}
|
|
|
|
comments.append(await format_comment(comment_data, user_data, current_user_id))
|
|
|
|
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,)
|
|
)
|
|
|
|
favorites = []
|
|
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']
|
|
}
|
|
|
|
favorites.append(await format_rant(rant_data, user_data, current_user_id))
|
|
|
|
notif_count = 0
|
|
if current_user:
|
|
notif_count = await db.count("notifications", {"user_id": current_user['id'], "read": 0})
|
|
|
|
profile_data = {
|
|
"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 ""
|
|
}
|
|
}
|
|
|
|
return templates.TemplateResponse("profile.html", {
|
|
"request": request,
|
|
"current_user": current_user,
|
|
"notif_count": notif_count,
|
|
"profile": profile_data,
|
|
"profile_user_id": user_id,
|
|
"rants": rants,
|
|
"comments": comments,
|
|
"favorites": favorites,
|
|
"active_tab": tab,
|
|
"escape_html": escape_html,
|
|
"format_time": format_time
|
|
})
|
|
|
|
@app.get("/search", response_class=HTMLResponse)
|
|
async def search_page(request: Request, term: str = None):
|
|
current_user = await get_current_user_from_cookie(request)
|
|
current_user_id = current_user['id'] if current_user else None
|
|
|
|
results = []
|
|
if term:
|
|
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}%')
|
|
)
|
|
|
|
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']
|
|
}
|
|
|
|
results.append(await format_rant(rant_data, user_data, current_user_id))
|
|
|
|
notif_count = 0
|
|
if current_user:
|
|
notif_count = await db.count("notifications", {"user_id": current_user['id'], "read": 0})
|
|
|
|
return templates.TemplateResponse("search.html", {
|
|
"request": request,
|
|
"current_user": current_user,
|
|
"notif_count": notif_count,
|
|
"search_term": term,
|
|
"results": results,
|
|
"escape_html": escape_html,
|
|
"format_time": format_time
|
|
})
|
|
|
|
@app.get("/notifications", response_class=HTMLResponse)
|
|
async def notifications(request: Request):
|
|
current_user = await get_current_user_from_cookie(request)
|
|
if not current_user:
|
|
return RedirectResponse("/", status_code=302)
|
|
|
|
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'],)
|
|
)
|
|
|
|
items = []
|
|
for row in rows:
|
|
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 rows:
|
|
await db.update("notifications", {"read": 1}, {"user_id": current_user['id']})
|
|
|
|
return templates.TemplateResponse("notifications.html", {
|
|
"request": request,
|
|
"current_user": current_user,
|
|
"notif_count": 0,
|
|
"items": items,
|
|
"format_time": format_time
|
|
})
|
|
|
|
@app.get("/classic", response_class=HTMLResponse)
|
|
async def classic():
|
|
return FileResponse("classic.html")
|
|
|
|
# Authentication endpoints
|
|
@app.get("/login", response_class=HTMLResponse)
|
|
async def login_page(request: Request):
|
|
return templates.TemplateResponse("login.html", {"request": request})
|
|
|
|
@app.post("/login", response_class=HTMLResponse)
|
|
async def login_form(
|
|
request: Request,
|
|
username: str = Form(...),
|
|
password: str = Form(...)
|
|
):
|
|
user = await db.query_one(
|
|
"SELECT * FROM users WHERE username = ? OR email = ?",
|
|
(username, username)
|
|
)
|
|
|
|
if not user or user['password_hash'] != hash_password(password):
|
|
return templates.TemplateResponse("login.html", {
|
|
"request": request,
|
|
"error": "Invalid login credentials entered. Please try again."
|
|
})
|
|
|
|
token_key = generate_token()
|
|
expire_time = int((datetime.now() + timedelta(days=30)).timestamp())
|
|
|
|
token_id = await db.insert("auth_tokens", {
|
|
"user_id": user['id'],
|
|
"token_key": token_key,
|
|
"expire_time": expire_time
|
|
}, return_id=True)
|
|
|
|
auth_token = {
|
|
"id": token_id,
|
|
"key": token_key,
|
|
"expire_time": expire_time,
|
|
"user_id": user['id']
|
|
}
|
|
|
|
response = RedirectResponse("/", status_code=302)
|
|
response.set_cookie(SESSION_COOKIE_NAME, json.dumps(auth_token), max_age=30*24*60*60)
|
|
|
|
return response
|
|
|
|
@app.get("/logout")
|
|
async def logout(response: Response):
|
|
response = RedirectResponse("/", status_code=302)
|
|
response.delete_cookie(SESSION_COOKIE_NAME)
|
|
return response
|
|
|
|
# API Endpoints (all preserved from main.py)
|
|
|
|
|
|
@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)
|
|
):
|
|
if len(username) < 4 or len(username) > 15:
|
|
return {
|
|
"success": False,
|
|
"error": "Your username must be between 4 and 15 characters.",
|
|
"error_field": "username"
|
|
}
|
|
|
|
if await db.exists("users", {"username": username}):
|
|
return {
|
|
"success": False,
|
|
"error": "Username already taken.",
|
|
"error_field": "username"
|
|
}
|
|
|
|
if await db.exists("users", {"email": email}):
|
|
return {
|
|
"success": False,
|
|
"error": "Email already registered.",
|
|
"error_field": "email"
|
|
}
|
|
|
|
password_hash = hash_password(password)
|
|
created_time = int(datetime.now().timestamp())
|
|
|
|
try:
|
|
user_id = await db.insert("users", {
|
|
"username": username,
|
|
"email": email,
|
|
"password_hash": password_hash,
|
|
"created_time": created_time
|
|
}, return_id=True)
|
|
|
|
# Add user to default channels
|
|
default_channels = await db.find("channels", {"created_by": "system"})
|
|
for channel in default_channels:
|
|
await db.insert("channel_members", {
|
|
"channel_id": channel["uid"],
|
|
"user_id": str(user_id),
|
|
"joined_at": datetime.now().isoformat()
|
|
})
|
|
|
|
return {"success": True}
|
|
except Exception as e:
|
|
return {"success": False, "error": str(e)}
|
|
|
|
@app.post("/api/rant/rants")
|
|
async def create_rant(
|
|
request: Request,
|
|
rant: str = Form(...),
|
|
tags: str = Form(...),
|
|
type: int = Form(1),
|
|
app: int = Form(3),
|
|
image: Optional[UploadFile] = File(None)
|
|
):
|
|
current_user = await get_current_user_from_cookie(request)
|
|
if not current_user:
|
|
return {"success": False, "error": "Authentication required"}
|
|
|
|
current_user_id = current_user['id']
|
|
|
|
# Check for duplicate rant
|
|
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)
|
|
)
|
|
|
|
if duplicate:
|
|
return {
|
|
"success": False,
|
|
"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!"
|
|
}
|
|
|
|
# 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()]
|
|
|
|
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)
|
|
|
|
return {"success": True, "rant_id": rant_id}
|
|
|
|
# Chat API endpoints
|
|
@app.get("/api/channels")
|
|
async def get_channels(request: Request):
|
|
current_user = await get_current_user_from_cookie(request)
|
|
if not current_user:
|
|
raise HTTPException(status_code=401, detail="Authentication required")
|
|
|
|
memberships = await db.find("channel_members", {"user_id": str(current_user["id"])})
|
|
channel_ids = [m["channel_id"] for m in memberships]
|
|
|
|
channels = []
|
|
for channel_id in channel_ids:
|
|
channel = await db.get("channels", {"uid": channel_id})
|
|
if channel and not channel.get("is_dm"):
|
|
last_read = next((m["last_read_message_id"] for m in memberships
|
|
if m["channel_id"] == channel_id), None)
|
|
|
|
unread = 0
|
|
if last_read:
|
|
messages = await db.query_raw(
|
|
"SELECT COUNT(*) as count FROM messages WHERE channel_id = ? AND uid > ?",
|
|
(channel_id, last_read)
|
|
)
|
|
unread = messages[0]["count"] if messages else 0
|
|
|
|
channels.append({
|
|
"id": channel["uid"],
|
|
"name": channel["name"],
|
|
"type": "channel",
|
|
"unread": unread
|
|
})
|
|
|
|
return channels
|
|
|
|
@app.post("/api/channels")
|
|
async def create_channel(channel: ChannelCreate, request: Request):
|
|
current_user = await get_current_user_from_cookie(request)
|
|
if not current_user:
|
|
raise HTTPException(status_code=401, detail="Authentication required")
|
|
|
|
existing = await db.get("channels", {"name": channel.name})
|
|
if existing:
|
|
raise HTTPException(status_code=400, detail="Channel already exists")
|
|
|
|
channel_id = await db.insert("channels", {
|
|
"name": channel.name.lower().replace(" ", "-"),
|
|
"description": channel.description,
|
|
"created_by": str(current_user["id"])
|
|
})
|
|
|
|
await db.insert("channel_members", {
|
|
"channel_id": channel_id,
|
|
"user_id": str(current_user["id"]),
|
|
"joined_at": datetime.now().isoformat()
|
|
})
|
|
|
|
return {"id": channel_id, "name": channel.name}
|
|
|
|
@app.get("/api/dms")
|
|
async def get_dms(request: Request):
|
|
current_user = await get_current_user_from_cookie(request)
|
|
if not current_user:
|
|
raise HTTPException(status_code=401, detail="Authentication required")
|
|
|
|
user_id_str = str(current_user["id"])
|
|
dms = await db.query_raw(
|
|
"""
|
|
SELECT dc.*, c.uid as channel_uid, u.username, u.avatar_b, u.status
|
|
FROM dm_channels dc
|
|
JOIN channels c ON dc.channel_id = c.uid
|
|
JOIN users u ON (
|
|
CASE
|
|
WHEN dc.user_id_1 = ? THEN dc.user_id_2 = CAST(u.id AS TEXT)
|
|
ELSE dc.user_id_1 = CAST(u.id AS TEXT)
|
|
END
|
|
)
|
|
WHERE dc.user_id_1 = ? OR dc.user_id_2 = ?
|
|
""",
|
|
(user_id_str, user_id_str, user_id_str)
|
|
)
|
|
|
|
result = []
|
|
for dm in dms:
|
|
membership = await db.get("channel_members", {
|
|
"channel_id": dm["channel_uid"],
|
|
"user_id": user_id_str
|
|
})
|
|
|
|
unread = 0
|
|
if membership and membership.get("last_read_message_id"):
|
|
messages = await db.query_raw(
|
|
"SELECT COUNT(*) as count FROM messages WHERE channel_id = ? AND uid > ?",
|
|
(dm["channel_uid"], membership["last_read_message_id"])
|
|
)
|
|
unread = messages[0]["count"] if messages else 0
|
|
|
|
result.append({
|
|
"id": dm["channel_uid"],
|
|
"name": dm["username"],
|
|
"type": "dm",
|
|
"user": {
|
|
"username": dm["username"],
|
|
"status": dm["status"],
|
|
"avatar_color": dm["avatar_b"]
|
|
},
|
|
"unread": unread
|
|
})
|
|
|
|
return result
|
|
|
|
@app.post("/api/dms")
|
|
async def create_dm(data: dict, request: Request):
|
|
current_user = await get_current_user_from_cookie(request)
|
|
if not current_user:
|
|
raise HTTPException(status_code=401, detail="Authentication required")
|
|
|
|
other_user_id = data.get("user_id")
|
|
if not other_user_id:
|
|
raise HTTPException(status_code=400, detail="User ID required")
|
|
|
|
user_id_1 = min(str(current_user["id"]), str(other_user_id))
|
|
user_id_2 = max(str(current_user["id"]), str(other_user_id))
|
|
|
|
existing = await db.get("dm_channels", {
|
|
"user_id_1": user_id_1,
|
|
"user_id_2": user_id_2
|
|
})
|
|
|
|
if existing:
|
|
return {"channel_id": existing["channel_id"]}
|
|
|
|
channel_id = await db.insert("channels", {
|
|
"name": f"dm_{user_id_1}_{user_id_2}",
|
|
"created_by": str(current_user["id"]),
|
|
"is_dm": 1
|
|
})
|
|
|
|
await db.insert("dm_channels", {
|
|
"user_id_1": user_id_1,
|
|
"user_id_2": user_id_2,
|
|
"channel_id": channel_id
|
|
})
|
|
|
|
for user_id in [user_id_1, user_id_2]:
|
|
await db.insert("channel_members", {
|
|
"channel_id": channel_id,
|
|
"user_id": user_id,
|
|
"joined_at": datetime.now().isoformat()
|
|
})
|
|
|
|
return {"channel_id": channel_id}
|
|
|
|
@app.get("/api/messages/{channel_id}")
|
|
async def get_messages(
|
|
channel_id: str,
|
|
request: Request,
|
|
limit: int = 50,
|
|
offset: int = 0
|
|
):
|
|
current_user = await get_current_user_from_cookie(request)
|
|
if not current_user:
|
|
raise HTTPException(status_code=401, detail="Authentication required")
|
|
|
|
membership = await db.get("channel_members", {
|
|
"channel_id": channel_id,
|
|
"user_id": str(current_user["id"])
|
|
})
|
|
|
|
if not membership:
|
|
raise HTTPException(status_code=403, detail="Not a member of this channel")
|
|
|
|
messages = await db.query_raw(
|
|
"""
|
|
SELECT m.*, u.username, u.avatar_b
|
|
FROM messages m
|
|
JOIN users u ON CAST(m.user_id AS INTEGER) = u.id
|
|
WHERE m.channel_id = ?
|
|
ORDER BY m.created_at DESC
|
|
LIMIT ? OFFSET ?
|
|
""",
|
|
(channel_id, limit, offset)
|
|
)
|
|
|
|
if messages:
|
|
await db.update("channel_members", {
|
|
"last_read_message_id": messages[0]["uid"]
|
|
}, {
|
|
"channel_id": channel_id,
|
|
"user_id": str(current_user["id"])
|
|
})
|
|
|
|
return list(reversed(messages))
|
|
|
|
@app.post("/api/messages")
|
|
async def send_message(message: MessageCreate, request: Request):
|
|
current_user = await get_current_user_from_cookie(request)
|
|
if not current_user:
|
|
raise HTTPException(status_code=401, detail="Authentication required")
|
|
|
|
if not message.channel_id and not message.dm_user_id:
|
|
raise HTTPException(status_code=400, detail="Channel ID or DM user ID required")
|
|
|
|
channel_id = message.channel_id
|
|
|
|
if message.dm_user_id:
|
|
user_id_1 = min(str(current_user["id"]), str(message.dm_user_id))
|
|
user_id_2 = max(str(current_user["id"]), str(message.dm_user_id))
|
|
|
|
dm_channel = await db.get("dm_channels", {
|
|
"user_id_1": user_id_1,
|
|
"user_id_2": user_id_2
|
|
})
|
|
|
|
if dm_channel:
|
|
channel_id = dm_channel["channel_id"]
|
|
else:
|
|
channel_id = await db.insert("channels", {
|
|
"name": f"dm_{user_id_1}_{user_id_2}",
|
|
"created_by": str(current_user["id"]),
|
|
"is_dm": 1
|
|
})
|
|
|
|
await db.insert("dm_channels", {
|
|
"user_id_1": user_id_1,
|
|
"user_id_2": user_id_2,
|
|
"channel_id": channel_id
|
|
})
|
|
|
|
for user_id in [user_id_1, user_id_2]:
|
|
await db.insert("channel_members", {
|
|
"channel_id": channel_id,
|
|
"user_id": user_id,
|
|
"joined_at": datetime.now().isoformat()
|
|
})
|
|
|
|
membership = await db.get("channel_members", {
|
|
"channel_id": channel_id,
|
|
"user_id": str(current_user["id"])
|
|
})
|
|
|
|
if not membership:
|
|
raise HTTPException(status_code=403, detail="Not a member of this channel")
|
|
|
|
message_id = await db.insert("messages", {
|
|
"channel_id": channel_id,
|
|
"user_id": str(current_user["id"]),
|
|
"content": message.content
|
|
})
|
|
|
|
msg_data = {
|
|
"id": message_id,
|
|
"channel_id": channel_id,
|
|
"content": message.content,
|
|
"author": {
|
|
"id": str(current_user["id"]),
|
|
"username": current_user["username"],
|
|
"avatar_color": current_user["avatar_b"]
|
|
},
|
|
"timestamp": datetime.now().isoformat()
|
|
}
|
|
|
|
await manager.broadcast_to_channel(
|
|
json.dumps({"type": "new_message", "data": msg_data}),
|
|
channel_id,
|
|
exclude_user=str(current_user["id"])
|
|
)
|
|
|
|
return msg_data
|
|
|
|
@app.get("/api/users/search")
|
|
async def search_users(q: str, request: Request):
|
|
current_user = await get_current_user_from_cookie(request)
|
|
if not current_user:
|
|
raise HTTPException(status_code=401, detail="Authentication required")
|
|
|
|
if len(q) < 2:
|
|
return []
|
|
|
|
users = await db.query_raw(
|
|
"SELECT id, username, avatar_b, status FROM users WHERE username LIKE ? AND id != ? LIMIT 10",
|
|
(f"%{q}%", current_user["id"])
|
|
)
|
|
|
|
return [{
|
|
"id": str(user["id"]),
|
|
"username": user["username"],
|
|
"avatar_color": user["avatar_b"],
|
|
"status": user["status"]
|
|
} for user in users]
|
|
|
|
# WebSocket endpoint
|
|
@app.websocket("/ws/{user_id}")
|
|
async def websocket_endpoint(websocket: WebSocket, user_id: str):
|
|
await manager.connect(websocket, user_id)
|
|
|
|
await db.update("users", {"status": "online"}, {"id": int(user_id)})
|
|
|
|
try:
|
|
while True:
|
|
data = await websocket.receive_text()
|
|
message_data = json.loads(data)
|
|
|
|
if message_data.get("type") == "typing":
|
|
channel_id = message_data.get("channel_id")
|
|
if channel_id:
|
|
user = await db.get("users", {"id": int(user_id)})
|
|
await manager.broadcast_to_channel(
|
|
json.dumps({
|
|
"type": "typing",
|
|
"data": {
|
|
"channel_id": channel_id,
|
|
"username": user["username"]
|
|
}
|
|
}),
|
|
channel_id,
|
|
exclude_user=user_id
|
|
)
|
|
|
|
except WebSocketDisconnect:
|
|
manager.disconnect(websocket, user_id)
|
|
await db.update("users", {
|
|
"status": "offline",
|
|
"last_seen": datetime.now().isoformat()
|
|
}, {"id": int(user_id)})
|
|
|
|
@app.get("/api/rant/rants")
|
|
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
|
|
):
|
|
current_user_id = await authenticate_user(token_id, token_key, user_id) if token_id else None
|
|
|
|
# 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)
|
|
)
|
|
|
|
rants = []
|
|
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']
|
|
}
|
|
|
|
rants.append(await format_rant(rant_data, user_data, current_user_id))
|
|
|
|
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"
|
|
}
|
|
}
|
|
|
|
@app.get("/api/rant/rants/{rant_id}")
|
|
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
|
|
):
|
|
current_user_id = await authenticate_user(token_id, token_key, user_id) if token_id else None
|
|
|
|
# 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,)
|
|
)
|
|
|
|
if not rant_row:
|
|
raise HTTPException(status_code=404, detail="Rant not found")
|
|
|
|
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']
|
|
}
|
|
|
|
rant = await format_rant(rant_data, user_data, current_user_id)
|
|
|
|
# Get comments
|
|
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,)
|
|
)
|
|
|
|
comments = []
|
|
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']
|
|
}
|
|
|
|
comments.append(await format_comment(comment_data, user_data, current_user_id))
|
|
|
|
# Check if subscribed (favorited)
|
|
subscribed = 0
|
|
if current_user_id:
|
|
if await db.exists("favorites", {"user_id": current_user_id, "rant_id": rant_id}):
|
|
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
|
|
}
|
|
|
|
@app.post("/api/rant/rants")
|
|
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)
|
|
):
|
|
current_user_id = await authenticate_user(token_id, token_key, user_id)
|
|
if not current_user_id:
|
|
return {"success": False, "error": "Authentication required"}
|
|
|
|
# Check for duplicate rant
|
|
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)
|
|
)
|
|
|
|
if duplicate:
|
|
return {
|
|
"success": False,
|
|
"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!"
|
|
}
|
|
|
|
# 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()]
|
|
|
|
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)
|
|
|
|
return {"success": True, "rant_id": rant_id}
|
|
|
|
@app.post("/api/rant/rants/{rant_id}")
|
|
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(...)
|
|
):
|
|
current_user_id = await authenticate_user(token_id, token_key, user_id)
|
|
if not current_user_id:
|
|
return {"success": False, "error": "Authentication required"}
|
|
|
|
# Check ownership
|
|
rant_row = await db.get("rants", {"id": rant_id})
|
|
|
|
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()]
|
|
|
|
await db.update("rants", {
|
|
"text": rant,
|
|
"tags": json.dumps(tags_list),
|
|
"edited": 1
|
|
}, {"id": rant_id})
|
|
|
|
return {"success": True}
|
|
|
|
@app.delete("/api/rant/rants/{rant_id}")
|
|
async def delete_rant(
|
|
rant_id: int,
|
|
app: int = 3,
|
|
token_id: int = None,
|
|
token_key: str = None,
|
|
user_id: int = None
|
|
):
|
|
current_user_id = await authenticate_user(token_id, token_key, user_id)
|
|
if not current_user_id:
|
|
return {"success": False, "error": "Authentication required"}
|
|
|
|
# Check ownership
|
|
rant_row = await db.get("rants", {"id": rant_id})
|
|
|
|
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
|
|
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})
|
|
|
|
return {"success": True}
|
|
|
|
@app.post("/api/rant/rants/{rant_id}/vote")
|
|
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(...)
|
|
):
|
|
current_user_id = await authenticate_user(token_id, token_key, user_id)
|
|
if not current_user_id:
|
|
return {"success": False, "error": "Authentication required"}
|
|
|
|
# Get rant
|
|
rant = await db.get("rants", {"id": rant_id})
|
|
|
|
if not rant:
|
|
return {"success": False, "error": "Rant not found"}
|
|
|
|
# Check for existing vote
|
|
existing_vote = await db.get("votes", {
|
|
"user_id": current_user_id,
|
|
"target_id": rant_id,
|
|
"target_type": "rant"
|
|
})
|
|
|
|
if vote == 0:
|
|
# Remove vote
|
|
if existing_vote:
|
|
await db.delete("votes", {"id": existing_vote['id']})
|
|
# Update score
|
|
await db.update("rants", {
|
|
"score": rant['score'] - existing_vote['vote']
|
|
}, {"id": rant_id})
|
|
else:
|
|
if existing_vote:
|
|
# Update vote
|
|
score_diff = vote - existing_vote['vote']
|
|
await db.update("votes", {
|
|
"vote": vote,
|
|
"reason": reason
|
|
}, {"id": existing_vote['id']})
|
|
await db.update("rants", {
|
|
"score": rant['score'] + score_diff
|
|
}, {"id": rant_id})
|
|
else:
|
|
# New vote
|
|
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})
|
|
|
|
# Update user score
|
|
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']
|
|
}
|
|
|
|
return {
|
|
"success": True,
|
|
"rant": await format_rant(rant_data, user_data, current_user_id)
|
|
}
|
|
|
|
@app.post("/api/rant/rants/{rant_id}/favorite")
|
|
async def favorite_rant(
|
|
rant_id: int,
|
|
app: int = Form(3),
|
|
token_id: int = Form(...),
|
|
token_key: str = Form(...),
|
|
user_id: int = Form(...)
|
|
):
|
|
current_user_id = await authenticate_user(token_id, token_key, user_id)
|
|
if not current_user_id:
|
|
return {"success": False, "error": "Authentication required"}
|
|
|
|
try:
|
|
await db.insert("favorites", {
|
|
"user_id": current_user_id,
|
|
"rant_id": rant_id
|
|
}, return_id=True)
|
|
return {"success": True}
|
|
except Exception:
|
|
return {"success": False, "error": "Already favorited"}
|
|
|
|
@app.post("/api/rant/rants/{rant_id}/unfavorite")
|
|
async def unfavorite_rant(
|
|
rant_id: int,
|
|
app: int = Form(3),
|
|
token_id: int = Form(...),
|
|
token_key: str = Form(...),
|
|
user_id: int = Form(...)
|
|
):
|
|
current_user_id = await authenticate_user(token_id, token_key, user_id)
|
|
if not current_user_id:
|
|
return {"success": False, "error": "Authentication required"}
|
|
|
|
await db.delete("favorites", {
|
|
"user_id": current_user_id,
|
|
"rant_id": rant_id
|
|
})
|
|
|
|
return {"success": True}
|
|
|
|
@app.post("/api/rant/rants/{rant_id}/comments")
|
|
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)
|
|
):
|
|
current_user_id = await authenticate_user(token_id, token_key, user_id)
|
|
if not current_user_id:
|
|
return {"success": False, "confirmed": False}
|
|
|
|
# Check if rant exists
|
|
if not await db.exists("rants", {"id": rant_id}):
|
|
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())
|
|
|
|
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)
|
|
|
|
# Create notification for rant owner
|
|
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)
|
|
|
|
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
|
|
):
|
|
current_user_id = await authenticate_user(token_id, token_key, user_id) if token_id else None
|
|
|
|
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,)
|
|
)
|
|
|
|
if not row:
|
|
return {"success": False, "error": "Invalid comment specified in path."}
|
|
|
|
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']
|
|
}
|
|
|
|
return {
|
|
"success": True,
|
|
"comment": await format_comment(comment_data, user_data, current_user_id)
|
|
}
|
|
|
|
@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(...)
|
|
):
|
|
current_user_id = await authenticate_user(token_id, token_key, user_id)
|
|
if not current_user_id:
|
|
return {"success": False, "error": "Authentication required"}
|
|
|
|
# Check ownership
|
|
comment_row = await db.get("comments", {"id": comment_id})
|
|
|
|
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
|
|
await db.update("comments", {
|
|
"body": comment,
|
|
"edited": 1
|
|
}, {"id": comment_id})
|
|
|
|
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
|
|
):
|
|
current_user_id = await authenticate_user(token_id, token_key, user_id)
|
|
if not current_user_id:
|
|
return {"success": False, "error": "Authentication required"}
|
|
|
|
# Check ownership
|
|
comment_row = await db.get("comments", {"id": comment_id})
|
|
|
|
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
|
|
await db.delete("votes", {"target_id": comment_id, "target_type": "comment"})
|
|
await db.delete("comments", {"id": comment_id})
|
|
|
|
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(...)
|
|
):
|
|
current_user_id = await authenticate_user(token_id, token_key, user_id)
|
|
if not current_user_id:
|
|
return {"success": False, "error": "Authentication required"}
|
|
|
|
# Get comment
|
|
comment = await db.get("comments", {"id": comment_id})
|
|
|
|
if not comment:
|
|
return {"success": False, "error": "Invalid comment specified in path."}
|
|
|
|
# Check for existing vote
|
|
existing_vote = await db.get("votes", {
|
|
"user_id": current_user_id,
|
|
"target_id": comment_id,
|
|
"target_type": "comment"
|
|
})
|
|
|
|
if vote == 0:
|
|
# Remove vote
|
|
if existing_vote:
|
|
await db.delete("votes", {"id": existing_vote['id']})
|
|
# Update score
|
|
await db.update("comments", {
|
|
"score": comment['score'] - existing_vote['vote']
|
|
}, {"id": comment_id})
|
|
else:
|
|
if existing_vote:
|
|
# Update vote
|
|
score_diff = vote - existing_vote['vote']
|
|
await db.update("votes", {
|
|
"vote": vote,
|
|
"reason": reason
|
|
}, {"id": existing_vote['id']})
|
|
await db.update("comments", {
|
|
"score": comment['score'] + score_diff
|
|
}, {"id": comment_id})
|
|
else:
|
|
# New vote
|
|
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})
|
|
|
|
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
|
|
):
|
|
current_user_id = await authenticate_user(token_id, token_key, auth_user_id) if token_id else None
|
|
|
|
# Get user
|
|
user = await db.get("users", {"id": user_id})
|
|
|
|
if not user:
|
|
return {"success": False, "error": "User not found"}
|
|
|
|
# Get user's rants
|
|
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,)
|
|
)
|
|
|
|
rants = []
|
|
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']
|
|
}
|
|
|
|
rants.append(await format_rant(rant_data, user_data, current_user_id))
|
|
|
|
# Get user's comments
|
|
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,)
|
|
)
|
|
|
|
comments = []
|
|
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']
|
|
}
|
|
|
|
comments.append(await format_comment(comment_data, user_data, current_user_id))
|
|
|
|
# Get favorited rants
|
|
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,)
|
|
)
|
|
|
|
favorites = []
|
|
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']
|
|
}
|
|
|
|
favorites.append(await format_rant(rant_data, user_data, current_user_id))
|
|
|
|
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
|
|
):
|
|
user = await db.get("users", {"username": username})
|
|
|
|
if not user:
|
|
return {"success": False, "error": "User not found"}
|
|
|
|
return {"success": True, "user_id": user['id']}
|
|
|
|
@app.get("/api/rant/search")
|
|
async def search(
|
|
term: str,
|
|
app: int = 3,
|
|
token_id: Optional[int] = None,
|
|
token_key: Optional[str] = None,
|
|
user_id: Optional[int] = None
|
|
):
|
|
current_user_id = await authenticate_user(token_id, token_key, user_id) if token_id else None
|
|
|
|
# Search rants
|
|
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}%')
|
|
)
|
|
|
|
results = []
|
|
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']
|
|
}
|
|
|
|
results.append(await format_rant(rant_data, user_data, current_user_id))
|
|
|
|
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,
|
|
token_id: Optional[int] = None,
|
|
token_key: Optional[str] = None,
|
|
user_id: Optional[int] = None
|
|
):
|
|
# Use the generic authenticate_user function
|
|
current_user_id = await authenticate_user(token_id, token_key, user_id)
|
|
if not current_user_id:
|
|
return {"success": False, "error": "Authentication required"}
|
|
|
|
# Get notifications
|
|
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,)
|
|
)
|
|
|
|
items = []
|
|
unread_count = 0
|
|
|
|
for row in rows:
|
|
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
|
|
|
|
# Mark notifications as read
|
|
if rows: # Only update if there are notifications
|
|
await db.update("notifications", {"read": 1}, {"user_id": current_user_id})
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
@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(...)
|
|
):
|
|
current_user_id = await authenticate_user(token_id, token_key, user_id)
|
|
if not current_user_id:
|
|
return {"success": False, "error": "Authentication required"}
|
|
|
|
await db.delete("notifications", {"user_id": current_user_id})
|
|
|
|
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(...)
|
|
):
|
|
current_user_id = await authenticate_user(token_id, token_key, user_id)
|
|
if not current_user_id:
|
|
return {"success": False, "error": "Authentication required"}
|
|
|
|
await db.update("users", {
|
|
"about": profile_about,
|
|
"skills": profile_skills,
|
|
"location": profile_location,
|
|
"website": profile_website,
|
|
"github": profile_github
|
|
}, {"id": current_user_id})
|
|
|
|
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(...)
|
|
):
|
|
current_user_id = await authenticate_user(token_id, token_key, user_id)
|
|
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(...)
|
|
):
|
|
current_user_id = await authenticate_user(token_id, token_key, user_id)
|
|
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")
|
|
|
|
# Static files
|
|
Path("static").mkdir(exist_ok=True)
|
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(app, host="0.0.0.0", port=8111)
|