317 lines
9.8 KiB
Rust
317 lines
9.8 KiB
Rust
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},
|
|
http::{header, HeaderMap},
|
|
response::{IntoResponse, Response},
|
|
Json,
|
|
};
|
|
use domain::models::feed::PageParams;
|
|
use domain::value_objects::UserId;
|
|
|
|
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",
|
|
}
|
|
}
|
|
|
|
pub 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: visibility_as_str(&e.thought.visibility).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<AppState>,
|
|
AuthUser(uid): AuthUser,
|
|
Query(q): Query<PaginationQuery>,
|
|
) -> Result<Json<serde_json::Value>, 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::<Vec<_>>(),
|
|
"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<AppState>,
|
|
OptionalAuthUser(viewer): OptionalAuthUser,
|
|
Query(q): Query<PaginationQuery>,
|
|
) -> Result<Json<serde_json::Value>, 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::<Vec<_>>(),
|
|
"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<AppState>,
|
|
OptionalAuthUser(viewer): OptionalAuthUser,
|
|
Query(q): Query<SearchQuery>,
|
|
) -> Result<Json<serde_json::Value>, 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::<Vec<_>>();
|
|
|
|
let users = users_result?
|
|
.items
|
|
.into_iter()
|
|
.map(|u| to_user_response(&u))
|
|
.collect::<Vec<_>>();
|
|
|
|
Ok(Json(serde_json::json!({
|
|
"query": query,
|
|
"thoughts": thoughts,
|
|
"users": users,
|
|
})))
|
|
}
|
|
|
|
pub async fn get_following_handler(
|
|
State(s): State<AppState>,
|
|
Path(param): Path<String>,
|
|
Query(q): Query<PaginationQuery>,
|
|
headers: HeaderMap,
|
|
) -> Result<Response, ApiError> {
|
|
let accept = headers
|
|
.get(header::ACCEPT)
|
|
.and_then(|v| v.to_str().ok())
|
|
.unwrap_or("");
|
|
|
|
if accept.contains("application/activity+json") {
|
|
let user_id = resolve_user_id(&s, ¶m).await?;
|
|
let page = q.page().try_into().ok();
|
|
let json = s
|
|
.federation
|
|
.following_collection_json(&user_id, page)
|
|
.await?;
|
|
return Ok(([(header::CONTENT_TYPE, "application/activity+json")], json).into_response());
|
|
}
|
|
|
|
let user = get_user_by_username(&*s.users, ¶m).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::<Vec<_>>()
|
|
}))
|
|
.into_response())
|
|
}
|
|
|
|
pub async fn get_followers_handler(
|
|
State(s): State<AppState>,
|
|
Path(param): Path<String>,
|
|
Query(q): Query<PaginationQuery>,
|
|
headers: HeaderMap,
|
|
) -> Result<Response, ApiError> {
|
|
let accept = headers
|
|
.get(header::ACCEPT)
|
|
.and_then(|v| v.to_str().ok())
|
|
.unwrap_or("");
|
|
|
|
if accept.contains("application/activity+json") {
|
|
let user_id = resolve_user_id(&s, ¶m).await?;
|
|
let page = q.page().try_into().ok();
|
|
let json = s
|
|
.federation
|
|
.followers_collection_json(&user_id, page)
|
|
.await?;
|
|
return Ok(([(header::CONTENT_TYPE, "application/activity+json")], json).into_response());
|
|
}
|
|
|
|
let user = get_user_by_username(&*s.users, ¶m).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::<Vec<_>>()
|
|
}))
|
|
.into_response())
|
|
}
|
|
|
|
async fn resolve_user_id(s: &AppState, param: &str) -> Result<UserId, ApiError> {
|
|
if let Ok(uuid) = uuid::Uuid::parse_str(param) {
|
|
s.users
|
|
.find_by_id(&UserId::from_uuid(uuid))
|
|
.await?
|
|
.map(|u| u.id)
|
|
.ok_or_else(|| ApiError::from(domain::errors::DomainError::NotFound))
|
|
} else {
|
|
Ok(get_user_by_username(&*s.users, param).await?.id)
|
|
}
|
|
}
|
|
|
|
#[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<AppState>,
|
|
Path(username): Path<String>,
|
|
OptionalAuthUser(viewer): OptionalAuthUser,
|
|
Query(q): Query<PaginationQuery>,
|
|
) -> Result<Json<serde_json::Value>, 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::<Vec<_>>()
|
|
})))
|
|
}
|
|
|
|
pub async fn get_popular_tags(
|
|
State(s): State<AppState>,
|
|
Query(params): Query<std::collections::HashMap<String, String>>,
|
|
) -> Result<Json<serde_json::Value>, 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::<Vec<_>>()
|
|
})))
|
|
}
|
|
|
|
#[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<AppState>,
|
|
Path(tag_name): Path<String>,
|
|
OptionalAuthUser(viewer): OptionalAuthUser,
|
|
Query(q): Query<PaginationQuery>,
|
|
) -> Result<Json<serde_json::Value>, 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::<Vec<_>>(),
|
|
})))
|
|
}
|