From 46b8488b09e1e6b7604ffbfaaf8c2283cb6e44a5 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Fri, 10 Jul 2026 16:31:05 +0200 Subject: [PATCH] refactor: Feed uses SocialIdentity matching, deps From impl, remove get_accepted_following_urls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Feed builds FollowingFilter by matching SocialIdentity::Local/Remote instead of URL-prefix heuristic. Removed get_accepted_following_urls from SocialQuery (no longer needed). Added From<&AppState> for deps structs — 20 construction sites collapsed to one-liners. --- .../activitypub/src/social_adapter.rs | 11 -- .../src/diary/get_activity_feed.rs | 20 +-- .../src/diary/tests/get_activity_feed.rs | 89 +++++------ crates/domain/src/ports/noop.rs | 3 - crates/domain/src/ports/social.rs | 4 - crates/domain/src/testing/in_memory.rs | 6 - crates/presentation/src/handlers/social.rs | 138 ++++++------------ 7 files changed, 87 insertions(+), 184 deletions(-) diff --git a/crates/adapters/activitypub/src/social_adapter.rs b/crates/adapters/activitypub/src/social_adapter.rs index 157fb27..856806a 100644 --- a/crates/adapters/activitypub/src/social_adapter.rs +++ b/crates/adapters/activitypub/src/social_adapter.rs @@ -243,15 +243,4 @@ impl SocialQuery for CompositeSocialAdapter { Ok(following.iter().any(|a| a.identity == *target)) } - async fn get_accepted_following_urls( - &self, - user_id: &UserId, - ) -> Result, DomainError> { - let actors = self - .ap_service - .get_following(user_id.value()) - .await - .map_err(ap_err)?; - Ok(actors.into_iter().map(|a| a.url).collect()) - } } diff --git a/crates/application/src/diary/get_activity_feed.rs b/crates/application/src/diary/get_activity_feed.rs index cdadbd3..6f5efe4 100644 --- a/crates/application/src/diary/get_activity_feed.rs +++ b/crates/application/src/diary/get_activity_feed.rs @@ -6,7 +6,7 @@ use domain::{ FeedEntry, collections::{PageParams, Paginated}, }, - value_objects::UserId, + value_objects::{SocialIdentity, UserId}, }; pub async fn execute( @@ -36,28 +36,24 @@ async fn build_following_filter( } let viewer_id = query.viewer_user_id?; let viewer = UserId::from_uuid(viewer_id); - let urls = deps + let actors = deps .social_query - .get_accepted_following_urls(&viewer) + .get_following(&viewer) .await .unwrap_or_default(); - if urls.is_empty() { + if actors.is_empty() { return Some(FollowingFilter { local_user_ids: vec![viewer_id], remote_actor_urls: vec![], }); } - let base_url = &deps.config.base_url; let mut local_ids = vec![viewer_id]; let mut remote_urls = Vec::new(); - for url in urls { - if let Some(suffix) = url.strip_prefix(&format!("{}/users/", base_url)) - && let Ok(parsed_id) = uuid::Uuid::parse_str(suffix) - { - local_ids.push(parsed_id); - continue; + for actor in actors { + match actor.identity { + SocialIdentity::Local(uid) => local_ids.push(uid.value()), + SocialIdentity::Remote { actor_url } => remote_urls.push(actor_url), } - remote_urls.push(url); } Some(FollowingFilter { local_user_ids: local_ids, diff --git a/crates/application/src/diary/tests/get_activity_feed.rs b/crates/application/src/diary/tests/get_activity_feed.rs index ab60583..512425d 100644 --- a/crates/application/src/diary/tests/get_activity_feed.rs +++ b/crates/application/src/diary/tests/get_activity_feed.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use async_trait::async_trait; use domain::errors::DomainError; use domain::testing::InMemorySocialRepository; -use domain::value_objects::SocialActor; +use domain::value_objects::{SocialActor, SocialIdentity, UserId}; use crate::{ config::AppConfig, diary::deps::GetActivityFeedDeps, diary::get_activity_feed, @@ -63,72 +63,59 @@ async fn returns_feed_with_following_filter() { assert!(result.items.is_empty()); } -struct FakeSocialWithFollowing(Vec); +struct FakeSocialWithFollowing(Vec); #[async_trait] impl domain::ports::SocialQuery for FakeSocialWithFollowing { async fn get_following( &self, - _: &domain::value_objects::UserId, + _: &UserId, ) -> Result, DomainError> { - Ok(vec![]) - } - async fn get_followers( - &self, - _: &domain::value_objects::UserId, - ) -> Result, DomainError> { - Ok(vec![]) - } - async fn get_pending_followers( - &self, - _: &domain::value_objects::UserId, - ) -> Result, DomainError> { - Ok(vec![]) - } - async fn count_following( - &self, - _: &domain::value_objects::UserId, - ) -> Result { - Ok(0) - } - async fn count_followers( - &self, - _: &domain::value_objects::UserId, - ) -> Result { - Ok(0) - } - async fn get_blocked( - &self, - _: &domain::value_objects::UserId, - ) -> Result, DomainError> { - Ok(vec![]) - } - async fn is_following( - &self, - _: &domain::value_objects::UserId, - _: &domain::value_objects::SocialIdentity, - ) -> Result { - Ok(false) - } - async fn get_accepted_following_urls( - &self, - _: &domain::value_objects::UserId, - ) -> Result, DomainError> { Ok(self.0.clone()) } + async fn get_followers(&self, _: &UserId) -> Result, DomainError> { + Ok(vec![]) + } + async fn get_pending_followers(&self, _: &UserId) -> Result, DomainError> { + Ok(vec![]) + } + async fn count_following(&self, _: &UserId) -> Result { + Ok(0) + } + async fn count_followers(&self, _: &UserId) -> Result { + Ok(0) + } + async fn get_blocked(&self, _: &UserId) -> Result, DomainError> { + Ok(vec![]) + } + async fn is_following(&self, _: &UserId, _: &SocialIdentity) -> Result { + Ok(false) + } } #[tokio::test] -async fn following_filter_parses_local_and_remote_urls() { +async fn following_filter_separates_local_and_remote() { let viewer = uuid::Uuid::new_v4(); let local_friend = uuid::Uuid::new_v4(); - let following_urls = vec![ - format!("http://localhost:3000/users/{}", local_friend), - "https://remote.example/actor/1".to_string(), + let following = vec![ + SocialActor { + identity: SocialIdentity::Local(UserId::from_uuid(local_friend)), + handle: "friend".into(), + display_name: None, + avatar_url: None, + }, + SocialActor { + identity: SocialIdentity::Remote { + actor_url: "https://remote.example/actor/1".into(), + }, + handle: "@alice@remote.example".into(), + display_name: None, + avatar_url: None, + }, ]; - let social = Arc::new(FakeSocialWithFollowing(following_urls)); + let social = Arc::new(FakeSocialWithFollowing(following)); let deps = GetActivityFeedDeps { diary: domain::testing::FakeDiaryQuery::new() as _, diff --git a/crates/domain/src/ports/noop.rs b/crates/domain/src/ports/noop.rs index ca0f202..670a0af 100644 --- a/crates/domain/src/ports/noop.rs +++ b/crates/domain/src/ports/noop.rs @@ -91,9 +91,6 @@ impl super::SocialQuery for NoopSocialQuery { async fn is_following(&self, _: &UserId, _: &SocialIdentity) -> Result { Ok(false) } - async fn get_accepted_following_urls(&self, _: &UserId) -> Result, DomainError> { - Ok(vec![]) - } } // ── NoopFederationAdminQuery ───────────────────────────────────────────────── diff --git a/crates/domain/src/ports/social.rs b/crates/domain/src/ports/social.rs index e5ee595..dd6b196 100644 --- a/crates/domain/src/ports/social.rs +++ b/crates/domain/src/ports/social.rs @@ -62,10 +62,6 @@ pub trait SocialQuery: Send + Sync { target: &SocialIdentity, ) -> Result; - async fn get_accepted_following_urls( - &self, - user_id: &UserId, - ) -> Result, DomainError>; } #[async_trait] diff --git a/crates/domain/src/testing/in_memory.rs b/crates/domain/src/testing/in_memory.rs index 1933771..2568868 100644 --- a/crates/domain/src/testing/in_memory.rs +++ b/crates/domain/src/testing/in_memory.rs @@ -1106,10 +1106,4 @@ impl SocialQuery for InMemorySocialRepository { })) } - async fn get_accepted_following_urls( - &self, - _user_id: &UserId, - ) -> Result, DomainError> { - Ok(vec![]) - } } diff --git a/crates/presentation/src/handlers/social.rs b/crates/presentation/src/handlers/social.rs index aa29a85..ce9709c 100644 --- a/crates/presentation/src/handlers/social.rs +++ b/crates/presentation/src/handlers/social.rs @@ -30,6 +30,24 @@ use template_askama::{ 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()) @@ -166,11 +184,7 @@ pub async fn block_actor_api( user: AuthenticatedUser, axum::Json(body): axum::Json, ) -> Result { - let deps = SocialCommandDeps { - 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(), - }; + let deps = SocialCommandDeps::from(&state); application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::Block { @@ -198,11 +212,7 @@ pub async fn unblock_actor_api( user: AuthenticatedUser, axum::Json(body): axum::Json, ) -> Result { - let deps = SocialCommandDeps { - 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(), - }; + let deps = SocialCommandDeps::from(&state); application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::Unblock { @@ -228,9 +238,7 @@ pub async fn get_blocked_actors_api( State(state): State, user: AuthenticatedUser, ) -> Result>, ApiError> { - let deps = SocialQueryDeps { - social_query: state.app_ctx.repos.social_query_unified.clone(), - }; + let deps = SocialQueryDeps::from(&state); let identities = application::social::execute::execute_query( &deps, application::social::queries::SocialQry::GetBlocked { @@ -258,9 +266,7 @@ pub async fn get_following( State(state): State, user: AuthenticatedUser, ) -> Result, ApiError> { - let deps = SocialQueryDeps { - social_query: state.app_ctx.repos.social_query_unified.clone(), - }; + let deps = SocialQueryDeps::from(&state); let identities = application::social::execute::execute_query( &deps, application::social::queries::SocialQry::GetFollowing { @@ -285,9 +291,7 @@ pub async fn get_followers( State(state): State, user: AuthenticatedUser, ) -> Result, ApiError> { - let deps = SocialQueryDeps { - social_query: state.app_ctx.repos.social_query_unified.clone(), - }; + let deps = SocialQueryDeps::from(&state); let identities = application::social::execute::execute_query( &deps, application::social::queries::SocialQry::GetFollowers { @@ -305,9 +309,7 @@ pub async fn get_user_following( _user: AuthenticatedUser, Path(user_id): Path, ) -> Result, ApiError> { - let deps = SocialQueryDeps { - social_query: state.app_ctx.repos.social_query_unified.clone(), - }; + let deps = SocialQueryDeps::from(&state); let identities = application::social::execute::execute_query( &deps, application::social::queries::SocialQry::GetFollowing { user_id }, @@ -323,9 +325,7 @@ pub async fn get_user_followers( _user: AuthenticatedUser, Path(user_id): Path, ) -> Result, ApiError> { - let deps = SocialQueryDeps { - social_query: state.app_ctx.repos.social_query_unified.clone(), - }; + let deps = SocialQueryDeps::from(&state); let identities = application::social::execute::execute_query( &deps, application::social::queries::SocialQry::GetFollowers { user_id }, @@ -350,11 +350,7 @@ pub async fn follow( user: AuthenticatedUser, Json(body): Json, ) -> Result { - let deps = SocialCommandDeps { - 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(), - }; + let deps = SocialCommandDeps::from(&state); application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::Follow { @@ -380,11 +376,7 @@ pub async fn unfollow( user: AuthenticatedUser, Json(body): Json, ) -> Result { - let deps = SocialCommandDeps { - 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(), - }; + let deps = SocialCommandDeps::from(&state); application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::Unfollow { @@ -412,11 +404,7 @@ pub async fn accept_follower( user: AuthenticatedUser, Json(body): Json, ) -> Result { - let deps = SocialCommandDeps { - 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(), - }; + let deps = SocialCommandDeps::from(&state); application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::AcceptFollow { @@ -444,11 +432,7 @@ pub async fn reject_follower( user: AuthenticatedUser, Json(body): Json, ) -> Result { - let deps = SocialCommandDeps { - 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(), - }; + let deps = SocialCommandDeps::from(&state); application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::RejectFollow { @@ -476,11 +460,7 @@ pub async fn remove_follower( user: AuthenticatedUser, Json(body): Json, ) -> Result { - let deps = SocialCommandDeps { - 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(), - }; + let deps = SocialCommandDeps::from(&state); application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::RemoveFollower { @@ -506,9 +486,7 @@ pub async fn get_pending_followers( State(state): State, user: AuthenticatedUser, ) -> Result, ApiError> { - let deps = SocialQueryDeps { - social_query: state.app_ctx.repos.social_query_unified.clone(), - }; + let deps = SocialQueryDeps::from(&state); let identities = application::social::execute::execute_query( &deps, application::social::queries::SocialQry::GetPending { @@ -543,11 +521,7 @@ pub async fn follow_remote_user( .unwrap_or(&format!("/users/{}", profile_user_uuid)) .to_string(); - let deps = SocialCommandDeps { - 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(), - }; + let deps = SocialCommandDeps::from(&state); match application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::Follow { @@ -584,11 +558,7 @@ pub async fn unfollow_remote_user( if crate::csrf::mismatch(&csrf, &form.csrf_token) { return StatusCode::FORBIDDEN.into_response(); } - let deps = SocialCommandDeps { - 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(), - }; + let deps = SocialCommandDeps::from(&state); match application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::Unfollow { @@ -627,11 +597,7 @@ pub async fn accept_follower_html( if crate::csrf::mismatch(&csrf, &form.csrf_token) { return StatusCode::FORBIDDEN.into_response(); } - let deps = SocialCommandDeps { - 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(), - }; + let deps = SocialCommandDeps::from(&state); match application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::AcceptFollow { @@ -664,11 +630,7 @@ pub async fn reject_follower_html( if crate::csrf::mismatch(&csrf, &form.csrf_token) { return StatusCode::FORBIDDEN.into_response(); } - let deps = SocialCommandDeps { - 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(), - }; + let deps = SocialCommandDeps::from(&state); match application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::RejectFollow { @@ -770,9 +732,7 @@ pub async fn get_following_page( "{}/users/{}/following-list", state.app_ctx.config.base_url, profile_user_uuid ); - let deps = SocialQueryDeps { - social_query: state.app_ctx.repos.social_query_unified.clone(), - }; + let deps = SocialQueryDeps::from(&state); match application::social::execute::execute_query( &deps, application::social::queries::SocialQry::GetFollowing { @@ -821,9 +781,7 @@ pub async fn get_followers_page( "{}/users/{}/followers-list", state.app_ctx.config.base_url, profile_user_uuid ); - let deps = SocialQueryDeps { - social_query: state.app_ctx.repos.social_query_unified.clone(), - }; + let deps = SocialQueryDeps::from(&state); match application::social::execute::execute_query( &deps, application::social::queries::SocialQry::GetFollowers { @@ -869,11 +827,7 @@ pub async fn remove_follower_html( if crate::csrf::mismatch(&csrf, &form.csrf_token) { return StatusCode::FORBIDDEN.into_response(); } - let deps = SocialCommandDeps { - 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(), - }; + let deps = SocialCommandDeps::from(&state); match application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::RemoveFollower { @@ -997,9 +951,7 @@ 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 { - social_query: state.app_ctx.repos.social_query_unified.clone(), - }; + let deps = SocialQueryDeps::from(&state); match application::social::execute::execute_query( &deps, application::social::queries::SocialQry::GetBlocked { @@ -1044,11 +996,7 @@ pub async fn post_block_actor_html( if crate::csrf::mismatch(&csrf, &form.csrf_token) { return StatusCode::FORBIDDEN.into_response(); } - let deps = SocialCommandDeps { - 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(), - }; + let deps = SocialCommandDeps::from(&state); match application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::Block { @@ -1077,11 +1025,7 @@ pub async fn post_unblock_actor( if crate::csrf::mismatch(&csrf, &form.csrf_token) { return StatusCode::FORBIDDEN.into_response(); } - let deps = SocialCommandDeps { - 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(), - }; + let deps = SocialCommandDeps::from(&state); match application::social::execute::execute_command( &deps, application::social::commands::SocialCmd::Unblock {