From dee2fe3ab7e886c04739708a20db9ee2d80665b2 Mon Sep 17 00:00:00 2001 From: Pitchfork-and-Torch <297513015+Pitchfork-and-Torch@users.noreply.github.com> Date: Fri, 4 Sep 2026 14:30:06 -0400 Subject: [PATCH] Drop Following posts whose author blocked the viewer. --- .../following_blocked_by_hydrator.rs | 162 +++++++++++++++++- 1 file changed, 158 insertions(+), 4 deletions(-) diff --git a/home-mixer/candidate_hydrators/following_blocked_by_hydrator.rs b/home-mixer/candidate_hydrators/following_blocked_by_hydrator.rs index d714cbd9..b8b5a29e 100644 --- a/home-mixer/candidate_hydrators/following_blocked_by_hydrator.rs +++ b/home-mixer/candidate_hydrators/following_blocked_by_hydrator.rs @@ -1,5 +1,6 @@ use crate::models::candidate::PostCandidate; use crate::models::query::ScoredPostsQuery; +use std::collections::HashSet; use std::sync::Arc; use tonic::async_trait; use xai_candidate_pipeline::component_library::clients::SocialGraphClientOps; @@ -24,7 +25,13 @@ impl Hydrator for FollowingBlockedByHydrator { ) -> Vec> { let user_ids: Vec = candidates .iter() - .flat_map(|c| c.quoted_user_id.into_iter().chain(c.retweeted_user_id)) + .flat_map(|c| { + std::iter::once(c.author_id) + .chain(c.quoted_user_id) + .chain(c.retweeted_user_id) + }) + .collect::>() + .into_iter() .collect(); let blocked_by_user_ids = match self @@ -41,9 +48,10 @@ impl Hydrator for FollowingBlockedByHydrator { candidates .iter() .map(|candidate| { - let author_blocks_viewer = candidate - .retweeted_user_id - .is_some_and(|uid| blocked_by_user_ids.contains(&uid)); + let author_blocks_viewer = blocked_by_user_ids.contains(&candidate.author_id) + || candidate + .retweeted_user_id + .is_some_and(|uid| blocked_by_user_ids.contains(&uid)); let quoted_author_blocks_viewer = candidate .quoted_user_id .map(|uid| blocked_by_user_ids.contains(&uid)); @@ -63,3 +71,149 @@ impl Hydrator for FollowingBlockedByHydrator { } } } + +#[cfg(test)] +mod tests { + use super::*; + use tonic::Status; + + struct MockSocialGraph { + blocked_by: HashSet, + } + + #[async_trait] + impl SocialGraphClientOps for MockSocialGraph { + async fn get_following_list(&self, _user_id: u64) -> Result, Status> { + Ok(vec![]) + } + async fn check_blocked_by( + &self, + _viewer_id: u64, + author_ids: &[u64], + ) -> Result, Status> { + Ok(author_ids + .iter() + .copied() + .filter(|id| self.blocked_by.contains(id)) + .collect()) + } + async fn check_followed_by( + &self, + _viewer_id: u64, + _user_ids: &[u64], + ) -> Result, Status> { + Ok(HashSet::new()) + } + async fn get_blocked_user_ids(&self, _viewer_id: u64) -> Result, Status> { + Ok(vec![]) + } + async fn get_muted_user_ids(&self, _viewer_id: u64) -> Result, Status> { + Ok(vec![]) + } + async fn get_followed_user_ids(&self, _viewer_id: u64) -> Result, Status> { + Ok(vec![]) + } + async fn get_follower_ids(&self, _user_id: u64) -> Result, Status> { + Ok(vec![]) + } + async fn get_subscribed_user_ids(&self, _viewer_id: u64) -> Result, Status> { + Ok(vec![]) + } + async fn get_device_following_user_ids(&self, _viewer_id: u64) -> Result, Status> { + Ok(vec![]) + } + async fn get_hide_recommendations_user_ids( + &self, + _viewer_id: u64, + ) -> Result, Status> { + Ok(vec![]) + } + } + + fn hydrator(blocked_by: HashSet) -> FollowingBlockedByHydrator { + FollowingBlockedByHydrator { + socialgraph_client: Arc::new(MockSocialGraph { blocked_by }), + } + } + + #[tokio::test] + async fn native_author_who_blocked_viewer_is_marked() { + let hydrator = hydrator(HashSet::from([10])); + let mut native = PostCandidate { + tweet_id: 1, + author_id: 10, + ..Default::default() + }; + let query = ScoredPostsQuery { + user_id: 1, + ..Default::default() + }; + let hydrated = hydrator.hydrate(&query, &[native.clone()]).await; + hydrator.update(&mut native, hydrated[0].clone().unwrap()); + assert_eq!(native.author_blocks_viewer, Some(true)); + } + + #[tokio::test] + async fn retweet_of_author_who_blocked_viewer_is_still_marked() { + let hydrator = hydrator(HashSet::from([99])); + let rt = PostCandidate { + tweet_id: 1, + author_id: 10, + retweeted_user_id: Some(99), + ..Default::default() + }; + let native = PostCandidate { + tweet_id: 2, + author_id: 10, + ..Default::default() + }; + let query = ScoredPostsQuery { + user_id: 1, + ..Default::default() + }; + let hydrated = hydrator.hydrate(&query, &[rt.clone(), native.clone()]).await; + let mut rt = rt; + let mut native = native; + hydrator.update(&mut rt, hydrated[0].clone().unwrap()); + hydrator.update(&mut native, hydrated[1].clone().unwrap()); + assert_eq!(rt.author_blocks_viewer, Some(true)); + assert_eq!(native.author_blocks_viewer, Some(false)); + } + + #[tokio::test] + async fn retweeter_who_blocked_viewer_is_marked() { + let hydrator = hydrator(HashSet::from([10])); + let mut rt = PostCandidate { + tweet_id: 1, + author_id: 10, + retweeted_user_id: Some(99), + ..Default::default() + }; + let query = ScoredPostsQuery { + user_id: 1, + ..Default::default() + }; + let hydrated = hydrator.hydrate(&query, &[rt.clone()]).await; + hydrator.update(&mut rt, hydrated[0].clone().unwrap()); + assert_eq!(rt.author_blocks_viewer, Some(true)); + } + + #[tokio::test] + async fn quoted_author_who_blocked_viewer_is_still_marked() { + let hydrator = hydrator(HashSet::from([77])); + let mut quote = PostCandidate { + tweet_id: 1, + author_id: 10, + quoted_user_id: Some(77), + ..Default::default() + }; + let query = ScoredPostsQuery { + user_id: 1, + ..Default::default() + }; + let hydrated = hydrator.hydrate(&query, &[quote.clone()]).await; + hydrator.update(&mut quote, hydrated[0].clone().unwrap()); + assert_eq!(quote.quoted_author_blocks_viewer, Some(true)); + assert_eq!(quote.author_blocks_viewer, Some(false)); + } +}