refactor: http-axum split into api/html/federation modules, router.rs, templates per page group
This commit is contained in:
279
crates/adapters/http-axum/src/api/user_profile.rs
Normal file
279
crates/adapters/http-axum/src/api/user_profile.rs
Normal file
@@ -0,0 +1,279 @@
|
||||
use crate::{extractors::AuthenticatedUser, state::AppState};
|
||||
use api_types::mappers::goal_with_progress_to_dto;
|
||||
use api_types::{
|
||||
ActorStatDto, DiaryResponse, DirectorStatDto, GenreStatDto, MonthActivityDto, MonthlyRatingDto,
|
||||
UserProfileQueryParams, UserProfileResponse, UserStatsDto, UserTrendsDto, WatchMediumStatDto,
|
||||
};
|
||||
use application::users::{
|
||||
get_federated_profile, get_federated_profile_stats, get_local_profile,
|
||||
queries::GetUserProfileQuery,
|
||||
};
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use domain::errors::DomainError;
|
||||
use std::str::FromStr;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[utoipa::path(
|
||||
get, path = "/api/v1/users/{id}",
|
||||
params(
|
||||
("id" = Uuid, Path, description = "User ID"),
|
||||
UserProfileQueryParams,
|
||||
),
|
||||
responses(
|
||||
(status = 200, body = UserProfileResponse),
|
||||
(status = 404, description = "User not found"),
|
||||
)
|
||||
)]
|
||||
pub async fn get_user_profile(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(viewer_id): AuthenticatedUser,
|
||||
Path(user_id): Path<Uuid>,
|
||||
Query(params): Query<UserProfileQueryParams>,
|
||||
) -> impl IntoResponse {
|
||||
let view_str = params.view.as_deref().unwrap_or("recent");
|
||||
let profile_view = match application::users::queries::ProfileView::from_str(view_str) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return StatusCode::BAD_REQUEST.into_response(),
|
||||
};
|
||||
|
||||
let profile = match get_local_profile::execute(
|
||||
&state.app_ctx.deps.users.get_local_profile,
|
||||
GetUserProfileQuery {
|
||||
user_id,
|
||||
view: profile_view,
|
||||
limit: params.limit,
|
||||
offset: params.offset,
|
||||
sort_by: domain::models::FeedSortBy::Date,
|
||||
search: params.search.clone(),
|
||||
is_own_profile: viewer_id.value() == user_id,
|
||||
include_remote: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(p) => p,
|
||||
Err(DomainError::NotFound(_)) => {
|
||||
if let Ok(Some(fed)) = get_federated_profile::execute(
|
||||
&state.app_ctx.deps.users.get_federated_profile,
|
||||
user_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return build_federated_profile_response(
|
||||
&state,
|
||||
user_id,
|
||||
fed,
|
||||
profile_view,
|
||||
¶ms,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
return StatusCode::NOT_FOUND.into_response();
|
||||
}
|
||||
Err(e) => return crate::errors::domain_error_response(e),
|
||||
};
|
||||
|
||||
let view_data = if let Some(p) = profile.entries {
|
||||
Some(api_types::ProfileViewData::Entries {
|
||||
entries: DiaryResponse {
|
||||
items: p
|
||||
.items
|
||||
.iter()
|
||||
.map(api_types::mappers::entry_to_dto)
|
||||
.collect(),
|
||||
total_count: p.total_count,
|
||||
limit: p.limit,
|
||||
offset: p.offset,
|
||||
},
|
||||
})
|
||||
} else if let Some(h) = profile.history {
|
||||
Some(api_types::ProfileViewData::History {
|
||||
history: application::users::group_by_month(h)
|
||||
.into_iter()
|
||||
.map(|m| MonthActivityDto {
|
||||
year_month: m.year_month,
|
||||
month_label: m.month_label,
|
||||
count: m.count,
|
||||
entries: m
|
||||
.entries
|
||||
.iter()
|
||||
.map(api_types::mappers::entry_to_dto)
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
} else {
|
||||
profile.trends.map(|t| api_types::ProfileViewData::Trends {
|
||||
trends: trends_to_dto(t),
|
||||
})
|
||||
};
|
||||
|
||||
Json(UserProfileResponse {
|
||||
user_id,
|
||||
profile: api_types::UserProfileBase {
|
||||
username: profile.identity.username.clone(),
|
||||
avatar_url: profile.identity.avatar_url.clone(),
|
||||
banner_url: profile.identity.banner_url.clone(),
|
||||
display_name: profile.identity.display_name.clone(),
|
||||
bio: profile.identity.bio.clone(),
|
||||
},
|
||||
stats: UserStatsDto {
|
||||
total_movies: profile.stats.total_movies,
|
||||
avg_rating: profile.stats.avg_rating,
|
||||
favorite_director: profile.stats.favorite_director,
|
||||
most_active_month: profile.stats.most_active_month,
|
||||
},
|
||||
following_count: profile.following_count,
|
||||
followers_count: profile.followers_count,
|
||||
view_data,
|
||||
goals: {
|
||||
let goals_list = application::goals::list::execute(
|
||||
&state.app_ctx.deps.goals.query,
|
||||
application::goals::queries::ListGoalsQuery { user_id },
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
if goals_list.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(goals_list.iter().map(goal_with_progress_to_dto).collect())
|
||||
}
|
||||
},
|
||||
is_federated: false,
|
||||
handle: Some(profile.identity.handle.clone()),
|
||||
actor_url: Some(profile.identity.actor_url.clone()),
|
||||
})
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn build_federated_profile_response(
|
||||
state: &AppState,
|
||||
user_id: Uuid,
|
||||
fed: domain::models::FederatedProfile,
|
||||
profile_view: application::users::queries::ProfileView,
|
||||
params: &UserProfileQueryParams,
|
||||
) -> axum::response::Response {
|
||||
let profile = match get_federated_profile_stats::execute(
|
||||
&state.app_ctx.deps.users.get_federated_profile_stats,
|
||||
GetUserProfileQuery {
|
||||
user_id,
|
||||
view: profile_view,
|
||||
limit: params.limit,
|
||||
offset: params.offset,
|
||||
sort_by: domain::models::FeedSortBy::Date,
|
||||
search: params.search.clone(),
|
||||
is_own_profile: false,
|
||||
include_remote: true,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(p) => p,
|
||||
Err(e) => return crate::errors::domain_error_response(e),
|
||||
};
|
||||
|
||||
let view_data = if let Some(p) = profile.entries {
|
||||
Some(api_types::ProfileViewData::Entries {
|
||||
entries: DiaryResponse {
|
||||
items: p
|
||||
.items
|
||||
.iter()
|
||||
.map(api_types::mappers::entry_to_dto)
|
||||
.collect(),
|
||||
total_count: p.total_count,
|
||||
limit: p.limit,
|
||||
offset: p.offset,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
profile.trends.map(|t| api_types::ProfileViewData::Trends {
|
||||
trends: trends_to_dto(t),
|
||||
})
|
||||
};
|
||||
|
||||
let username = fed
|
||||
.display_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| fed.handle.clone());
|
||||
|
||||
Json(UserProfileResponse {
|
||||
user_id,
|
||||
profile: api_types::UserProfileBase {
|
||||
username,
|
||||
avatar_url: fed.avatar_url,
|
||||
banner_url: fed.banner_url,
|
||||
display_name: fed.display_name,
|
||||
bio: fed.bio,
|
||||
},
|
||||
stats: UserStatsDto {
|
||||
total_movies: profile.stats.total_movies,
|
||||
avg_rating: profile.stats.avg_rating,
|
||||
favorite_director: profile.stats.favorite_director,
|
||||
most_active_month: profile.stats.most_active_month,
|
||||
},
|
||||
following_count: 0,
|
||||
followers_count: 0,
|
||||
view_data,
|
||||
goals: None,
|
||||
is_federated: true,
|
||||
handle: Some(fed.handle),
|
||||
actor_url: Some(fed.actor_url),
|
||||
})
|
||||
.into_response()
|
||||
}
|
||||
|
||||
fn trends_to_dto(t: domain::models::UserTrends) -> UserTrendsDto {
|
||||
UserTrendsDto {
|
||||
monthly_ratings: t
|
||||
.monthly_ratings
|
||||
.into_iter()
|
||||
.map(|r| MonthlyRatingDto {
|
||||
year_month: r.year_month,
|
||||
month_label: r.month_label,
|
||||
avg_rating: r.avg_rating,
|
||||
count: r.count,
|
||||
})
|
||||
.collect(),
|
||||
top_directors: t
|
||||
.top_directors
|
||||
.into_iter()
|
||||
.map(|d| DirectorStatDto {
|
||||
director: d.director,
|
||||
count: d.count,
|
||||
})
|
||||
.collect(),
|
||||
max_director_count: t.max_director_count,
|
||||
top_genres: t
|
||||
.top_genres
|
||||
.into_iter()
|
||||
.map(|g| GenreStatDto {
|
||||
genre: g.genre,
|
||||
count: g.count,
|
||||
})
|
||||
.collect(),
|
||||
rating_distribution: t.rating_distribution,
|
||||
watch_medium_distribution: t
|
||||
.watch_medium_distribution
|
||||
.into_iter()
|
||||
.map(|m| WatchMediumStatDto {
|
||||
medium: m.medium,
|
||||
count: m.count,
|
||||
})
|
||||
.collect(),
|
||||
top_actors: t
|
||||
.top_actors
|
||||
.into_iter()
|
||||
.map(|a| ActorStatDto {
|
||||
name: a.name,
|
||||
profile_path: a.profile_path,
|
||||
movie_count: a.movie_count,
|
||||
score: a.score,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user