use crate::{ errors::ApiError, extractors::{AuthUser, OptionalAuthUser}, handlers::auth::to_user_response, state::AppState, }; use api_types::{ requests::{CreateThoughtRequest, EditThoughtRequest}, responses::ErrorResponse, }; use application::use_cases::thoughts::{ create_thought, delete_thought, edit_thought, get_thought, get_thread, CreateThoughtInput, }; use axum::{ extract::{Path, State}, http::StatusCode, response::IntoResponse, Json, }; use domain::value_objects::ThoughtId; use uuid::Uuid; fn visibility_as_str(v: &domain::models::thought::Visibility) -> &'static str { use domain::models::thought::Visibility; match v { Visibility::Public => "public", Visibility::Followers => "followers", Visibility::Unlisted => "unlisted", Visibility::Direct => "direct", } } fn thought_to_json( t: &domain::models::thought::Thought, author: &domain::models::user::User, like_count: i64, boost_count: i64, reply_count: i64, ) -> serde_json::Value { serde_json::json!({ "id": t.id.as_uuid(), "content": t.content.as_str(), "author": to_user_response(author), "replyToId": t.in_reply_to_id.as_ref().map(|x| x.as_uuid()), "visibility": visibility_as_str(&t.visibility), "contentWarning": t.content_warning, "sensitive": t.sensitive, "likeCount": like_count, "boostCount": boost_count, "replyCount": reply_count, "likedByViewer": false, "boostedByViewer": false, "createdAt": t.created_at, "updatedAt": t.updated_at, }) } #[utoipa::path( post, path = "/thoughts", request_body = CreateThoughtRequest, responses( (status = 201, description = "Thought created"), (status = 401, description = "Unauthorized", body = ErrorResponse), (status = 422, description = "Content too long", body = ErrorResponse), ), security(("bearer_auth" = [])) )] pub async fn post_thought( State(s): State, AuthUser(uid): AuthUser, Json(body): Json, ) -> Result { let in_reply_to = body.in_reply_to_id.map(ThoughtId::from_uuid); let out = create_thought( &*s.thoughts, &*s.users, &*s.tags, &*s.events, CreateThoughtInput { user_id: uid.clone(), content: body.content, in_reply_to_id: in_reply_to, visibility: body.visibility, content_warning: body.content_warning, sensitive: body.sensitive.unwrap_or(false), }, ) .await?; let author = s .users .find_by_id(&uid) .await? .ok_or(domain::errors::DomainError::NotFound)?; Ok(( StatusCode::CREATED, Json(thought_to_json(&out.thought, &author, 0, 0, 0)), )) } #[utoipa::path( get, path = "/thoughts/{id}", params(("id" = uuid::Uuid, Path, description = "Thought ID")), responses( (status = 200, description = "Thought with author info"), (status = 404, description = "Not found", body = ErrorResponse), ) )] pub async fn get_thought_handler( State(s): State, Path(id): Path, OptionalAuthUser(_viewer): OptionalAuthUser, ) -> Result, ApiError> { let thought = get_thought(&*s.thoughts, &ThoughtId::from_uuid(id)).await?; let author = s .users .find_by_id(&thought.user_id) .await? .ok_or(domain::errors::DomainError::NotFound)?; Ok(Json(thought_to_json(&thought, &author, 0, 0, 0))) } #[utoipa::path( delete, path = "/thoughts/{id}", params(("id" = uuid::Uuid, Path, description = "Thought ID")), responses( (status = 204, description = "Deleted"), (status = 401, description = "Unauthorized", body = ErrorResponse), (status = 404, description = "Not found or not owner", body = ErrorResponse), ), security(("bearer_auth" = [])) )] pub async fn delete_thought_handler( State(s): State, AuthUser(uid): AuthUser, Path(id): Path, ) -> Result { delete_thought(&*s.thoughts, &*s.events, &ThoughtId::from_uuid(id), &uid).await?; Ok(StatusCode::NO_CONTENT) } #[utoipa::path( patch, path = "/thoughts/{id}", params(("id" = uuid::Uuid, Path, description = "Thought ID")), request_body = EditThoughtRequest, responses( (status = 204, description = "Updated"), (status = 401, description = "Unauthorized", body = ErrorResponse), (status = 404, description = "Not found or not owner", body = ErrorResponse), ), security(("bearer_auth" = [])) )] pub async fn patch_thought( State(s): State, AuthUser(uid): AuthUser, Path(id): Path, Json(body): Json, ) -> Result { edit_thought( &*s.thoughts, &*s.events, &ThoughtId::from_uuid(id), &uid, body.content, ) .await?; Ok(StatusCode::NO_CONTENT) } #[utoipa::path( get, path = "/thoughts/{id}/thread", params(("id" = uuid::Uuid, Path, description = "Root thought ID")), responses( (status = 200, description = "Thread (root + replies)"), ) )] pub async fn get_thread_handler( State(s): State, Path(id): Path, ) -> Result>, ApiError> { let thoughts = get_thread(&*s.thoughts, &ThoughtId::from_uuid(id)).await?; let mut items = Vec::new(); for t in &thoughts { if let Ok(Some(author)) = s.users.find_by_id(&t.user_id).await { items.push(thought_to_json(t, &author, 0, 0, 0)); } } Ok(Json(items)) }