use axum::extract::{Path, State}; use axum::http::StatusCode; use axum::Json; use serde::Deserialize; use uuid::Uuid; use crate::auth::middleware::AuthUser; use crate::error::AppError; use crate::models::comment::{Comment, CommentDto}; use crate::models::hashtag::{self, Hashtag}; use crate::state::AppState; pub async fn toggle_like( State(state): State, auth: AuthUser, Path(upload_id): Path, ) -> Result { // Check if user is banned let user = crate::models::user::User::find_by_id(&state.pool, auth.user_id) .await? .ok_or_else(|| AppError::NotFound("Benutzer nicht gefunden.".into()))?; if user.is_banned { return Err(AppError::Forbidden("Du bist gesperrt.".into())); } // Try to insert; if conflict, delete (toggle) let result = sqlx::query( "INSERT INTO \"like\" (upload_id, user_id) VALUES ($1, $2) ON CONFLICT (upload_id, user_id) DO NOTHING", ) .bind(upload_id) .bind(auth.user_id) .execute(&state.pool) .await?; if result.rows_affected() == 0 { // Already liked — remove sqlx::query("DELETE FROM \"like\" WHERE upload_id = $1 AND user_id = $2") .bind(upload_id) .bind(auth.user_id) .execute(&state.pool) .await?; } // Broadcast SSE let _ = state.sse_tx.send(crate::state::SseEvent { event_type: "like-update".to_string(), data: serde_json::json!({ "upload_id": upload_id }).to_string(), }); Ok(StatusCode::NO_CONTENT) } pub async fn list_comments( State(state): State, _auth: AuthUser, Path(upload_id): Path, ) -> Result>, AppError> { let comments = Comment::list_for_upload(&state.pool, upload_id).await?; Ok(Json(comments)) } #[derive(Deserialize)] pub struct AddCommentRequest { pub body: String, } pub async fn add_comment( State(state): State, auth: AuthUser, Path(upload_id): Path, Json(body): Json, ) -> Result<(StatusCode, Json), AppError> { let user = crate::models::user::User::find_by_id(&state.pool, auth.user_id) .await? .ok_or_else(|| AppError::NotFound("Benutzer nicht gefunden.".into()))?; if user.is_banned { return Err(AppError::Forbidden("Du bist gesperrt.".into())); } let text = body.body.trim(); if text.is_empty() || text.len() > 500 { return Err(AppError::BadRequest( "Kommentar muss zwischen 1 und 500 Zeichen lang sein.".into(), )); } let comment = Comment::create(&state.pool, upload_id, auth.user_id, text).await?; // Process hashtags in comment body let tags = hashtag::extract_hashtags(text); for tag in &tags { let h = Hashtag::upsert(&state.pool, auth.event_id, tag).await?; sqlx::query( "INSERT INTO comment_hashtag (comment_id, hashtag_id) VALUES ($1, $2) ON CONFLICT DO NOTHING", ) .bind(comment.id) .bind(h.id) .execute(&state.pool) .await?; } // Broadcast SSE let _ = state.sse_tx.send(crate::state::SseEvent { event_type: "new-comment".to_string(), data: serde_json::json!({ "upload_id": upload_id }).to_string(), }); let dto = CommentDto { id: comment.id, upload_id, user_id: auth.user_id, uploader_name: user.display_name, body: comment.body, created_at: comment.created_at, }; Ok((StatusCode::CREATED, Json(dto))) } pub async fn delete_comment( State(state): State, auth: AuthUser, Path(comment_id): Path, ) -> Result { let comment = Comment::find_by_id(&state.pool, comment_id) .await? .ok_or_else(|| AppError::NotFound("Kommentar nicht gefunden.".into()))?; if comment.user_id != auth.user_id { return Err(AppError::Forbidden("Nur eigene Kommentare löschen.".into())); } Comment::soft_delete(&state.pool, comment_id).await?; Ok(StatusCode::NO_CONTENT) }