use crate::{ errors::ApiError, extractors::{AuthUser, OptionalAuthUser}, handlers::auth::to_user_response, state::AppState, }; use api_types::requests::{PaginationQuery, SearchQuery}; use api_types::responses::ThoughtResponse; use application::use_cases::feed::{ get_by_tag, get_followers, get_following, get_home_feed, get_popular_tags as uc_get_popular_tags, get_public_feed, get_user_feed, }; use application::use_cases::profile::get_user_by_username; use application::use_cases::search::{search_thoughts, search_users}; use axum::{ extract::{Path, Query, State}, Json, }; use domain::models::feed::PageParams; fn to_thought_response(e: &domain::models::feed::FeedEntry) -> ThoughtResponse { ThoughtResponse { id: e.thought.id.as_uuid(), content: e.thought.content.as_str().to_string(), author: to_user_response(&e.author), in_reply_to_id: e.thought.in_reply_to_id.as_ref().map(|id| id.as_uuid()), visibility: e.thought.visibility.as_str().to_string(), content_warning: e.thought.content_warning.clone(), sensitive: e.thought.sensitive, like_count: e.like_count, boost_count: e.boost_count, reply_count: e.reply_count, liked_by_viewer: e.liked_by_viewer, boosted_by_viewer: e.boosted_by_viewer, created_at: e.thought.created_at, updated_at: e.thought.updated_at, } } #[utoipa::path( get, path = "/feed", params(PaginationQuery), responses((status = 200, description = "Home feed")), security(("bearer_auth" = [])) )] pub async fn home_feed( State(s): State, AuthUser(uid): AuthUser, Query(q): Query, ) -> Result, ApiError> { let page = PageParams { page: q.page(), per_page: q.per_page(), }; let result = get_home_feed(&*s.feed, &*s.follows, &uid, page).await?; Ok(Json(serde_json::json!({ "items": result.items.iter().map(to_thought_response).collect::>(), "total": result.total, "page": result.page, "per_page": result.per_page, }))) } #[utoipa::path( get, path = "/feed/public", params(PaginationQuery), responses((status = 200, description = "Public feed")) )] pub async fn public_feed( State(s): State, OptionalAuthUser(viewer): OptionalAuthUser, Query(q): Query, ) -> Result, ApiError> { let page = PageParams { page: q.page(), per_page: q.per_page(), }; let result = get_public_feed(&*s.feed, viewer.as_ref(), page).await?; Ok(Json(serde_json::json!({ "items": result.items.iter().map(to_thought_response).collect::>(), "total": result.total, "page": result.page, "per_page": result.per_page, }))) } #[utoipa::path( get, path = "/search", params(SearchQuery), responses((status = 200, description = "Search results: thoughts and users")) )] pub async fn search_handler( State(s): State, OptionalAuthUser(viewer): OptionalAuthUser, Query(q): Query, ) -> Result, ApiError> { let page = PageParams { page: q.page.unwrap_or(1), per_page: q.per_page.unwrap_or(20), }; let query = q.q.trim().to_string(); let (thoughts_result, users_result) = tokio::join!( search_thoughts( &*s.search, &query, PageParams { page: page.page, per_page: page.per_page }, viewer.as_ref() ), search_users( &*s.search, &query, PageParams { page: page.page, per_page: page.per_page } ), ); let thoughts = thoughts_result? .items .into_iter() .map(|e| { serde_json::json!({ "id": e.thought.id.as_uuid(), "content": e.thought.content.as_str(), "author": to_user_response(&e.author), "like_count": e.like_count, "boost_count": e.boost_count, "reply_count": e.reply_count, "created_at": e.thought.created_at, }) }) .collect::>(); let users = users_result? .items .into_iter() .map(|u| to_user_response(&u)) .collect::>(); Ok(Json(serde_json::json!({ "query": query, "thoughts": thoughts, "users": users, }))) } pub async fn get_following_handler( State(s): State, Path(username): Path, Query(q): Query, ) -> Result, ApiError> { let user = get_user_by_username(&*s.users, &username).await?; let page = PageParams { page: q.page(), per_page: q.per_page(), }; let result = get_following(&*s.follows, &user.id, page).await?; Ok(Json( serde_json::json!({ "total": result.total, "items": result.items.iter().map(to_user_response).collect::>() }), )) } pub async fn get_followers_handler( State(s): State, Path(username): Path, Query(q): Query, ) -> Result, ApiError> { let user = get_user_by_username(&*s.users, &username).await?; let page = PageParams { page: q.page(), per_page: q.per_page(), }; let result = get_followers(&*s.follows, &user.id, page).await?; Ok(Json( serde_json::json!({ "total": result.total, "items": result.items.iter().map(to_user_response).collect::>() }), )) } #[utoipa::path( get, path = "/users/{username}/thoughts", params( ("username" = String, Path, description = "Username"), PaginationQuery, ), responses((status = 200, description = "User's public thoughts")) )] pub async fn user_thoughts_handler( State(s): State, Path(username): Path, OptionalAuthUser(viewer): OptionalAuthUser, Query(q): Query, ) -> Result, ApiError> { let user = get_user_by_username(&*s.users, &username).await?; let page = PageParams { page: q.page(), per_page: q.per_page(), }; let result = get_user_feed(&*s.feed, &user.id, page, viewer.as_ref()).await?; Ok(Json(serde_json::json!({ "total": result.total, "page": result.page, "per_page": result.per_page, "items": result.items.iter().map(to_thought_response).collect::>() }))) } pub async fn get_popular_tags( State(s): State, Query(params): Query>, ) -> Result, ApiError> { let limit: usize = params .get("limit") .and_then(|v| v.parse().ok()) .unwrap_or(20); let tags = uc_get_popular_tags(&*s.tags, limit.min(100)).await?; Ok(Json(serde_json::json!({ "tags": tags.iter().map(|(name, count)| serde_json::json!({ "name": name, "thought_count": count, })).collect::>() }))) } #[utoipa::path( get, path = "/tags/{name}", params( ("name" = String, Path, description = "Tag name"), PaginationQuery, ), responses((status = 200, description = "Thoughts with this tag")) )] pub async fn tag_thoughts_handler( State(s): State, Path(tag_name): Path, OptionalAuthUser(viewer): OptionalAuthUser, Query(q): Query, ) -> Result, ApiError> { let page = PageParams { page: q.page(), per_page: q.per_page(), }; let result = get_by_tag(&*s.feed, &tag_name, page, viewer.as_ref()).await?; Ok(Json(serde_json::json!({ "tag": tag_name, "total": result.total, "page": result.page, "per_page": result.per_page, "items": result.items.iter().map(to_thought_response).collect::>(), }))) }