refactor: SocialActor rich queries, migrate remaining handlers, slim SocialQueryPort

SocialQuery returns SocialActor (identity+handle+display_name+avatar_url)
instead of bare SocialIdentity. Migrated get_following_page,
get_followers_page, get_blocked_actors_page to use cases. Moved
get_activity_feed + get_profile from SocialQueryPort to SocialQuery.
Legacy SocialQueryPort remains only for get_users listing.
This commit is contained in:
2026-07-10 15:52:37 +02:00
parent 3ee75305a9
commit 7cfa234902
18 changed files with 276 additions and 148 deletions

View File

@@ -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<Vec<SocialIdentity>, DomainError> {
async fn get_following(&self, _: &UserId) -> Result<Vec<SocialActor>, DomainError> {
Ok(vec![])
}
async fn get_followers(&self, _: &UserId) -> Result<Vec<SocialIdentity>, DomainError> {
async fn get_followers(&self, _: &UserId) -> Result<Vec<SocialActor>, DomainError> {
Ok(vec![])
}
async fn get_pending_followers(&self, _: &UserId) -> Result<Vec<SocialIdentity>, DomainError> {
async fn get_pending_followers(&self, _: &UserId) -> Result<Vec<SocialActor>, DomainError> {
Ok(vec![])
}
async fn count_following(&self, _: &UserId) -> Result<usize, DomainError> {
@@ -85,12 +85,20 @@ impl super::SocialQuery for NoopSocialQuery {
async fn count_followers(&self, _: &UserId) -> Result<usize, DomainError> {
Ok(0)
}
async fn get_blocked(&self, _: &UserId) -> Result<Vec<SocialIdentity>, DomainError> {
async fn get_blocked(&self, _: &UserId) -> Result<Vec<SocialActor>, DomainError> {
Ok(vec![])
}
async fn is_following(&self, _: &UserId, _: &SocialIdentity) -> Result<bool, DomainError> {
Ok(false)
}
async fn get_accepted_following_urls(&self, _: &UserId) -> Result<Vec<String>, DomainError> {
Ok(vec![])
}
async fn list_all_followed_remote_actors(
&self,
) -> Result<Vec<crate::models::RemoteActorInfo>, DomainError> {
Ok(vec![])
}
}
// ── NoopSocialQueryPort ───────────────────────────────────────────────────────

View File

@@ -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<Vec<SocialIdentity>, DomainError>;
) -> Result<Vec<SocialActor>, DomainError>;
async fn get_followers(
&self,
user: &UserId,
) -> Result<Vec<SocialIdentity>, DomainError>;
) -> Result<Vec<SocialActor>, DomainError>;
async fn get_pending_followers(
&self,
user: &UserId,
) -> Result<Vec<SocialIdentity>, DomainError>;
) -> Result<Vec<SocialActor>, DomainError>;
async fn count_following(&self, user: &UserId) -> Result<usize, DomainError>;
@@ -81,13 +81,22 @@ pub trait SocialQuery: Send + Sync {
async fn get_blocked(
&self,
user: &UserId,
) -> Result<Vec<SocialIdentity>, DomainError>;
) -> Result<Vec<SocialActor>, DomainError>;
async fn is_following(
&self,
follower: &UserId,
target: &SocialIdentity,
) -> Result<bool, DomainError>;
async fn get_accepted_following_urls(
&self,
user_id: &UserId,
) -> Result<Vec<String>, DomainError>;
async fn list_all_followed_remote_actors(
&self,
) -> Result<Vec<crate::models::RemoteActorInfo>, DomainError>;
}
// ── Legacy ports (pre-unification, still used by AP adapter + handlers) ─────

View File

@@ -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<Vec<SocialIdentity>, DomainError> {
) -> Result<Vec<SocialActor>, 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<Vec<SocialIdentity>, DomainError> {
) -> Result<Vec<SocialActor>, 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<Vec<SocialIdentity>, DomainError> {
) -> Result<Vec<SocialActor>, 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<Vec<SocialIdentity>, DomainError> {
) -> Result<Vec<SocialActor>, 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<Vec<String>, DomainError> {
Ok(vec![])
}
async fn list_all_followed_remote_actors(
&self,
) -> Result<Vec<crate::models::RemoteActorInfo>, DomainError> {
Ok(vec![])
}
}

View File

@@ -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<String>,
pub avatar_url: Option<String>,
}