Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 84 additions & 11 deletions home-mixer/candidate_hydrators/gizmoduck_hydrator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,29 @@ use xai_candidate_pipeline::component_library::utils::{default_quick_cache, Quic
use xai_candidate_pipeline::hydrator::{CacheStore, CachedHydrator};
use xai_x_thrift::user_labels::LabelValue;

/// Phoenix hashes the origin author. `nsfw_author_phoenix` must be that
/// account's bit, not the retweeter's. Filter/ads bits stay on the poster.
pub(crate) fn phoenix_author_nsfw_bit(
is_retweet: bool,
poster_nsfw: Option<bool>,
origin_nsfw: Option<bool>,
) -> Option<bool> {
if is_retweet {
origin_nsfw
} else {
poster_nsfw
}
}

fn phoenix_nsfw_from_safety(
nsfw_user: bool,
nsfw_admin: bool,
high_precision: bool,
possibly: bool,
) -> bool {
nsfw_user || nsfw_admin || high_precision || possibly
}

pub struct GizmoduckCandidateHydrator {
pub gizmoduck_client: Arc<dyn GizmoduckClient + Send + Sync>,
pub cache: QuickCache<GizmoduckCacheKey, GizmoduckCacheValue>,
Expand Down Expand Up @@ -122,6 +145,7 @@ impl CachedHydrator<ScoredPostsQuery, PostCandidate> for GizmoduckCandidateHydra
retweet_profile.map(|x| x.screen_name.clone());

let author = user.and_then(|u| u.user.as_ref());
let origin_author = retweet_user.and_then(|u| u.user.as_ref());
let nsfw_author: Option<bool> = author.map(|u| {
u.safety.nsfw_admin
|| u.safety.nsfw_user
Expand All @@ -136,17 +160,27 @@ impl CachedHydrator<ScoredPostsQuery, PostCandidate> for GizmoduckCandidateHydra
label.label_value == LabelValue::POSSIBLY_NSFW_ACCOUNT.0
})
});
let nsfw_author_phoenix: Option<bool> = author.map(|u| {
u.safety.nsfw_user
|| u.safety.nsfw_admin
|| u.labels.labels.iter().any(|l| {
matches!(
LabelValue(l.label_value),
LabelValue::NSFW_HIGH_PRECISION
| LabelValue::POSSIBLY_NSFW_ACCOUNT
)
})
});
let phoenix_nsfw = |u: &_| {
phoenix_nsfw_from_safety(
u.safety.nsfw_user,
u.safety.nsfw_admin,
u.labels
.labels
.iter()
.any(|l| l.label_value == LabelValue::NSFW_HIGH_PRECISION.0),
u.labels
.labels
.iter()
.any(|l| l.label_value == LabelValue::POSSIBLY_NSFW_ACCOUNT.0),
)
};
let poster_phoenix = author.map(phoenix_nsfw);
let origin_phoenix = origin_author.map(phoenix_nsfw);
let nsfw_author_phoenix = phoenix_author_nsfw_bit(
candidate.retweeted_user_id.is_some(),
poster_phoenix,
origin_phoenix,
);

Ok(PostCandidate {
author_followers_count,
Expand Down Expand Up @@ -191,3 +225,42 @@ pub struct GizmoduckCacheValue {
pub nsfw_author_ads: Option<bool>,
pub nsfw_author_phoenix: Option<bool>,
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn phoenix_bit_uses_poster_on_originals() {
assert_eq!(
phoenix_author_nsfw_bit(false, Some(true), Some(false)),
Some(true)
);
assert_eq!(
phoenix_author_nsfw_bit(false, Some(false), Some(true)),
Some(false)
);
}

#[test]
fn phoenix_bit_uses_origin_on_retweets() {
assert_eq!(
phoenix_author_nsfw_bit(true, Some(false), Some(true)),
Some(true)
);
assert_eq!(
phoenix_author_nsfw_bit(true, Some(true), Some(false)),
Some(false)
);
assert_eq!(phoenix_author_nsfw_bit(true, Some(true), None), None);
}

#[test]
fn phoenix_nsfw_matches_existing_label_union() {
assert!(phoenix_nsfw_from_safety(true, false, false, false));
assert!(phoenix_nsfw_from_safety(false, true, false, false));
assert!(phoenix_nsfw_from_safety(false, false, true, false));
assert!(phoenix_nsfw_from_safety(false, false, false, true));
assert!(!phoenix_nsfw_from_safety(false, false, false, false));
}
}
56 changes: 53 additions & 3 deletions home-mixer/models/candidate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,10 @@ impl CandidateHelpers for PostCandidate {
quoted_author_id: self.quoted_user_id.unwrap_or(0),
in_reply_to_tweet_id: self.in_reply_to_tweet_id.unwrap_or(0),
is_author_followed_by_user: is_followed_by_viewer,
safety_label_mask: if self.retweeted_user_id.is_none()
&& self.nsfw_author_phoenix.unwrap_or(false)
{
// Phoenix hashes get_original_author_id(). authorIsNsfwSeq must
// describe that account on retweets too. The old
// retweeted_user_id.is_none() guard zeroed the bit on every RT.
safety_label_mask: if self.nsfw_author_phoenix.unwrap_or(false) {
SAFETY_BIT_AUTHOR_NSFW
} else {
0
Expand Down Expand Up @@ -354,4 +355,53 @@ mod tests {
SafetyLabelType::BOUNCE
);
}

#[test]
fn as_tweet_info_stamps_author_nsfw_on_originals() {
let nsfw = PostCandidate {
tweet_id: 11,
author_id: 21,
nsfw_author_phoenix: Some(true),
..Default::default()
};
assert_eq!(
nsfw.as_tweet_info(false).safety_label_mask,
SAFETY_BIT_AUTHOR_NSFW
);

let clean = PostCandidate {
tweet_id: 11,
author_id: 21,
nsfw_author_phoenix: Some(false),
..Default::default()
};
assert_eq!(clean.as_tweet_info(false).safety_label_mask, 0);
}

#[test]
fn as_tweet_info_stamps_author_nsfw_on_retweets() {
let rt = PostCandidate {
tweet_id: 11,
author_id: 21,
retweeted_tweet_id: Some(12),
retweeted_user_id: Some(22),
nsfw_author_phoenix: Some(true),
..Default::default()
};
let info = rt.as_tweet_info(true);
assert_eq!(info.author_id, 22);
assert_eq!(info.safety_label_mask, SAFETY_BIT_AUTHOR_NSFW);

let clean_origin = PostCandidate {
tweet_id: 11,
author_id: 21,
retweeted_tweet_id: Some(12),
retweeted_user_id: Some(22),
nsfw_author_phoenix: Some(false),
nsfw_author: Some(true),
nsfw_author_ads: Some(true),
..Default::default()
};
assert_eq!(clean_origin.as_tweet_info(false).safety_label_mask, 0);
}
}