Files
LeDiscord/backend/api/routers/posts.py

348 lines
11 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
from sqlalchemy.orm import Session
from typing import List
from datetime import datetime
import os
import uuid
from pathlib import Path
from PIL import Image
from config.database import get_db
from config.settings import settings
from models.post import Post, PostMention, PostLike, PostComment
from models.user import User
from models.notification import Notification, NotificationType
from utils.notification_service import NotificationService
from schemas.post import PostCreate, PostUpdate, PostResponse, PostCommentCreate
from utils.security import get_current_active_user
from utils.settings_service import SettingsService
router = APIRouter()
@router.post("/", response_model=PostResponse)
async def create_post(
post_data: PostCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Create a new post."""
post = Post(
author_id=current_user.id,
content=post_data.content,
image_url=post_data.image_url
)
db.add(post)
db.flush() # Get the post ID before creating mentions
# Create mentions
for user_id in post_data.mentioned_user_ids:
mentioned_user = db.query(User).filter(User.id == user_id).first()
if mentioned_user:
mention = PostMention(
post_id=post.id,
mentioned_user_id=user_id
)
db.add(mention)
# Create notification for mentioned user
NotificationService.create_mention_notification(
db=db,
mentioned_user_id=user_id,
author=current_user,
content_type="post",
content_id=post.id
)
db.commit()
db.refresh(post)
return format_post_response(post, db)
@router.get("/", response_model=List[PostResponse])
async def get_posts(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user),
limit: int = 50,
offset: int = 0
):
"""Get all posts."""
posts = db.query(Post).order_by(Post.created_at.desc()).limit(limit).offset(offset).all()
return [format_post_response(post, db) for post in posts]
@router.get("/{post_id}", response_model=PostResponse)
async def get_post(
post_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Get a specific post."""
post = db.query(Post).filter(Post.id == post_id).first()
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Post not found"
)
return format_post_response(post, db)
@router.put("/{post_id}", response_model=PostResponse)
async def update_post(
post_id: int,
post_update: PostUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Update a post."""
post = db.query(Post).filter(Post.id == post_id).first()
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Post not found"
)
if post.author_id != current_user.id and not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to update this post"
)
for field, value in post_update.dict(exclude_unset=True).items():
setattr(post, field, value)
post.updated_at = datetime.utcnow()
db.commit()
db.refresh(post)
return format_post_response(post, db)
@router.delete("/{post_id}")
async def delete_post(
post_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Delete a post."""
post = db.query(Post).filter(Post.id == post_id).first()
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Post not found"
)
if post.author_id != current_user.id and not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to delete this post"
)
db.delete(post)
db.commit()
return {"message": "Post deleted successfully"}
@router.post("/upload-image")
async def upload_post_image(
file: UploadFile = File(...),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Upload an image for a post."""
# Validate file type
if not SettingsService.is_file_type_allowed(db, file.content_type or "unknown"):
allowed_types = SettingsService.get_setting(db, "allowed_image_types",
["image/jpeg", "image/png", "image/gif", "image/webp"])
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid file type. Allowed types: {', '.join(allowed_types)}"
)
# Check file size
contents = await file.read()
file_size = len(contents)
max_size = SettingsService.get_max_upload_size(db, file.content_type or "image/jpeg")
if file_size > max_size:
max_size_mb = max_size // (1024 * 1024)
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail=f"File too large. Maximum size: {max_size_mb}MB"
)
# Create posts directory
posts_dir = Path(settings.UPLOAD_PATH) / "posts" / str(current_user.id)
posts_dir.mkdir(parents=True, exist_ok=True)
# Generate unique filename
file_extension = file.filename.split(".")[-1]
filename = f"{uuid.uuid4()}.{file_extension}"
file_path = posts_dir / filename
# Save file
with open(file_path, "wb") as f:
f.write(contents)
# Resize image if it's REALLY too large (4K support)
try:
img = Image.open(file_path)
if img.size[0] > 3840 or img.size[1] > 2160: # 4K threshold
img.thumbnail((3840, 2160), Image.Resampling.LANCZOS)
img.save(file_path, quality=95, optimize=True) # High quality
except Exception as e:
print(f"Error processing image: {e}")
# Return the image URL
image_url = f"/posts/{current_user.id}/{filename}"
return {"image_url": image_url}
@router.post("/{post_id}/like")
async def toggle_post_like(
post_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Toggle like on a post."""
post = db.query(Post).filter(Post.id == post_id).first()
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Post not found"
)
# Check if user already liked the post
existing_like = db.query(PostLike).filter(
PostLike.post_id == post_id,
PostLike.user_id == current_user.id
).first()
if existing_like:
# Unlike
db.delete(existing_like)
post.likes_count = max(0, post.likes_count - 1)
message = "Like removed"
is_liked = False
else:
# Like
like = PostLike(post_id=post_id, user_id=current_user.id)
db.add(like)
post.likes_count += 1
message = "Post liked"
is_liked = True
db.commit()
return {"message": message, "is_liked": is_liked, "likes_count": post.likes_count}
@router.post("/{post_id}/comment")
async def add_post_comment(
post_id: int,
comment_data: PostCommentCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Add a comment to a post."""
post = db.query(Post).filter(Post.id == post_id).first()
if not post:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Post not found"
)
comment = PostComment(
post_id=post_id,
author_id=current_user.id,
content=comment_data.content
)
db.add(comment)
db.commit()
db.refresh(comment)
return {
"message": "Comment added successfully",
"comment": {
"id": comment.id,
"content": comment.content,
"author_id": comment.author_id,
"author_name": current_user.full_name,
"author_avatar": current_user.avatar_url,
"created_at": comment.created_at
}
}
@router.delete("/{post_id}/comment/{comment_id}")
async def delete_post_comment(
post_id: int,
comment_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Delete a comment from a post."""
comment = db.query(PostComment).filter(
PostComment.id == comment_id,
PostComment.post_id == post_id
).first()
if not comment:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Comment not found"
)
if comment.author_id != current_user.id and not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to delete this comment"
)
db.delete(comment)
db.commit()
return {"message": "Comment deleted successfully"}
def format_post_response(post: Post, db: Session) -> dict:
"""Format post response with author and mentions information."""
mentioned_users = []
for mention in post.mentions:
user = mention.mentioned_user
mentioned_users.append({
"id": user.id,
"username": user.username,
"full_name": user.full_name,
"avatar_url": user.avatar_url
})
# Get likes and comments
likes = []
for like in post.likes:
user = db.query(User).filter(User.id == like.user_id).first()
if user:
likes.append({
"id": like.id,
"user_id": user.id,
"username": user.username,
"full_name": user.full_name,
"avatar_url": user.avatar_url,
"created_at": like.created_at
})
comments = []
for comment in post.comments:
user = db.query(User).filter(User.id == comment.author_id).first()
if user:
comments.append({
"id": comment.id,
"content": comment.content,
"author_id": user.id,
"author_name": user.full_name,
"author_avatar": user.avatar_url,
"created_at": comment.created_at
})
return {
"id": post.id,
"author_id": post.author_id,
"content": post.content,
"image_url": post.image_url,
"likes_count": post.likes_count,
"comments_count": post.comments_count,
"created_at": post.created_at,
"updated_at": post.updated_at,
"author_name": post.author.full_name,
"author_avatar": post.author.avatar_url,
"mentioned_users": mentioned_users,
"likes": likes,
"comments": comments
}