diff --git a/crates/adapters/activitypub/src/social_adapter.rs b/crates/adapters/activitypub/src/social_adapter.rs index 92cf4ad..f48b1ef 100644 --- a/crates/adapters/activitypub/src/social_adapter.rs +++ b/crates/adapters/activitypub/src/social_adapter.rs @@ -4,9 +4,11 @@ use async_trait::async_trait; use domain::{ errors::DomainError, ports::{SocialCommand, SocialQuery, UserRepository}, - value_objects::{SocialIdentity, UserId}, + value_objects::{SocialActor, SocialIdentity, UserId}, }; +use k_ap::RemoteActor; + use super::ActivityPubPort; pub struct CompositeSocialAdapter { @@ -51,6 +53,16 @@ impl CompositeSocialAdapter { } } + fn remote_actor_to_social_actor(&self, actor: RemoteActor) -> SocialActor { + let identity = self.identity_from_actor_url(&actor.url); + SocialActor { + identity, + handle: actor.handle, + display_name: actor.display_name, + avatar_url: actor.avatar_url, + } + } + async fn resolve_handle(&self, identity: &SocialIdentity) -> Result { match identity { SocialIdentity::Local(uid) => { @@ -172,7 +184,7 @@ impl SocialQuery for CompositeSocialAdapter { async fn get_following( &self, user: &UserId, - ) -> Result, DomainError> { + ) -> Result, DomainError> { let actors = self .ap_service .get_following(user.value()) @@ -180,14 +192,14 @@ impl SocialQuery for CompositeSocialAdapter { .map_err(ap_err)?; Ok(actors .into_iter() - .map(|a| self.identity_from_actor_url(&a.url)) + .map(|a| self.remote_actor_to_social_actor(a)) .collect()) } async fn get_followers( &self, user: &UserId, - ) -> Result, DomainError> { + ) -> Result, DomainError> { let actors = self .ap_service .get_accepted_followers(user.value()) @@ -195,14 +207,14 @@ impl SocialQuery for CompositeSocialAdapter { .map_err(ap_err)?; Ok(actors .into_iter() - .map(|a| self.identity_from_actor_url(&a.url)) + .map(|a| self.remote_actor_to_social_actor(a)) .collect()) } async fn get_pending_followers( &self, user: &UserId, - ) -> Result, DomainError> { + ) -> Result, DomainError> { let actors = self .ap_service .get_pending_followers(user.value()) @@ -210,7 +222,7 @@ impl SocialQuery for CompositeSocialAdapter { .map_err(ap_err)?; Ok(actors .into_iter() - .map(|a| self.identity_from_actor_url(&a.url)) + .map(|a| self.remote_actor_to_social_actor(a)) .collect()) } @@ -231,7 +243,7 @@ impl SocialQuery for CompositeSocialAdapter { async fn get_blocked( &self, user: &UserId, - ) -> Result, DomainError> { + ) -> Result, DomainError> { let actors = self .ap_service .get_blocked_actors(user.value()) @@ -239,7 +251,7 @@ impl SocialQuery for CompositeSocialAdapter { .map_err(ap_err)?; Ok(actors .into_iter() - .map(|a| self.identity_from_actor_url(&a.url)) + .map(|a| self.remote_actor_to_social_actor(a)) .collect()) } @@ -249,6 +261,24 @@ impl SocialQuery for CompositeSocialAdapter { target: &SocialIdentity, ) -> Result { let following = self.get_following(follower).await?; - Ok(following.contains(target)) + 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()) + } + + async fn list_all_followed_remote_actors( + &self, + ) -> Result, DomainError> { + Ok(vec![]) } } diff --git a/crates/application/src/diary/deps.rs b/crates/application/src/diary/deps.rs index a7d7832..946b3a1 100644 --- a/crates/application/src/diary/deps.rs +++ b/crates/application/src/diary/deps.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use domain::ports::{ DiaryQuery, EventPublisher, MovieCommand, MovieProfileRepository, MovieQuery, ReviewRepository, - SocialQueryPort, + SocialQuery, }; use crate::config::AppConfig; @@ -27,6 +27,6 @@ pub struct GetMovieSocialPageDeps { pub struct GetActivityFeedDeps { pub diary: Arc, - pub social_query: Arc, + pub social_query: Arc, pub config: AppConfig, } diff --git a/crates/application/src/diary/tests/get_activity_feed.rs b/crates/application/src/diary/tests/get_activity_feed.rs index e677d61..e99c48d 100644 --- a/crates/application/src/diary/tests/get_activity_feed.rs +++ b/crates/application/src/diary/tests/get_activity_feed.rs @@ -2,7 +2,8 @@ use std::sync::Arc; use async_trait::async_trait; use domain::errors::DomainError; -use domain::testing::{FakeDiaryQuery, NoopSocialQueryPort}; +use domain::testing::InMemorySocialRepository; +use domain::value_objects::SocialActor; use crate::{ config::AppConfig, diary::deps::GetActivityFeedDeps, diary::get_activity_feed, @@ -11,8 +12,8 @@ use crate::{ fn default_deps() -> GetActivityFeedDeps { GetActivityFeedDeps { - diary: FakeDiaryQuery::new() as _, - social_query: Arc::new(NoopSocialQueryPort), + diary: domain::testing::FakeDiaryQuery::new() as _, + social_query: InMemorySocialRepository::new() as _, config: TestContextBuilder::new().config, } } @@ -59,20 +60,30 @@ async fn returns_feed_with_following_filter() { .await .unwrap(); - // NoopSocialQueryPort returns empty following, so FollowingFilter - // contains only the viewer's id. Feed is empty but the code path is hit. assert!(result.items.is_empty()); } struct FakeSocialWithFollowing(Vec); #[async_trait] -impl domain::ports::SocialQueryPort for FakeSocialWithFollowing { - async fn get_accepted_following_urls( +impl domain::ports::SocialQuery for FakeSocialWithFollowing { + async fn get_following( &self, _: &domain::value_objects::UserId, - ) -> Result, DomainError> { - Ok(self.0.clone()) + ) -> 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, @@ -80,18 +91,31 @@ impl domain::ports::SocialQueryPort for FakeSocialWithFollowing { ) -> Result { Ok(0) } - async fn count_accepted_followers( + async fn count_followers( &self, _: &domain::value_objects::UserId, ) -> Result { Ok(0) } - async fn get_pending_followers( + async fn get_blocked( &self, _: &domain::value_objects::UserId, - ) -> Result, DomainError> { + ) -> 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 list_all_followed_remote_actors( &self, ) -> Result, DomainError> { @@ -112,7 +136,7 @@ async fn following_filter_parses_local_and_remote_urls() { let social = Arc::new(FakeSocialWithFollowing(following_urls)); let deps = GetActivityFeedDeps { - diary: FakeDiaryQuery::new() as _, + diary: domain::testing::FakeDiaryQuery::new() as _, social_query: social as _, config: AppConfig { allow_registration: true, @@ -141,8 +165,6 @@ async fn following_filter_parses_local_and_remote_urls() { .await .unwrap(); - // Feed is empty (no data seeded), but the build_following_filter code path - // with actual URL parsing ran without errors. assert!(result.items.is_empty()); } @@ -164,6 +186,5 @@ async fn following_filter_without_viewer_returns_none() { .await .unwrap(); - // filter_following=true but viewer_user_id=None → build_following_filter returns None assert!(result.items.is_empty()); } diff --git a/crates/application/src/social/get_blocked.rs b/crates/application/src/social/get_blocked.rs index ac7eee3..528fe5e 100644 --- a/crates/application/src/social/get_blocked.rs +++ b/crates/application/src/social/get_blocked.rs @@ -1,11 +1,11 @@ -use domain::{errors::DomainError, value_objects::{SocialIdentity, UserId}}; +use domain::{errors::DomainError, value_objects::{SocialActor, UserId}}; use super::{deps::SocialQueryDeps, queries::GetBlockedQuery}; pub async fn execute( deps: &SocialQueryDeps, query: GetBlockedQuery, -) -> Result, DomainError> { +) -> Result, DomainError> { let user_id = UserId::from_uuid(query.user_id); deps.social_query.get_blocked(&user_id).await } diff --git a/crates/application/src/social/get_followers.rs b/crates/application/src/social/get_followers.rs index 9b619b6..b743455 100644 --- a/crates/application/src/social/get_followers.rs +++ b/crates/application/src/social/get_followers.rs @@ -1,11 +1,11 @@ -use domain::{errors::DomainError, value_objects::{SocialIdentity, UserId}}; +use domain::{errors::DomainError, value_objects::{SocialActor, UserId}}; use super::{deps::SocialQueryDeps, queries::GetFollowersQuery}; pub async fn execute( deps: &SocialQueryDeps, query: GetFollowersQuery, -) -> Result, DomainError> { +) -> Result, DomainError> { let user_id = UserId::from_uuid(query.user_id); deps.social_query.get_followers(&user_id).await } diff --git a/crates/application/src/social/get_following.rs b/crates/application/src/social/get_following.rs index 51be132..c024c4e 100644 --- a/crates/application/src/social/get_following.rs +++ b/crates/application/src/social/get_following.rs @@ -1,11 +1,11 @@ -use domain::{errors::DomainError, value_objects::{SocialIdentity, UserId}}; +use domain::{errors::DomainError, value_objects::{SocialActor, UserId}}; use super::{deps::SocialQueryDeps, queries::GetFollowingQuery}; pub async fn execute( deps: &SocialQueryDeps, query: GetFollowingQuery, -) -> Result, DomainError> { +) -> Result, DomainError> { let user_id = UserId::from_uuid(query.user_id); deps.social_query.get_following(&user_id).await } diff --git a/crates/application/src/social/get_pending.rs b/crates/application/src/social/get_pending.rs index a375f40..319af4c 100644 --- a/crates/application/src/social/get_pending.rs +++ b/crates/application/src/social/get_pending.rs @@ -1,11 +1,11 @@ -use domain::{errors::DomainError, value_objects::{SocialIdentity, UserId}}; +use domain::{errors::DomainError, value_objects::{SocialActor, UserId}}; use super::{deps::SocialQueryDeps, queries::GetPendingFollowersQuery}; pub async fn execute( deps: &SocialQueryDeps, query: GetPendingFollowersQuery, -) -> Result, DomainError> { +) -> Result, DomainError> { let user_id = UserId::from_uuid(query.user_id); deps.social_query.get_pending_followers(&user_id).await } diff --git a/crates/application/src/users/deps.rs b/crates/application/src/users/deps.rs index caaecb1..fb85f52 100644 --- a/crates/application/src/users/deps.rs +++ b/crates/application/src/users/deps.rs @@ -1,13 +1,19 @@ use std::sync::Arc; use domain::ports::{ - DiaryQuery, EventPublisher, ObjectStorage, SocialQueryPort, StatsRepository, UserRepository, + DiaryQuery, EventPublisher, ObjectStorage, SocialQuery, SocialQueryPort, StatsRepository, + UserRepository, }; pub struct GetProfileDeps { pub stats: Arc, pub diary: Arc, - pub social_query: Arc, + pub social_query: Arc, +} + +pub struct GetUsersListDeps { + pub user: Arc, + pub social_query_legacy: Arc, } pub struct UpdateProfileDeps { diff --git a/crates/application/src/users/get_profile.rs b/crates/application/src/users/get_profile.rs index 4b6ec56..5c6a078 100644 --- a/crates/application/src/users/get_profile.rs +++ b/crates/application/src/users/get_profile.rs @@ -86,7 +86,7 @@ async fn load_social_counts( .unwrap_or(0); let followers = deps .social_query - .count_accepted_followers(user_id) + .count_followers(user_id) .await .unwrap_or(0); if !is_own_profile { @@ -98,11 +98,19 @@ async fn load_social_counts( .await .unwrap_or_default() .into_iter() - .map(|p| PendingFollowerView { - url: p.url, - handle: p.handle, - display_name: p.display_name, - avatar_url: p.avatar_url, + .map(|p| { + let url = match &p.identity { + domain::value_objects::SocialIdentity::Remote { actor_url } => actor_url.clone(), + domain::value_objects::SocialIdentity::Local(uid) => { + format!("local:{}", uid.value()) + } + }; + PendingFollowerView { + url, + handle: p.handle, + display_name: p.display_name, + avatar_url: p.avatar_url, + } }) .collect(); (following, followers, pending) diff --git a/crates/application/src/users/get_users.rs b/crates/application/src/users/get_users.rs index 498ed04..ed0b430 100644 --- a/crates/application/src/users/get_users.rs +++ b/crates/application/src/users/get_users.rs @@ -1,10 +1,7 @@ -use std::sync::Arc; - -use crate::users::queries::GetUsersQuery; +use crate::users::{deps::GetUsersListDeps, queries::GetUsersQuery}; use domain::{ errors::DomainError, models::{RemoteActorInfo, UserSummary}, - ports::{SocialQueryPort, UserRepository}, }; pub struct UsersListData { @@ -13,13 +10,12 @@ pub struct UsersListData { } pub async fn execute( - user: Arc, - social_query: Arc, + deps: &GetUsersListDeps, _query: GetUsersQuery, ) -> Result { let (users_result, actors_result) = tokio::join!( - user.list_with_stats(), - social_query.list_all_followed_remote_actors() + deps.user.list_with_stats(), + deps.social_query_legacy.list_all_followed_remote_actors() ); Ok(UsersListData { @@ -27,7 +23,3 @@ pub async fn execute( remote_actors: actors_result?, }) } - -#[cfg(test)] -#[path = "tests/get_users.rs"] -mod tests; diff --git a/crates/application/src/users/tests/get_profile.rs b/crates/application/src/users/tests/get_profile.rs index a5ee7b9..52be317 100644 --- a/crates/application/src/users/tests/get_profile.rs +++ b/crates/application/src/users/tests/get_profile.rs @@ -35,7 +35,7 @@ async fn returns_profile_with_empty_stats() { let deps = GetProfileDeps { stats: b.stats_repo.clone(), diary: b.diary_repo.clone(), - social_query: b.social_query.clone(), + social_query: b.social_query_unified.clone(), }; setup_user(&b, "profile@test.com", "profuser").await; @@ -70,7 +70,7 @@ async fn returns_history_view() { let deps = GetProfileDeps { stats: b.stats_repo.clone(), diary: b.diary_repo.clone(), - social_query: b.social_query.clone(), + social_query: b.social_query_unified.clone(), }; setup_user(&b, "hist@test.com", "histuser").await; @@ -107,7 +107,7 @@ async fn returns_trends_view() { let deps = GetProfileDeps { stats: b.stats_repo.clone(), diary: b.diary_repo.clone(), - social_query: b.social_query.clone(), + social_query: b.social_query_unified.clone(), }; setup_user(&b, "trends@test.com", "trendsuser").await; @@ -144,7 +144,7 @@ async fn returns_ratings_view() { let deps = GetProfileDeps { stats: b.stats_repo.clone(), diary: b.diary_repo.clone(), - social_query: b.social_query.clone(), + social_query: b.social_query_unified.clone(), }; setup_user(&b, "ratings@test.com", "ratingsuser").await; @@ -179,7 +179,7 @@ async fn returns_recent_with_search() { let deps = GetProfileDeps { stats: b.stats_repo.clone(), diary: b.diary_repo.clone(), - social_query: b.social_query.clone(), + social_query: b.social_query_unified.clone(), }; setup_user(&b, "search@test.com", "searchuser").await; @@ -214,7 +214,7 @@ async fn non_own_profile_skips_pending_followers() { let deps = GetProfileDeps { stats: b.stats_repo.clone(), diary: b.diary_repo.clone(), - social_query: b.social_query.clone(), + social_query: b.social_query_unified.clone(), }; setup_user(&b, "other@test.com", "otheruser").await; diff --git a/crates/domain/src/ports/noop.rs b/crates/domain/src/ports/noop.rs index 270dbbf..e09b754 100644 --- a/crates/domain/src/ports/noop.rs +++ b/crates/domain/src/ports/noop.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; use crate::{ errors::DomainError, - value_objects::{SocialIdentity, UserId}, + value_objects::{SocialActor, SocialIdentity, UserId}, }; // ── NoopRemoteWatchlistRepository ───────────────────────────────────────────── @@ -70,13 +70,13 @@ pub struct NoopSocialQuery; #[async_trait] impl super::SocialQuery for NoopSocialQuery { - async fn get_following(&self, _: &UserId) -> Result, DomainError> { + async fn get_following(&self, _: &UserId) -> Result, DomainError> { Ok(vec![]) } - async fn get_followers(&self, _: &UserId) -> Result, DomainError> { + async fn get_followers(&self, _: &UserId) -> Result, DomainError> { Ok(vec![]) } - async fn get_pending_followers(&self, _: &UserId) -> Result, DomainError> { + async fn get_pending_followers(&self, _: &UserId) -> Result, DomainError> { Ok(vec![]) } async fn count_following(&self, _: &UserId) -> Result { @@ -85,12 +85,20 @@ impl super::SocialQuery for NoopSocialQuery { async fn count_followers(&self, _: &UserId) -> Result { Ok(0) } - async fn get_blocked(&self, _: &UserId) -> Result, DomainError> { + async fn get_blocked(&self, _: &UserId) -> Result, DomainError> { Ok(vec![]) } async fn is_following(&self, _: &UserId, _: &SocialIdentity) -> Result { Ok(false) } + async fn get_accepted_following_urls(&self, _: &UserId) -> Result, DomainError> { + Ok(vec![]) + } + async fn list_all_followed_remote_actors( + &self, + ) -> Result, DomainError> { + Ok(vec![]) + } } // ── NoopSocialQueryPort ─────────────────────────────────────────────────────── diff --git a/crates/domain/src/ports/social.rs b/crates/domain/src/ports/social.rs index 482949f..5d2fe1d 100644 --- a/crates/domain/src/ports/social.rs +++ b/crates/domain/src/ports/social.rs @@ -7,7 +7,7 @@ use crate::{ DiaryEntry, FederationFlags, PendingFollowerInfo, RemoteActorInfo, RemoteGoalEntry, RemoteWatchlistEntry, WatchlistWithMovie, }, - value_objects::{MovieId, SocialIdentity, UserId}, + value_objects::{MovieId, SocialActor, SocialIdentity, UserId}, }; // ── Unified social ports (ADR-0002) ───────────────────────────────────────── @@ -62,17 +62,17 @@ pub trait SocialQuery: Send + Sync { async fn get_following( &self, user: &UserId, - ) -> Result, DomainError>; + ) -> Result, DomainError>; async fn get_followers( &self, user: &UserId, - ) -> Result, DomainError>; + ) -> Result, DomainError>; async fn get_pending_followers( &self, user: &UserId, - ) -> Result, DomainError>; + ) -> Result, DomainError>; async fn count_following(&self, user: &UserId) -> Result; @@ -81,13 +81,22 @@ pub trait SocialQuery: Send + Sync { async fn get_blocked( &self, user: &UserId, - ) -> Result, DomainError>; + ) -> Result, DomainError>; async fn is_following( &self, follower: &UserId, target: &SocialIdentity, ) -> Result; + + async fn get_accepted_following_urls( + &self, + user_id: &UserId, + ) -> Result, DomainError>; + + async fn list_all_followed_remote_actors( + &self, + ) -> Result, DomainError>; } // ── Legacy ports (pre-unification, still used by AP adapter + handlers) ───── diff --git a/crates/domain/src/testing/in_memory.rs b/crates/domain/src/testing/in_memory.rs index 69728d0..8c86f2c 100644 --- a/crates/domain/src/testing/in_memory.rs +++ b/crates/domain/src/testing/in_memory.rs @@ -25,7 +25,8 @@ use crate::{ }, value_objects::{ Email, ExternalMetadataId, GoalId, ImportProfileId, ImportSessionId, MovieId, MovieTitle, - ReleaseYear, ReviewId, SocialIdentity, UserId, Username, WatchEventId, WebhookTokenId, + ReleaseYear, ReviewId, SocialActor, SocialIdentity, UserId, Username, WatchEventId, + WebhookTokenId, }, }; @@ -875,6 +876,19 @@ impl InMemorySocialRepository { blocked: Mutex::new(Vec::new()), }) } + + fn identity_to_actor(identity: &SocialIdentity) -> SocialActor { + let handle = match identity { + SocialIdentity::Local(uid) => format!("user-{}", uid.value()), + SocialIdentity::Remote { actor_url } => actor_url.clone(), + }; + SocialActor { + identity: identity.clone(), + handle, + display_name: None, + avatar_url: None, + } + } } #[async_trait] @@ -1023,38 +1037,44 @@ impl SocialQuery for InMemorySocialRepository { async fn get_following( &self, user: &UserId, - ) -> Result, DomainError> { + ) -> Result, DomainError> { let store = self.follows.lock().unwrap(); Ok(store .iter() .filter(|(f, _, state)| *f == user.value() && *state == FollowState::Accepted) - .map(|(_, t, _)| t.clone()) + .map(|(_, t, _)| Self::identity_to_actor(t)) .collect()) } async fn get_followers( &self, user: &UserId, - ) -> Result, DomainError> { + ) -> Result, DomainError> { let store = self.follows.lock().unwrap(); let target = SocialIdentity::Local(user.clone()); Ok(store .iter() .filter(|(_, t, state)| *t == target && *state == FollowState::Accepted) - .map(|(f, _, _)| SocialIdentity::Local(UserId::from_uuid(*f))) + .map(|(f, _, _)| { + let id = SocialIdentity::Local(UserId::from_uuid(*f)); + Self::identity_to_actor(&id) + }) .collect()) } async fn get_pending_followers( &self, user: &UserId, - ) -> Result, DomainError> { + ) -> Result, DomainError> { let store = self.follows.lock().unwrap(); let target = SocialIdentity::Local(user.clone()); Ok(store .iter() .filter(|(_, t, state)| *t == target && *state == FollowState::Pending) - .map(|(f, _, _)| SocialIdentity::Local(UserId::from_uuid(*f))) + .map(|(f, _, _)| { + let id = SocialIdentity::Local(UserId::from_uuid(*f)); + Self::identity_to_actor(&id) + }) .collect()) } @@ -1078,12 +1098,12 @@ impl SocialQuery for InMemorySocialRepository { async fn get_blocked( &self, user: &UserId, - ) -> Result, DomainError> { + ) -> Result, DomainError> { let store = self.blocked.lock().unwrap(); Ok(store .iter() .filter(|(b, _)| *b == user.value()) - .map(|(_, t)| t.clone()) + .map(|(_, t)| Self::identity_to_actor(t)) .collect()) } @@ -1097,4 +1117,17 @@ impl SocialQuery for InMemorySocialRepository { .iter() .any(|(f, t, state)| *f == follower.value() && t == target && *state == FollowState::Accepted)) } + + async fn get_accepted_following_urls( + &self, + _user_id: &UserId, + ) -> Result, DomainError> { + Ok(vec![]) + } + + async fn list_all_followed_remote_actors( + &self, + ) -> Result, DomainError> { + Ok(vec![]) + } } diff --git a/crates/domain/src/value_objects/social.rs b/crates/domain/src/value_objects/social.rs index 0638bed..a9088e0 100644 --- a/crates/domain/src/value_objects/social.rs +++ b/crates/domain/src/value_objects/social.rs @@ -15,3 +15,11 @@ impl SocialIdentity { matches!(self, Self::Remote { .. }) } } + +#[derive(Clone, Debug)] +pub struct SocialActor { + pub identity: SocialIdentity, + pub handle: String, + pub display_name: Option, + pub avatar_url: Option, +} diff --git a/crates/presentation/src/handlers/diary.rs b/crates/presentation/src/handlers/diary.rs index 148d384..0f1cf1a 100644 --- a/crates/presentation/src/handlers/diary.rs +++ b/crates/presentation/src/handlers/diary.rs @@ -182,7 +182,7 @@ pub async fn get_activity_feed( ) -> Result, ApiError> { let deps = GetActivityFeedDeps { diary: state.app_ctx.repos.diary.clone(), - social_query: state.app_ctx.repos.social_query.clone(), + social_query: state.app_ctx.repos.social_query_unified.clone(), config: state.app_ctx.config.clone(), }; let page = get_feed_uc::execute( @@ -338,7 +338,7 @@ pub async fn get_activity_feed_html( let deps = GetActivityFeedDeps { diary: state.app_ctx.repos.diary.clone(), - social_query: state.app_ctx.repos.social_query.clone(), + social_query: state.app_ctx.repos.social_query_unified.clone(), config: state.app_ctx.config.clone(), }; diff --git a/crates/presentation/src/handlers/social.rs b/crates/presentation/src/handlers/social.rs index 237f513..f82d826 100644 --- a/crates/presentation/src/handlers/social.rs +++ b/crates/presentation/src/handlers/social.rs @@ -22,7 +22,7 @@ use api_types::{ BlockedDomainResponse, FollowRequest, RemoteActorDto, }; use application::social::deps::{SocialCommandDeps, SocialQueryDeps}; -use domain::value_objects::SocialIdentity; +use domain::value_objects::{SocialActor, SocialIdentity}; use template_askama::{ BlockedActorsTemplate, BlockedDomainsTemplate, FollowersTemplate, FollowingTemplate, RemoteActorData, @@ -35,35 +35,36 @@ fn ap_to_domain(e: anyhow::Error) -> domain::errors::DomainError { domain::errors::DomainError::InfrastructureError(e.to_string()) } -fn social_identity_to_dto(id: SocialIdentity) -> RemoteActorDto { - match id { - SocialIdentity::Remote { actor_url } => RemoteActorDto { - url: actor_url, - handle: String::new(), - display_name: None, - }, - SocialIdentity::Local(uid) => RemoteActorDto { - url: format!("local:{}", uid.value()), - handle: String::new(), - display_name: None, - }, +fn actor_url(identity: &SocialIdentity) -> String { + match identity { + SocialIdentity::Remote { actor_url } => actor_url.clone(), + SocialIdentity::Local(uid) => format!("local:{}", uid.value()), } } -fn social_identity_to_blocked_dto(id: SocialIdentity) -> BlockedActorResponse { - match id { - SocialIdentity::Remote { actor_url } => BlockedActorResponse { - url: actor_url, - handle: String::new(), - display_name: None, - avatar_url: None, - }, - SocialIdentity::Local(uid) => BlockedActorResponse { - url: format!("local:{}", uid.value()), - handle: String::new(), - display_name: None, - avatar_url: None, - }, +fn social_actor_to_dto(actor: SocialActor) -> 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), + handle: actor.handle, + display_name: actor.display_name, + avatar_url: actor.avatar_url, + } +} + +fn social_actor_to_template(actor: SocialActor) -> RemoteActorData { + RemoteActorData { + url: actor_url(&actor.identity), + handle: actor.handle, + display_name: actor.display_name, + avatar_url: actor.avatar_url, } } @@ -234,7 +235,7 @@ pub async fn get_blocked_actors_api( Ok(Json( identities .into_iter() - .map(social_identity_to_blocked_dto) + .map(social_actor_to_blocked_dto) .collect(), )) } @@ -264,7 +265,7 @@ pub async fn get_following( Ok(Json(ActorListResponse { actors: identities .into_iter() - .map(social_identity_to_dto) + .map(social_actor_to_dto) .collect(), })) } @@ -294,7 +295,7 @@ pub async fn get_followers( Ok(Json(ActorListResponse { actors: identities .into_iter() - .map(social_identity_to_dto) + .map(social_actor_to_dto) .collect(), })) } @@ -315,7 +316,7 @@ pub async fn get_user_following( Ok(Json(ActorListResponse { actors: identities .into_iter() - .map(social_identity_to_dto) + .map(social_actor_to_dto) .collect(), })) } @@ -336,7 +337,7 @@ pub async fn get_user_followers( Ok(Json(ActorListResponse { actors: identities .into_iter() - .map(social_identity_to_dto) + .map(social_actor_to_dto) .collect(), })) } @@ -526,7 +527,7 @@ pub async fn get_pending_followers( Ok(Json(ActorListResponse { actors: identities .into_iter() - .map(social_identity_to_dto) + .map(social_actor_to_dto) .collect(), })) } @@ -770,16 +771,19 @@ pub async fn get_following_page( "{}/users/{}/following-list", state.app_ctx.config.base_url, profile_user_uuid ); - match state.app_ctx.services.ap_service.get_following(user_id.value()).await { + let deps = SocialQueryDeps { + social_query: state.app_ctx.repos.social_query_unified.clone(), + }; + match application::social::get_following::execute( + &deps, + application::social::queries::GetFollowingQuery { user_id: user_id.value() }, + ) + .await + { Ok(following) => { let actors: Vec = following .into_iter() - .map(|a| RemoteActorData { - handle: a.handle, - display_name: a.display_name, - url: a.url, - avatar_url: a.avatar_url.clone(), - }) + .map(social_actor_to_template) .collect(); render_page(FollowingTemplate { ctx, @@ -816,20 +820,19 @@ pub async fn get_followers_page( "{}/users/{}/followers-list", state.app_ctx.config.base_url, profile_user_uuid ); - match state - .app_ctx.services.ap_service - .get_accepted_followers(user_id.value()) - .await + let deps = SocialQueryDeps { + social_query: state.app_ctx.repos.social_query_unified.clone(), + }; + match application::social::get_followers::execute( + &deps, + application::social::queries::GetFollowersQuery { user_id: user_id.value() }, + ) + .await { Ok(followers) => { let actors: Vec = followers .into_iter() - .map(|a| RemoteActorData { - handle: a.handle, - display_name: a.display_name, - url: a.url, - avatar_url: a.avatar_url.clone(), - }) + .map(social_actor_to_template) .collect(); render_page(FollowersTemplate { ctx, @@ -975,12 +978,20 @@ 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); - match state.app_ctx.services.ap_service.get_blocked_actors(user_id.value()).await { - Ok(actors) => { - let entries: Vec = actors + let deps = SocialQueryDeps { + social_query: state.app_ctx.repos.social_query_unified.clone(), + }; + match application::social::get_blocked::execute( + &deps, + application::social::queries::GetBlockedQuery { user_id: user_id.value() }, + ) + .await + { + Ok(blocked) => { + let entries: Vec = blocked .into_iter() .map(|a| template_askama::BlockedActorEntry { - url: a.url, + url: actor_url(&a.identity), handle: a.handle, display_name: a.display_name, avatar_url: a.avatar_url, diff --git a/crates/presentation/src/handlers/users.rs b/crates/presentation/src/handlers/users.rs index b62af9f..9724933 100644 --- a/crates/presentation/src/handlers/users.rs +++ b/crates/presentation/src/handlers/users.rs @@ -176,12 +176,11 @@ pub async fn update_profile_fields_handler( responses((status = 200, body = UsersResponse)), )] pub async fn list_users(State(state): State) -> Result, ApiError> { - let result = get_users::execute( - state.app_ctx.repos.user.clone(), - state.app_ctx.repos.social_query.clone(), - GetUsersQuery, - ) - .await?; + let deps = application::users::deps::GetUsersListDeps { + user: state.app_ctx.repos.user.clone(), + social_query_legacy: state.app_ctx.repos.social_query.clone(), + }; + let result = get_users::execute(&deps, GetUsersQuery).await?; Ok(Json(UsersResponse { users: result .users @@ -248,7 +247,7 @@ pub async fn get_user_profile( 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.clone(), + social_query: state.app_ctx.repos.social_query_unified.clone(), }; let profile = match get_user_profile_uc::execute( &get_profile_deps, @@ -381,7 +380,7 @@ async fn build_federated_profile_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.clone(), + social_query: state.app_ctx.repos.social_query_unified.clone(), }; let profile = match get_user_profile_uc::execute( &get_profile_deps, @@ -485,9 +484,12 @@ 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(), + social_query_legacy: state.app_ctx.repos.social_query.clone(), + }; match application::users::get_users::execute( - state.app_ctx.repos.user.clone(), - state.app_ctx.repos.social_query.clone(), + &users_deps, application::users::queries::GetUsersQuery, ) .await @@ -731,7 +733,7 @@ pub async fn get_user_profile_html( 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.clone(), + social_query: state.app_ctx.repos.social_query_unified.clone(), }; match application::users::get_profile::execute(&html_profile_deps, query).await { Ok(profile) => {