Files
launcher/server/friends.py
T

177 lines
6.6 KiB
Python

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"}