diff --git a/home-mixer/candidate_hydrators/blocked_by_hydrator.rs b/home-mixer/candidate_hydrators/blocked_by_hydrator.rs index b284df4d..afcaa9d3 100644 --- a/home-mixer/candidate_hydrators/blocked_by_hydrator.rs +++ b/home-mixer/candidate_hydrators/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; @@ -26,11 +27,16 @@ impl Hydrator for BlockedByHydrator { query: &ScoredPostsQuery, candidates: &[PostCandidate], ) -> Vec> { - let author_ids: Vec = candidates.iter().map(|x| x.author_id).collect(); + let user_ids: Vec = candidates + .iter() + .flat_map(|c| std::iter::once(c.author_id).chain(c.ancestor_users.iter().copied())) + .collect::>() + .into_iter() + .collect(); let blocked_by_user_ids = match self .socialgraph_client - .check_blocked_by(query.user_id, &author_ids) + .check_blocked_by(query.user_id, &user_ids) .await { Ok(ids) => ids, @@ -42,7 +48,11 @@ impl Hydrator for BlockedByHydrator { candidates .iter() .map(|candidate| { - let author_blocks_viewer = blocked_by_user_ids.contains(&candidate.author_id); + let author_blocks_viewer = blocked_by_user_ids.contains(&candidate.author_id) + || candidate + .ancestor_users + .iter() + .any(|uid| blocked_by_user_ids.contains(uid)); Ok(PostCandidate { author_blocks_viewer: Some(author_blocks_viewer), ..Default::default() @@ -55,3 +65,103 @@ impl Hydrator for BlockedByHydrator { candidate.author_blocks_viewer = hydrated.author_blocks_viewer; } } + +#[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) -> BlockedByHydrator { + BlockedByHydrator { + socialgraph_client: Arc::new(MockSocialGraph { blocked_by }), + } + } + + #[tokio::test] + async fn ancestor_who_blocked_viewer_is_marked() { + let hydrator = hydrator(HashSet::from([77])); + let mut reply = PostCandidate { + tweet_id: 1, + author_id: 10, + ancestor_users: vec![77], + ..Default::default() + }; + let query = ScoredPostsQuery { + user_id: 1, + ..Default::default() + }; + let hydrated = hydrator.hydrate(&query, &[reply.clone()]).await; + hydrator.update(&mut reply, hydrated[0].clone().unwrap()); + assert_eq!(reply.author_blocks_viewer, Some(true)); + } + + #[tokio::test] + async fn primary_author_who_blocked_viewer_is_still_marked() { + let hydrator = hydrator(HashSet::from([10])); + let mut candidate = PostCandidate { + tweet_id: 1, + author_id: 10, + ..Default::default() + }; + let query = ScoredPostsQuery { + user_id: 1, + ..Default::default() + }; + let hydrated = hydrator.hydrate(&query, &[candidate.clone()]).await; + hydrator.update(&mut candidate, hydrated[0].clone().unwrap()); + assert_eq!(candidate.author_blocks_viewer, Some(true)); + } +} diff --git a/home-mixer/candidate_hydrators/following_blocked_by_hydrator.rs b/home-mixer/candidate_hydrators/following_blocked_by_hydrator.rs index d714cbd9..715ff642 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,14 @@ 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| { + c.quoted_user_id + .into_iter() + .chain(c.retweeted_user_id) + .chain(c.ancestor_users.iter().copied()) + }) + .collect::>() + .into_iter() .collect(); let blocked_by_user_ids = match self @@ -43,7 +51,11 @@ impl Hydrator for FollowingBlockedByHydrator { .map(|candidate| { let author_blocks_viewer = candidate .retweeted_user_id - .is_some_and(|uid| blocked_by_user_ids.contains(&uid)); + .is_some_and(|uid| blocked_by_user_ids.contains(&uid)) + || candidate + .ancestor_users + .iter() + .any(|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 +75,104 @@ 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 ancestor_who_blocked_viewer_is_marked() { + let hydrator = hydrator(HashSet::from([77])); + let mut reply = PostCandidate { + tweet_id: 1, + author_id: 10, + ancestor_users: vec![77], + ..Default::default() + }; + let query = ScoredPostsQuery { + user_id: 1, + ..Default::default() + }; + let hydrated = hydrator.hydrate(&query, &[reply.clone()]).await; + hydrator.update(&mut reply, hydrated[0].clone().unwrap()); + assert_eq!(reply.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 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)); + } +}