import uuid from fastapi import APIRouter, Depends, HTTPException, Request from sqlalchemy.orm import Session, joinedload from services import notification_manager from core.database import get_db from core.dependencies import get_current_user, get_user_id_from_request, limiter from core.models import Friendship as FriendshipModel from core.models import Notification as NotificationModel from core.models import User as UserModel from routers.notifications import _serialize_notification router = APIRouter() @router.post("/users/{username}/friend-request") @limiter.limit("10/minute", key_func=get_user_id_from_request) async def send_friend_request(request: Request, username: str, user: UserModel = Depends(get_current_user), db: Session = Depends(get_db)): addressee = db.query(UserModel).filter(UserModel.username == username).first() if not addressee: raise HTTPException(status_code=404, detail="User not found") if addressee.id == user.id: raise HTTPException(status_code=400, detail="Cannot send friend request to yourself") # Check for any existing friendship in either direction existing = db.query(FriendshipModel).filter( ((FriendshipModel.requester_id == user.id) & (FriendshipModel.addressee_id == addressee.id)) | ((FriendshipModel.requester_id == addressee.id) & (FriendshipModel.addressee_id == user.id)), ).first() if existing and existing.status != "declined": raise HTTPException(status_code=400, detail="Friend request already exists or already friends") # Clear stale declined row so the unique constraint allows re-requesting if existing: db.delete(existing) db.flush() friendship = FriendshipModel(requester_id=user.id, addressee_id=addressee.id, status="pending") db.add(friendship) db.flush() # get friendship.id before notification notif = NotificationModel( user_id=addressee.id, type="friend_request", payload={"friendship_id": str(friendship.id), "from_username": user.username}, ) db.add(notif) db.commit() await notification_manager.send_notification(str(addressee.id), _serialize_notification(notif)) return {"ok": True} @router.post("/friendships/{friendship_id}/accept") def accept_friend_request(friendship_id: str, user: UserModel = Depends(get_current_user), db: Session = Depends(get_db)): friendship = db.query(FriendshipModel).filter(FriendshipModel.id == uuid.UUID(friendship_id)).first() if not friendship: raise HTTPException(status_code=404, detail="Friendship not found") if friendship.addressee_id != user.id: raise HTTPException(status_code=403, detail="Not authorized") if friendship.status != "pending": raise HTTPException(status_code=400, detail="Friendship is not pending") friendship.status = "accepted" db.commit() return {"ok": True} @router.post("/friendships/{friendship_id}/decline") def decline_friend_request(friendship_id: str, user: UserModel = Depends(get_current_user), db: Session = Depends(get_db)): friendship = db.query(FriendshipModel).filter(FriendshipModel.id == uuid.UUID(friendship_id)).first() if not friendship: raise HTTPException(status_code=404, detail="Friendship not found") if friendship.addressee_id != user.id: raise HTTPException(status_code=403, detail="Not authorized") if friendship.status != "pending": raise HTTPException(status_code=400, detail="Friendship is not pending") friendship.status = "declined" # Clean up the associated notification so it disappears from the bell db.query(NotificationModel).filter( NotificationModel.user_id == user.id, NotificationModel.type == "friend_request", NotificationModel.payload["friendship_id"].astext == friendship_id, ).delete(synchronize_session=False) db.commit() return {"ok": True} @router.get("/friends") def get_friends(user: UserModel = Depends(get_current_user), db: Session = Depends(get_db)): friendships = db.query(FriendshipModel).options( joinedload(FriendshipModel.requester), joinedload(FriendshipModel.addressee), ).filter( (FriendshipModel.requester_id == user.id) | (FriendshipModel.addressee_id == user.id), FriendshipModel.status == "accepted", ).all() result = [] for f in friendships: other = f.addressee if f.requester_id == user.id else f.requester result.append({"id": str(other.id), "username": other.username, "friendship_id": str(f.id)}) return result @router.get("/friendship-status/{username}") def get_friendship_status(username: str, user: UserModel = Depends(get_current_user), db: Session = Depends(get_db)): """Returns the friendship status between the current user and the given username.""" other = db.query(UserModel).filter(UserModel.username == username).first() if not other: raise HTTPException(status_code=404, detail="User not found") friendship = db.query(FriendshipModel).filter( ((FriendshipModel.requester_id == user.id) & (FriendshipModel.addressee_id == other.id)) | ((FriendshipModel.requester_id == other.id) & (FriendshipModel.addressee_id == user.id)), FriendshipModel.status != "declined", ).first() if not friendship: return {"status": "none"} if friendship.status == "accepted": return {"status": "friends", "friendship_id": str(friendship.id)} # pending: distinguish sent vs received if friendship.requester_id == user.id: return {"status": "pending_sent", "friendship_id": str(friendship.id)} return {"status": "pending_received", "friendship_id": str(friendship.id)} @router.delete("/friendships/{friendship_id}") def remove_friend(friendship_id: str, user: UserModel = Depends(get_current_user), db: Session = Depends(get_db)): friendship = db.query(FriendshipModel).filter(FriendshipModel.id == uuid.UUID(friendship_id)).first() if not friendship: raise HTTPException(status_code=404, detail="Friendship not found") if friendship.requester_id != user.id and friendship.addressee_id != user.id: raise HTTPException(status_code=403, detail="Not authorized") db.delete(friendship) db.commit() return {"ok": True}