structural refactor and codebase improvements
This commit is contained in:
@@ -7,11 +7,7 @@ use axum::{
|
||||
use chrono::Utc;
|
||||
|
||||
use application::auth::{
|
||||
commands::RegisterCommand,
|
||||
deps::{LoginDeps, RefreshDeps, RegisterAndLoginDeps, RegisterDeps},
|
||||
login as login_uc,
|
||||
queries::LoginCommand,
|
||||
register as register_uc,
|
||||
commands::RegisterCommand, login as login_uc, queries::LoginCommand, register as register_uc,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -56,15 +52,8 @@ pub async fn login(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<LoginRequest>,
|
||||
) -> Result<Json<LoginResponse>, ApiError> {
|
||||
let deps = LoginDeps {
|
||||
user: state.app_ctx.repos.user.clone(),
|
||||
password_hasher: state.app_ctx.services.password_hasher.clone(),
|
||||
auth: state.app_ctx.services.auth.clone(),
|
||||
refresh_session: state.app_ctx.repos.refresh_session.clone(),
|
||||
config: state.app_ctx.config.clone(),
|
||||
};
|
||||
let result = login_uc::execute(
|
||||
&deps,
|
||||
&state.app_ctx.deps.auth.login,
|
||||
LoginCommand {
|
||||
email: req.email,
|
||||
password: req.password,
|
||||
@@ -93,13 +82,8 @@ pub async fn register(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<RegisterRequest>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
let deps = RegisterDeps {
|
||||
user: state.app_ctx.repos.user.clone(),
|
||||
password_hasher: state.app_ctx.services.password_hasher.clone(),
|
||||
config: state.app_ctx.config.clone(),
|
||||
};
|
||||
register_uc::execute(
|
||||
&deps,
|
||||
&state.app_ctx.deps.auth.register,
|
||||
RegisterCommand {
|
||||
email: req.email,
|
||||
username: req.username,
|
||||
@@ -123,12 +107,9 @@ pub async fn refresh(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<RefreshRequest>,
|
||||
) -> Result<Json<RefreshResponse>, ApiError> {
|
||||
let deps = RefreshDeps {
|
||||
refresh_session: state.app_ctx.repos.refresh_session.clone(),
|
||||
auth: state.app_ctx.services.auth.clone(),
|
||||
config: state.app_ctx.config.clone(),
|
||||
};
|
||||
let result = application::auth::refresh::execute(&deps, &req.refresh_token).await?;
|
||||
let result =
|
||||
application::auth::refresh::execute(&state.app_ctx.deps.auth.refresh, &req.refresh_token)
|
||||
.await?;
|
||||
Ok(Json(RefreshResponse {
|
||||
token: result.token,
|
||||
refresh_token: result.refresh_token,
|
||||
@@ -147,11 +128,8 @@ pub async fn api_logout(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<LogoutRequest>,
|
||||
) -> StatusCode {
|
||||
let _ = application::auth::logout::execute(
|
||||
state.app_ctx.repos.refresh_session.clone(),
|
||||
&req.refresh_token,
|
||||
)
|
||||
.await;
|
||||
let _ = application::auth::logout::execute(&state.app_ctx.deps.auth.logout, &req.refresh_token)
|
||||
.await;
|
||||
StatusCode::NO_CONTENT
|
||||
}
|
||||
|
||||
@@ -172,6 +150,7 @@ pub async fn get_login_page(
|
||||
canonical_url: format!("{}/login", state.app_ctx.config.base_url),
|
||||
csrf_token: csrf.0,
|
||||
page_rss_url: None,
|
||||
pending_follow_count: 0,
|
||||
};
|
||||
render_page(LoginTemplate {
|
||||
ctx: &ctx,
|
||||
@@ -187,15 +166,8 @@ pub async fn post_login(
|
||||
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
let deps = LoginDeps {
|
||||
user: state.app_ctx.repos.user.clone(),
|
||||
password_hasher: state.app_ctx.services.password_hasher.clone(),
|
||||
auth: state.app_ctx.services.auth.clone(),
|
||||
refresh_session: state.app_ctx.repos.refresh_session.clone(),
|
||||
config: state.app_ctx.config.clone(),
|
||||
};
|
||||
match login_uc::execute(
|
||||
&deps,
|
||||
&state.app_ctx.deps.auth.login,
|
||||
LoginCommand {
|
||||
email: form.email,
|
||||
password: form.password,
|
||||
@@ -242,6 +214,7 @@ pub async fn get_register_page(
|
||||
canonical_url: format!("{}/register", state.app_ctx.config.base_url),
|
||||
csrf_token: csrf.0,
|
||||
page_rss_url: None,
|
||||
pending_follow_count: 0,
|
||||
};
|
||||
render_page(RegisterTemplate {
|
||||
ctx: &ctx,
|
||||
@@ -261,15 +234,8 @@ pub async fn post_register(
|
||||
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
let deps = RegisterAndLoginDeps {
|
||||
user: state.app_ctx.repos.user.clone(),
|
||||
password_hasher: state.app_ctx.services.password_hasher.clone(),
|
||||
auth: state.app_ctx.services.auth.clone(),
|
||||
refresh_session: state.app_ctx.repos.refresh_session.clone(),
|
||||
config: state.app_ctx.config.clone(),
|
||||
};
|
||||
match application::auth::register_and_login::execute(
|
||||
&deps,
|
||||
&state.app_ctx.deps.auth.register_and_login,
|
||||
application::auth::commands::RegisterAndLoginCommand {
|
||||
email: form.email,
|
||||
username: form.username,
|
||||
|
||||
@@ -8,9 +8,7 @@ use uuid::Uuid;
|
||||
|
||||
use application::diary::{
|
||||
commands::{DeleteReviewCommand, EditReviewCommand},
|
||||
delete_review,
|
||||
deps::{DeleteReviewDeps, EditReviewDeps, GetActivityFeedDeps},
|
||||
edit_review, get_activity_feed as get_feed_uc, get_diary, log_review,
|
||||
delete_review, edit_review, get_activity_feed as get_feed_uc, get_diary, log_review,
|
||||
queries::GetActivityFeedQuery,
|
||||
};
|
||||
|
||||
@@ -45,7 +43,8 @@ pub async fn get_diary(
|
||||
State(state): State<AppState>,
|
||||
Query(params): Query<DiaryQueryParams>,
|
||||
) -> Result<Json<DiaryResponse>, ApiError> {
|
||||
let page = get_diary::execute(&state.app_ctx.repos.diary, to_diary_query(params)).await?;
|
||||
let page =
|
||||
get_diary::execute(&state.app_ctx.deps.diary.get_diary, to_diary_query(params)).await?;
|
||||
|
||||
Ok(Json(DiaryResponse {
|
||||
items: page
|
||||
@@ -103,13 +102,7 @@ pub async fn delete_review(
|
||||
review_id,
|
||||
requesting_user_id: user_id.value(),
|
||||
};
|
||||
let deps = DeleteReviewDeps {
|
||||
review: state.app_ctx.repos.review.clone(),
|
||||
diary: state.app_ctx.repos.diary.clone(),
|
||||
movie_command: state.app_ctx.repos.movie_command.clone(),
|
||||
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
||||
};
|
||||
delete_review::execute(&deps, cmd).await?;
|
||||
delete_review::execute(&state.app_ctx.deps.diary.delete_review, cmd).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
@@ -145,11 +138,7 @@ pub async fn patch_review(
|
||||
watched_at,
|
||||
watch_medium: req.watch_medium,
|
||||
};
|
||||
let deps = EditReviewDeps {
|
||||
review: state.app_ctx.repos.review.clone(),
|
||||
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
||||
};
|
||||
edit_review::execute(&deps, cmd).await?;
|
||||
edit_review::execute(&state.app_ctx.deps.diary.edit_review, cmd).await?;
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
@@ -180,13 +169,8 @@ pub async fn get_activity_feed(
|
||||
State(state): State<AppState>,
|
||||
Query(params): Query<ActivityFeedQueryParams>,
|
||||
) -> Result<Json<ActivityFeedResponse>, ApiError> {
|
||||
let deps = GetActivityFeedDeps {
|
||||
diary: state.app_ctx.repos.diary.clone(),
|
||||
social_query: state.app_ctx.repos.social_query_unified.clone(),
|
||||
config: state.app_ctx.config.clone(),
|
||||
};
|
||||
let page = get_feed_uc::execute(
|
||||
&deps,
|
||||
&state.app_ctx.deps.diary.get_activity_feed,
|
||||
GetActivityFeedQuery {
|
||||
limit: params.limit.unwrap_or(20),
|
||||
offset: params.offset.unwrap_or(0),
|
||||
@@ -274,13 +258,7 @@ pub async fn post_delete_review_html(
|
||||
review_id,
|
||||
requesting_user_id: user_id.value(),
|
||||
};
|
||||
let deps = DeleteReviewDeps {
|
||||
review: state.app_ctx.repos.review.clone(),
|
||||
diary: state.app_ctx.repos.diary.clone(),
|
||||
movie_command: state.app_ctx.repos.movie_command.clone(),
|
||||
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
||||
};
|
||||
match delete_review::execute(&deps, cmd).await {
|
||||
match delete_review::execute(&state.app_ctx.deps.diary.delete_review, cmd).await {
|
||||
Ok(()) => {
|
||||
let redirect_url = form
|
||||
.redirect_after
|
||||
@@ -335,13 +313,12 @@ pub async fn get_activity_feed_html(
|
||||
filter_following,
|
||||
};
|
||||
|
||||
let deps = GetActivityFeedDeps {
|
||||
diary: state.app_ctx.repos.diary.clone(),
|
||||
social_query: state.app_ctx.repos.social_query_unified.clone(),
|
||||
config: state.app_ctx.config.clone(),
|
||||
};
|
||||
|
||||
match application::diary::get_activity_feed::execute(&deps, query).await {
|
||||
match application::diary::get_activity_feed::execute(
|
||||
&state.app_ctx.deps.diary.get_activity_feed,
|
||||
query,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(entries) => {
|
||||
let entry_limit = entries.limit;
|
||||
let entry_offset = entries.offset;
|
||||
|
||||
@@ -10,8 +10,6 @@ use api_types::{
|
||||
CreateGoalRequest, GoalDto, GoalsResponse, UpdateGoalRequest, UpdateUserSettingsRequest,
|
||||
UserSettingsDto,
|
||||
};
|
||||
use application::goals::deps::{GoalCommandDeps, GoalQueryDeps};
|
||||
|
||||
// ── Shared mapper ────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn goal_with_progress_to_dto(g: &domain::models::GoalWithProgress) -> GoalDto {
|
||||
@@ -39,12 +37,8 @@ pub async fn list_goals(
|
||||
State(state): State<AppState>,
|
||||
user: AuthenticatedUser,
|
||||
) -> Result<Json<GoalsResponse>, ApiError> {
|
||||
let deps = GoalQueryDeps {
|
||||
goal_query: state.app_ctx.repos.goal_query.clone(),
|
||||
stats: state.app_ctx.repos.stats.clone(),
|
||||
};
|
||||
let goals = application::goals::list::execute(
|
||||
&deps,
|
||||
&state.app_ctx.deps.goals.query,
|
||||
application::goals::queries::ListGoalsQuery {
|
||||
user_id: user.0.value(),
|
||||
},
|
||||
@@ -69,14 +63,8 @@ pub async fn create_goal(
|
||||
user: AuthenticatedUser,
|
||||
Json(req): Json<CreateGoalRequest>,
|
||||
) -> Result<Json<GoalDto>, ApiError> {
|
||||
let deps = GoalCommandDeps {
|
||||
goal_command: state.app_ctx.repos.goal_command.clone(),
|
||||
goal_query: state.app_ctx.repos.goal_query.clone(),
|
||||
stats: state.app_ctx.repos.stats.clone(),
|
||||
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
||||
};
|
||||
let g = application::goals::create::execute(
|
||||
&deps,
|
||||
&state.app_ctx.deps.goals.command,
|
||||
application::goals::commands::CreateGoalCommand {
|
||||
user_id: user.0.value(),
|
||||
year: req.year,
|
||||
@@ -103,14 +91,8 @@ pub async fn update_goal(
|
||||
Path(year): Path<u16>,
|
||||
Json(req): Json<UpdateGoalRequest>,
|
||||
) -> Result<Json<GoalDto>, ApiError> {
|
||||
let deps = GoalCommandDeps {
|
||||
goal_command: state.app_ctx.repos.goal_command.clone(),
|
||||
goal_query: state.app_ctx.repos.goal_query.clone(),
|
||||
stats: state.app_ctx.repos.stats.clone(),
|
||||
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
||||
};
|
||||
let g = application::goals::update::execute(
|
||||
&deps,
|
||||
&state.app_ctx.deps.goals.command,
|
||||
application::goals::commands::UpdateGoalCommand {
|
||||
user_id: user.0.value(),
|
||||
year,
|
||||
@@ -135,14 +117,8 @@ pub async fn delete_goal(
|
||||
user: AuthenticatedUser,
|
||||
Path(year): Path<u16>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
let deps = GoalCommandDeps {
|
||||
goal_command: state.app_ctx.repos.goal_command.clone(),
|
||||
goal_query: state.app_ctx.repos.goal_query.clone(),
|
||||
stats: state.app_ctx.repos.stats.clone(),
|
||||
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
||||
};
|
||||
application::goals::delete::execute(
|
||||
&deps,
|
||||
&state.app_ctx.deps.goals.command,
|
||||
application::goals::commands::DeleteGoalCommand {
|
||||
user_id: user.0.value(),
|
||||
year,
|
||||
@@ -165,12 +141,8 @@ pub async fn get_user_goals(
|
||||
AuthenticatedUser(_viewer): AuthenticatedUser,
|
||||
Path(user_id): Path<Uuid>,
|
||||
) -> Result<Json<GoalsResponse>, ApiError> {
|
||||
let deps = GoalQueryDeps {
|
||||
goal_query: state.app_ctx.repos.goal_query.clone(),
|
||||
stats: state.app_ctx.repos.stats.clone(),
|
||||
};
|
||||
let goals = application::goals::list::execute(
|
||||
&deps,
|
||||
&state.app_ctx.deps.goals.query,
|
||||
application::goals::queries::ListGoalsQuery { user_id },
|
||||
)
|
||||
.await?;
|
||||
@@ -194,7 +166,7 @@ pub async fn get_settings(
|
||||
user: AuthenticatedUser,
|
||||
) -> Result<Json<UserSettingsDto>, ApiError> {
|
||||
let settings = application::users::get_settings::execute(
|
||||
state.app_ctx.repos.user_settings.clone(),
|
||||
&state.app_ctx.deps.users.get_settings,
|
||||
user.0.value(),
|
||||
)
|
||||
.await?;
|
||||
@@ -220,7 +192,7 @@ pub async fn update_settings(
|
||||
Json(req): Json<UpdateUserSettingsRequest>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
application::users::update_settings::execute(
|
||||
state.app_ctx.repos.user_settings.clone(),
|
||||
&state.app_ctx.deps.users.update_settings,
|
||||
application::users::update_settings::UpdateUserSettingsCommand {
|
||||
user_id: user.0.value(),
|
||||
federate_goals: req.federate_goals,
|
||||
|
||||
@@ -29,11 +29,7 @@ pub(crate) fn build_export_response(
|
||||
ExportFormat::Json => ("application/json", "diary.json"),
|
||||
};
|
||||
let query = ExportQuery { user_id, format };
|
||||
let stream = export_diary_uc::execute(
|
||||
&state.app_ctx.repos.diary,
|
||||
&state.app_ctx.services.diary_exporter,
|
||||
query,
|
||||
);
|
||||
let stream = export_diary_uc::execute(&state.app_ctx.deps.diary.export_diary, query);
|
||||
let stream = stream.map(|r| {
|
||||
if let Err(ref e) = r {
|
||||
tracing::error!("diary export stream error: {e}");
|
||||
@@ -146,16 +142,25 @@ pub(crate) async fn build_page_context(
|
||||
csrf_token: String,
|
||||
) -> HtmlPageContext {
|
||||
let uuid = user_id.as_ref().map(|u| u.value());
|
||||
let (user_email, is_admin) = if let Some(ref id) = user_id {
|
||||
let user = state.app_ctx.repos.user.find_by_id(id).await.ok().flatten();
|
||||
let email = user.as_ref().map(|u| u.email().value().to_string());
|
||||
let admin = user
|
||||
.as_ref()
|
||||
.map(|u| matches!(u.role(), domain::models::UserRole::Admin))
|
||||
.unwrap_or(false);
|
||||
(email, admin)
|
||||
// Page chrome (email/admin badge, pending-follow badge) degrades rather than
|
||||
// blanking the page: a failed lookup or count logs a warning and falls back
|
||||
// to a zeroed `PageViewer`, same as the previous silent `.unwrap_or(0)` — now
|
||||
// logged and explicit, per the error policy in `get_page_viewer`'s doc comment.
|
||||
let (user_email, is_admin, pending_follow_count) = if let Some(ref id) = user_id {
|
||||
match application::users::get_page_viewer::execute(
|
||||
&state.app_ctx.deps.users.get_page_viewer,
|
||||
id.value(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(viewer) => (viewer.email, viewer.is_admin, viewer.pending_follow_count),
|
||||
Err(e) => {
|
||||
tracing::warn!("get_page_viewer failed for {}: {e}", id.value());
|
||||
(None, false, 0)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(None, false)
|
||||
(None, false, 0)
|
||||
};
|
||||
HtmlPageContext {
|
||||
user_email,
|
||||
@@ -167,5 +172,6 @@ pub(crate) async fn build_page_context(
|
||||
canonical_url: state.app_ctx.config.base_url.clone(),
|
||||
csrf_token,
|
||||
page_rss_url: None,
|
||||
pending_follow_count,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,17 +13,14 @@ use std::collections::HashMap;
|
||||
|
||||
use crate::render::render_page;
|
||||
use application::import::{
|
||||
apply_mapping as apply_import_mapping, apply_profile as apply_import_profile,
|
||||
apply_mapping as apply_import_mapping, apply_profile_and_map,
|
||||
commands::{
|
||||
ApplyImportMappingCommand, ApplyImportProfileCommand, CreateImportSessionCommand,
|
||||
ApplyImportMappingCommand, ApplyProfileAndMapCommand, CreateImportSessionCommand,
|
||||
DeleteImportProfileCommand, ExecuteImportCommand, SaveImportProfileCommand,
|
||||
},
|
||||
create_session as create_import_session, delete_profile as delete_import_profile,
|
||||
deps::{
|
||||
ApplyMappingDeps, ApplyProfileDeps, CreateSessionDeps, ExecuteImportDeps, SaveProfileDeps,
|
||||
},
|
||||
execute as execute_import, list_profiles as list_import_profiles,
|
||||
save_profile as save_import_profile,
|
||||
execute as execute_import, get_mapping_stage, get_preview_stage, get_session_state,
|
||||
list_profiles as list_import_profiles, save_profile as save_import_profile,
|
||||
};
|
||||
use domain::errors::DomainError;
|
||||
use domain::models::{
|
||||
@@ -111,7 +108,7 @@ pub async fn get_import_page(
|
||||
) -> impl IntoResponse {
|
||||
let ctx = super::helpers::build_page_context(&state, Some(user_id.clone()), csrf.0).await;
|
||||
let profiles =
|
||||
list_import_profiles::execute(state.app_ctx.repos.import_profile.clone(), &user_id)
|
||||
list_import_profiles::execute(&state.app_ctx.deps.import.list_profiles, &user_id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
@@ -163,10 +160,7 @@ pub async fn post_upload(
|
||||
};
|
||||
|
||||
match create_import_session::execute(
|
||||
&CreateSessionDeps {
|
||||
import_session: state.app_ctx.repos.import_session.clone(),
|
||||
document_parser: state.app_ctx.services.document_parser.clone(),
|
||||
},
|
||||
&state.app_ctx.deps.import.create_session,
|
||||
CreateImportSessionCommand {
|
||||
user_id: user_id.value(),
|
||||
bytes,
|
||||
@@ -194,21 +188,17 @@ pub async fn get_mapping_page(
|
||||
else {
|
||||
return Redirect::to("/import").into_response();
|
||||
};
|
||||
let Ok(Some(session)) = state
|
||||
.app_ctx
|
||||
.repos
|
||||
.import_session
|
||||
.get(&session_id, &user_id)
|
||||
.await
|
||||
let Ok(stage) = get_mapping_stage::execute(
|
||||
&state.app_ctx.deps.import.get_mapping_stage,
|
||||
session_id,
|
||||
user_id.value(),
|
||||
)
|
||||
.await
|
||||
else {
|
||||
return Redirect::to("/import").into_response();
|
||||
};
|
||||
let Some(parsed) = session.parsed_file else {
|
||||
return Redirect::to("/import").into_response();
|
||||
};
|
||||
|
||||
let ctx = super::helpers::build_page_context(&state, Some(user_id), csrf.0).await;
|
||||
let sample_rows: Vec<Vec<String>> = parsed.rows.into_iter().take(5).collect();
|
||||
let domain_fields: Vec<(&str, &str)> = vec![
|
||||
("title", "Title"),
|
||||
("release_year", "Release Year"),
|
||||
@@ -221,8 +211,8 @@ pub async fn get_mapping_page(
|
||||
render_page(ImportMappingTemplate {
|
||||
ctx: &ctx,
|
||||
session_id: &session_id_str,
|
||||
columns: &parsed.columns,
|
||||
sample_rows: &sample_rows,
|
||||
columns: &stage.columns,
|
||||
sample_rows: &stage.sample_rows,
|
||||
domain_fields: &domain_fields,
|
||||
error: None,
|
||||
})
|
||||
@@ -255,11 +245,7 @@ pub async fn post_mapping(
|
||||
.into_response();
|
||||
}
|
||||
match apply_import_mapping::execute(
|
||||
&ApplyMappingDeps {
|
||||
import_session: state.app_ctx.repos.import_session.clone(),
|
||||
document_parser: state.app_ctx.services.document_parser.clone(),
|
||||
movie_query: state.app_ctx.repos.movie_query.clone(),
|
||||
},
|
||||
&state.app_ctx.deps.import.apply_mapping,
|
||||
ApplyImportMappingCommand {
|
||||
user_id: user_id.value(),
|
||||
session_id: session_id.value(),
|
||||
@@ -290,24 +276,25 @@ pub async fn get_preview_page(
|
||||
else {
|
||||
return Redirect::to("/import").into_response();
|
||||
};
|
||||
let Ok(Some(session)) = state
|
||||
.app_ctx
|
||||
.repos
|
||||
.import_session
|
||||
.get(&session_id, &user_id)
|
||||
.await
|
||||
let Ok(stage) = get_preview_stage::execute(
|
||||
&state.app_ctx.deps.import.get_preview_stage,
|
||||
session_id,
|
||||
user_id.value(),
|
||||
)
|
||||
.await
|
||||
else {
|
||||
return Redirect::to("/import").into_response();
|
||||
};
|
||||
|
||||
if session.row_results.is_none() {
|
||||
return Redirect::to(&format!("/import/{}/mapping", session_id_str)).into_response();
|
||||
}
|
||||
let preview = match stage {
|
||||
get_preview_stage::PreviewStage::Ready(preview) => preview,
|
||||
get_preview_stage::PreviewStage::NotYetMapped => {
|
||||
return Redirect::to(&format!("/import/{}/mapping", session_id_str)).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let parsed = session.parsed_file.unwrap_or_default();
|
||||
let annotated: Vec<AnnotatedRow> = session.row_results.unwrap_or_default();
|
||||
|
||||
let rows: Vec<ImportPreviewRow> = annotated
|
||||
let rows: Vec<ImportPreviewRow> = preview
|
||||
.rows
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, a)| annotated_to_preview_row(i, a))
|
||||
@@ -317,7 +304,7 @@ pub async fn get_preview_page(
|
||||
render_page(ImportPreviewTemplate {
|
||||
ctx: &ctx,
|
||||
session_id: &session_id_str,
|
||||
columns: &parsed.columns,
|
||||
columns: &preview.columns,
|
||||
rows: &rows,
|
||||
})
|
||||
.into_response()
|
||||
@@ -353,10 +340,7 @@ pub async fn post_confirm(
|
||||
.filter(|n| !n.trim().is_empty());
|
||||
if let Some(name) = profile_name {
|
||||
let _ = save_import_profile::execute(
|
||||
&SaveProfileDeps {
|
||||
import_session: state.app_ctx.repos.import_session.clone(),
|
||||
import_profile: state.app_ctx.repos.import_profile.clone(),
|
||||
},
|
||||
&state.app_ctx.deps.import.save_profile,
|
||||
SaveImportProfileCommand {
|
||||
user_id: user_id.value(),
|
||||
session_id: session_id.value(),
|
||||
@@ -374,10 +358,7 @@ pub async fn post_confirm(
|
||||
.collect();
|
||||
|
||||
match execute_import::execute(
|
||||
&ExecuteImportDeps {
|
||||
import_session: state.app_ctx.repos.import_session.clone(),
|
||||
review_logger: state.app_ctx.services.review_logger.clone(),
|
||||
},
|
||||
&state.app_ctx.deps.import.execute_import,
|
||||
ExecuteImportCommand {
|
||||
user_id: user_id.value(),
|
||||
session_id: session_id.value(),
|
||||
@@ -412,7 +393,7 @@ pub async fn post_delete_profile(
|
||||
}
|
||||
if let Ok(profile_id) = profile_id_str.parse::<uuid::Uuid>() {
|
||||
let _ = delete_import_profile::execute(
|
||||
state.app_ctx.repos.import_profile.clone(),
|
||||
&state.app_ctx.deps.import.delete_profile,
|
||||
DeleteImportProfileCommand {
|
||||
user_id: user_id.value(),
|
||||
profile_id,
|
||||
@@ -498,10 +479,7 @@ pub async fn api_post_session(
|
||||
_ => FileFormat::Csv,
|
||||
};
|
||||
let r = create_import_session::execute(
|
||||
&CreateSessionDeps {
|
||||
import_session: state.app_ctx.repos.import_session.clone(),
|
||||
document_parser: state.app_ctx.services.document_parser.clone(),
|
||||
},
|
||||
&state.app_ctx.deps.import.create_session,
|
||||
CreateImportSessionCommand {
|
||||
user_id: user_id.value(),
|
||||
bytes,
|
||||
@@ -535,20 +513,17 @@ pub async fn api_get_session(
|
||||
.parse::<uuid::Uuid>()
|
||||
.map(ImportSessionId::from_uuid)
|
||||
.map_err(|_| DomainError::ValidationError("invalid session id".into()))?;
|
||||
let session = state
|
||||
.app_ctx
|
||||
.repos
|
||||
.import_session
|
||||
.get(&session_id, &user_id)
|
||||
.await?
|
||||
.ok_or(DomainError::NotFound("session not found".into()))?;
|
||||
let parsed = session.parsed_file.unwrap_or_default();
|
||||
let row_count = parsed.rows.len();
|
||||
let state_data = get_session_state::execute(
|
||||
&state.app_ctx.deps.import.get_session_state,
|
||||
session_id,
|
||||
user_id.value(),
|
||||
)
|
||||
.await?;
|
||||
Ok(axum::Json(SessionStateResponse {
|
||||
session_id: session_id_str,
|
||||
columns: parsed.columns,
|
||||
has_mappings: session.field_mappings.is_some(),
|
||||
row_count,
|
||||
columns: state_data.columns,
|
||||
has_mappings: state_data.has_mappings,
|
||||
row_count: state_data.row_count,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -596,11 +571,7 @@ pub async fn api_put_mapping(
|
||||
.collect();
|
||||
|
||||
let rows = apply_import_mapping::execute(
|
||||
&ApplyMappingDeps {
|
||||
import_session: state.app_ctx.repos.import_session.clone(),
|
||||
document_parser: state.app_ctx.services.document_parser.clone(),
|
||||
movie_query: state.app_ctx.repos.movie_query.clone(),
|
||||
},
|
||||
&state.app_ctx.deps.import.apply_mapping,
|
||||
ApplyImportMappingCommand {
|
||||
user_id: user_id.value(),
|
||||
session_id: session_id.value(),
|
||||
@@ -621,15 +592,21 @@ pub async fn api_get_preview(
|
||||
.map(ImportSessionId::from_uuid)
|
||||
.map_err(|_| DomainError::ValidationError("invalid session id".into()))?;
|
||||
|
||||
let session = state
|
||||
.app_ctx
|
||||
.repos
|
||||
.import_session
|
||||
.get(&session_id, &user_id)
|
||||
.await?
|
||||
.ok_or(DomainError::NotFound("session not found".into()))?;
|
||||
let stage = get_preview_stage::execute(
|
||||
&state.app_ctx.deps.import.get_preview_stage,
|
||||
session_id,
|
||||
user_id.value(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let annotated: Vec<AnnotatedRow> = session.row_results.unwrap_or_default();
|
||||
// Not-yet-mapped sessions render as an empty preview here (200, `rows: []`),
|
||||
// matching this endpoint's behavior before the stage-gate moved into
|
||||
// `get_preview_stage` — unlike the HTML handler, this API route never
|
||||
// redirected on the same condition, so its response shape is unchanged.
|
||||
let annotated: Vec<AnnotatedRow> = match stage {
|
||||
get_preview_stage::PreviewStage::Ready(preview) => preview.rows,
|
||||
get_preview_stage::PreviewStage::NotYetMapped => Vec::new(),
|
||||
};
|
||||
let rows = annotated
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -689,10 +666,7 @@ pub async fn api_post_confirm(
|
||||
.map(ImportSessionId::from_uuid)
|
||||
.map_err(|_| DomainError::ValidationError("invalid session id".into()))?;
|
||||
let s = execute_import::execute(
|
||||
&ExecuteImportDeps {
|
||||
import_session: state.app_ctx.repos.import_session.clone(),
|
||||
review_logger: state.app_ctx.services.review_logger.clone(),
|
||||
},
|
||||
&state.app_ctx.deps.import.execute_import,
|
||||
ExecuteImportCommand {
|
||||
user_id: user_id.value(),
|
||||
session_id: session_id.value(),
|
||||
@@ -720,7 +694,7 @@ pub async fn api_get_profiles(
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let profiles =
|
||||
list_import_profiles::execute(state.app_ctx.repos.import_profile.clone(), &user_id).await?;
|
||||
list_import_profiles::execute(&state.app_ctx.deps.import.list_profiles, &user_id).await?;
|
||||
Ok(axum::Json(
|
||||
profiles
|
||||
.into_iter()
|
||||
@@ -756,10 +730,7 @@ pub async fn api_post_profile(
|
||||
.map(ImportSessionId::from_uuid)
|
||||
.map_err(|_| DomainError::ValidationError("invalid session id".into()))?;
|
||||
let id = save_import_profile::execute(
|
||||
&SaveProfileDeps {
|
||||
import_session: state.app_ctx.repos.import_session.clone(),
|
||||
import_profile: state.app_ctx.repos.import_profile.clone(),
|
||||
},
|
||||
&state.app_ctx.deps.import.save_profile,
|
||||
SaveImportProfileCommand {
|
||||
user_id: user_id.value(),
|
||||
session_id: session_id.value(),
|
||||
@@ -791,7 +762,7 @@ pub async fn api_delete_profile(
|
||||
.parse::<uuid::Uuid>()
|
||||
.map_err(|_| DomainError::ValidationError("invalid profile id".into()))?;
|
||||
delete_import_profile::execute(
|
||||
state.app_ctx.repos.import_profile.clone(),
|
||||
&state.app_ctx.deps.import.delete_profile,
|
||||
DeleteImportProfileCommand {
|
||||
user_id: user_id.value(),
|
||||
profile_id,
|
||||
@@ -828,42 +799,14 @@ pub async fn api_apply_profile(
|
||||
.parse::<uuid::Uuid>()
|
||||
.map_err(|_| DomainError::ValidationError("invalid profile id".into()))?;
|
||||
|
||||
apply_import_profile::execute(
|
||||
&ApplyProfileDeps {
|
||||
import_profile: state.app_ctx.repos.import_profile.clone(),
|
||||
import_session: state.app_ctx.repos.import_session.clone(),
|
||||
},
|
||||
ApplyImportProfileCommand {
|
||||
let rows = apply_profile_and_map::execute(
|
||||
&state.app_ctx.deps.import.apply_profile_and_map,
|
||||
ApplyProfileAndMapCommand {
|
||||
user_id: user_id.value(),
|
||||
session_id,
|
||||
profile_id,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let session = state
|
||||
.app_ctx
|
||||
.repos
|
||||
.import_session
|
||||
.get(&ImportSessionId::from_uuid(session_id), &user_id)
|
||||
.await?
|
||||
.ok_or(DomainError::NotFound(
|
||||
"session not found after profile apply".into(),
|
||||
))?;
|
||||
|
||||
let mappings = session.field_mappings.unwrap_or_default();
|
||||
let rows = apply_import_mapping::execute(
|
||||
&ApplyMappingDeps {
|
||||
import_session: state.app_ctx.repos.import_session.clone(),
|
||||
document_parser: state.app_ctx.services.document_parser.clone(),
|
||||
movie_query: state.app_ctx.repos.movie_query.clone(),
|
||||
},
|
||||
ApplyImportMappingCommand {
|
||||
user_id: user_id.value(),
|
||||
session_id,
|
||||
mappings,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(axum::Json(serde_json::json!({"row_count": rows.len()})))
|
||||
}
|
||||
|
||||
@@ -41,9 +41,10 @@ pub async fn get_integrations_page(
|
||||
let query = GetWebhookTokensQuery {
|
||||
user_id: user_id.value(),
|
||||
};
|
||||
let tokens = get_webhook_tokens::execute(state.app_ctx.repos.webhook_token.clone(), query)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let tokens =
|
||||
get_webhook_tokens::execute(&state.app_ctx.deps.integrations.get_webhook_tokens, query)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let token_views: Vec<template_askama::WebhookTokenView> = tokens
|
||||
.iter()
|
||||
@@ -81,7 +82,12 @@ pub async fn post_generate_token(
|
||||
label: form.label.filter(|l| !l.trim().is_empty()),
|
||||
};
|
||||
|
||||
match generate_webhook_token::execute(state.app_ctx.repos.webhook_token.clone(), cmd).await {
|
||||
match generate_webhook_token::execute(
|
||||
&state.app_ctx.deps.integrations.generate_webhook_token,
|
||||
cmd,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
let encoded = percent_encoding::utf8_percent_encode(
|
||||
&result.token_plaintext,
|
||||
@@ -112,7 +118,8 @@ pub async fn post_revoke_token(
|
||||
token_id,
|
||||
};
|
||||
if let Err(e) =
|
||||
revoke_webhook_token::execute(state.app_ctx.repos.webhook_token.clone(), cmd).await
|
||||
revoke_webhook_token::execute(&state.app_ctx.deps.integrations.revoke_webhook_token, cmd)
|
||||
.await
|
||||
{
|
||||
tracing::error!("revoke token failed: {:?}", e);
|
||||
}
|
||||
@@ -135,7 +142,7 @@ pub async fn get_watch_queue_page(
|
||||
let query = GetWatchQueueQuery {
|
||||
user_id: user_id.value(),
|
||||
};
|
||||
let events = get_watch_queue::execute(state.app_ctx.repos.watch_event_query.clone(), query)
|
||||
let events = get_watch_queue::execute(&state.app_ctx.deps.integrations.get_watch_queue, query)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -172,13 +179,8 @@ pub async fn post_confirm_single(
|
||||
}],
|
||||
};
|
||||
|
||||
match confirm_watch_events::execute(
|
||||
state.app_ctx.repos.watch_event_command.clone(),
|
||||
state.app_ctx.repos.watch_event_query.clone(),
|
||||
state.app_ctx.services.review_logger.clone(),
|
||||
cmd,
|
||||
)
|
||||
.await
|
||||
match confirm_watch_events::execute(&state.app_ctx.deps.integrations.confirm_watch_events, cmd)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Redirect::to("/watch-queue").into_response(),
|
||||
Err(e) => {
|
||||
@@ -204,12 +206,8 @@ pub async fn post_dismiss_single(
|
||||
event_ids: vec![event_id],
|
||||
};
|
||||
|
||||
match dismiss_watch_events::execute(
|
||||
state.app_ctx.repos.watch_event_command.clone(),
|
||||
state.app_ctx.repos.watch_event_query.clone(),
|
||||
cmd,
|
||||
)
|
||||
.await
|
||||
match dismiss_watch_events::execute(&state.app_ctx.deps.integrations.dismiss_watch_events, cmd)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Redirect::to("/watch-queue").into_response(),
|
||||
Err(e) => {
|
||||
|
||||
@@ -8,7 +8,6 @@ pub mod integrations;
|
||||
pub mod movies;
|
||||
pub mod rss;
|
||||
pub mod search;
|
||||
#[cfg(feature = "federation")]
|
||||
pub mod social;
|
||||
pub mod users;
|
||||
pub mod watchlist;
|
||||
|
||||
@@ -9,11 +9,10 @@ use uuid::Uuid;
|
||||
use application::{
|
||||
diary::{
|
||||
commands::SyncPosterCommand,
|
||||
deps::GetMovieSocialPageDeps,
|
||||
get_movie_social_page, get_review_history,
|
||||
queries::{GetMovieSocialPageQuery, GetReviewHistoryQuery},
|
||||
},
|
||||
movies::{deps::SyncPosterDeps, get_movies, queries::GetMoviesQuery, sync_poster},
|
||||
movies::{get_movies, queries::GetMoviesQuery, sync_poster},
|
||||
watchlist::{is_on as is_on_watchlist, queries::IsOnWatchlistQuery},
|
||||
};
|
||||
use domain::services::review_history::Trend;
|
||||
@@ -48,7 +47,7 @@ pub async fn list_movies(
|
||||
Query(params): Query<MoviesQueryParams>,
|
||||
) -> Result<Json<MoviesResponse>, ApiError> {
|
||||
let page = get_movies::execute(
|
||||
state.app_ctx.repos.movie_query.clone(),
|
||||
&state.app_ctx.deps.movies.get_movies,
|
||||
GetMoviesQuery {
|
||||
limit: params.limit,
|
||||
offset: params.offset,
|
||||
@@ -84,7 +83,7 @@ pub async fn get_review_history(
|
||||
Path(movie_id): Path<Uuid>,
|
||||
) -> Result<Json<ReviewHistoryResponse>, ApiError> {
|
||||
let (history, trend) = get_review_history::execute(
|
||||
&state.app_ctx.repos.diary,
|
||||
&state.app_ctx.deps.diary.get_review_history,
|
||||
GetReviewHistoryQuery { movie_id },
|
||||
)
|
||||
.await?;
|
||||
@@ -121,16 +120,7 @@ pub async fn sync_poster(
|
||||
Path(movie_id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
sync_poster::execute(
|
||||
&SyncPosterDeps {
|
||||
movie_command: state.app_ctx.repos.movie_command.clone(),
|
||||
movie_query: state.app_ctx.repos.movie_query.clone(),
|
||||
movie_profile: state.app_ctx.repos.movie_profile.clone(),
|
||||
metadata: state.app_ctx.services.metadata.clone(),
|
||||
poster_fetcher: state.app_ctx.services.poster_fetcher.clone(),
|
||||
object_storage: state.app_ctx.services.object_storage.clone(),
|
||||
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
||||
search_command: state.app_ctx.repos.search_command.clone(),
|
||||
},
|
||||
&state.app_ctx.deps.movies.sync_poster,
|
||||
SyncPosterCommand { movie_id },
|
||||
)
|
||||
.await?;
|
||||
@@ -154,11 +144,7 @@ pub async fn get_movie_detail(
|
||||
let offset = params.offset.unwrap_or(0);
|
||||
|
||||
let result = get_movie_social_page::execute(
|
||||
&GetMovieSocialPageDeps {
|
||||
movie_query: state.app_ctx.repos.movie_query.clone(),
|
||||
diary: state.app_ctx.repos.diary.clone(),
|
||||
movie_profile: state.app_ctx.repos.movie_profile.clone(),
|
||||
},
|
||||
&state.app_ctx.deps.diary.get_movie_social_page,
|
||||
GetMovieSocialPageQuery {
|
||||
movie_id,
|
||||
limit,
|
||||
@@ -210,7 +196,7 @@ pub async fn get_movie_profile(
|
||||
) -> impl IntoResponse {
|
||||
use application::movies::get_movie_profile;
|
||||
let query = get_movie_profile::GetMovieProfileQuery { movie_id };
|
||||
match get_movie_profile::execute(state.app_ctx.repos.movie_profile.clone(), query).await {
|
||||
match get_movie_profile::execute(&state.app_ctx.deps.movies.get_movie_profile, query).await {
|
||||
Ok(Some(result)) => {
|
||||
let p = result.profile;
|
||||
Json(MovieProfileResponse {
|
||||
@@ -288,11 +274,7 @@ pub async fn get_movie_detail_html(
|
||||
let offset = params.offset.unwrap_or(0);
|
||||
|
||||
match get_movie_social_page::execute(
|
||||
&GetMovieSocialPageDeps {
|
||||
movie_query: state.app_ctx.repos.movie_query.clone(),
|
||||
diary: state.app_ctx.repos.diary.clone(),
|
||||
movie_profile: state.app_ctx.repos.movie_profile.clone(),
|
||||
},
|
||||
&state.app_ctx.deps.diary.get_movie_social_page,
|
||||
GetMovieSocialPageQuery {
|
||||
movie_id,
|
||||
limit,
|
||||
@@ -314,7 +296,7 @@ pub async fn get_movie_detail_html(
|
||||
result.reviews.offset + result.reviews.limit < result.reviews.total_count as u32;
|
||||
let on_watchlist = match &user_id {
|
||||
Some(uid) => is_on_watchlist::execute(
|
||||
state.app_ctx.repos.watchlist.clone(),
|
||||
&state.app_ctx.deps.watchlist.is_on_watchlist,
|
||||
IsOnWatchlistQuery {
|
||||
user_id: uid.value(),
|
||||
movie_id,
|
||||
|
||||
@@ -5,8 +5,8 @@ use axum::{
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use application::{diary::get_diary, diary::queries::GetDiaryQuery};
|
||||
use domain::{errors::DomainError, models::ReviewSortBy, value_objects::UserId};
|
||||
use application::{diary::get_diary, diary::get_user_feed, diary::queries::GetDiaryQuery};
|
||||
use domain::{errors::DomainError, models::ReviewSortBy};
|
||||
|
||||
use crate::{errors::ApiError, state::AppState};
|
||||
|
||||
@@ -18,7 +18,7 @@ pub async fn get_feed(State(state): State<AppState>) -> Result<impl IntoResponse
|
||||
movie_id: None,
|
||||
user_id: None,
|
||||
};
|
||||
let page = get_diary::execute(&state.app_ctx.repos.diary, query).await?;
|
||||
let page = get_diary::execute(&state.app_ctx.deps.diary.get_diary, query).await?;
|
||||
let xml = state
|
||||
.rss_renderer
|
||||
.render_feed(&page.items, "Movie Diary")
|
||||
@@ -33,30 +33,18 @@ pub async fn get_user_feed(
|
||||
State(state): State<AppState>,
|
||||
Path(user_id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let user = state
|
||||
.app_ctx
|
||||
.repos
|
||||
.user
|
||||
.find_by_id(&UserId::from_uuid(user_id))
|
||||
.await
|
||||
.map_err(ApiError)?
|
||||
.ok_or_else(|| ApiError(DomainError::NotFound(format!("User {user_id}"))))?;
|
||||
let feed = get_user_feed::execute(
|
||||
&state.app_ctx.deps.diary.get_user_feed,
|
||||
user_id,
|
||||
super::RSS_FEED_LIMIT,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let query = GetDiaryQuery {
|
||||
limit: Some(super::RSS_FEED_LIMIT),
|
||||
offset: Some(0),
|
||||
sort_by: Some(ReviewSortBy::Descending),
|
||||
movie_id: None,
|
||||
user_id: Some(user_id),
|
||||
};
|
||||
let page = get_diary::execute(&state.app_ctx.repos.diary, query).await?;
|
||||
|
||||
let display_name = user.email().value().split('@').next().unwrap_or("User");
|
||||
let title = format!("{}'s Movie Diary", display_name);
|
||||
let title = format!("{}'s Movie Diary", feed.author.display_name);
|
||||
|
||||
let xml = state
|
||||
.rss_renderer
|
||||
.render_feed(&page.items, &title)
|
||||
.render_feed(&feed.entries, &title)
|
||||
.map_err(|e| ApiError(DomainError::InfrastructureError(e)))?;
|
||||
|
||||
Ok((
|
||||
|
||||
@@ -5,7 +5,7 @@ use axum::{
|
||||
};
|
||||
|
||||
use application::{
|
||||
person::{deps::GetPersonDeps, get as get_person, get_credits as get_person_credits},
|
||||
person::{get as get_person, get_credits as get_person_credits},
|
||||
search::execute as search_uc,
|
||||
};
|
||||
use domain::models::{PersonId, collections::PageParams};
|
||||
@@ -45,7 +45,7 @@ pub async fn get_search(
|
||||
},
|
||||
};
|
||||
|
||||
match search_uc::execute(state.app_ctx.repos.search_port.clone(), query).await {
|
||||
match search_uc::execute(&state.app_ctx.deps.search.execute, query).await {
|
||||
Ok(results) => axum::Json(SearchResponse {
|
||||
movies: PaginatedMovieHits {
|
||||
items: results
|
||||
@@ -101,11 +101,12 @@ pub async fn get_person_handler(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<uuid::Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let deps = GetPersonDeps {
|
||||
person_query: state.app_ctx.repos.person_query.clone(),
|
||||
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
||||
};
|
||||
match get_person::execute(&deps, PersonId::from_uuid(id)).await {
|
||||
match get_person::execute(
|
||||
&state.app_ctx.deps.person.get_person,
|
||||
PersonId::from_uuid(id),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Some(person)) => {
|
||||
axum::Json(crate::mappers::search::person_to_dto(&person)).into_response()
|
||||
}
|
||||
@@ -127,11 +128,12 @@ pub async fn get_person_credits_handler(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<uuid::Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let deps = GetPersonDeps {
|
||||
person_query: state.app_ctx.repos.person_query.clone(),
|
||||
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
||||
};
|
||||
match get_person_credits::execute(&deps, PersonId::from_uuid(id)).await {
|
||||
match get_person_credits::execute(
|
||||
&state.app_ctx.deps.person.get_person,
|
||||
PersonId::from_uuid(id),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(credits) => axum::Json(PersonCreditsDto {
|
||||
person: crate::mappers::search::person_to_dto(&credits.person),
|
||||
cast: credits
|
||||
|
||||
@@ -9,77 +9,58 @@ use uuid::Uuid;
|
||||
use crate::{
|
||||
csrf::CsrfToken,
|
||||
errors::ApiError,
|
||||
extractors::{AdminApiUser, AuthenticatedUser, RequiredCookieUser},
|
||||
forms::{
|
||||
ActorUrlForm, BlockDomainForm, FollowForm, FollowerActionForm, RemoveDomainForm,
|
||||
UnfollowForm,
|
||||
},
|
||||
extractors::{AuthenticatedUser, RequiredCookieUser},
|
||||
forms::{FollowForm, FollowerActionForm, UnfollowForm},
|
||||
render::render_page,
|
||||
state::AppState,
|
||||
};
|
||||
#[cfg(feature = "federation")]
|
||||
use crate::{
|
||||
extractors::AdminApiUser,
|
||||
forms::{ActorUrlForm, BlockDomainForm, RemoveDomainForm},
|
||||
};
|
||||
use api_types::{
|
||||
ActorListResponse, ActorUrlRequest, AddBlockedDomainRequest, BlockedActorResponse,
|
||||
BlockedDomainResponse, FollowRequest, RemoteActorDto,
|
||||
};
|
||||
use application::social::deps::{SocialCommandDeps, SocialQueryDeps};
|
||||
use domain::value_objects::{FollowTarget, SocialActor, SocialIdentity};
|
||||
use template_askama::{
|
||||
BlockedActorsTemplate, BlockedDomainsTemplate, FollowersTemplate, FollowingTemplate,
|
||||
RemoteActorData,
|
||||
ActorListResponse, ActorUrlRequest, FollowRelationResponse, FollowRequest,
|
||||
PendingCountResponse, RemoteActorDto,
|
||||
};
|
||||
#[cfg(feature = "federation")]
|
||||
use api_types::{AddBlockedDomainRequest, BlockedActorResponse, BlockedDomainResponse};
|
||||
use domain::value_objects::{FollowTarget, InstanceIdentity, SocialActor, SocialIdentity};
|
||||
#[cfg(feature = "federation")]
|
||||
use template_askama::{BlockedActorsTemplate, BlockedDomainsTemplate};
|
||||
use template_askama::{FollowersTemplate, FollowingTemplate, RemoteActorData};
|
||||
|
||||
use super::helpers::{build_page_context, encode_error};
|
||||
|
||||
impl From<&AppState> for SocialCommandDeps {
|
||||
fn from(state: &AppState) -> Self {
|
||||
Self {
|
||||
social_command: state.app_ctx.repos.social_command.clone(),
|
||||
social_query: state.app_ctx.repos.social_query_unified.clone(),
|
||||
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&AppState> for SocialQueryDeps {
|
||||
fn from(state: &AppState) -> Self {
|
||||
Self {
|
||||
social_query: state.app_ctx.repos.social_query_unified.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ap_to_domain(e: anyhow::Error) -> domain::errors::DomainError {
|
||||
tracing::error!("ActivityPub error: {:?}", e);
|
||||
domain::errors::DomainError::InfrastructureError(e.to_string())
|
||||
}
|
||||
|
||||
fn actor_url(identity: &SocialIdentity) -> String {
|
||||
match identity {
|
||||
SocialIdentity::Remote { actor_url } => actor_url.clone(),
|
||||
SocialIdentity::Local(uid) => format!("local:{}", uid.value()),
|
||||
}
|
||||
}
|
||||
|
||||
fn social_actor_to_dto(actor: SocialActor) -> RemoteActorDto {
|
||||
fn social_actor_to_dto(actor: SocialActor, instance: &InstanceIdentity) -> RemoteActorDto {
|
||||
RemoteActorDto {
|
||||
url: actor_url(&actor.identity),
|
||||
handle: actor.handle,
|
||||
display_name: actor.display_name,
|
||||
}
|
||||
}
|
||||
|
||||
fn social_actor_to_blocked_dto(actor: SocialActor) -> BlockedActorResponse {
|
||||
BlockedActorResponse {
|
||||
url: actor_url(&actor.identity),
|
||||
url: instance.actor_url_of(&actor.identity),
|
||||
user_id: match &actor.identity {
|
||||
SocialIdentity::Local(uid) => Some(uid.value()),
|
||||
SocialIdentity::Remote { .. } => None,
|
||||
},
|
||||
handle: actor.handle,
|
||||
display_name: actor.display_name,
|
||||
avatar_url: actor.avatar_url,
|
||||
}
|
||||
}
|
||||
|
||||
fn social_actor_to_template(actor: SocialActor) -> RemoteActorData {
|
||||
#[cfg(feature = "federation")]
|
||||
fn social_actor_to_blocked_dto(
|
||||
actor: SocialActor,
|
||||
instance: &InstanceIdentity,
|
||||
) -> BlockedActorResponse {
|
||||
BlockedActorResponse {
|
||||
url: instance.actor_url_of(&actor.identity),
|
||||
handle: actor.handle,
|
||||
display_name: actor.display_name,
|
||||
avatar_url: actor.avatar_url,
|
||||
}
|
||||
}
|
||||
|
||||
fn social_actor_to_template(actor: SocialActor, instance: &InstanceIdentity) -> RemoteActorData {
|
||||
RemoteActorData {
|
||||
url: actor_url(&actor.identity),
|
||||
url: instance.actor_url_of(&actor.identity),
|
||||
handle: actor.handle,
|
||||
display_name: actor.display_name,
|
||||
avatar_url: actor.avatar_url,
|
||||
@@ -88,6 +69,7 @@ fn social_actor_to_template(actor: SocialActor) -> RemoteActorData {
|
||||
|
||||
// ── API ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
#[utoipa::path(
|
||||
get, path = "/api/v1/admin/blocked-domains",
|
||||
responses(
|
||||
@@ -101,13 +83,7 @@ pub async fn get_blocked_domains_admin(
|
||||
State(state): State<AppState>,
|
||||
_admin: AdminApiUser,
|
||||
) -> Result<Json<Vec<BlockedDomainResponse>>, ApiError> {
|
||||
let domains = state
|
||||
.app_ctx
|
||||
.services
|
||||
.ap_service
|
||||
.get_blocked_domains()
|
||||
.await
|
||||
.map_err(ap_to_domain)?;
|
||||
let domains = state.app_ctx.ap_blocklist.get_blocked_domains().await?;
|
||||
Ok(Json(
|
||||
domains
|
||||
.into_iter()
|
||||
@@ -120,6 +96,7 @@ pub async fn get_blocked_domains_admin(
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
#[utoipa::path(
|
||||
post, path = "/api/v1/admin/blocked-domains",
|
||||
request_body = AddBlockedDomainRequest,
|
||||
@@ -137,14 +114,13 @@ pub async fn add_blocked_domain_admin(
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
state
|
||||
.app_ctx
|
||||
.services
|
||||
.ap_service
|
||||
.ap_blocklist
|
||||
.add_blocked_domain(&body.domain, body.reason.as_deref())
|
||||
.await
|
||||
.map_err(ap_to_domain)?;
|
||||
.await?;
|
||||
Ok(StatusCode::CREATED)
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
#[utoipa::path(
|
||||
delete, path = "/api/v1/admin/blocked-domains/{domain}",
|
||||
params(("domain" = String, Path, description = "Domain to unblock")),
|
||||
@@ -162,14 +138,13 @@ pub async fn remove_blocked_domain_admin(
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
state
|
||||
.app_ctx
|
||||
.services
|
||||
.ap_service
|
||||
.ap_blocklist
|
||||
.remove_blocked_domain(&domain)
|
||||
.await
|
||||
.map_err(ap_to_domain)?;
|
||||
.await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
#[utoipa::path(
|
||||
post, path = "/api/v1/social/block",
|
||||
request_body = ActorUrlRequest,
|
||||
@@ -184,18 +159,19 @@ pub async fn block_actor_api(
|
||||
user: AuthenticatedUser,
|
||||
axum::Json(body): axum::Json<ActorUrlRequest>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let deps = SocialCommandDeps::from(&state);
|
||||
let instance = state.app_ctx.instance.clone();
|
||||
application::social::execute::execute_command(
|
||||
&deps,
|
||||
&state.app_ctx.deps.social.command,
|
||||
application::social::commands::SocialCmd::Block {
|
||||
blocker_id: user.0.value(),
|
||||
target: SocialIdentity::from_actor_url(&body.actor_url, &state.app_ctx.config.base_url),
|
||||
target: instance.identify(&body.actor_url),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
#[utoipa::path(
|
||||
post, path = "/api/v1/social/unblock",
|
||||
request_body = ActorUrlRequest,
|
||||
@@ -210,18 +186,19 @@ pub async fn unblock_actor_api(
|
||||
user: AuthenticatedUser,
|
||||
axum::Json(body): axum::Json<ActorUrlRequest>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let deps = SocialCommandDeps::from(&state);
|
||||
let instance = state.app_ctx.instance.clone();
|
||||
application::social::execute::execute_command(
|
||||
&deps,
|
||||
&state.app_ctx.deps.social.command,
|
||||
application::social::commands::SocialCmd::Unblock {
|
||||
blocker_id: user.0.value(),
|
||||
target: SocialIdentity::from_actor_url(&body.actor_url, &state.app_ctx.config.base_url),
|
||||
target: instance.identify(&body.actor_url),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
#[utoipa::path(
|
||||
get, path = "/api/v1/social/blocked",
|
||||
responses(
|
||||
@@ -234,18 +211,14 @@ pub async fn get_blocked_actors_api(
|
||||
State(state): State<AppState>,
|
||||
user: AuthenticatedUser,
|
||||
) -> Result<Json<Vec<BlockedActorResponse>>, ApiError> {
|
||||
let deps = SocialQueryDeps::from(&state);
|
||||
let identities = application::social::execute::execute_query(
|
||||
&deps,
|
||||
application::social::queries::SocialQry::GetBlocked {
|
||||
user_id: user.0.value(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let instance = state.app_ctx.instance.clone();
|
||||
let identities =
|
||||
application::social::get_blocked::execute(&state.app_ctx.deps.social.query, user.0.value())
|
||||
.await?;
|
||||
Ok(Json(
|
||||
identities
|
||||
.into_iter()
|
||||
.map(social_actor_to_blocked_dto)
|
||||
.map(|a| social_actor_to_blocked_dto(a, &instance))
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
@@ -262,16 +235,17 @@ pub async fn get_following(
|
||||
State(state): State<AppState>,
|
||||
user: AuthenticatedUser,
|
||||
) -> Result<Json<ActorListResponse>, ApiError> {
|
||||
let deps = SocialQueryDeps::from(&state);
|
||||
let identities = application::social::execute::execute_query(
|
||||
&deps,
|
||||
application::social::queries::SocialQry::GetFollowing {
|
||||
user_id: user.0.value(),
|
||||
},
|
||||
let instance = state.app_ctx.instance.clone();
|
||||
let identities = application::social::get_following::execute(
|
||||
&state.app_ctx.deps.social.query,
|
||||
user.0.value(),
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ActorListResponse {
|
||||
actors: identities.into_iter().map(social_actor_to_dto).collect(),
|
||||
actors: identities
|
||||
.into_iter()
|
||||
.map(|a| social_actor_to_dto(a, &instance))
|
||||
.collect(),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -287,16 +261,17 @@ pub async fn get_followers(
|
||||
State(state): State<AppState>,
|
||||
user: AuthenticatedUser,
|
||||
) -> Result<Json<ActorListResponse>, ApiError> {
|
||||
let deps = SocialQueryDeps::from(&state);
|
||||
let identities = application::social::execute::execute_query(
|
||||
&deps,
|
||||
application::social::queries::SocialQry::GetFollowers {
|
||||
user_id: user.0.value(),
|
||||
},
|
||||
let instance = state.app_ctx.instance.clone();
|
||||
let identities = application::social::get_followers::execute(
|
||||
&state.app_ctx.deps.social.query,
|
||||
user.0.value(),
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ActorListResponse {
|
||||
actors: identities.into_iter().map(social_actor_to_dto).collect(),
|
||||
actors: identities
|
||||
.into_iter()
|
||||
.map(|a| social_actor_to_dto(a, &instance))
|
||||
.collect(),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -305,14 +280,15 @@ pub async fn get_user_following(
|
||||
_user: AuthenticatedUser,
|
||||
Path(user_id): Path<Uuid>,
|
||||
) -> Result<Json<ActorListResponse>, ApiError> {
|
||||
let deps = SocialQueryDeps::from(&state);
|
||||
let identities = application::social::execute::execute_query(
|
||||
&deps,
|
||||
application::social::queries::SocialQry::GetFollowing { user_id },
|
||||
)
|
||||
.await?;
|
||||
let instance = state.app_ctx.instance.clone();
|
||||
let identities =
|
||||
application::social::get_following::execute(&state.app_ctx.deps.social.query, user_id)
|
||||
.await?;
|
||||
Ok(Json(ActorListResponse {
|
||||
actors: identities.into_iter().map(social_actor_to_dto).collect(),
|
||||
actors: identities
|
||||
.into_iter()
|
||||
.map(|a| social_actor_to_dto(a, &instance))
|
||||
.collect(),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -321,14 +297,15 @@ pub async fn get_user_followers(
|
||||
_user: AuthenticatedUser,
|
||||
Path(user_id): Path<Uuid>,
|
||||
) -> Result<Json<ActorListResponse>, ApiError> {
|
||||
let deps = SocialQueryDeps::from(&state);
|
||||
let identities = application::social::execute::execute_query(
|
||||
&deps,
|
||||
application::social::queries::SocialQry::GetFollowers { user_id },
|
||||
)
|
||||
.await?;
|
||||
let instance = state.app_ctx.instance.clone();
|
||||
let identities =
|
||||
application::social::get_followers::execute(&state.app_ctx.deps.social.query, user_id)
|
||||
.await?;
|
||||
Ok(Json(ActorListResponse {
|
||||
actors: identities.into_iter().map(social_actor_to_dto).collect(),
|
||||
actors: identities
|
||||
.into_iter()
|
||||
.map(|a| social_actor_to_dto(a, &instance))
|
||||
.collect(),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -346,9 +323,8 @@ pub async fn follow(
|
||||
user: AuthenticatedUser,
|
||||
Json(body): Json<FollowRequest>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let deps = SocialCommandDeps::from(&state);
|
||||
application::social::execute::execute_command(
|
||||
&deps,
|
||||
&state.app_ctx.deps.social.command,
|
||||
application::social::commands::SocialCmd::Follow {
|
||||
follower_id: user.0.value(),
|
||||
target: FollowTarget::Handle(body.handle),
|
||||
@@ -372,12 +348,12 @@ pub async fn unfollow(
|
||||
user: AuthenticatedUser,
|
||||
Json(body): Json<ActorUrlRequest>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let deps = SocialCommandDeps::from(&state);
|
||||
let instance = state.app_ctx.instance.clone();
|
||||
application::social::execute::execute_command(
|
||||
&deps,
|
||||
&state.app_ctx.deps.social.command,
|
||||
application::social::commands::SocialCmd::Unfollow {
|
||||
follower_id: user.0.value(),
|
||||
target: SocialIdentity::from_actor_url(&body.actor_url, &state.app_ctx.config.base_url),
|
||||
target: instance.identify(&body.actor_url),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -398,15 +374,12 @@ pub async fn accept_follower(
|
||||
user: AuthenticatedUser,
|
||||
Json(body): Json<ActorUrlRequest>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let deps = SocialCommandDeps::from(&state);
|
||||
let instance = state.app_ctx.instance.clone();
|
||||
application::social::execute::execute_command(
|
||||
&deps,
|
||||
&state.app_ctx.deps.social.command,
|
||||
application::social::commands::SocialCmd::AcceptFollow {
|
||||
owner_id: user.0.value(),
|
||||
requester: SocialIdentity::from_actor_url(
|
||||
&body.actor_url,
|
||||
&state.app_ctx.config.base_url,
|
||||
),
|
||||
requester: instance.identify(&body.actor_url),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -427,15 +400,12 @@ pub async fn reject_follower(
|
||||
user: AuthenticatedUser,
|
||||
Json(body): Json<ActorUrlRequest>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let deps = SocialCommandDeps::from(&state);
|
||||
let instance = state.app_ctx.instance.clone();
|
||||
application::social::execute::execute_command(
|
||||
&deps,
|
||||
&state.app_ctx.deps.social.command,
|
||||
application::social::commands::SocialCmd::RejectFollow {
|
||||
owner_id: user.0.value(),
|
||||
requester: SocialIdentity::from_actor_url(
|
||||
&body.actor_url,
|
||||
&state.app_ctx.config.base_url,
|
||||
),
|
||||
requester: instance.identify(&body.actor_url),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -456,15 +426,12 @@ pub async fn remove_follower(
|
||||
user: AuthenticatedUser,
|
||||
Json(body): Json<ActorUrlRequest>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let deps = SocialCommandDeps::from(&state);
|
||||
let instance = state.app_ctx.instance.clone();
|
||||
application::social::execute::execute_command(
|
||||
&deps,
|
||||
&state.app_ctx.deps.social.command,
|
||||
application::social::commands::SocialCmd::RemoveFollower {
|
||||
owner_id: user.0.value(),
|
||||
follower: SocialIdentity::from_actor_url(
|
||||
&body.actor_url,
|
||||
&state.app_ctx.config.base_url,
|
||||
),
|
||||
follower: instance.identify(&body.actor_url),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -483,22 +450,101 @@ pub async fn get_pending_followers(
|
||||
State(state): State<AppState>,
|
||||
user: AuthenticatedUser,
|
||||
) -> Result<Json<ActorListResponse>, ApiError> {
|
||||
let deps = SocialQueryDeps::from(&state);
|
||||
let identities = application::social::execute::execute_query(
|
||||
&deps,
|
||||
application::social::queries::SocialQry::GetPending {
|
||||
user_id: user.0.value(),
|
||||
},
|
||||
let instance = state.app_ctx.instance.clone();
|
||||
let identities = application::social::get_pending_followers::execute(
|
||||
&state.app_ctx.deps.social.query,
|
||||
user.0.value(),
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ActorListResponse {
|
||||
actors: identities.into_iter().map(social_actor_to_dto).collect(),
|
||||
actors: identities
|
||||
.into_iter()
|
||||
.map(|a| social_actor_to_dto(a, &instance))
|
||||
.collect(),
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get, path = "/api/v1/social/followers/pending/count",
|
||||
responses(
|
||||
(status = 200, body = PendingCountResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
),
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn get_pending_follower_count(
|
||||
State(state): State<AppState>,
|
||||
user: AuthenticatedUser,
|
||||
) -> Result<Json<PendingCountResponse>, ApiError> {
|
||||
let count = application::social::count_pending_followers::execute(
|
||||
&state.app_ctx.deps.social.query,
|
||||
user.0.value(),
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(PendingCountResponse { count }))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get, path = "/api/v1/social/following/pending",
|
||||
responses(
|
||||
(status = 200, body = ActorListResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
),
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn get_pending_following(
|
||||
State(state): State<AppState>,
|
||||
user: AuthenticatedUser,
|
||||
) -> Result<Json<ActorListResponse>, ApiError> {
|
||||
let instance = state.app_ctx.instance.clone();
|
||||
let actors = application::social::get_pending_following::execute(
|
||||
&state.app_ctx.deps.social.query,
|
||||
user.0.value(),
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ActorListResponse {
|
||||
actors: actors
|
||||
.into_iter()
|
||||
.map(|a| social_actor_to_dto(a, &instance))
|
||||
.collect(),
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct RelationshipQuery {
|
||||
pub actor_url: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get, path = "/api/v1/social/relationship",
|
||||
params(("actor_url" = String, Query, description = "Canonical actor URL of the target")),
|
||||
responses(
|
||||
(status = 200, body = FollowRelationResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
),
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn get_relationship(
|
||||
State(state): State<AppState>,
|
||||
user: AuthenticatedUser,
|
||||
Query(q): Query<RelationshipQuery>,
|
||||
) -> Result<Json<FollowRelationResponse>, ApiError> {
|
||||
let target = state.app_ctx.instance.identify(&q.actor_url);
|
||||
let rel = application::social::get_relation::execute(
|
||||
&state.app_ctx.deps.social.query,
|
||||
user.0.value(),
|
||||
target,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(FollowRelationResponse {
|
||||
following: rel.following.into(),
|
||||
followed_by: rel.followed_by.into(),
|
||||
}))
|
||||
}
|
||||
|
||||
// ── HTML ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
pub async fn follow_remote_user(
|
||||
pub async fn follow_user(
|
||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||
State(state): State<AppState>,
|
||||
Path(profile_user_uuid): Path<Uuid>,
|
||||
@@ -518,9 +564,8 @@ pub async fn follow_remote_user(
|
||||
.unwrap_or(&format!("/users/{}", profile_user_uuid))
|
||||
.to_string();
|
||||
|
||||
let deps = SocialCommandDeps::from(&state);
|
||||
match application::social::execute::execute_command(
|
||||
&deps,
|
||||
&state.app_ctx.deps.social.command,
|
||||
application::social::commands::SocialCmd::Follow {
|
||||
follower_id: user_id.value(),
|
||||
target: FollowTarget::Handle(form.handle),
|
||||
@@ -542,7 +587,7 @@ pub async fn follow_remote_user(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn unfollow_remote_user(
|
||||
pub async fn unfollow_user(
|
||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||
State(state): State<AppState>,
|
||||
Path(profile_user_uuid): Path<Uuid>,
|
||||
@@ -555,12 +600,12 @@ pub async fn unfollow_remote_user(
|
||||
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
let deps = SocialCommandDeps::from(&state);
|
||||
let instance = state.app_ctx.instance.clone();
|
||||
match application::social::execute::execute_command(
|
||||
&deps,
|
||||
&state.app_ctx.deps.social.command,
|
||||
application::social::commands::SocialCmd::Unfollow {
|
||||
follower_id: user_id.value(),
|
||||
target: SocialIdentity::from_actor_url(&form.actor_url, &state.app_ctx.config.base_url),
|
||||
target: instance.identify(&form.actor_url),
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -592,15 +637,12 @@ pub async fn accept_follower_html(
|
||||
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
let deps = SocialCommandDeps::from(&state);
|
||||
let instance = state.app_ctx.instance.clone();
|
||||
match application::social::execute::execute_command(
|
||||
&deps,
|
||||
&state.app_ctx.deps.social.command,
|
||||
application::social::commands::SocialCmd::AcceptFollow {
|
||||
owner_id: user_id.value(),
|
||||
requester: SocialIdentity::from_actor_url(
|
||||
&form.actor_url,
|
||||
&state.app_ctx.config.base_url,
|
||||
),
|
||||
requester: instance.identify(&form.actor_url),
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -626,15 +668,12 @@ pub async fn reject_follower_html(
|
||||
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
let deps = SocialCommandDeps::from(&state);
|
||||
let instance = state.app_ctx.instance.clone();
|
||||
match application::social::execute::execute_command(
|
||||
&deps,
|
||||
&state.app_ctx.deps.social.command,
|
||||
application::social::commands::SocialCmd::RejectFollow {
|
||||
owner_id: user_id.value(),
|
||||
requester: SocialIdentity::from_actor_url(
|
||||
&form.actor_url,
|
||||
&state.app_ctx.config.base_url,
|
||||
),
|
||||
requester: instance.identify(&form.actor_url),
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -647,6 +686,7 @@ pub async fn reject_follower_html(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
pub async fn get_followers_collection(
|
||||
State(state): State<AppState>,
|
||||
Path(user_id): Path<Uuid>,
|
||||
@@ -661,8 +701,7 @@ pub async fn get_followers_collection(
|
||||
let page = params.get("page").and_then(|p| p.parse::<u32>().ok());
|
||||
return match state
|
||||
.app_ctx
|
||||
.services
|
||||
.ap_service
|
||||
.ap_document
|
||||
.followers_collection_json(user_id, page)
|
||||
.await
|
||||
{
|
||||
@@ -674,12 +713,16 @@ pub async fn get_followers_collection(
|
||||
json,
|
||||
)
|
||||
.into_response(),
|
||||
Err(_) => StatusCode::NOT_FOUND.into_response(),
|
||||
Err(e) => {
|
||||
tracing::error!("followers_collection_json error: {:?}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
}
|
||||
};
|
||||
}
|
||||
axum::response::Redirect::to(&format!("/users/{}/followers-list", user_id)).into_response()
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
pub async fn get_following_collection(
|
||||
State(state): State<AppState>,
|
||||
Path(user_id): Path<Uuid>,
|
||||
@@ -694,8 +737,7 @@ pub async fn get_following_collection(
|
||||
let page = params.get("page").and_then(|p| p.parse::<u32>().ok());
|
||||
return match state
|
||||
.app_ctx
|
||||
.services
|
||||
.ap_service
|
||||
.ap_document
|
||||
.following_collection_json(user_id, page)
|
||||
.await
|
||||
{
|
||||
@@ -707,7 +749,10 @@ pub async fn get_following_collection(
|
||||
json,
|
||||
)
|
||||
.into_response(),
|
||||
Err(_) => StatusCode::NOT_FOUND.into_response(),
|
||||
Err(e) => {
|
||||
tracing::error!("following_collection_json error: {:?}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
}
|
||||
};
|
||||
}
|
||||
axum::response::Redirect::to(&format!("/users/{}/following-list", user_id)).into_response()
|
||||
@@ -729,24 +774,39 @@ pub async fn get_following_page(
|
||||
"{}/users/{}/following-list",
|
||||
state.app_ctx.config.base_url, profile_user_uuid
|
||||
);
|
||||
let deps = SocialQueryDeps::from(&state);
|
||||
match application::social::execute::execute_query(
|
||||
&deps,
|
||||
application::social::queries::SocialQry::GetFollowing {
|
||||
user_id: user_id.value(),
|
||||
},
|
||||
let instance = state.app_ctx.instance.clone();
|
||||
match application::social::get_following::execute(
|
||||
&state.app_ctx.deps.social.query,
|
||||
user_id.value(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(following) => {
|
||||
let actors: Vec<RemoteActorData> = following
|
||||
.into_iter()
|
||||
.map(social_actor_to_template)
|
||||
.map(|a| social_actor_to_template(a, &instance))
|
||||
.collect();
|
||||
let pending_actors: Vec<RemoteActorData> =
|
||||
match application::social::get_pending_following::execute(
|
||||
&state.app_ctx.deps.social.query,
|
||||
user_id.value(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(pending) => pending
|
||||
.into_iter()
|
||||
.map(|a| social_actor_to_template(a, &instance))
|
||||
.collect(),
|
||||
Err(e) => {
|
||||
tracing::error!("get_pending_following error: {:?}", e);
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
render_page(FollowingTemplate {
|
||||
ctx,
|
||||
user_id: profile_user_uuid,
|
||||
actors,
|
||||
pending_actors,
|
||||
error: params.error,
|
||||
})
|
||||
.into_response()
|
||||
@@ -778,19 +838,17 @@ pub async fn get_followers_page(
|
||||
"{}/users/{}/followers-list",
|
||||
state.app_ctx.config.base_url, profile_user_uuid
|
||||
);
|
||||
let deps = SocialQueryDeps::from(&state);
|
||||
match application::social::execute::execute_query(
|
||||
&deps,
|
||||
application::social::queries::SocialQry::GetFollowers {
|
||||
user_id: user_id.value(),
|
||||
},
|
||||
let instance = state.app_ctx.instance.clone();
|
||||
match application::social::get_followers::execute(
|
||||
&state.app_ctx.deps.social.query,
|
||||
user_id.value(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(followers) => {
|
||||
let actors: Vec<RemoteActorData> = followers
|
||||
.into_iter()
|
||||
.map(social_actor_to_template)
|
||||
.map(|a| social_actor_to_template(a, &instance))
|
||||
.collect();
|
||||
render_page(FollowersTemplate {
|
||||
ctx,
|
||||
@@ -824,15 +882,12 @@ pub async fn remove_follower_html(
|
||||
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
let deps = SocialCommandDeps::from(&state);
|
||||
let instance = state.app_ctx.instance.clone();
|
||||
match application::social::execute::execute_command(
|
||||
&deps,
|
||||
&state.app_ctx.deps.social.command,
|
||||
application::social::commands::SocialCmd::RemoveFollower {
|
||||
owner_id: user_id.value(),
|
||||
follower: SocialIdentity::from_actor_url(
|
||||
&form.actor_url,
|
||||
&state.app_ctx.config.base_url,
|
||||
),
|
||||
follower: instance.identify(&form.actor_url),
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -851,6 +906,7 @@ pub async fn remove_follower_html(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
pub async fn get_blocked_domains_page(
|
||||
crate::extractors::AdminUser(user_id): crate::extractors::AdminUser,
|
||||
State(state): State<AppState>,
|
||||
@@ -859,13 +915,7 @@ pub async fn get_blocked_domains_page(
|
||||
let mut ctx = build_page_context(&state, Some(user_id), csrf.0).await;
|
||||
ctx.page_title = "Blocked Domains — Movies Diary".to_string();
|
||||
ctx.canonical_url = format!("{}/admin/blocked-domains", state.app_ctx.config.base_url);
|
||||
match state
|
||||
.app_ctx
|
||||
.services
|
||||
.ap_service
|
||||
.get_blocked_domains()
|
||||
.await
|
||||
{
|
||||
match state.app_ctx.ap_blocklist.get_blocked_domains().await {
|
||||
Ok(domains) => {
|
||||
let entries: Vec<template_askama::BlockedDomainEntry> = domains
|
||||
.into_iter()
|
||||
@@ -892,6 +942,7 @@ pub async fn get_blocked_domains_page(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
pub async fn post_blocked_domain(
|
||||
crate::extractors::AdminUser(_): crate::extractors::AdminUser,
|
||||
State(state): State<AppState>,
|
||||
@@ -904,8 +955,7 @@ pub async fn post_blocked_domain(
|
||||
let reason = form.reason.as_deref().filter(|s| !s.trim().is_empty());
|
||||
match state
|
||||
.app_ctx
|
||||
.services
|
||||
.ap_service
|
||||
.ap_blocklist
|
||||
.add_blocked_domain(&form.domain, reason)
|
||||
.await
|
||||
{
|
||||
@@ -917,6 +967,7 @@ pub async fn post_blocked_domain(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
pub async fn post_remove_blocked_domain(
|
||||
crate::extractors::AdminUser(_): crate::extractors::AdminUser,
|
||||
State(state): State<AppState>,
|
||||
@@ -928,8 +979,7 @@ pub async fn post_remove_blocked_domain(
|
||||
}
|
||||
match state
|
||||
.app_ctx
|
||||
.services
|
||||
.ap_service
|
||||
.ap_blocklist
|
||||
.remove_blocked_domain(&form.domain)
|
||||
.await
|
||||
{
|
||||
@@ -941,6 +991,7 @@ pub async fn post_remove_blocked_domain(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
pub async fn get_blocked_actors_page(
|
||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||
State(state): State<AppState>,
|
||||
@@ -949,12 +1000,10 @@ pub async fn get_blocked_actors_page(
|
||||
let mut ctx = build_page_context(&state, Some(user_id.clone()), csrf.0).await;
|
||||
ctx.page_title = "Blocked Users — Movies Diary".to_string();
|
||||
ctx.canonical_url = format!("{}/social/blocked", state.app_ctx.config.base_url);
|
||||
let deps = SocialQueryDeps::from(&state);
|
||||
match application::social::execute::execute_query(
|
||||
&deps,
|
||||
application::social::queries::SocialQry::GetBlocked {
|
||||
user_id: user_id.value(),
|
||||
},
|
||||
let instance = state.app_ctx.instance.clone();
|
||||
match application::social::get_blocked::execute(
|
||||
&state.app_ctx.deps.social.query,
|
||||
user_id.value(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -962,7 +1011,7 @@ pub async fn get_blocked_actors_page(
|
||||
let entries: Vec<template_askama::BlockedActorEntry> = blocked
|
||||
.into_iter()
|
||||
.map(|a| template_askama::BlockedActorEntry {
|
||||
url: actor_url(&a.identity),
|
||||
url: instance.actor_url_of(&a.identity),
|
||||
handle: a.handle,
|
||||
display_name: a.display_name,
|
||||
avatar_url: a.avatar_url,
|
||||
@@ -985,6 +1034,7 @@ pub async fn get_blocked_actors_page(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
pub async fn post_block_actor_html(
|
||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||
State(state): State<AppState>,
|
||||
@@ -994,12 +1044,12 @@ pub async fn post_block_actor_html(
|
||||
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
let deps = SocialCommandDeps::from(&state);
|
||||
let instance = state.app_ctx.instance.clone();
|
||||
match application::social::execute::execute_command(
|
||||
&deps,
|
||||
&state.app_ctx.deps.social.command,
|
||||
application::social::commands::SocialCmd::Block {
|
||||
blocker_id: user_id.value(),
|
||||
target: SocialIdentity::from_actor_url(&form.actor_url, &state.app_ctx.config.base_url),
|
||||
target: instance.identify(&form.actor_url),
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -1012,6 +1062,7 @@ pub async fn post_block_actor_html(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
pub async fn post_unblock_actor(
|
||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||
State(state): State<AppState>,
|
||||
@@ -1021,12 +1072,12 @@ pub async fn post_unblock_actor(
|
||||
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
let deps = SocialCommandDeps::from(&state);
|
||||
let instance = state.app_ctx.instance.clone();
|
||||
match application::social::execute::execute_command(
|
||||
&deps,
|
||||
&state.app_ctx.deps.social.command,
|
||||
application::social::commands::SocialCmd::Unblock {
|
||||
blocker_id: user_id.value(),
|
||||
target: SocialIdentity::from_actor_url(&form.actor_url, &state.app_ctx.config.base_url),
|
||||
target: instance.identify(&form.actor_url),
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -1038,3 +1089,110 @@ pub async fn post_unblock_actor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use domain::value_objects::{InstanceIdentity, SocialActor, SocialIdentity, UserId};
|
||||
|
||||
fn local_actor(uid: &UserId) -> SocialActor {
|
||||
SocialActor {
|
||||
identity: SocialIdentity::Local(uid.clone()),
|
||||
handle: "@gabriel@md.example".into(),
|
||||
display_name: None,
|
||||
avatar_url: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The bug: a local actor used to serialize as `local:{uuid}`, which
|
||||
/// `identify` cannot parse, so every POST echoing this url back resolved
|
||||
/// to Remote and was routed to ActivityPub.
|
||||
#[test]
|
||||
fn local_actor_dto_url_round_trips_back_to_the_same_identity() {
|
||||
let instance = InstanceIdentity::new("https://md.example");
|
||||
let uid = UserId::from_uuid(uuid::Uuid::new_v4());
|
||||
|
||||
let dto = social_actor_to_dto(local_actor(&uid), &instance);
|
||||
|
||||
assert!(!dto.url.starts_with("local:"), "url was {}", dto.url);
|
||||
assert_eq!(
|
||||
instance.identify(&dto.url),
|
||||
SocialIdentity::Local(uid),
|
||||
"a local actor's dto url must parse back as Local"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "federation")]
|
||||
#[test]
|
||||
fn local_blocked_dto_url_round_trips_back_to_the_same_identity() {
|
||||
let instance = InstanceIdentity::new("https://md.example");
|
||||
let uid = UserId::from_uuid(uuid::Uuid::new_v4());
|
||||
|
||||
let dto = social_actor_to_blocked_dto(local_actor(&uid), &instance);
|
||||
|
||||
assert!(!dto.url.starts_with("local:"), "url was {}", dto.url);
|
||||
assert_eq!(instance.identify(&dto.url), SocialIdentity::Local(uid));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_template_url_round_trips_back_to_the_same_identity() {
|
||||
let instance = InstanceIdentity::new("https://md.example");
|
||||
let uid = UserId::from_uuid(uuid::Uuid::new_v4());
|
||||
|
||||
let data = social_actor_to_template(local_actor(&uid), &instance);
|
||||
|
||||
assert!(!data.url.starts_with("local:"), "url was {}", data.url);
|
||||
assert_eq!(instance.identify(&data.url), SocialIdentity::Local(uid));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_actor_dto_url_is_passed_through_unchanged() {
|
||||
let instance = InstanceIdentity::new("https://md.example");
|
||||
let actor = SocialActor {
|
||||
identity: SocialIdentity::Remote {
|
||||
actor_url: "https://other.example/users/bob".into(),
|
||||
},
|
||||
handle: "@bob@other.example".into(),
|
||||
display_name: None,
|
||||
avatar_url: None,
|
||||
};
|
||||
|
||||
let dto = social_actor_to_dto(actor, &instance);
|
||||
|
||||
assert_eq!(dto.url, "https://other.example/users/bob");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_actor_dto_carries_user_id_and_avatar() {
|
||||
let instance = InstanceIdentity::new("https://md.example");
|
||||
let uid = UserId::from_uuid(uuid::Uuid::new_v4());
|
||||
let mut actor = local_actor(&uid);
|
||||
actor.avatar_url = Some("https://md.example/images/a.webp".into());
|
||||
|
||||
let dto = social_actor_to_dto(actor, &instance);
|
||||
|
||||
assert_eq!(
|
||||
dto.user_id,
|
||||
Some(uid.value()),
|
||||
"local actors must carry user_id for internal links"
|
||||
);
|
||||
assert_eq!(
|
||||
dto.avatar_url.as_deref(),
|
||||
Some("https://md.example/images/a.webp")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_actor_dto_has_no_user_id() {
|
||||
let instance = InstanceIdentity::new("https://md.example");
|
||||
let actor = SocialActor {
|
||||
identity: SocialIdentity::Remote {
|
||||
actor_url: "https://other.example/users/bob".into(),
|
||||
},
|
||||
handle: "@bob@other.example".into(),
|
||||
display_name: None,
|
||||
avatar_url: None,
|
||||
};
|
||||
assert_eq!(social_actor_to_dto(actor, &instance).user_id, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,12 @@ use axum::{
|
||||
use uuid::Uuid;
|
||||
|
||||
use application::users::{
|
||||
deps::{GetProfileDeps, UpdateProfileDeps},
|
||||
get_profile as get_user_profile_uc, get_users,
|
||||
get_federated_profile, get_federated_profile_stats, get_local_profile,
|
||||
get_profile_settings as get_profile_settings_uc, get_users,
|
||||
queries::{GetUserProfileQuery, GetUsersQuery},
|
||||
update_profile, update_profile_fields,
|
||||
resolve_username_to_id, update_profile, update_profile_fields,
|
||||
};
|
||||
use domain::value_objects::UserId;
|
||||
use domain::{errors::DomainError, value_objects::Username};
|
||||
|
||||
use crate::{
|
||||
csrf::CsrfToken,
|
||||
@@ -53,24 +53,20 @@ pub async fn get_profile(
|
||||
AuthenticatedUser(user_id): AuthenticatedUser,
|
||||
) -> Result<Json<ProfileResponse>, ApiError> {
|
||||
let profile = application::users::get_current_profile::execute(
|
||||
state.app_ctx.repos.user.clone(),
|
||||
&state.app_ctx.deps.users.get_current_profile,
|
||||
application::users::queries::GetCurrentProfileQuery {
|
||||
user_id: user_id.value(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let base_url = &state.app_ctx.config.base_url;
|
||||
let instance = &state.app_ctx.instance;
|
||||
Ok(Json(ProfileResponse {
|
||||
profile: api_types::UserProfileBase {
|
||||
username: profile.username,
|
||||
display_name: profile.display_name,
|
||||
bio: profile.bio,
|
||||
avatar_url: profile
|
||||
.avatar_path
|
||||
.map(|p| format!("{}/images/{}", base_url, p)),
|
||||
banner_url: profile
|
||||
.banner_path
|
||||
.map(|p| format!("{}/images/{}", base_url, p)),
|
||||
avatar_url: profile.avatar_path.map(|p| instance.image_url_for(&p)),
|
||||
banner_url: profile.banner_path.map(|p| instance.image_url_for(&p)),
|
||||
},
|
||||
also_known_as: profile.also_known_as,
|
||||
fields: profile
|
||||
@@ -113,12 +109,7 @@ pub async fn update_profile_handler(
|
||||
also_known_as: data.also_known_as,
|
||||
};
|
||||
|
||||
let deps = UpdateProfileDeps {
|
||||
user: state.app_ctx.repos.user.clone(),
|
||||
object_storage: state.app_ctx.services.object_storage.clone(),
|
||||
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
||||
};
|
||||
match update_profile::execute(&deps, cmd).await {
|
||||
match update_profile::execute(&state.app_ctx.deps.users.update_profile, cmd).await {
|
||||
Ok(()) => StatusCode::NO_CONTENT.into_response(),
|
||||
Err(e) => crate::errors::domain_error_response(e),
|
||||
}
|
||||
@@ -159,12 +150,7 @@ pub async fn update_profile_fields_handler(
|
||||
fields,
|
||||
};
|
||||
|
||||
match update_profile_fields::execute(
|
||||
state.app_ctx.repos.profile_fields.clone(),
|
||||
state.app_ctx.services.event_publisher.clone(),
|
||||
cmd,
|
||||
)
|
||||
.await
|
||||
match update_profile_fields::execute(&state.app_ctx.deps.users.update_profile_fields, cmd).await
|
||||
{
|
||||
Ok(()) => StatusCode::NO_CONTENT.into_response(),
|
||||
Err(e) => crate::errors::domain_error_response(e),
|
||||
@@ -176,11 +162,8 @@ pub async fn update_profile_fields_handler(
|
||||
responses((status = 200, body = UsersResponse)),
|
||||
)]
|
||||
pub async fn list_users(State(state): State<AppState>) -> Result<Json<UsersResponse>, ApiError> {
|
||||
let deps = application::users::deps::GetUsersListDeps {
|
||||
user: state.app_ctx.repos.user.clone(),
|
||||
federation_admin: state.app_ctx.repos.federation_admin.clone(),
|
||||
};
|
||||
let result = get_users::execute(&deps, GetUsersQuery).await?;
|
||||
let result =
|
||||
get_users::execute(&state.app_ctx.deps.users.get_users_list, GetUsersQuery).await?;
|
||||
Ok(Json(UsersResponse {
|
||||
users: result
|
||||
.users
|
||||
@@ -220,44 +203,15 @@ pub async fn get_user_profile(
|
||||
Err(_) => return StatusCode::BAD_REQUEST.into_response(),
|
||||
};
|
||||
|
||||
let local_user = match state
|
||||
.app_ctx
|
||||
.repos
|
||||
.user
|
||||
.find_by_id(&UserId::from_uuid(user_id))
|
||||
.await
|
||||
{
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
return crate::errors::domain_error_response(e);
|
||||
}
|
||||
};
|
||||
|
||||
if local_user.is_none() {
|
||||
if let Some(ref fed_query) = state.app_ctx.repos.federated_profile
|
||||
&& let Ok(Some(fed)) = fed_query.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();
|
||||
}
|
||||
let user = local_user.unwrap();
|
||||
|
||||
let get_profile_deps = GetProfileDeps {
|
||||
stats: state.app_ctx.repos.stats.clone(),
|
||||
diary: state.app_ctx.repos.diary.clone(),
|
||||
social_query: state.app_ctx.repos.social_query_unified.clone(),
|
||||
};
|
||||
let profile = match get_user_profile_uc::execute(
|
||||
&get_profile_deps,
|
||||
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,
|
||||
search: params.search.clone(),
|
||||
is_own_profile: viewer_id.value() == user_id,
|
||||
include_remote: false,
|
||||
},
|
||||
@@ -265,6 +219,24 @@ pub async fn get_user_profile(
|
||||
.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),
|
||||
};
|
||||
|
||||
@@ -306,15 +278,11 @@ pub async fn get_user_profile(
|
||||
Json(UserProfileResponse {
|
||||
user_id,
|
||||
profile: api_types::UserProfileBase {
|
||||
username: user.username().value().to_string(),
|
||||
avatar_url: user
|
||||
.avatar_path()
|
||||
.map(|p| format!("{}/images/{}", state.app_ctx.config.base_url, p)),
|
||||
banner_url: user
|
||||
.banner_path()
|
||||
.map(|p| format!("{}/images/{}", state.app_ctx.config.base_url, p)),
|
||||
display_name: None,
|
||||
bio: None,
|
||||
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,
|
||||
@@ -327,10 +295,7 @@ pub async fn get_user_profile(
|
||||
view_data,
|
||||
goals: {
|
||||
let goals_list = application::goals::list::execute(
|
||||
&application::goals::deps::GoalQueryDeps {
|
||||
goal_query: state.app_ctx.repos.goal_query.clone(),
|
||||
stats: state.app_ctx.repos.stats.clone(),
|
||||
},
|
||||
&state.app_ctx.deps.goals.query,
|
||||
application::goals::queries::ListGoalsQuery { user_id },
|
||||
)
|
||||
.await
|
||||
@@ -342,8 +307,8 @@ pub async fn get_user_profile(
|
||||
}
|
||||
},
|
||||
is_federated: false,
|
||||
handle: None,
|
||||
actor_url: None,
|
||||
handle: Some(profile.identity.handle.clone()),
|
||||
actor_url: Some(profile.identity.actor_url.clone()),
|
||||
})
|
||||
.into_response()
|
||||
}
|
||||
@@ -355,13 +320,8 @@ async fn build_federated_profile_response(
|
||||
profile_view: application::users::queries::ProfileView,
|
||||
params: &UserProfileQueryParams,
|
||||
) -> axum::response::Response {
|
||||
let get_profile_deps = GetProfileDeps {
|
||||
stats: state.app_ctx.repos.stats.clone(),
|
||||
diary: state.app_ctx.repos.diary.clone(),
|
||||
social_query: state.app_ctx.repos.social_query_unified.clone(),
|
||||
};
|
||||
let profile = match get_user_profile_uc::execute(
|
||||
&get_profile_deps,
|
||||
let profile = match get_federated_profile_stats::execute(
|
||||
&state.app_ctx.deps.users.get_federated_profile_stats,
|
||||
GetUserProfileQuery {
|
||||
user_id,
|
||||
view: profile_view,
|
||||
@@ -481,12 +441,8 @@ pub async fn get_users_list(
|
||||
ctx.page_title = "Members — Movies Diary".to_string();
|
||||
ctx.canonical_url = format!("{}/users", state.app_ctx.config.base_url);
|
||||
|
||||
let users_deps = application::users::deps::GetUsersListDeps {
|
||||
user: state.app_ctx.repos.user.clone(),
|
||||
federation_admin: state.app_ctx.repos.federation_admin.clone(),
|
||||
};
|
||||
match application::users::get_users::execute(
|
||||
&users_deps,
|
||||
&state.app_ctx.deps.users.get_users_list,
|
||||
application::users::queries::GetUsersQuery,
|
||||
)
|
||||
.await
|
||||
@@ -517,14 +473,14 @@ pub async fn get_user_by_username(
|
||||
State(state): State<AppState>,
|
||||
Path(username): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
let uname = match domain::value_objects::Username::new(username) {
|
||||
let uname = match Username::new(username) {
|
||||
Ok(u) => u,
|
||||
Err(_) => return StatusCode::NOT_FOUND.into_response(),
|
||||
};
|
||||
match state.app_ctx.repos.user.find_by_username(&uname).await {
|
||||
Ok(Some(user)) => {
|
||||
axum::response::Redirect::permanent(&format!("/users/{}", user.id().value()))
|
||||
.into_response()
|
||||
match resolve_username_to_id::execute(&state.app_ctx.deps.users.resolve_username, &uname).await
|
||||
{
|
||||
Ok(Some(uid)) => {
|
||||
axum::response::Redirect::permanent(&format!("/users/{}", uid.value())).into_response()
|
||||
}
|
||||
_ => StatusCode::NOT_FOUND.into_response(),
|
||||
}
|
||||
@@ -607,10 +563,7 @@ async fn fetch_profile_goals(
|
||||
user_id: Uuid,
|
||||
) -> Vec<template_askama::GoalViewData> {
|
||||
let goals_list = application::goals::list::execute(
|
||||
&application::goals::deps::GoalQueryDeps {
|
||||
goal_query: state.app_ctx.repos.goal_query.clone(),
|
||||
stats: state.app_ctx.repos.stats.clone(),
|
||||
},
|
||||
&state.app_ctx.deps.goals.query,
|
||||
application::goals::queries::ListGoalsQuery { user_id },
|
||||
)
|
||||
.await
|
||||
@@ -647,8 +600,7 @@ pub async fn get_user_profile_html(
|
||||
if accept.contains("application/activity+json") || accept.contains("application/ld+json") {
|
||||
return match state
|
||||
.app_ctx
|
||||
.services
|
||||
.ap_service
|
||||
.ap_document
|
||||
.actor_json(&profile_user_uuid.to_string())
|
||||
.await
|
||||
{
|
||||
@@ -681,25 +633,6 @@ pub async fn get_user_profile_html(
|
||||
}
|
||||
};
|
||||
|
||||
let profile_user = match state
|
||||
.app_ctx
|
||||
.repos
|
||||
.user
|
||||
.find_by_id(&domain::value_objects::UserId::from_uuid(profile_user_uuid))
|
||||
.await
|
||||
{
|
||||
Ok(Some(u)) => u,
|
||||
Ok(None) => return StatusCode::NOT_FOUND.into_response(),
|
||||
Err(e) => return crate::errors::domain_error_response(e),
|
||||
};
|
||||
|
||||
let display_name = profile_user.username().value();
|
||||
ctx.page_title = format!("{}'s Diary — Movies Diary", display_name);
|
||||
ctx.canonical_url = format!(
|
||||
"{}/users/{}",
|
||||
state.app_ctx.config.base_url, profile_user_uuid
|
||||
);
|
||||
|
||||
let sort_by_str = match params.sort_by.as_str() {
|
||||
"date_asc" => "date_asc",
|
||||
"rating" => "rating",
|
||||
@@ -727,19 +660,25 @@ pub async fn get_user_profile_html(
|
||||
include_remote: false,
|
||||
};
|
||||
|
||||
let html_profile_deps = GetProfileDeps {
|
||||
stats: state.app_ctx.repos.stats.clone(),
|
||||
diary: state.app_ctx.repos.diary.clone(),
|
||||
social_query: state.app_ctx.repos.social_query_unified.clone(),
|
||||
};
|
||||
match application::users::get_profile::execute(&html_profile_deps, query).await {
|
||||
match get_local_profile::execute(&state.app_ctx.deps.users.get_local_profile, query).await {
|
||||
Ok(profile) => {
|
||||
ctx.page_title = format!("{}'s Diary — Movies Diary", profile.identity.username);
|
||||
ctx.canonical_url = format!(
|
||||
"{}/users/{}",
|
||||
state.app_ctx.config.base_url, profile_user_uuid
|
||||
);
|
||||
|
||||
let pag = compute_pagination(profile.entries.as_ref());
|
||||
if !is_own_profile {
|
||||
ctx.page_rss_url = Some(format!("/users/{}/feed.rss", profile_user_uuid));
|
||||
}
|
||||
let email = profile_user.email().value().to_string();
|
||||
let display_name = email.split('@').next().unwrap_or("?").to_string();
|
||||
let display_name = profile
|
||||
.identity
|
||||
.email
|
||||
.split('@')
|
||||
.next()
|
||||
.unwrap_or("?")
|
||||
.to_string();
|
||||
let stats_disp = build_stats_display(&profile.stats);
|
||||
let history = profile.history.map(application::users::group_by_month);
|
||||
let heatmap = history.as_deref().map(build_heatmap).unwrap_or_default();
|
||||
@@ -809,6 +748,7 @@ pub async fn get_user_profile_html(
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
Err(DomainError::NotFound(_)) => StatusCode::NOT_FOUND.into_response(),
|
||||
Err(e) => crate::errors::domain_error_response(e),
|
||||
}
|
||||
}
|
||||
@@ -828,43 +768,26 @@ pub async fn get_profile_settings(
|
||||
ctx.page_title = "Profile Settings — Movies Diary".to_string();
|
||||
ctx.canonical_url = format!("{}/settings/profile", state.app_ctx.config.base_url);
|
||||
|
||||
let user = match state.app_ctx.repos.user.find_by_id(&user_id).await {
|
||||
Ok(Some(u)) => u,
|
||||
Ok(None) => return StatusCode::NOT_FOUND.into_response(),
|
||||
let settings = match get_profile_settings_uc::execute(
|
||||
&state.app_ctx.deps.users.get_profile_settings,
|
||||
user_id.value(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(s) => s,
|
||||
Err(DomainError::NotFound(_)) => return StatusCode::NOT_FOUND.into_response(),
|
||||
Err(e) => return crate::errors::domain_error_response(e),
|
||||
};
|
||||
|
||||
let base_url = &state.app_ctx.config.base_url;
|
||||
let avatar_url = user
|
||||
.avatar_path()
|
||||
.map(|path| format!("{}/images/{}", base_url, path));
|
||||
let banner_url = user
|
||||
.banner_path()
|
||||
.map(|path| format!("{}/images/{}", base_url, path));
|
||||
|
||||
let profile_fields: Vec<(String, String)> = state
|
||||
.app_ctx
|
||||
.repos
|
||||
.profile_fields
|
||||
.get_fields(&user_id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|f| (f.name, f.value))
|
||||
.collect();
|
||||
|
||||
let saved = params.saved.as_deref() == Some("1");
|
||||
|
||||
let bio = user.bio().map(|s| s.to_string());
|
||||
let also_known_as = user.also_known_as().map(|s| s.to_string());
|
||||
|
||||
render_page(ProfileSettingsTemplate {
|
||||
ctx: &ctx,
|
||||
bio: bio.as_deref(),
|
||||
avatar_url: avatar_url.as_deref(),
|
||||
banner_url: banner_url.as_deref(),
|
||||
also_known_as: also_known_as.as_deref(),
|
||||
profile_fields: &profile_fields,
|
||||
bio: settings.bio.as_deref(),
|
||||
avatar_url: settings.avatar_url.as_deref(),
|
||||
banner_url: settings.banner_url.as_deref(),
|
||||
also_known_as: settings.also_known_as.as_deref(),
|
||||
profile_fields: &settings.fields,
|
||||
saved,
|
||||
embed_url: format!(
|
||||
"{}/users/{}?embed=true",
|
||||
@@ -892,12 +815,7 @@ pub async fn post_profile_settings(
|
||||
banner_content_type: data.banner_content_type,
|
||||
also_known_as: data.also_known_as,
|
||||
};
|
||||
let update_deps = UpdateProfileDeps {
|
||||
user: state.app_ctx.repos.user.clone(),
|
||||
object_storage: state.app_ctx.services.object_storage.clone(),
|
||||
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
||||
};
|
||||
if let Err(e) = update_profile::execute(&update_deps, cmd).await {
|
||||
if let Err(e) = update_profile::execute(&state.app_ctx.deps.users.update_profile, cmd).await {
|
||||
tracing::error!("update_profile error: {:?}", e);
|
||||
return axum::response::Redirect::to(&format!(
|
||||
"/settings/profile?error={}",
|
||||
@@ -925,12 +843,9 @@ pub async fn post_profile_settings(
|
||||
user_id: user_id.value(),
|
||||
fields,
|
||||
};
|
||||
if let Err(e) = update_profile_fields::execute(
|
||||
state.app_ctx.repos.profile_fields.clone(),
|
||||
state.app_ctx.services.event_publisher.clone(),
|
||||
fields_cmd,
|
||||
)
|
||||
.await
|
||||
if let Err(e) =
|
||||
update_profile_fields::execute(&state.app_ctx.deps.users.update_profile_fields, fields_cmd)
|
||||
.await
|
||||
{
|
||||
tracing::error!("update_profile_fields error: {:?}", e);
|
||||
return axum::response::Redirect::to(&format!(
|
||||
|
||||
@@ -11,8 +11,7 @@ use application::{
|
||||
watchlist::{
|
||||
add as add_to_watchlist,
|
||||
commands::{AddToWatchlistCommand, RemoveFromWatchlistCommand},
|
||||
deps::WatchlistAddDeps,
|
||||
get as get_watchlist, is_on as is_on_watchlist,
|
||||
get as get_watchlist, get_watchlist_for_owner, is_on as is_on_watchlist,
|
||||
queries::{GetWatchlistQuery, IsOnWatchlistQuery},
|
||||
remove as remove_from_watchlist,
|
||||
},
|
||||
@@ -54,7 +53,7 @@ pub async fn get_watchlist_handler(
|
||||
Query(params): Query<PaginationQueryParams>,
|
||||
) -> Result<Json<WatchlistResponse>, ApiError> {
|
||||
let page = get_watchlist::execute(
|
||||
state.app_ctx.repos.watchlist.clone(),
|
||||
&state.app_ctx.deps.watchlist.get_watchlist,
|
||||
GetWatchlistQuery {
|
||||
user_id: user.0.value(),
|
||||
limit: params.limit,
|
||||
@@ -94,15 +93,8 @@ pub async fn post_watchlist_add(
|
||||
user: AuthenticatedUser,
|
||||
Json(req): Json<AddToWatchlistRequest>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let deps = WatchlistAddDeps {
|
||||
movie_command: state.app_ctx.repos.movie_command.clone(),
|
||||
movie_query: state.app_ctx.repos.movie_query.clone(),
|
||||
metadata: state.app_ctx.services.metadata.clone(),
|
||||
watchlist: state.app_ctx.repos.watchlist.clone(),
|
||||
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
||||
};
|
||||
add_to_watchlist::execute(
|
||||
&deps,
|
||||
&state.app_ctx.deps.watchlist.add,
|
||||
AddToWatchlistCommand {
|
||||
user_id: user.0.value(),
|
||||
input: MovieInput {
|
||||
@@ -134,8 +126,7 @@ pub async fn delete_watchlist_entry(
|
||||
Path(movie_id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
remove_from_watchlist::execute(
|
||||
state.app_ctx.repos.watchlist.clone(),
|
||||
state.app_ctx.services.event_publisher.clone(),
|
||||
&state.app_ctx.deps.watchlist.remove_from_watchlist,
|
||||
RemoveFromWatchlistCommand {
|
||||
user_id: user.0.value(),
|
||||
movie_id,
|
||||
@@ -160,7 +151,7 @@ pub async fn get_watchlist_status(
|
||||
Path(movie_id): Path<Uuid>,
|
||||
) -> Result<Json<WatchlistStatusResponse>, ApiError> {
|
||||
let on_watchlist = is_on_watchlist::execute(
|
||||
state.app_ctx.repos.watchlist.clone(),
|
||||
&state.app_ctx.deps.watchlist.is_on_watchlist,
|
||||
IsOnWatchlistQuery {
|
||||
user_id: user.0.value(),
|
||||
movie_id,
|
||||
@@ -182,39 +173,25 @@ pub async fn get_watchlist_page(
|
||||
let ctx = build_page_context(&state, viewer_id.clone(), csrf.0).await;
|
||||
let is_owner = viewer_id.map(|u| u.value() == owner_id).unwrap_or(false);
|
||||
|
||||
let user_id = domain::value_objects::UserId::from_uuid(owner_id);
|
||||
let is_local = state
|
||||
.app_ctx
|
||||
.repos
|
||||
.user
|
||||
.find_by_id(&user_id)
|
||||
.await
|
||||
.map(|u| u.is_some())
|
||||
.unwrap_or(false);
|
||||
let view = match get_watchlist_for_owner::execute(
|
||||
&state.app_ctx.deps.watchlist.get_watchlist_for_owner,
|
||||
owner_id,
|
||||
params.limit.or(Some(20)),
|
||||
params.offset.or(Some(0)),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(view) => view,
|
||||
Err(e) => return crate::errors::domain_error_response(e),
|
||||
};
|
||||
|
||||
let result = if is_local {
|
||||
match get_watchlist::execute(
|
||||
state.app_ctx.repos.watchlist.clone(),
|
||||
application::watchlist::queries::GetWatchlistQuery {
|
||||
user_id: owner_id,
|
||||
limit: params.limit.or(Some(20)),
|
||||
offset: params.offset.or(Some(0)),
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(page) => crate::mappers::watchlist::build_watchlist_page(page, is_owner),
|
||||
Err(e) => return crate::errors::domain_error_response(e),
|
||||
let result = match view {
|
||||
get_watchlist_for_owner::WatchlistView::Local(page) => {
|
||||
crate::mappers::watchlist::build_watchlist_page(page, is_owner)
|
||||
}
|
||||
get_watchlist_for_owner::WatchlistView::Remote(entries) => {
|
||||
crate::mappers::watchlist::build_remote_watchlist_page(entries)
|
||||
}
|
||||
} else {
|
||||
let remote_entries = state
|
||||
.app_ctx
|
||||
.repos
|
||||
.remote_watchlist
|
||||
.get_by_derived_uuid(owner_id)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
crate::mappers::watchlist::build_remote_watchlist_page(remote_entries)
|
||||
};
|
||||
|
||||
render_page(WatchlistTemplate {
|
||||
@@ -280,16 +257,8 @@ pub async fn post_watchlist_add_html(
|
||||
}
|
||||
};
|
||||
|
||||
let deps = WatchlistAddDeps {
|
||||
movie_command: state.app_ctx.repos.movie_command.clone(),
|
||||
movie_query: state.app_ctx.repos.movie_query.clone(),
|
||||
metadata: state.app_ctx.services.metadata.clone(),
|
||||
watchlist: state.app_ctx.repos.watchlist.clone(),
|
||||
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
||||
};
|
||||
|
||||
match add_to_watchlist::execute(
|
||||
&deps,
|
||||
&state.app_ctx.deps.watchlist.add,
|
||||
AddToWatchlistCommand {
|
||||
user_id: user_id.value(),
|
||||
input,
|
||||
@@ -323,8 +292,7 @@ pub async fn post_watchlist_remove_html(
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
}
|
||||
match remove_from_watchlist::execute(
|
||||
state.app_ctx.repos.watchlist.clone(),
|
||||
state.app_ctx.services.event_publisher.clone(),
|
||||
&state.app_ctx.deps.watchlist.remove_from_watchlist,
|
||||
RemoveFromWatchlistCommand {
|
||||
user_id: user_id.value(),
|
||||
movie_id,
|
||||
|
||||
@@ -15,10 +15,9 @@ use application::integrations::{
|
||||
ConfirmWatchEventsCommand, DismissWatchEventsCommand, GenerateWebhookTokenCommand,
|
||||
IngestWatchEventCommand, RevokeWebhookTokenCommand, WatchEventConfirmation,
|
||||
},
|
||||
confirm as confirm_watch_events,
|
||||
deps::IngestWatchEventDeps,
|
||||
dismiss as dismiss_watch_events, generate_token as generate_webhook_token,
|
||||
get_queue as get_watch_queue, get_tokens as get_webhook_tokens, ingest as ingest_watch_event,
|
||||
confirm as confirm_watch_events, dismiss as dismiss_watch_events,
|
||||
generate_token as generate_webhook_token, get_queue as get_watch_queue,
|
||||
get_tokens as get_webhook_tokens, ingest as ingest_watch_event,
|
||||
queries::{GetWatchQueueQuery, GetWebhookTokensQuery},
|
||||
revoke_token as revoke_webhook_token,
|
||||
};
|
||||
@@ -72,7 +71,12 @@ pub async fn post_jellyfin_webhook(
|
||||
source: WatchEventSource::Jellyfin,
|
||||
};
|
||||
|
||||
run_ingest(&state, cmd, &jellyfin::JellyfinParser).await
|
||||
run_ingest(
|
||||
&state,
|
||||
cmd,
|
||||
&*state.app_ctx.deps.integrations.jellyfin_parser,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
// ── Plex webhook (multipart form data with `payload` JSON field) ──────────────
|
||||
@@ -119,7 +123,7 @@ pub async fn post_plex_webhook(
|
||||
source: WatchEventSource::Plex,
|
||||
};
|
||||
|
||||
run_ingest(&state, cmd, &plex::PlexParser).await
|
||||
run_ingest(&state, cmd, &*state.app_ctx.deps.integrations.plex_parser).await
|
||||
}
|
||||
|
||||
async fn run_ingest(
|
||||
@@ -127,13 +131,13 @@ async fn run_ingest(
|
||||
cmd: IngestWatchEventCommand,
|
||||
parser: &dyn domain::ports::MediaServerParser,
|
||||
) -> StatusCode {
|
||||
let deps = IngestWatchEventDeps {
|
||||
webhook_token: state.app_ctx.repos.webhook_token.clone(),
|
||||
watch_event_command: state.app_ctx.repos.watch_event_command.clone(),
|
||||
watch_event_query: state.app_ctx.repos.watch_event_query.clone(),
|
||||
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
||||
};
|
||||
match ingest_watch_event::execute(&deps, cmd, parser).await {
|
||||
match ingest_watch_event::execute(
|
||||
&state.app_ctx.deps.integrations.ingest_watch_event,
|
||||
cmd,
|
||||
parser,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => StatusCode::OK,
|
||||
Err(e) => crate::errors::domain_error_status(&e),
|
||||
}
|
||||
@@ -166,8 +170,11 @@ pub async fn post_generate_webhook_token(
|
||||
label: req.label,
|
||||
};
|
||||
|
||||
let result =
|
||||
generate_webhook_token::execute(state.app_ctx.repos.webhook_token.clone(), cmd).await?;
|
||||
let result = generate_webhook_token::execute(
|
||||
&state.app_ctx.deps.integrations.generate_webhook_token,
|
||||
cmd,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let base_url = &state.app_ctx.config.base_url;
|
||||
let webhook_url = format!("{base_url}/api/v1/webhooks/{provider}");
|
||||
@@ -195,7 +202,8 @@ pub async fn get_webhook_tokens(
|
||||
user_id: user.0.value(),
|
||||
};
|
||||
let tokens =
|
||||
get_webhook_tokens::execute(state.app_ctx.repos.webhook_token.clone(), query).await?;
|
||||
get_webhook_tokens::execute(&state.app_ctx.deps.integrations.get_webhook_tokens, query)
|
||||
.await?;
|
||||
|
||||
let dtos = tokens
|
||||
.into_iter()
|
||||
@@ -230,7 +238,8 @@ pub async fn delete_webhook_token(
|
||||
user_id: user.0.value(),
|
||||
token_id: id,
|
||||
};
|
||||
revoke_webhook_token::execute(state.app_ctx.repos.webhook_token.clone(), cmd).await?;
|
||||
revoke_webhook_token::execute(&state.app_ctx.deps.integrations.revoke_webhook_token, cmd)
|
||||
.await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
@@ -252,7 +261,7 @@ pub async fn get_watch_queue(
|
||||
user_id: user.0.value(),
|
||||
};
|
||||
let events =
|
||||
get_watch_queue::execute(state.app_ctx.repos.watch_event_query.clone(), query).await?;
|
||||
get_watch_queue::execute(&state.app_ctx.deps.integrations.get_watch_queue, query).await?;
|
||||
|
||||
let dtos = events
|
||||
.into_iter()
|
||||
@@ -297,13 +306,9 @@ pub async fn post_confirm_watch_events(
|
||||
.collect(),
|
||||
};
|
||||
|
||||
let confirmed = confirm_watch_events::execute(
|
||||
state.app_ctx.repos.watch_event_command.clone(),
|
||||
state.app_ctx.repos.watch_event_query.clone(),
|
||||
state.app_ctx.services.review_logger.clone(),
|
||||
cmd,
|
||||
)
|
||||
.await?;
|
||||
let confirmed =
|
||||
confirm_watch_events::execute(&state.app_ctx.deps.integrations.confirm_watch_events, cmd)
|
||||
.await?;
|
||||
Ok(Json(ConfirmWatchResponse { confirmed }))
|
||||
}
|
||||
|
||||
@@ -327,11 +332,8 @@ pub async fn post_dismiss_watch_events(
|
||||
event_ids: req.event_ids,
|
||||
};
|
||||
|
||||
let dismissed = dismiss_watch_events::execute(
|
||||
state.app_ctx.repos.watch_event_command.clone(),
|
||||
state.app_ctx.repos.watch_event_query.clone(),
|
||||
cmd,
|
||||
)
|
||||
.await?;
|
||||
let dismissed =
|
||||
dismiss_watch_events::execute(&state.app_ctx.deps.integrations.dismiss_watch_events, cmd)
|
||||
.await?;
|
||||
Ok(Json(DismissWatchResponse { dismissed }))
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ use uuid::Uuid;
|
||||
|
||||
use application::wrapup::{
|
||||
commands::RequestWrapUpCommand,
|
||||
delete as delete_wrapup, generate, get_wrapup,
|
||||
delete as delete_wrapup, generate, get_ready_report, get_wrapup,
|
||||
list_wrapups::{self, ListWrapUpsQuery},
|
||||
};
|
||||
use domain::errors::DomainError;
|
||||
use domain::models::wrapup::{WrapUpRecord, WrapUpReport, WrapUpStatus};
|
||||
use domain::models::wrapup::{WrapUpRecord, WrapUpReport, WrapUpScope, WrapUpStatus};
|
||||
use domain::value_objects::WrapUpId;
|
||||
|
||||
use crate::{
|
||||
@@ -66,12 +66,7 @@ pub async fn post_generate(
|
||||
start_date: start,
|
||||
end_date: end,
|
||||
};
|
||||
let id = generate::execute(
|
||||
state.app_ctx.repos.wrapup_repo.clone(),
|
||||
state.app_ctx.services.event_publisher.clone(),
|
||||
cmd,
|
||||
)
|
||||
.await?;
|
||||
let id = generate::execute(&state.app_ctx.deps.wrapup.generate, cmd).await?;
|
||||
Ok(Json(WrapUpGeneratedResponse {
|
||||
id: id.value().to_string(),
|
||||
}))
|
||||
@@ -90,7 +85,7 @@ pub async fn get_list(
|
||||
user: AuthenticatedUser,
|
||||
) -> Result<Json<WrapUpListResponse>, ApiError> {
|
||||
let records = list_wrapups::execute(
|
||||
state.app_ctx.repos.wrapup_repo.clone(),
|
||||
&state.app_ctx.deps.wrapup.list_wrapups,
|
||||
ListWrapUpsQuery {
|
||||
user_id: Some(user.0.value()),
|
||||
},
|
||||
@@ -117,7 +112,7 @@ pub async fn get_status(
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<WrapUpStatusResponse>, ApiError> {
|
||||
let record = get_wrapup::execute(
|
||||
state.app_ctx.repos.wrapup_repo.clone(),
|
||||
&state.app_ctx.deps.wrapup.get_wrapup,
|
||||
WrapUpId::from_uuid(id),
|
||||
)
|
||||
.await?
|
||||
@@ -142,7 +137,7 @@ pub async fn get_report(
|
||||
Path(id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
match get_wrapup::execute(
|
||||
state.app_ctx.repos.wrapup_repo.clone(),
|
||||
&state.app_ctx.deps.wrapup.get_wrapup,
|
||||
WrapUpId::from_uuid(id),
|
||||
)
|
||||
.await
|
||||
@@ -177,7 +172,7 @@ pub async fn delete_wrapup_handler(
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
delete_wrapup::execute(
|
||||
state.app_ctx.repos.wrapup_repo.clone(),
|
||||
&state.app_ctx.deps.wrapup.delete_wrapup,
|
||||
WrapUpId::from_uuid(id),
|
||||
)
|
||||
.await?;
|
||||
@@ -243,30 +238,15 @@ pub async fn get_user_wrapup_html(
|
||||
Path((user_id, year)): Path<(Uuid, i32)>,
|
||||
Extension(csrf): Extension<CsrfToken>,
|
||||
) -> impl IntoResponse {
|
||||
let start = match NaiveDate::from_ymd_opt(year, 1, 1) {
|
||||
Some(d) => d,
|
||||
None => return StatusCode::BAD_REQUEST.into_response(),
|
||||
};
|
||||
let end = match NaiveDate::from_ymd_opt(year + 1, 1, 1) {
|
||||
Some(d) => d,
|
||||
None => return StatusCode::BAD_REQUEST.into_response(),
|
||||
};
|
||||
|
||||
let record = match state
|
||||
.app_ctx
|
||||
.repos
|
||||
.wrapup_repo
|
||||
.find_existing(Some(user_id), start, end)
|
||||
.await
|
||||
{
|
||||
Ok(Some(r)) if r.status == WrapUpStatus::Ready => r,
|
||||
_ => return StatusCode::NOT_FOUND.into_response(),
|
||||
};
|
||||
|
||||
let report = match record.report {
|
||||
Some(r) => r,
|
||||
None => return StatusCode::NOT_FOUND.into_response(),
|
||||
};
|
||||
let scope = WrapUpScope::User(user_id);
|
||||
let report =
|
||||
match get_ready_report::execute(&state.app_ctx.deps.wrapup.get_ready_report, scope, year)
|
||||
.await
|
||||
{
|
||||
Ok(report) => report,
|
||||
Err(DomainError::ValidationError(_)) => return StatusCode::BAD_REQUEST.into_response(),
|
||||
Err(_) => return StatusCode::NOT_FOUND.into_response(),
|
||||
};
|
||||
|
||||
let ctx = super::helpers::build_page_context(&state, viewer, csrf.0).await;
|
||||
render_wrapup(&report, year, &ctx)
|
||||
@@ -278,29 +258,16 @@ pub async fn get_global_wrapup_html(
|
||||
Path(year): Path<i32>,
|
||||
Extension(csrf): Extension<CsrfToken>,
|
||||
) -> impl IntoResponse {
|
||||
let start = match NaiveDate::from_ymd_opt(year, 1, 1) {
|
||||
Some(d) => d,
|
||||
None => return StatusCode::BAD_REQUEST.into_response(),
|
||||
};
|
||||
let end = match NaiveDate::from_ymd_opt(year + 1, 1, 1) {
|
||||
Some(d) => d,
|
||||
None => return StatusCode::BAD_REQUEST.into_response(),
|
||||
};
|
||||
|
||||
let record = match state
|
||||
.app_ctx
|
||||
.repos
|
||||
.wrapup_repo
|
||||
.find_existing(None, start, end)
|
||||
.await
|
||||
let report = match get_ready_report::execute(
|
||||
&state.app_ctx.deps.wrapup.get_ready_report,
|
||||
WrapUpScope::Global,
|
||||
year,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Some(r)) if r.status == WrapUpStatus::Ready => r,
|
||||
_ => return StatusCode::NOT_FOUND.into_response(),
|
||||
};
|
||||
|
||||
let report = match record.report {
|
||||
Some(r) => r,
|
||||
None => return StatusCode::NOT_FOUND.into_response(),
|
||||
Ok(report) => report,
|
||||
Err(DomainError::ValidationError(_)) => return StatusCode::BAD_REQUEST.into_response(),
|
||||
Err(_) => return StatusCode::NOT_FOUND.into_response(),
|
||||
};
|
||||
|
||||
let ctx = super::helpers::build_page_context(&state, viewer, csrf.0).await;
|
||||
|
||||
Reference in New Issue
Block a user