This commit is contained in:
2026-04-01 18:31:33 +02:00
parent 6e23e32bb0
commit b5c7c5305a
95 changed files with 9609 additions and 2374 deletions

134
backend/routers/friends.py Normal file
View File

@@ -0,0 +1,134 @@
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}