refactor: extract handler business logic into application use cases
11 handlers were calling repos/ports directly, bypassing the
application layer. Extracted into proper use cases:
feed: get_public_feed, get_user_feed, get_tag_feed, get_popular_tags
profile: get_user_profile (with follow check), list_users,
count_local_users, list_local_followers, list_local_following
federation_management: set_also_known_as
Also registers 9 previously undocumented handlers in OpenAPI modules.
This commit is contained in:
@@ -10,7 +10,7 @@ use domain::{
|
|||||||
ports::{
|
ports::{
|
||||||
EventPublisher, FederationActionPort, FederationFollowPort, FederationFollowRequestPort,
|
EventPublisher, FederationActionPort, FederationFollowPort, FederationFollowRequestPort,
|
||||||
FederationSchedulerPort, FeedOptions, FeedQuery, FeedRepository, FeedRequest,
|
FederationSchedulerPort, FeedOptions, FeedQuery, FeedRepository, FeedRequest,
|
||||||
FollowRepository, RemoteActorConnectionRepository, UserReader,
|
FollowRepository, RemoteActorConnectionRepository, UserReader, UserWriter,
|
||||||
},
|
},
|
||||||
value_objects::UserId,
|
value_objects::UserId,
|
||||||
};
|
};
|
||||||
@@ -187,5 +187,13 @@ pub async fn get_actor_connections_page(
|
|||||||
Ok((items, has_more))
|
Ok((items, has_more))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn set_also_known_as(
|
||||||
|
users: &dyn UserWriter,
|
||||||
|
user_id: &UserId,
|
||||||
|
value: Option<String>,
|
||||||
|
) -> Result<(), DomainError> {
|
||||||
|
users.set_also_known_as(user_id, value).await
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
models::feed::{FeedEntry, PageParams, Paginated},
|
models::feed::{FeedEntry, PageParams, Paginated},
|
||||||
ports::{FeedOptions, FeedQuery, FeedRepository, FeedRequest, FollowRepository},
|
ports::{FeedOptions, FeedQuery, FeedRepository, FeedRequest, FollowRepository, TagRepository},
|
||||||
value_objects::UserId,
|
value_objects::UserId,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -20,3 +20,51 @@ pub async fn get_home_feed(
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_public_feed(
|
||||||
|
feed: &dyn FeedRepository,
|
||||||
|
viewer: Option<UserId>,
|
||||||
|
page: PageParams,
|
||||||
|
opts: FeedOptions,
|
||||||
|
) -> Result<Paginated<FeedEntry>, DomainError> {
|
||||||
|
feed.query(&FeedRequest {
|
||||||
|
query: FeedQuery::public(page, viewer),
|
||||||
|
options: opts,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_user_feed(
|
||||||
|
feed: &dyn FeedRepository,
|
||||||
|
user_id: UserId,
|
||||||
|
page: PageParams,
|
||||||
|
opts: FeedOptions,
|
||||||
|
viewer: Option<UserId>,
|
||||||
|
) -> Result<Paginated<FeedEntry>, DomainError> {
|
||||||
|
feed.query(&FeedRequest {
|
||||||
|
query: FeedQuery::user(user_id, page, viewer),
|
||||||
|
options: opts,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_tag_feed(
|
||||||
|
feed: &dyn FeedRepository,
|
||||||
|
tag: &str,
|
||||||
|
page: PageParams,
|
||||||
|
opts: FeedOptions,
|
||||||
|
viewer: Option<UserId>,
|
||||||
|
) -> Result<Paginated<FeedEntry>, DomainError> {
|
||||||
|
feed.query(&FeedRequest {
|
||||||
|
query: FeedQuery::tag(tag, page, viewer),
|
||||||
|
options: opts,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_popular_tags(
|
||||||
|
tags: &dyn TagRepository,
|
||||||
|
limit: usize,
|
||||||
|
) -> Result<Vec<(String, i64)>, DomainError> {
|
||||||
|
tags.popular_tags(limit).await
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ use domain::{
|
|||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
events::DomainEvent,
|
events::DomainEvent,
|
||||||
models::{
|
models::{
|
||||||
|
feed::{PageParams, Paginated, UserSummary},
|
||||||
top_friend::TopFriend,
|
top_friend::TopFriend,
|
||||||
user::{UpdateProfileInput, User},
|
user::{UpdateProfileInput, User},
|
||||||
},
|
},
|
||||||
ports::{
|
ports::{
|
||||||
EventPublisher, MediaStore, TopFriendRepository, UserReader, UserRepository, UserWriter,
|
EventPublisher, FollowRepository, MediaStore, TopFriendRepository, UserReader,
|
||||||
|
UserRepository, UserWriter,
|
||||||
},
|
},
|
||||||
value_objects::{UserId, Username},
|
value_objects::{UserId, Username},
|
||||||
};
|
};
|
||||||
@@ -230,5 +232,46 @@ pub async fn upload_banner(
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_user_profile(
|
||||||
|
users: &dyn UserReader,
|
||||||
|
follows: &dyn FollowRepository,
|
||||||
|
id_or_username: &str,
|
||||||
|
viewer_id: Option<&UserId>,
|
||||||
|
) -> Result<(User, bool), DomainError> {
|
||||||
|
let user = get_user_by_id_or_username(users, id_or_username).await?;
|
||||||
|
let is_followed = match viewer_id {
|
||||||
|
Some(vid) if vid != &user.id => follows.find(vid, &user.id).await?.is_some(),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
Ok((user, is_followed))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_users(
|
||||||
|
users: &dyn UserReader,
|
||||||
|
page: PageParams,
|
||||||
|
) -> Result<Paginated<UserSummary>, DomainError> {
|
||||||
|
users.list_paginated(page).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn count_local_users(users: &dyn UserReader) -> Result<i64, DomainError> {
|
||||||
|
users.count().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_local_followers(
|
||||||
|
follows: &dyn FollowRepository,
|
||||||
|
user_id: &UserId,
|
||||||
|
page: PageParams,
|
||||||
|
) -> Result<Paginated<User>, DomainError> {
|
||||||
|
follows.list_followers(user_id, &page).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_local_following(
|
||||||
|
follows: &dyn FollowRepository,
|
||||||
|
user_id: &UserId,
|
||||||
|
page: PageParams,
|
||||||
|
) -> Result<Paginated<User>, DomainError> {
|
||||||
|
follows.list_following(user_id, &page).await
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use api_types::responses::{ErrorResponse, ProfileField, RemoteActorResponse};
|
|||||||
use application::use_cases::federation_management::{
|
use application::use_cases::federation_management::{
|
||||||
accept_follow_request, get_remote_friends, initiate_actor_move, list_pending_requests,
|
accept_follow_request, get_remote_friends, initiate_actor_move, list_pending_requests,
|
||||||
list_remote_followers, list_remote_following, reject_follow_request, remove_remote_following,
|
list_remote_followers, list_remote_following, reject_follow_request, remove_remote_following,
|
||||||
|
set_also_known_as,
|
||||||
};
|
};
|
||||||
use axum::{http::StatusCode, Json};
|
use axum::{http::StatusCode, Json};
|
||||||
use domain::ports::{EventPublisher, FederationActionPort, FollowRepository, UserRepository};
|
use domain::ports::{EventPublisher, FederationActionPort, FollowRepository, UserRepository};
|
||||||
@@ -208,6 +209,6 @@ pub async fn patch_also_known_as(
|
|||||||
AuthUser(uid): AuthUser,
|
AuthUser(uid): AuthUser,
|
||||||
Json(body): Json<AlsoKnownAsBody>,
|
Json(body): Json<AlsoKnownAsBody>,
|
||||||
) -> Result<StatusCode, ApiError> {
|
) -> Result<StatusCode, ApiError> {
|
||||||
d.users.set_also_known_as(&uid, body.also_known_as).await?;
|
set_also_known_as(&*d.users, &uid, body.also_known_as).await?;
|
||||||
Ok(StatusCode::NO_CONTENT)
|
Ok(StatusCode::NO_CONTENT)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,13 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use api_types::requests::{PaginationQuery, SearchQuery};
|
use api_types::requests::{PaginationQuery, SearchQuery};
|
||||||
use api_types::responses::ThoughtResponse;
|
use api_types::responses::ThoughtResponse;
|
||||||
use application::use_cases::feed::get_home_feed;
|
use application::use_cases::feed::{
|
||||||
use application::use_cases::profile::{get_user_by_id_or_username, get_user_by_username};
|
get_home_feed, get_popular_tags as uc_get_popular_tags, get_public_feed, get_tag_feed,
|
||||||
|
get_user_feed,
|
||||||
|
};
|
||||||
|
use application::use_cases::profile::{
|
||||||
|
get_user_by_id_or_username, get_user_by_username, list_local_followers, list_local_following,
|
||||||
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, Query},
|
extract::{Path, Query},
|
||||||
http::{header, HeaderMap},
|
http::{header, HeaderMap},
|
||||||
@@ -17,8 +22,8 @@ use axum::{
|
|||||||
use domain::{
|
use domain::{
|
||||||
models::feed::PageParams,
|
models::feed::PageParams,
|
||||||
ports::{
|
ports::{
|
||||||
FederationActionPort, FeedFilter, FeedOptions, FeedQuery, FeedRepository, FeedRequest,
|
FederationActionPort, FeedFilter, FeedOptions, FeedRepository, FeedSort, FollowRepository,
|
||||||
FeedSort, FollowRepository, SearchPort, TagRepository, UserRepository,
|
SearchPort, TagRepository, UserRepository,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -142,13 +147,7 @@ pub async fn public_feed(
|
|||||||
per_page: q.per_page(),
|
per_page: q.per_page(),
|
||||||
};
|
};
|
||||||
let opts = FeedOptions::try_from(opts_q)?;
|
let opts = FeedOptions::try_from(opts_q)?;
|
||||||
let result = d
|
let result = get_public_feed(&*d.feed, viewer, page, opts).await?;
|
||||||
.feed
|
|
||||||
.query(&FeedRequest {
|
|
||||||
query: FeedQuery::public(page, viewer),
|
|
||||||
options: opts,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
Ok(Json(serde_json::json!({
|
Ok(Json(serde_json::json!({
|
||||||
"items": result.items.iter().map(to_thought_response).collect::<Vec<_>>(),
|
"items": result.items.iter().map(to_thought_response).collect::<Vec<_>>(),
|
||||||
"total": result.total,
|
"total": result.total,
|
||||||
@@ -232,7 +231,7 @@ pub async fn get_following_handler(
|
|||||||
page: q.page(),
|
page: q.page(),
|
||||||
per_page: q.per_page(),
|
per_page: q.per_page(),
|
||||||
};
|
};
|
||||||
let result = d.follows.list_following(&user.id, &page).await?;
|
let result = list_local_following(&*d.follows, &user.id, page).await?;
|
||||||
Ok(Json(serde_json::json!({
|
Ok(Json(serde_json::json!({
|
||||||
"total": result.total,
|
"total": result.total,
|
||||||
"items": result.items.iter().map(to_user_response).collect::<Vec<_>>()
|
"items": result.items.iter().map(to_user_response).collect::<Vec<_>>()
|
||||||
@@ -275,7 +274,7 @@ pub async fn get_followers_handler(
|
|||||||
page: q.page(),
|
page: q.page(),
|
||||||
per_page: q.per_page(),
|
per_page: q.per_page(),
|
||||||
};
|
};
|
||||||
let result = d.follows.list_followers(&user.id, &page).await?;
|
let result = list_local_followers(&*d.follows, &user.id, page).await?;
|
||||||
Ok(Json(serde_json::json!({
|
Ok(Json(serde_json::json!({
|
||||||
"total": result.total,
|
"total": result.total,
|
||||||
"items": result.items.iter().map(to_user_response).collect::<Vec<_>>()
|
"items": result.items.iter().map(to_user_response).collect::<Vec<_>>()
|
||||||
@@ -305,13 +304,7 @@ pub async fn user_thoughts_handler(
|
|||||||
per_page: q.per_page(),
|
per_page: q.per_page(),
|
||||||
};
|
};
|
||||||
let opts = FeedOptions::try_from(opts_q)?;
|
let opts = FeedOptions::try_from(opts_q)?;
|
||||||
let result = d
|
let result = get_user_feed(&*d.feed, user.id.clone(), page, opts, viewer).await?;
|
||||||
.feed
|
|
||||||
.query(&FeedRequest {
|
|
||||||
query: FeedQuery::user(user.id.clone(), page, viewer),
|
|
||||||
options: opts,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
Ok(Json(serde_json::json!({
|
Ok(Json(serde_json::json!({
|
||||||
"total": result.total,
|
"total": result.total,
|
||||||
"page": result.page,
|
"page": result.page,
|
||||||
@@ -334,11 +327,9 @@ pub async fn get_popular_tags(
|
|||||||
let limit: usize = params
|
let limit: usize = params
|
||||||
.get("limit")
|
.get("limit")
|
||||||
.and_then(|v| v.parse().ok())
|
.and_then(|v| v.parse().ok())
|
||||||
.unwrap_or(api_types::requests::DEFAULT_PER_PAGE as usize);
|
.unwrap_or(api_types::requests::DEFAULT_PER_PAGE as usize)
|
||||||
let tags = d
|
.min(api_types::requests::MAX_PER_PAGE as usize);
|
||||||
.tags
|
let tags = uc_get_popular_tags(&*d.tags, limit).await?;
|
||||||
.popular_tags(limit.min(api_types::requests::MAX_PER_PAGE as usize))
|
|
||||||
.await?;
|
|
||||||
Ok(Json(serde_json::json!({
|
Ok(Json(serde_json::json!({
|
||||||
"tags": tags.iter().map(|(name, count)| serde_json::json!({
|
"tags": tags.iter().map(|(name, count)| serde_json::json!({
|
||||||
"name": name,
|
"name": name,
|
||||||
@@ -368,13 +359,7 @@ pub async fn tag_thoughts_handler(
|
|||||||
per_page: q.per_page(),
|
per_page: q.per_page(),
|
||||||
};
|
};
|
||||||
let opts = FeedOptions::try_from(opts_q)?;
|
let opts = FeedOptions::try_from(opts_q)?;
|
||||||
let result = d
|
let result = get_tag_feed(&*d.feed, &tag_name, page, opts, viewer).await?;
|
||||||
.feed
|
|
||||||
.query(&FeedRequest {
|
|
||||||
query: FeedQuery::tag(&tag_name, page, viewer),
|
|
||||||
options: opts,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
Ok(Json(serde_json::json!({
|
Ok(Json(serde_json::json!({
|
||||||
"tag": tag_name,
|
"tag": tag_name,
|
||||||
"total": result.total,
|
"total": result.total,
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ use api_types::{
|
|||||||
responses::{ErrorResponse, ProfileField, RemoteActorResponse, UserResponse},
|
responses::{ErrorResponse, ProfileField, RemoteActorResponse, UserResponse},
|
||||||
};
|
};
|
||||||
use application::use_cases::profile::{
|
use application::use_cases::profile::{
|
||||||
get_user as fetch_user, get_user_by_id_or_username, update_profile,
|
count_local_users, get_user as fetch_user, get_user_by_id_or_username, get_user_profile,
|
||||||
upload_avatar as upload_avatar_uc, upload_banner as upload_banner_uc, UploadConfig,
|
list_local_following, list_users, update_profile, upload_avatar as upload_avatar_uc,
|
||||||
|
upload_banner as upload_banner_uc, UploadConfig,
|
||||||
};
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Multipart, Path, Query},
|
extract::{Multipart, Path, Query},
|
||||||
@@ -67,22 +68,18 @@ pub async fn get_user(
|
|||||||
OptionalAuthUser(viewer): OptionalAuthUser,
|
OptionalAuthUser(viewer): OptionalAuthUser,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> Result<Response, ApiError> {
|
) -> Result<Response, ApiError> {
|
||||||
let user = get_user_by_id_or_username(&*d.users, &username).await?;
|
|
||||||
|
|
||||||
let accept = headers
|
let accept = headers
|
||||||
.get(header::ACCEPT)
|
.get(header::ACCEPT)
|
||||||
.and_then(|v| v.to_str().ok())
|
.and_then(|v| v.to_str().ok())
|
||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
|
|
||||||
if accept.contains("application/activity+json") {
|
if accept.contains("application/activity+json") {
|
||||||
|
let user = get_user_by_id_or_username(&*d.users, &username).await?;
|
||||||
let json = d.federation.actor_json(&user.id).await?;
|
let json = d.federation.actor_json(&user.id).await?;
|
||||||
Ok(([(header::CONTENT_TYPE, "application/activity+json")], json).into_response())
|
Ok(([(header::CONTENT_TYPE, "application/activity+json")], json).into_response())
|
||||||
} else {
|
} else {
|
||||||
let is_followed = if let Some(viewer_id) = viewer {
|
let (user, is_followed) =
|
||||||
d.follows.find(&viewer_id, &user.id).await?.is_some()
|
get_user_profile(&*d.users, &*d.follows, &username, viewer.as_ref()).await?;
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
let mut resp = to_user_response(&user);
|
let mut resp = to_user_response(&user);
|
||||||
resp.is_followed_by_viewer = is_followed;
|
resp.is_followed_by_viewer = is_followed;
|
||||||
Ok(Json(resp).into_response())
|
Ok(Json(resp).into_response())
|
||||||
@@ -154,7 +151,7 @@ pub async fn get_me_following(
|
|||||||
page: q.page(),
|
page: q.page(),
|
||||||
per_page: q.per_page(),
|
per_page: q.per_page(),
|
||||||
};
|
};
|
||||||
let result = d.follows.list_following(&uid, &page).await?;
|
let result = list_local_following(&*d.follows, &uid, page).await?;
|
||||||
Ok(Json(serde_json::json!({
|
Ok(Json(serde_json::json!({
|
||||||
"total": result.total,
|
"total": result.total,
|
||||||
"items": result.items.iter().map(to_user_response).collect::<Vec<_>>(),
|
"items": result.items.iter().map(to_user_response).collect::<Vec<_>>(),
|
||||||
@@ -197,7 +194,7 @@ pub async fn get_users(
|
|||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = d.users.list_paginated(page_params).await?;
|
let result = list_users(&*d.users, page_params).await?;
|
||||||
let items: Vec<_> = result
|
let items: Vec<_> = result
|
||||||
.items
|
.items
|
||||||
.iter()
|
.iter()
|
||||||
@@ -226,7 +223,7 @@ pub async fn get_users(
|
|||||||
responses((status = 200, description = "Total number of local users"))
|
responses((status = 200, description = "Total number of local users"))
|
||||||
)]
|
)]
|
||||||
pub async fn get_user_count(Deps(d): Deps<UsersDeps>) -> Result<Json<serde_json::Value>, ApiError> {
|
pub async fn get_user_count(Deps(d): Deps<UsersDeps>) -> Result<Json<serde_json::Value>, ApiError> {
|
||||||
let count = d.users.count().await?;
|
let count = count_local_users(&*d.users).await?;
|
||||||
Ok(Json(serde_json::json!({ "count": count })))
|
Ok(Json(serde_json::json!({ "count": count })))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ use utoipa::OpenApi;
|
|||||||
crate::handlers::feed::public_feed,
|
crate::handlers::feed::public_feed,
|
||||||
crate::handlers::feed::search_handler,
|
crate::handlers::feed::search_handler,
|
||||||
crate::handlers::feed::user_thoughts_handler,
|
crate::handlers::feed::user_thoughts_handler,
|
||||||
|
crate::handlers::feed::get_followers_handler,
|
||||||
|
crate::handlers::feed::get_following_handler,
|
||||||
crate::handlers::feed::tag_thoughts_handler,
|
crate::handlers::feed::tag_thoughts_handler,
|
||||||
|
crate::handlers::feed::get_popular_tags,
|
||||||
))]
|
))]
|
||||||
pub struct FeedDoc;
|
pub struct FeedDoc;
|
||||||
|
|||||||
@@ -7,9 +7,15 @@ use utoipa::OpenApi;
|
|||||||
#[derive(OpenApi)]
|
#[derive(OpenApi)]
|
||||||
#[openapi(
|
#[openapi(
|
||||||
paths(
|
paths(
|
||||||
|
crate::handlers::users::get_users,
|
||||||
|
crate::handlers::users::get_user_count,
|
||||||
|
crate::handlers::users::lookup_handler,
|
||||||
crate::handlers::users::get_me,
|
crate::handlers::users::get_me,
|
||||||
|
crate::handlers::users::get_me_following,
|
||||||
crate::handlers::users::get_user,
|
crate::handlers::users::get_user,
|
||||||
crate::handlers::users::patch_profile,
|
crate::handlers::users::patch_profile,
|
||||||
|
crate::handlers::users::upload_avatar,
|
||||||
|
crate::handlers::users::upload_banner,
|
||||||
),
|
),
|
||||||
components(schemas(UserResponse, UpdateProfileRequest, ErrorResponse))
|
components(schemas(UserResponse, UpdateProfileRequest, ErrorResponse))
|
||||||
)]
|
)]
|
||||||
|
|||||||
Reference in New Issue
Block a user