from fastapi import APIRouter, HTTPException, Depends from pydantic import BaseModel from typing import Optional import structlog import time from auth import get_db, get_current_user logger = structlog.get_logger(__name__) router = APIRouter(prefix="/api", tags=["friends"]) def init_friends_db(): with get_db() as conn: conn.executescript(""" CREATE TABLE IF NOT EXISTS friendships ( id INTEGER PRIMARY KEY AUTOINCREMENT, requester_id INTEGER NOT NULL, target_id INTEGER NOT NULL, status TEXT NOT NULL DEFAULT 'pending', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(requester_id, target_id), FOREIGN KEY (requester_id) REFERENCES users(id), FOREIGN KEY (target_id) REFERENCES users(id) ); CREATE TABLE IF NOT EXISTS user_status ( user_id INTEGER PRIMARY KEY, is_online INTEGER DEFAULT 0, current_pack TEXT DEFAULT '', last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ); CREATE INDEX IF NOT EXISTS idx_friendships_requester ON friendships(requester_id); CREATE INDEX IF NOT EXISTS idx_friendships_target ON friendships(target_id); """) class AddFriendRequest(BaseModel): username: str class RemoveFriendRequest(BaseModel): user_id: int class AcceptFriendRequest(BaseModel): user_id: int class StatusUpdateRequest(BaseModel): online: bool = True current_pack: Optional[str] = None @router.post("/friends/add") async def add_friend( req: AddFriendRequest, current_user: dict = Depends(get_current_user) ): with get_db() as conn: cursor = conn.execute("SELECT id FROM users WHERE username = ?", (req.username,)) target = cursor.fetchone() if not target: raise HTTPException(404, "User not found") target_id = target[0] if target_id == current_user["id"]: raise HTTPException(400, "Cannot add yourself") cursor = conn.execute( "SELECT status FROM friendships WHERE requester_id = ? AND target_id = ?", (current_user["id"], target_id) ) existing = cursor.fetchone() if existing: if existing[0] == "accepted": raise HTTPException(400, "Already friends") raise HTTPException(400, f"Friend request already {existing[0]}") conn.execute( "INSERT INTO friendships (requester_id, target_id, status) VALUES (?, ?, 'pending')", (current_user["id"], target_id) ) logger.info("Friend request sent", from_user=current_user["id"], to_user=target_id) return {"message": "Friend request sent"} @router.post("/friends/accept") async def accept_friend( req: AcceptFriendRequest, current_user: dict = Depends(get_current_user) ): with get_db() as conn: cursor = conn.execute( "SELECT id, requester_id FROM friendships WHERE target_id = ? AND requester_id = ? AND status = 'pending'", (current_user["id"], req.user_id) ) row = cursor.fetchone() if not row: raise HTTPException(404, "No pending friend request from this user") conn.execute("UPDATE friendships SET status = 'accepted' WHERE id = ?", (row[0],)) logger.info("Friend request accepted", from_user=req.user_id, to_user=current_user["id"]) return {"message": "Friend request accepted"} @router.post("/friends/remove") async def remove_friend( req: RemoveFriendRequest, current_user: dict = Depends(get_current_user) ): with get_db() as conn: cursor = conn.execute( "SELECT id FROM friendships WHERE (requester_id = ? AND target_id = ?) OR (requester_id = ? AND target_id = ?)", (current_user["id"], req.user_id, req.user_id, current_user["id"]) ) row = cursor.fetchone() if not row: raise HTTPException(404, "Not friends") conn.execute("DELETE FROM friendships WHERE id = ?", (row[0],)) logger.info("Friend removed", user=current_user["id"], target=req.user_id) return {"message": "Friend removed"} @router.get("/friends/list") async def list_friends(current_user: dict = Depends(get_current_user)): friends = [] with get_db() as conn: rows = conn.execute(""" SELECT u.id, u.username, u.role, COALESCE(us.is_online, 0) as online, COALESCE(us.current_pack, '') as current_pack, us.last_seen FROM friendships f JOIN users u ON (CASE WHEN f.requester_id = ? THEN f.target_id ELSE f.requester_id END) = u.id LEFT JOIN user_status us ON u.id = us.user_id WHERE (f.requester_id = ? OR f.target_id = ?) AND f.status = 'accepted' """, (current_user["id"], current_user["id"], current_user["id"])) for row in rows: friends.append({ "id": row[0], "username": row[1], "role": row[2], "online": bool(row[3]), "current_pack": row[4], "last_seen": row[5].isoformat() if row[5] else None }) return {"friends": friends} @router.get("/friends/requests") async def list_friend_requests(current_user: dict = Depends(get_current_user)): requests = [] with get_db() as conn: rows = conn.execute(""" SELECT u.id, u.username, u.role, f.created_at FROM friendships f JOIN users u ON f.requester_id = u.id WHERE f.target_id = ? AND f.status = 'pending' """, (current_user["id"],)) for row in rows: requests.append({ "id": row[0], "username": row[1], "role": row[2], "created_at": row[3].isoformat() if row[3] else None }) return {"requests": requests} @router.post("/friends/status") async def update_status( req: StatusUpdateRequest, current_user: dict = Depends(get_current_user) ): with get_db() as conn: conn.execute(""" INSERT INTO user_status (user_id, is_online, current_pack, last_seen) VALUES (?, ?, ?, CURRENT_TIMESTAMP) ON CONFLICT(user_id) DO UPDATE SET is_online = excluded.is_online, current_pack = COALESCE(excluded.current_pack, user_status.current_pack), last_seen = CURRENT_TIMESTAMP """, (current_user["id"], int(req.online), req.current_pack or "")) return {"status": "ok"}