mirror of
https://github.com/twitter/the-algorithm.git
synced 2025-06-12 07:38:18 -05:00
Twitter Recommendation Algorithm
Please note we have force-pushed a new initial commit in order to remove some publicly-available Twitter user information. Note that this process may be required in the future.
This commit is contained in:
6
visibilitylib/src/main/resources/config/BUILD
Normal file
6
visibilitylib/src/main/resources/config/BUILD
Normal file
@ -0,0 +1,6 @@
|
||||
resources(
|
||||
sources = [
|
||||
"com/twitter/visibility/*.csv",
|
||||
"com/twitter/visibility/*.yml",
|
||||
],
|
||||
)
|
@ -0,0 +1,906 @@
|
||||
|
||||
visibility_library_enable_all_subscribed_lists_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_ads_business_settings_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_ads_campaign_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_ads_manager_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_appeals_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_article_tweet_timeline_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_birdwatch_note_author_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_birdwatch_note_tweets_timeline_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_birdwatch_needs_your_help_notifications_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_block_mute_users_timeline_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_brand_safety_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_card_poll_voting_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_cards_service_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_communities_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_conversation_focal_prehydration_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_conversation_focal_tweet_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_conversation_injected_tweet_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_conversation_reply_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_curated_trends_representative_tweet:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_curation_policy_violations:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_deprecated_safety_level_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_dev_platform_get_list_tweets_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_des_following_and_followers_user_list_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_des_home_timeline_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_des_quote_tweet_timeline_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_des_realtime_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_des_realtime_spam_enrichment_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_des_realtime_tweet_filter_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_des_retweeting_users_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_des_tweet_detail_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_des_tweet_liking_users_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_des_user_bookmarks_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_des_user_liked_tweets_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_des_user_mentions_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_des_user_tweets_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_dev_platform_compliance_stream_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_direct_messages_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_direct_messages_conversation_list_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_direct_messages_conversation_timeline_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_direct_messages_inbox_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_direct_messages_muted_users_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_direct_messages_pinned_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_elevated_quote_tweet_timeline_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_direct_messages_search_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_embedded_tweet_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_embeds_public_interest_notice_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_embed_tweet_markup_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_write_path_limited_actions_enforcement_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_filter_all_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_filter_all_placeholder_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_filter_default_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_filter_none_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_followed_topics_timeline_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_follower_connections_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_following_and_followers_user_list_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_for_development_only_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_friends_following_list_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_graphql_default_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_gryphon_decks_and_columns_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_humanization_nudge_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_kitchen_sink_development_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_list_header_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_list_memberships_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_list_ownerships_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_list_recommendations_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_list_search_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_list_subscriptions_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_live_pipeline_engagement_counts_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_live_video_timeline_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_magic_recs_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_magic_recs_aggressive_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_magic_recs_aggressive_v2_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_magic_recs_v2_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_minimal_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_moderated_tweets_timeline_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_moments_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_nearby_timeline_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_new_user_experience_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_notifications_ibis_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_notifications_platform_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_notifications_platform_push_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_notifications_read_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_notifications_timeline_device_follow_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_notifications_write_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_notification_writer_v2_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_notifications_writer_tweet_hydrator_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_quick_promote_tweet_eligibility_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_quote_tweet_timeline_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_quoted_tweet_rules_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_recommendations_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_recos_video_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_recos_write_path_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_replies_grouping_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_report_center_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_returning_user_experience_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_returning_user_experience_focal_tweet_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_revenue_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_rito_actioned_tweet_timeline_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_safe_search_minimal_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_safe_search_strict_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_search_hydration_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_search_latest_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_search_mixer_srp_minimal_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_search_mixer_srp_strict_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_user_search_srp_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_user_search_typeahead_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_search_people_srp_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_search_people_typeahead_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_search_photo_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_search_top_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_search_trend_takeover_promoted_tweet_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_search_video_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_search_latest_user_rules_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_shopping_manager_spy_mode_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_signals_reactions_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_signals_tweet_reacting_users_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_social_proof_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_soft_intervention_pivot_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_space_fleetline_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_space_home_timeline_upranking_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_space_join_screen_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_space_notifications_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_spaces_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_spaces_participants_safety_level:
|
||||
default_availability: 0
|
||||
|
||||
visibility_library_enable_spaces_seller_application_status_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_spaces_sharing_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_space_tweet_avatar_home_timeline_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_stickers_timeline_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_strato_ext_limited_engagements_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_stream_services_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_test_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_bookmark_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_content_controls_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_conversations_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_conversations_downranking_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_conversations_downranking_minimal_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_favorites_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_self_view_timeline_favorites_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_focal_tweet_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_following_activity_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_home_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_home_communities_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_home_hydration_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_home_latest_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_home_recommendations_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_home_topic_follow_recommendations_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_scorer_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_topics_landing_page_topic_recommendations_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_explore_recommendations_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_moderated_tweets_hydration_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_injection_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_liked_by_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_lists_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_media_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_mentions_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_profile_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_profile_all_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_profile_spaces_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_profile_super_follows_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_reactive_blending_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_retweeted_by_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_super_liked_by_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_tombstoning_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_trends_representative_tweet_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_trusted_friends_user_list_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_tweet_detail_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_tweet_detail_non_too_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_tweet_detail_with_injections_hydration_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_tweet_engagers_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_tweet_reply_nudge_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_tweet_scoped_timeline_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_tweet_writes_api_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_twitter_article_compose_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_twitter_article_profile_tab_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_twitter_article_read_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_user_profile_header_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_user_milestone_recommendation_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_user_scoped_timeline_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_user_settings_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_video_ads_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_home_promoted_hydration_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_super_follower_connnections_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_super_like_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_topic_recommendations_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_ads_reporting_dashboard_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_search_top_qig_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_content_control_tool_install_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_conversation_control_rules:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_community_tweets_rules:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_drop_community_tweet_with_undefined_community_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_p_spammy_tweet_downrank_convos_low_quality:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_high_p_spammy_tweet_score_search_tweet_label_drop_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_rito_actioned_tweet_downrank_convos_low_quality:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_toxic_reply_filter_conversation:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_toxic_reply_filter_notifications:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_new_sensitive_media_settings_interstitial_rules_home_timeline:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_legacy_sensitive_media_rules_home_timeline:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_new_sensitive_media_settings_interstitial_rules_conversation:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_legacy_sensitive_media_rules_conversation:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_new_sensitive_media_settings_interstitials_rules_profile_timeline:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_legacy_sensitive_media_rules_profile_timeline:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_new_sensitive_media_settings_interstitials_rules_tweet_detail:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_legacy_sensitive_media_rules_tweet_detail:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_legacy_sensitive_media_rules_direct_messages:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_smyte_spam_tweet_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_high_spammy_tweet_content_score_search_latest_tweet_label_drop_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_high_spammy_tweet_content_score_search_top_tweet_label_drop_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_high_spammy_tweet_content_score_convo_downrank_abusive_quality_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_high_cryptospam_score_convo_downrank_abusive_quality_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_high_spammy_tweet_content_score_trends_top_tweet_label_drop_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_high_spammy_tweet_content_score_trends_latest_tweet_label_drop_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_gore_and_violence_topic_high_recall_tweet_label_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_limit_replies_followers_conversation_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_blink_bad_downranking_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_blink_worst_downranking_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_copypasta_spam_downrank_convos_abusive_quality_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_copypasta_spam_search_drop_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_spammy_user_model_high_precision_drop_tweet_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_avoid_nsfw_rules:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_reported_tweet_interstitial_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_reported_tweet_interstitial_search_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_drop_exclusive_tweet_content_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_drop_exclusive_tweet_content_rule_fail_closed:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_drop_all_exclusive_tweets_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_drop_all_exclusive_tweets_rule_fail_closed:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_tombstone_exclusive_quoted_tweet_content_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_downrank_spam_reply_sectioning_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_nsfw_text_sectioning_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_search_ipi_safe_search_without_user_in_query_drop_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_timeline_home_promoted_tweet_health_enforcement_rules:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_muted_keyword_filtering_space_title_notifications_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_drop_tweets_with_georestricted_media_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_drop_all_trusted_friends_tweets_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_drop_all_trusted_friends_tweet_content_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_drop_all_collab_invitation_tweets_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_fetch_tweet_reported_perspective:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_fetch_tweet_media_metadata:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_follow_check_in_mutedkeyword:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_media_interstitial_composition:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_verdict_scribing_from_tweet_visibility_library:
|
||||
default_availability: 0
|
||||
|
||||
visibility_library_enable_verdict_logger_event_publisher_instantiation_from_tweet_visibility_library:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_verdict_scribing_from_timeline_conversations_visibility_library:
|
||||
default_availability: 0
|
||||
|
||||
visibility_library_enable_verdict_logger_event_publisher_instantiation_from_timeline_conversations_visibility_library:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_verdict_scribing_from_blender_visibility_library:
|
||||
default_availability: 0
|
||||
|
||||
visibility_library_enable_verdict_logger_event_publisher_instantiation_from_blender_visibility_library:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_verdict_scribing_from_search_visibility_library:
|
||||
default_availability: 0
|
||||
|
||||
visibility_library_enable_verdict_logger_event_publisher_instantiation_from_search_visibility_library:
|
||||
default_availability: 0
|
||||
|
||||
visibility_library_enable_localized_tombstones_on_visibility_results:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_short_circuiting_from_tweet_visibility_library:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_card_visibility_library_card_uri_parsing:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_short_circuiting_from_timeline_conversations_visibility_library:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_short_circuiting_from_blender_visibility_library:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_short_circuiting_from_search_visibility_library:
|
||||
default_availability: 0
|
||||
|
||||
visibility_library_enable_nsfw_text_topics_drop_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_spammy_tweet_rule_verdict_logging:
|
||||
default_availability: 0
|
||||
|
||||
visibility_library_enable_downlevel_rule_verdict_logging:
|
||||
default_availability: 0
|
||||
|
||||
visibility_library_enable_likely_likely_ivs_user_label_drop_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_card_uri_root_domain_deny_list_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_community_non_member_poll_card_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_community_non_member_poll_card_rule_fail_closed:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_experimental_nudge_label_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_user_self_view_only_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_nsfw_high_precision_user_label_avoid_tweet_rule_enabled:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_new_ad_avoidance_rules:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_nsfa_high_recall_ad_avoidance_rules:
|
||||
default_availability: 0
|
||||
|
||||
visibility_library_enable_nsfa_keywords_high_precision_ad_avoidance_rules:
|
||||
default_availability: 0
|
||||
|
||||
visibility_library_enable_stale_tweet_drop_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_stale_tweet_drop_rule_fail_closed:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_edit_history_timeline_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_delete_state_tweet_rules:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_spaces_sharing_nsfw_drop_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_viewer_is_soft_user_drop_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_backend_limited_actions:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_base_qig_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_notifications_qig_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_access_internal_promoted_content_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_pdna_quoted_tweet_tombstone_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_spam_quoted_tweet_tombstone_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_nsfw_hp_quoted_tweet_drop_experiment_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_nsfw_hp_quoted_tweet_tombstone_experiment_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_inner_quoted_tweet_viewer_blocks_author_interstitial_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_inner_quoted_tweet_viewer_mutes_author_interstitial_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_experimental_rule_engine:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_fosnr_rules:
|
||||
default_availability: 0
|
||||
|
||||
visibility_library_enable_localized_interstitial_generator:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_convos_enable_legacy_interstitial:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_convos_enable_localized_interstitial:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_profile_mixer_media_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_profile_mixer_favorites_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_zipbird_consumer_archives_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_tweet_award_safety_level:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_disable_legacy_interstitial_filtered_reason:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_search_basic_block_mute_rules:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_localized_interstitial_user_state_lib:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_abusive_behavior_drop_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_abusive_behavior_interstitial_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_abusive_behavior_limited_engagements_rule:
|
||||
default_availability: 10000
|
||||
|
||||
visibility_library_enable_not_graduated_downrank_convos_abusive_quality_rule:
|
||||
default_availability: 0
|
||||
|
||||
visibility_library_enable_not_graduated_search_drop_rule:
|
||||
default_availability: 0
|
||||
|
||||
visibility_library_enable_not_graduated_drop_rule:
|
||||
default_availability: 0
|
||||
|
||||
visibility_library_enable_memoize_safety_level_params:
|
||||
default_availability: 0
|
||||
|
||||
visibility_library_enable_author_blocks_viewer_drop_rule:
|
||||
default_availability: 0
|
42
visibilitylib/src/main/scala/com/twitter/visibility/BUILD
Normal file
42
visibilitylib/src/main/scala/com/twitter/visibility/BUILD
Normal file
@ -0,0 +1,42 @@
|
||||
scala_library(
|
||||
sources = ["*.scala"],
|
||||
compiler_option_sets = ["fatal_warnings"],
|
||||
platform = "java8",
|
||||
strict_deps = True,
|
||||
tags = ["bazel-compatible"],
|
||||
dependencies = [
|
||||
"abdecider/src/main/scala",
|
||||
"configapi/configapi-core",
|
||||
"decider/src/main/scala",
|
||||
"featureswitches/featureswitches-core/src/main/scala",
|
||||
"servo/decider/src/main/scala",
|
||||
"servo/util/src/main/scala",
|
||||
"stitch/stitch-core",
|
||||
"util/util-logging/src/main/scala",
|
||||
"util/util-stats/src/main/scala",
|
||||
"visibility/common/src/main/scala/com/twitter/visibility/common/actions",
|
||||
"visibility/common/src/main/scala/com/twitter/visibility/common/stitch",
|
||||
"visibility/lib/src/main/resources/config",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/builder",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/configapi",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/configapi/configs",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/configapi/params",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/engine",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/features",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/generators",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/models",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/rules",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/rules/generators",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/rules/providers",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/util",
|
||||
],
|
||||
exports = [
|
||||
"configapi/configapi-core",
|
||||
"visibility/common/src/main/scala/com/twitter/visibility/common/actions",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/builder",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/configapi",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/configapi/configs",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/models",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/rules",
|
||||
],
|
||||
)
|
@ -0,0 +1,387 @@
|
||||
package com.twitter.visibility
|
||||
|
||||
import com.twitter.abdecider.LoggingABDecider
|
||||
import com.twitter.abdecider.NullABDecider
|
||||
import com.twitter.decider.Decider
|
||||
import com.twitter.decider.NullDecider
|
||||
import com.twitter.featureswitches.v2.FeatureSwitches
|
||||
import com.twitter.featureswitches.v2.NullFeatureSwitches
|
||||
import com.twitter.finagle.stats.NullStatsReceiver
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.logging.Logger
|
||||
import com.twitter.logging.NullLogger
|
||||
import com.twitter.servo.util.Gate
|
||||
import com.twitter.servo.util.MemoizingStatsReceiver
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.timelines.configapi.Params
|
||||
import com.twitter.util.Try
|
||||
import com.twitter.visibility.builder._
|
||||
import com.twitter.visibility.common.stitch.StitchHelpers
|
||||
import com.twitter.visibility.configapi.VisibilityParams
|
||||
import com.twitter.visibility.configapi.configs.VisibilityDeciderGates
|
||||
import com.twitter.visibility.engine.DeciderableVisibilityRuleEngine
|
||||
import com.twitter.visibility.engine.VisibilityResultsMetricRecorder
|
||||
import com.twitter.visibility.engine.VisibilityRuleEngine
|
||||
import com.twitter.visibility.engine.VisibilityRulePreprocessor
|
||||
import com.twitter.visibility.features.FeatureMap
|
||||
import com.twitter.visibility.models.ContentId
|
||||
import com.twitter.visibility.models.SafetyLevel
|
||||
import com.twitter.visibility.models.ViewerContext
|
||||
import com.twitter.visibility.rules.EvaluationContext
|
||||
import com.twitter.visibility.rules.Rule
|
||||
import com.twitter.visibility.rules.generators.TweetRuleGenerator
|
||||
import com.twitter.visibility.rules.providers.InjectedPolicyProvider
|
||||
import com.twitter.visibility.util.DeciderUtil
|
||||
import com.twitter.visibility.util.FeatureSwitchUtil
|
||||
import com.twitter.visibility.util.LoggingUtil
|
||||
|
||||
object VisibilityLibrary {
|
||||
|
||||
object Builder {
|
||||
|
||||
def apply(log: Logger, statsReceiver: StatsReceiver): Builder = new Builder(
|
||||
log,
|
||||
new MemoizingStatsReceiver(statsReceiver)
|
||||
)
|
||||
}
|
||||
|
||||
case class Builder(
|
||||
log: Logger,
|
||||
statsReceiver: StatsReceiver,
|
||||
decider: Option[Decider] = None,
|
||||
abDecider: Option[LoggingABDecider] = None,
|
||||
featureSwitches: Option[FeatureSwitches] = None,
|
||||
enableStitchProfiling: Gate[Unit] = Gate.False,
|
||||
captureDebugStats: Gate[Unit] = Gate.False,
|
||||
enableComposableActions: Gate[Unit] = Gate.False,
|
||||
enableFailClosed: Gate[Unit] = Gate.False,
|
||||
enableShortCircuiting: Gate[Unit] = Gate.True,
|
||||
memoizeSafetyLevelParams: Gate[Unit] = Gate.False) {
|
||||
|
||||
def withDecider(decider: Decider): Builder = copy(decider = Some(decider))
|
||||
|
||||
@deprecated("use .withDecider and pass in a decider that is properly configured per DC")
|
||||
def withDefaultDecider(isLocal: Boolean, useLocalOverrides: Boolean = false): Builder = {
|
||||
if (isLocal) {
|
||||
withLocalDecider
|
||||
} else {
|
||||
withDecider(
|
||||
DeciderUtil.mkDecider(
|
||||
useLocalDeciderOverrides = useLocalOverrides,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
def withLocalDecider(): Builder = withDecider(DeciderUtil.mkLocalDecider)
|
||||
|
||||
def withNullDecider(): Builder =
|
||||
withDecider(new NullDecider(isAvail = true, availabilityDefined = true))
|
||||
|
||||
def withABDecider(abDecider: LoggingABDecider, featureSwitches: FeatureSwitches): Builder =
|
||||
abDecider match {
|
||||
case abd: NullABDecider =>
|
||||
copy(abDecider = Some(abd), featureSwitches = Some(NullFeatureSwitches))
|
||||
case _ =>
|
||||
copy(
|
||||
abDecider = Some(abDecider),
|
||||
featureSwitches = Some(featureSwitches)
|
||||
)
|
||||
}
|
||||
|
||||
def withABDecider(abDecider: LoggingABDecider): Builder = abDecider match {
|
||||
case abd: NullABDecider =>
|
||||
withABDecider(abDecider = abd, featureSwitches = NullFeatureSwitches)
|
||||
case _ =>
|
||||
withABDecider(
|
||||
abDecider = abDecider,
|
||||
featureSwitches =
|
||||
FeatureSwitchUtil.mkVisibilityLibraryFeatureSwitches(abDecider, statsReceiver)
|
||||
)
|
||||
}
|
||||
|
||||
def withClientEventsLogger(clientEventsLogger: Logger): Builder =
|
||||
withABDecider(DeciderUtil.mkABDecider(Some(clientEventsLogger)))
|
||||
|
||||
def withDefaultABDecider(isLocal: Boolean): Builder =
|
||||
if (isLocal) {
|
||||
withABDecider(NullABDecider)
|
||||
} else {
|
||||
withClientEventsLogger(LoggingUtil.mkDefaultLogger(statsReceiver))
|
||||
}
|
||||
|
||||
def withNullABDecider(): Builder = withABDecider(NullABDecider)
|
||||
|
||||
def withEnableStitchProfiling(gate: Gate[Unit]): Builder =
|
||||
copy(enableStitchProfiling = gate)
|
||||
|
||||
def withCaptureDebugStats(gate: Gate[Unit]): Builder =
|
||||
copy(captureDebugStats = gate)
|
||||
|
||||
def withEnableComposableActions(gate: Gate[Unit]): Builder =
|
||||
copy(enableComposableActions = gate)
|
||||
|
||||
def withEnableComposableActions(gateBoolean: Boolean): Builder = {
|
||||
val gate = Gate.const(gateBoolean)
|
||||
copy(enableComposableActions = gate)
|
||||
}
|
||||
|
||||
def withEnableFailClosed(gate: Gate[Unit]): Builder =
|
||||
copy(enableFailClosed = gate)
|
||||
|
||||
def withEnableFailClosed(gateBoolean: Boolean): Builder = {
|
||||
val gate = Gate.const(gateBoolean)
|
||||
copy(enableFailClosed = gate)
|
||||
}
|
||||
|
||||
def withEnableShortCircuiting(gate: Gate[Unit]): Builder =
|
||||
copy(enableShortCircuiting = gate)
|
||||
|
||||
def withEnableShortCircuiting(gateBoolean: Boolean): Builder = {
|
||||
val gate = Gate.const(gateBoolean)
|
||||
copy(enableShortCircuiting = gate)
|
||||
}
|
||||
|
||||
def memoizeSafetyLevelParams(gate: Gate[Unit]): Builder =
|
||||
copy(memoizeSafetyLevelParams = gate)
|
||||
|
||||
def memoizeSafetyLevelParams(gateBoolean: Boolean): Builder = {
|
||||
val gate = Gate.const(gateBoolean)
|
||||
copy(memoizeSafetyLevelParams = gate)
|
||||
}
|
||||
|
||||
def build(): VisibilityLibrary = {
|
||||
|
||||
(decider, abDecider, featureSwitches) match {
|
||||
case (None, _, _) =>
|
||||
throw new IllegalStateException(
|
||||
"Decider is unset! If intentional, please call .withNullDecider()."
|
||||
)
|
||||
|
||||
case (_, None, _) =>
|
||||
throw new IllegalStateException(
|
||||
"ABDecider is unset! If intentional, please call .withNullABDecider()."
|
||||
)
|
||||
|
||||
case (_, _, None) =>
|
||||
throw new IllegalStateException(
|
||||
"FeatureSwitches is unset! This is a bug."
|
||||
)
|
||||
|
||||
case (Some(d), Some(abd), Some(fs)) =>
|
||||
new VisibilityLibrary(
|
||||
statsReceiver,
|
||||
d,
|
||||
abd,
|
||||
VisibilityParams(log, statsReceiver, d, abd, fs),
|
||||
enableStitchProfiling = enableStitchProfiling,
|
||||
captureDebugStats = captureDebugStats,
|
||||
enableComposableActions = enableComposableActions,
|
||||
enableFailClosed = enableFailClosed,
|
||||
enableShortCircuiting = enableShortCircuiting,
|
||||
memoizeSafetyLevelParams = memoizeSafetyLevelParams)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val nullDecider = new NullDecider(true, true)
|
||||
|
||||
lazy val NullLibrary: VisibilityLibrary = new VisibilityLibrary(
|
||||
NullStatsReceiver,
|
||||
nullDecider,
|
||||
NullABDecider,
|
||||
VisibilityParams(
|
||||
NullLogger,
|
||||
NullStatsReceiver,
|
||||
nullDecider,
|
||||
NullABDecider,
|
||||
NullFeatureSwitches),
|
||||
enableStitchProfiling = Gate.False,
|
||||
captureDebugStats = Gate.False,
|
||||
enableComposableActions = Gate.False,
|
||||
enableFailClosed = Gate.False,
|
||||
enableShortCircuiting = Gate.True,
|
||||
memoizeSafetyLevelParams = Gate.False
|
||||
)
|
||||
}
|
||||
|
||||
class VisibilityLibrary private[VisibilityLibrary] (
|
||||
baseStatsReceiver: StatsReceiver,
|
||||
decider: Decider,
|
||||
abDecider: LoggingABDecider,
|
||||
visibilityParams: VisibilityParams,
|
||||
enableStitchProfiling: Gate[Unit],
|
||||
captureDebugStats: Gate[Unit],
|
||||
enableComposableActions: Gate[Unit],
|
||||
enableFailClosed: Gate[Unit],
|
||||
enableShortCircuiting: Gate[Unit],
|
||||
memoizeSafetyLevelParams: Gate[Unit]) {
|
||||
|
||||
val statsReceiver: StatsReceiver =
|
||||
new MemoizingStatsReceiver(baseStatsReceiver.scope("visibility_library"))
|
||||
|
||||
val metricsRecorder = VisibilityResultsMetricRecorder(statsReceiver, captureDebugStats)
|
||||
|
||||
val visParams: VisibilityParams = visibilityParams
|
||||
|
||||
val visibilityDeciderGates = VisibilityDeciderGates(decider)
|
||||
|
||||
val profileStats: MemoizingStatsReceiver = new MemoizingStatsReceiver(
|
||||
statsReceiver.scope("profiling"))
|
||||
|
||||
val perSafetyLevelProfileStats: StatsReceiver = profileStats.scope("for_safety_level")
|
||||
|
||||
val featureMapBuilder: FeatureMapBuilder.Build =
|
||||
FeatureMapBuilder(statsReceiver, enableStitchProfiling)
|
||||
|
||||
private lazy val tweetRuleGenerator = new TweetRuleGenerator()
|
||||
lazy val policyProvider = new InjectedPolicyProvider(
|
||||
visibilityDeciderGates = visibilityDeciderGates,
|
||||
tweetRuleGenerator = tweetRuleGenerator)
|
||||
|
||||
val candidateVisibilityRulePreprocessor: VisibilityRulePreprocessor = VisibilityRulePreprocessor(
|
||||
metricsRecorder,
|
||||
policyProviderOpt = Some(policyProvider)
|
||||
)
|
||||
|
||||
val fallbackVisibilityRulePreprocessor: VisibilityRulePreprocessor = VisibilityRulePreprocessor(
|
||||
metricsRecorder)
|
||||
|
||||
lazy val candidateVisibilityRuleEngine: VisibilityRuleEngine = VisibilityRuleEngine(
|
||||
Some(candidateVisibilityRulePreprocessor),
|
||||
metricsRecorder,
|
||||
enableComposableActions,
|
||||
enableFailClosed,
|
||||
policyProviderOpt = Some(policyProvider)
|
||||
)
|
||||
|
||||
lazy val fallbackVisibilityRuleEngine: VisibilityRuleEngine = VisibilityRuleEngine(
|
||||
Some(fallbackVisibilityRulePreprocessor),
|
||||
metricsRecorder,
|
||||
enableComposableActions,
|
||||
enableFailClosed)
|
||||
|
||||
val ruleEngineVersionStatsReceiver = statsReceiver.scope("rule_engine_version")
|
||||
def isReleaseCandidateEnabled: Boolean = visibilityDeciderGates.enableExperimentalRuleEngine()
|
||||
|
||||
private def visibilityRuleEngine: DeciderableVisibilityRuleEngine = {
|
||||
if (isReleaseCandidateEnabled) {
|
||||
ruleEngineVersionStatsReceiver.counter("release_candidate").incr()
|
||||
candidateVisibilityRuleEngine
|
||||
} else {
|
||||
ruleEngineVersionStatsReceiver.counter("fallback").incr()
|
||||
fallbackVisibilityRuleEngine
|
||||
}
|
||||
}
|
||||
|
||||
private def profileStitch[A](result: Stitch[A], safetyLevelName: String): Stitch[A] =
|
||||
if (enableStitchProfiling()) {
|
||||
StitchHelpers.profileStitch(
|
||||
result,
|
||||
Seq(profileStats, perSafetyLevelProfileStats.scope(safetyLevelName))
|
||||
)
|
||||
} else {
|
||||
result
|
||||
}
|
||||
|
||||
def getParams(viewerContext: ViewerContext, safetyLevel: SafetyLevel): Params = {
|
||||
if (memoizeSafetyLevelParams()) {
|
||||
visibilityParams.memoized(viewerContext, safetyLevel)
|
||||
} else {
|
||||
visibilityParams(viewerContext, safetyLevel)
|
||||
}
|
||||
}
|
||||
|
||||
def evaluationContextBuilder(viewerContext: ViewerContext): EvaluationContext.Builder =
|
||||
EvaluationContext
|
||||
.Builder(statsReceiver, visibilityParams, viewerContext)
|
||||
.withMemoizedParams(memoizeSafetyLevelParams)
|
||||
|
||||
def runRuleEngine(
|
||||
contentId: ContentId,
|
||||
featureMap: FeatureMap,
|
||||
evaluationContextBuilder: EvaluationContext.Builder,
|
||||
safetyLevel: SafetyLevel
|
||||
): Stitch[VisibilityResult] =
|
||||
profileStitch(
|
||||
visibilityRuleEngine(
|
||||
evaluationContextBuilder.build(safetyLevel),
|
||||
safetyLevel,
|
||||
new VisibilityResultBuilder(contentId, featureMap),
|
||||
enableShortCircuiting
|
||||
),
|
||||
safetyLevel.name
|
||||
)
|
||||
|
||||
def runRuleEngine(
|
||||
contentId: ContentId,
|
||||
featureMap: FeatureMap,
|
||||
viewerContext: ViewerContext,
|
||||
safetyLevel: SafetyLevel
|
||||
): Stitch[VisibilityResult] =
|
||||
profileStitch(
|
||||
visibilityRuleEngine(
|
||||
EvaluationContext(safetyLevel, getParams(viewerContext, safetyLevel), statsReceiver),
|
||||
safetyLevel,
|
||||
new VisibilityResultBuilder(contentId, featureMap),
|
||||
enableShortCircuiting
|
||||
),
|
||||
safetyLevel.name
|
||||
)
|
||||
|
||||
def runRuleEngine(
|
||||
viewerContext: ViewerContext,
|
||||
safetyLevel: SafetyLevel,
|
||||
preprocessedResultBuilder: VisibilityResultBuilder,
|
||||
preprocessedRules: Seq[Rule]
|
||||
): Stitch[VisibilityResult] =
|
||||
profileStitch(
|
||||
visibilityRuleEngine(
|
||||
EvaluationContext(safetyLevel, getParams(viewerContext, safetyLevel), statsReceiver),
|
||||
safetyLevel,
|
||||
preprocessedResultBuilder,
|
||||
enableShortCircuiting,
|
||||
Some(preprocessedRules)
|
||||
),
|
||||
safetyLevel.name
|
||||
)
|
||||
|
||||
def runRuleEngineBatch(
|
||||
contentIds: Seq[ContentId],
|
||||
featureMapProvider: (ContentId, SafetyLevel) => FeatureMap,
|
||||
viewerContext: ViewerContext,
|
||||
safetyLevel: SafetyLevel,
|
||||
): Stitch[Seq[Try[VisibilityResult]]] = {
|
||||
val params = getParams(viewerContext, safetyLevel)
|
||||
profileStitch(
|
||||
Stitch.traverse(contentIds) { contentId =>
|
||||
visibilityRuleEngine(
|
||||
EvaluationContext(safetyLevel, params, NullStatsReceiver),
|
||||
safetyLevel,
|
||||
new VisibilityResultBuilder(contentId, featureMapProvider(contentId, safetyLevel)),
|
||||
enableShortCircuiting
|
||||
).liftToTry
|
||||
},
|
||||
safetyLevel.name
|
||||
)
|
||||
}
|
||||
|
||||
def runRuleEngineBatch(
|
||||
contentIds: Seq[ContentId],
|
||||
featureMapProvider: (ContentId, SafetyLevel) => FeatureMap,
|
||||
evaluationContextBuilder: EvaluationContext.Builder,
|
||||
safetyLevel: SafetyLevel
|
||||
): Stitch[Seq[Try[VisibilityResult]]] = {
|
||||
val evaluationContext = evaluationContextBuilder.build(safetyLevel)
|
||||
profileStitch(
|
||||
Stitch.traverse(contentIds) { contentId =>
|
||||
visibilityRuleEngine(
|
||||
evaluationContext,
|
||||
safetyLevel,
|
||||
new VisibilityResultBuilder(contentId, featureMapProvider(contentId, safetyLevel)),
|
||||
enableShortCircuiting
|
||||
).liftToTry
|
||||
},
|
||||
safetyLevel.name
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
scala_library(
|
||||
sources = ["*.scala"],
|
||||
compiler_option_sets = ["fatal_warnings"],
|
||||
platform = "java8",
|
||||
strict_deps = True,
|
||||
tags = ["bazel-compatible"],
|
||||
dependencies = [
|
||||
"3rdparty/jvm/com/twitter/src/java/com/twitter/logpipeline/client:logpipeline-event-publisher-thin",
|
||||
"configapi/configapi-core",
|
||||
"decider/src/main/scala",
|
||||
"servo/util/src/main/scala",
|
||||
"src/thrift/com/twitter/search/common:constants-scala",
|
||||
"src/thrift/com/twitter/spam/rtf:safety-level-scala",
|
||||
"src/thrift/com/twitter/spam/rtf:safety-result-scala",
|
||||
"src/thrift/com/twitter/spam/rtf:tweet-rtf-event-scala",
|
||||
"stitch/stitch-core",
|
||||
"util/util-stats/src/main/scala/com/twitter/finagle/stats",
|
||||
"visibility/common/src/main/scala/com/twitter/visibility/common/actions",
|
||||
"visibility/common/src/main/scala/com/twitter/visibility/common/actions/converter/scala",
|
||||
"visibility/common/src/main/scala/com/twitter/visibility/common/stitch",
|
||||
"visibility/common/src/main/thrift/com/twitter/visibility:action-scala",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/configapi/configs",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/features",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/models",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/rules",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/util",
|
||||
"visibility/lib/src/main/thrift/com/twitter/visibility/logging:vf-logging-scala",
|
||||
],
|
||||
)
|
@ -0,0 +1,64 @@
|
||||
package com.twitter.visibility.builder
|
||||
|
||||
import com.twitter.finagle.stats.NullStatsReceiver
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.servo.util.Gate
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.visibility.features._
|
||||
import com.twitter.visibility.common.stitch.StitchHelpers
|
||||
import scala.collection.mutable
|
||||
|
||||
object FeatureMapBuilder {
|
||||
type Build = Seq[FeatureMapBuilder => FeatureMapBuilder] => FeatureMap
|
||||
|
||||
def apply(
|
||||
statsReceiver: StatsReceiver = NullStatsReceiver,
|
||||
enableStitchProfiling: Gate[Unit] = Gate.False
|
||||
): Build =
|
||||
fns =>
|
||||
Function
|
||||
.chain(fns).apply(
|
||||
new FeatureMapBuilder(statsReceiver, enableStitchProfiling)
|
||||
).build
|
||||
}
|
||||
|
||||
class FeatureMapBuilder private[builder] (
|
||||
statsReceiver: StatsReceiver,
|
||||
enableStitchProfiling: Gate[Unit] = Gate.False) {
|
||||
|
||||
private[this] val hydratedScope =
|
||||
statsReceiver.scope("visibility_result_builder").scope("hydrated")
|
||||
|
||||
val mapBuilder: mutable.Builder[(Feature[_], Stitch[_]), Map[Feature[_], Stitch[_]]] =
|
||||
Map.newBuilder[Feature[_], Stitch[_]]
|
||||
|
||||
val constantMapBuilder: mutable.Builder[(Feature[_], Any), Map[Feature[_], Any]] =
|
||||
Map.newBuilder[Feature[_], Any]
|
||||
|
||||
def build: FeatureMap = new FeatureMap(mapBuilder.result(), constantMapBuilder.result())
|
||||
|
||||
def withConstantFeature[T](feature: Feature[T], value: T): FeatureMapBuilder = {
|
||||
val anyValue: Any = value.asInstanceOf[Any]
|
||||
constantMapBuilder += (feature -> anyValue)
|
||||
this
|
||||
}
|
||||
|
||||
def withFeature[T](feature: Feature[T], stitch: Stitch[T]): FeatureMapBuilder = {
|
||||
val profiledStitch = if (enableStitchProfiling()) {
|
||||
val featureScope = hydratedScope.scope(feature.name)
|
||||
StitchHelpers.profileStitch(stitch, Seq(hydratedScope, featureScope))
|
||||
} else {
|
||||
stitch
|
||||
}
|
||||
|
||||
val featureStitchRef = Stitch.ref(profiledStitch)
|
||||
|
||||
mapBuilder += FeatureMap.rescueFeatureTuple(feature -> featureStitchRef)
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
def withConstantFeature[T](feature: Feature[T], option: Option[T]): FeatureMapBuilder = {
|
||||
option.map(withConstantFeature(feature, _)).getOrElse(this)
|
||||
}
|
||||
}
|
@ -0,0 +1,187 @@
|
||||
package com.twitter.visibility.builder
|
||||
|
||||
import com.twitter.datatools.entityservice.entities.thriftscala.FleetInterstitial
|
||||
import com.twitter.decider.Decider
|
||||
import com.twitter.decider.Decider.NullDecider
|
||||
import com.twitter.finagle.stats.NullStatsReceiver
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.logpipeline.client.common.EventPublisher
|
||||
import com.twitter.logpipeline.client.EventPublisherManager
|
||||
import com.twitter.logpipeline.client.serializers.EventLogMsgThriftStructSerializer
|
||||
import com.twitter.spam.rtf.thriftscala.SafetyLevel
|
||||
import com.twitter.visibility.builder.VerdictLogger.FailureCounterName
|
||||
import com.twitter.visibility.builder.VerdictLogger.SuccessCounterName
|
||||
import com.twitter.visibility.features.Feature
|
||||
import com.twitter.visibility.logging.thriftscala.ActionSource
|
||||
import com.twitter.visibility.logging.thriftscala.EntityId
|
||||
import com.twitter.visibility.logging.thriftscala.EntityIdType
|
||||
import com.twitter.visibility.logging.thriftscala.EntityIdValue
|
||||
import com.twitter.visibility.logging.thriftscala.HealthActionType
|
||||
import com.twitter.visibility.logging.thriftscala.MisinfoPolicyCategory
|
||||
import com.twitter.visibility.logging.thriftscala.VFLibType
|
||||
import com.twitter.visibility.logging.thriftscala.VFVerdictLogEntry
|
||||
import com.twitter.visibility.models.ContentId
|
||||
import com.twitter.visibility.rules._
|
||||
|
||||
object VerdictLogger {
|
||||
|
||||
private val BaseStatsNamespace = "vf_verdict_logger"
|
||||
private val FailureCounterName = "failures"
|
||||
private val SuccessCounterName = "successes"
|
||||
val LogCategoryName: String = "visibility_filtering_verdicts"
|
||||
|
||||
val Empty: VerdictLogger = new VerdictLogger(NullStatsReceiver, NullDecider, None)
|
||||
|
||||
def apply(
|
||||
statsReceiver: StatsReceiver,
|
||||
decider: Decider
|
||||
): VerdictLogger = {
|
||||
val eventPublisher: EventPublisher[VFVerdictLogEntry] =
|
||||
EventPublisherManager
|
||||
.newScribePublisherBuilder(
|
||||
LogCategoryName,
|
||||
EventLogMsgThriftStructSerializer.getNewSerializer[VFVerdictLogEntry]()).build()
|
||||
new VerdictLogger(statsReceiver.scope(BaseStatsNamespace), decider, Some(eventPublisher))
|
||||
}
|
||||
}
|
||||
|
||||
class VerdictLogger(
|
||||
statsReceiver: StatsReceiver,
|
||||
decider: Decider,
|
||||
publisherOpt: Option[EventPublisher[VFVerdictLogEntry]]) {
|
||||
|
||||
def log(
|
||||
verdictLogEntry: VFVerdictLogEntry,
|
||||
publisher: EventPublisher[VFVerdictLogEntry]
|
||||
): Unit = {
|
||||
publisher
|
||||
.publish(verdictLogEntry)
|
||||
.onSuccess(_ => statsReceiver.counter(SuccessCounterName).incr())
|
||||
.onFailure { e =>
|
||||
statsReceiver.counter(FailureCounterName).incr()
|
||||
statsReceiver.scope(FailureCounterName).counter(e.getClass.getName).incr()
|
||||
}
|
||||
}
|
||||
|
||||
private def toEntityId(contentId: ContentId): Option[EntityId] = {
|
||||
contentId match {
|
||||
case ContentId.TweetId(id) => Some(EntityId(EntityIdType.TweetId, EntityIdValue.EntityId(id)))
|
||||
case ContentId.UserId(id) => Some(EntityId(EntityIdType.UserId, EntityIdValue.EntityId(id)))
|
||||
case ContentId.QuotedTweetRelationship(outerTweetId, _) =>
|
||||
Some(EntityId(EntityIdType.TweetId, EntityIdValue.EntityId(outerTweetId)))
|
||||
case ContentId.NotificationId(Some(id)) =>
|
||||
Some(EntityId(EntityIdType.NotificationId, EntityIdValue.EntityId(id)))
|
||||
case ContentId.DmId(id) => Some(EntityId(EntityIdType.DmId, EntityIdValue.EntityId(id)))
|
||||
case ContentId.BlenderTweetId(id) =>
|
||||
Some(EntityId(EntityIdType.TweetId, EntityIdValue.EntityId(id)))
|
||||
case ContentId.SpacePlusUserId(_) =>
|
||||
}
|
||||
}
|
||||
|
||||
private def getLogEntryData(
|
||||
actingRule: Option[Rule],
|
||||
secondaryActingRules: Seq[Rule],
|
||||
verdict: Action,
|
||||
secondaryVerdicts: Seq[Action],
|
||||
resolvedFeatureMap: Map[Feature[_], Any]
|
||||
): (Seq[String], Seq[ActionSource], Seq[HealthActionType], Option[FleetInterstitial]) = {
|
||||
actingRule
|
||||
.filter {
|
||||
case decideredRule: DoesLogVerdictDecidered =>
|
||||
decider.isAvailable(decideredRule.verdictLogDeciderKey.toString)
|
||||
case rule: DoesLogVerdict => true
|
||||
case _ => false
|
||||
}
|
||||
.map { primaryRule =>
|
||||
val secondaryRulesAndVerdicts = secondaryActingRules zip secondaryVerdicts
|
||||
var actingRules: Seq[Rule] = Seq(primaryRule)
|
||||
var actingRuleNames: Seq[String] = Seq(primaryRule.name)
|
||||
var actionSources: Seq[ActionSource] = Seq()
|
||||
var healthActionTypes: Seq[HealthActionType] = Seq(verdict.toHealthActionTypeThrift.get)
|
||||
|
||||
val misinfoPolicyCategory: Option[FleetInterstitial] = {
|
||||
verdict match {
|
||||
case softIntervention: SoftIntervention =>
|
||||
softIntervention.fleetInterstitial
|
||||
case tweetInterstitial: TweetInterstitial =>
|
||||
tweetInterstitial.softIntervention.flatMap(_.fleetInterstitial)
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
|
||||
secondaryRulesAndVerdicts.foreach(ruleAndVerdict => {
|
||||
if (ruleAndVerdict._1.isInstanceOf[DoesLogVerdict]) {
|
||||
actingRules = actingRules :+ ruleAndVerdict._1
|
||||
actingRuleNames = actingRuleNames :+ ruleAndVerdict._1.name
|
||||
healthActionTypes = healthActionTypes :+ ruleAndVerdict._2.toHealthActionTypeThrift.get
|
||||
}
|
||||
})
|
||||
|
||||
actingRules.foreach(rule => {
|
||||
rule.actionSourceBuilder
|
||||
.flatMap(_.build(resolvedFeatureMap, verdict))
|
||||
.map(actionSource => {
|
||||
actionSources = actionSources :+ actionSource
|
||||
})
|
||||
})
|
||||
(actingRuleNames, actionSources, healthActionTypes, misinfoPolicyCategory)
|
||||
}
|
||||
.getOrElse((Seq.empty[String], Seq.empty[ActionSource], Seq.empty[HealthActionType], None))
|
||||
}
|
||||
|
||||
def scribeVerdict(
|
||||
visibilityResult: VisibilityResult,
|
||||
safetyLevel: SafetyLevel,
|
||||
vfLibType: VFLibType,
|
||||
viewerId: Option[Long] = None
|
||||
): Unit = {
|
||||
publisherOpt.foreach { publisher =>
|
||||
toEntityId(visibilityResult.contentId).foreach { entityId =>
|
||||
visibilityResult.verdict.toHealthActionTypeThrift.foreach { healthActionType =>
|
||||
val (actioningRules, actionSources, healthActionTypes, misinfoPolicyCategory) =
|
||||
getLogEntryData(
|
||||
actingRule = visibilityResult.actingRule,
|
||||
secondaryActingRules = visibilityResult.secondaryActingRules,
|
||||
verdict = visibilityResult.verdict,
|
||||
secondaryVerdicts = visibilityResult.secondaryVerdicts,
|
||||
resolvedFeatureMap = visibilityResult.resolvedFeatureMap
|
||||
)
|
||||
|
||||
if (actioningRules.nonEmpty) {
|
||||
log(
|
||||
VFVerdictLogEntry(
|
||||
entityId = entityId,
|
||||
viewerId = viewerId,
|
||||
timestampMsec = System.currentTimeMillis(),
|
||||
vfLibType = vfLibType,
|
||||
healthActionType = healthActionType,
|
||||
safetyLevel = safetyLevel,
|
||||
actioningRules = actioningRules,
|
||||
actionSources = actionSources,
|
||||
healthActionTypes = healthActionTypes,
|
||||
misinfoPolicyCategory =
|
||||
fleetInterstitialToMisinfoPolicyCategory(misinfoPolicyCategory)
|
||||
),
|
||||
publisher
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def fleetInterstitialToMisinfoPolicyCategory(
|
||||
fleetInterstitialOption: Option[FleetInterstitial]
|
||||
): Option[MisinfoPolicyCategory] = {
|
||||
fleetInterstitialOption.map {
|
||||
case FleetInterstitial.Generic =>
|
||||
MisinfoPolicyCategory.Generic
|
||||
case FleetInterstitial.Samm =>
|
||||
MisinfoPolicyCategory.Samm
|
||||
case FleetInterstitial.CivicIntegrity =>
|
||||
MisinfoPolicyCategory.CivicIntegrity
|
||||
case _ => MisinfoPolicyCategory.Unknown
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
package com.twitter.visibility.builder
|
||||
|
||||
import com.twitter.spam.rtf.thriftscala.SafetyResult
|
||||
import com.twitter.visibility.common.actions.converter.scala.DropReasonConverter
|
||||
import com.twitter.visibility.rules.ComposableActions._
|
||||
import com.twitter.visibility.features.Feature
|
||||
import com.twitter.visibility.features.FeatureMap
|
||||
import com.twitter.visibility.models.ContentId
|
||||
import com.twitter.visibility.rules._
|
||||
import com.twitter.visibility.{thriftscala => t}
|
||||
|
||||
case class VisibilityResult(
|
||||
contentId: ContentId,
|
||||
featureMap: FeatureMap = FeatureMap.empty,
|
||||
ruleResultMap: Map[Rule, RuleResult] = Map.empty,
|
||||
verdict: Action = Allow,
|
||||
finished: Boolean = false,
|
||||
actingRule: Option[Rule] = None,
|
||||
secondaryActingRules: Seq[Rule] = Seq(),
|
||||
secondaryVerdicts: Seq[Action] = Seq(),
|
||||
resolvedFeatureMap: Map[Feature[_], Any] = Map.empty) {
|
||||
|
||||
def getSafetyResult: SafetyResult =
|
||||
verdict match {
|
||||
case InterstitialLimitedEngagements(reason: Reason, _, _, _)
|
||||
if PublicInterest.Reasons
|
||||
.contains(reason) =>
|
||||
SafetyResult(
|
||||
Some(PublicInterest.ReasonToSafetyResultReason(reason)),
|
||||
verdict.toActionThrift()
|
||||
)
|
||||
case ComposableActionsWithInterstitialLimitedEngagements(tweetInterstitial)
|
||||
if PublicInterest.Reasons.contains(tweetInterstitial.reason) =>
|
||||
SafetyResult(
|
||||
Some(PublicInterest.ReasonToSafetyResultReason(tweetInterstitial.reason)),
|
||||
verdict.toActionThrift()
|
||||
)
|
||||
case FreedomOfSpeechNotReachReason(appealableReason) =>
|
||||
SafetyResult(
|
||||
Some(FreedomOfSpeechNotReach.reasonToSafetyResultReason(appealableReason)),
|
||||
verdict.toActionThrift()
|
||||
)
|
||||
case _ => SafetyResult(None, verdict.toActionThrift())
|
||||
}
|
||||
|
||||
def getUserVisibilityResult: Option[t.UserVisibilityResult] =
|
||||
(verdict match {
|
||||
case Drop(reason, _) =>
|
||||
Some(
|
||||
t.UserAction.Drop(t.Drop(Reason.toDropReason(reason).map(DropReasonConverter.toThrift))))
|
||||
case _ => None
|
||||
}).map(userAction => t.UserVisibilityResult(Some(userAction)))
|
||||
}
|
||||
|
||||
object VisibilityResult {
|
||||
class Builder {
|
||||
var featureMap: FeatureMap = FeatureMap.empty
|
||||
var ruleResultMap: Map[Rule, RuleResult] = Map.empty
|
||||
var verdict: Action = Allow
|
||||
var finished: Boolean = false
|
||||
var actingRule: Option[Rule] = None
|
||||
var secondaryActingRules: Seq[Rule] = Seq()
|
||||
var secondaryVerdicts: Seq[Action] = Seq()
|
||||
var resolvedFeatureMap: Map[Feature[_], Any] = Map.empty
|
||||
|
||||
def withFeatureMap(featureMapBld: FeatureMap) = {
|
||||
featureMap = featureMapBld
|
||||
this
|
||||
}
|
||||
|
||||
def withRuleResultMap(ruleResultMapBld: Map[Rule, RuleResult]) = {
|
||||
ruleResultMap = ruleResultMapBld
|
||||
this
|
||||
}
|
||||
|
||||
def withVerdict(verdictBld: Action) = {
|
||||
verdict = verdictBld
|
||||
this
|
||||
}
|
||||
|
||||
def withFinished(finishedBld: Boolean) = {
|
||||
finished = finishedBld
|
||||
this
|
||||
}
|
||||
|
||||
def withActingRule(actingRuleBld: Option[Rule]) = {
|
||||
actingRule = actingRuleBld
|
||||
this
|
||||
}
|
||||
|
||||
def withSecondaryActingRules(secondaryActingRulesBld: Seq[Rule]) = {
|
||||
secondaryActingRules = secondaryActingRulesBld
|
||||
this
|
||||
}
|
||||
|
||||
def withSecondaryVerdicts(secondaryVerdictsBld: Seq[Action]) = {
|
||||
secondaryVerdicts = secondaryVerdictsBld
|
||||
this
|
||||
}
|
||||
|
||||
def build(contentId: ContentId) = VisibilityResult(
|
||||
contentId,
|
||||
featureMap,
|
||||
ruleResultMap,
|
||||
verdict,
|
||||
finished,
|
||||
actingRule,
|
||||
secondaryActingRules,
|
||||
secondaryVerdicts,
|
||||
resolvedFeatureMap)
|
||||
}
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
package com.twitter.visibility.builder
|
||||
|
||||
import com.twitter.visibility.features.Feature
|
||||
import com.twitter.visibility.features.FeatureMap
|
||||
import com.twitter.visibility.models.ContentId
|
||||
import com.twitter.visibility.rules.Action
|
||||
import com.twitter.visibility.rules.Allow
|
||||
import com.twitter.visibility.rules.EvaluationContext
|
||||
import com.twitter.visibility.rules.FailClosedException
|
||||
import com.twitter.visibility.rules.FeaturesFailedException
|
||||
import com.twitter.visibility.rules.MissingFeaturesException
|
||||
import com.twitter.visibility.rules.Rule
|
||||
import com.twitter.visibility.rules.RuleFailedException
|
||||
import com.twitter.visibility.rules.RuleResult
|
||||
import com.twitter.visibility.rules.State.FeatureFailed
|
||||
import com.twitter.visibility.rules.State.MissingFeature
|
||||
import com.twitter.visibility.rules.State.RuleFailed
|
||||
|
||||
class VisibilityResultBuilder(
|
||||
val contentId: ContentId,
|
||||
val featureMap: FeatureMap = FeatureMap.empty,
|
||||
private var ruleResultMap: Map[Rule, RuleResult] = Map.empty) {
|
||||
private var mapBuilder = Map.newBuilder[Rule, RuleResult]
|
||||
mapBuilder ++= ruleResultMap
|
||||
var verdict: Action = Allow
|
||||
var finished: Boolean = false
|
||||
var features: FeatureMap = featureMap
|
||||
var actingRule: Option[Rule] = None
|
||||
var secondaryVerdicts: Seq[Action] = Seq()
|
||||
var secondaryActingRules: Seq[Rule] = Seq()
|
||||
var resolvedFeatureMap: Map[Feature[_], Any] = Map.empty
|
||||
|
||||
def ruleResults: Map[Rule, RuleResult] = mapBuilder.result()
|
||||
|
||||
def withFeatureMap(featureMap: FeatureMap): VisibilityResultBuilder = {
|
||||
this.features = featureMap
|
||||
this
|
||||
}
|
||||
|
||||
def withRuleResultMap(ruleResultMap: Map[Rule, RuleResult]): VisibilityResultBuilder = {
|
||||
this.ruleResultMap = ruleResultMap
|
||||
mapBuilder = Map.newBuilder[Rule, RuleResult]
|
||||
mapBuilder ++= ruleResultMap
|
||||
this
|
||||
}
|
||||
|
||||
def withRuleResult(rule: Rule, result: RuleResult): VisibilityResultBuilder = {
|
||||
mapBuilder += ((rule, result))
|
||||
this
|
||||
}
|
||||
|
||||
def withVerdict(verdict: Action, ruleOpt: Option[Rule] = None): VisibilityResultBuilder = {
|
||||
this.verdict = verdict
|
||||
this.actingRule = ruleOpt
|
||||
this
|
||||
}
|
||||
|
||||
def withSecondaryVerdict(verdict: Action, rule: Rule): VisibilityResultBuilder = {
|
||||
this.secondaryVerdicts = this.secondaryVerdicts :+ verdict
|
||||
this.secondaryActingRules = this.secondaryActingRules :+ rule
|
||||
this
|
||||
}
|
||||
|
||||
def withFinished(finished: Boolean): VisibilityResultBuilder = {
|
||||
this.finished = finished
|
||||
this
|
||||
}
|
||||
|
||||
def withResolvedFeatureMap(
|
||||
resolvedFeatureMap: Map[Feature[_], Any]
|
||||
): VisibilityResultBuilder = {
|
||||
this.resolvedFeatureMap = resolvedFeatureMap
|
||||
this
|
||||
}
|
||||
|
||||
def isVerdictComposable(): Boolean = this.verdict.isComposable
|
||||
|
||||
def failClosedException(evaluationContext: EvaluationContext): Option[FailClosedException] = {
|
||||
mapBuilder
|
||||
.result().collect {
|
||||
case (r: Rule, RuleResult(_, MissingFeature(mf)))
|
||||
if r.shouldFailClosed(evaluationContext.params) =>
|
||||
Some(MissingFeaturesException(r.name, mf))
|
||||
case (r: Rule, RuleResult(_, FeatureFailed(ff)))
|
||||
if r.shouldFailClosed(evaluationContext.params) =>
|
||||
Some(FeaturesFailedException(r.name, ff))
|
||||
case (r: Rule, RuleResult(_, RuleFailed(t)))
|
||||
if r.shouldFailClosed(evaluationContext.params) =>
|
||||
Some(RuleFailedException(r.name, t))
|
||||
}.toList.foldLeft(None: Option[FailClosedException]) { (acc, arg) =>
|
||||
(acc, arg) match {
|
||||
case (None, Some(_)) => arg
|
||||
case (Some(FeaturesFailedException(_, _)), Some(MissingFeaturesException(_, _))) => arg
|
||||
case (Some(RuleFailedException(_, _)), Some(MissingFeaturesException(_, _))) => arg
|
||||
case (Some(RuleFailedException(_, _)), Some(FeaturesFailedException(_, _))) => arg
|
||||
case _ => acc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def build: VisibilityResult = {
|
||||
VisibilityResult(
|
||||
contentId = contentId,
|
||||
featureMap = features,
|
||||
ruleResultMap = mapBuilder.result(),
|
||||
verdict = verdict,
|
||||
finished = finished,
|
||||
actingRule = actingRule,
|
||||
secondaryActingRules = secondaryActingRules,
|
||||
secondaryVerdicts = secondaryVerdicts,
|
||||
resolvedFeatureMap = resolvedFeatureMap
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
scala_library(
|
||||
sources = ["*.scala"],
|
||||
compiler_option_sets = ["fatal_warnings"],
|
||||
platform = "java8",
|
||||
strict_deps = True,
|
||||
tags = ["bazel-compatible"],
|
||||
dependencies = [
|
||||
"3rdparty/jvm/com/google/guava",
|
||||
"communities/thrift/src/main/thrift/com/twitter/communities:thrift-scala",
|
||||
"communities/thrift/src/main/thrift/com/twitter/communities/moderation:thrift-scala",
|
||||
"escherbird/src/thrift/com/twitter/escherbird/softintervention:softintervention_thrift-scala",
|
||||
"snowflake/src/main/scala/com/twitter/snowflake/id",
|
||||
"src/thrift/com/twitter/context:twitter-context-scala",
|
||||
"src/thrift/com/twitter/escherbird/common:common-scala",
|
||||
"src/thrift/com/twitter/gizmoduck:user-thrift-scala",
|
||||
"src/thrift/com/twitter/search/common:constants-scala",
|
||||
"src/thrift/com/twitter/spam/rtf:safety-level-scala",
|
||||
"src/thrift/com/twitter/spam/rtf:tweet-rtf-event-scala",
|
||||
"src/thrift/com/twitter/tweetypie:tweet-scala",
|
||||
"stitch/stitch-core",
|
||||
"tweetypie/src/scala/com/twitter/tweetypie/additionalfields",
|
||||
"twitter-context/src/main/scala",
|
||||
"visibility/common/src/main/scala/com/twitter/visibility/common",
|
||||
"visibility/common/src/main/scala/com/twitter/visibility/common/stitch",
|
||||
"visibility/lib/src/main/resources/config",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/builder/users",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/features",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/interfaces/common/blender",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/interfaces/common/tweets",
|
||||
],
|
||||
)
|
@ -0,0 +1,228 @@
|
||||
package com.twitter.visibility.builder.common
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.gizmoduck.thriftscala.MuteOption
|
||||
import com.twitter.gizmoduck.thriftscala.MuteSurface
|
||||
import com.twitter.gizmoduck.thriftscala.{MutedKeyword => GdMutedKeyword}
|
||||
import com.twitter.servo.util.Gate
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.tweetypie.thriftscala.Tweet
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.common._
|
||||
import com.twitter.visibility.features._
|
||||
import com.twitter.visibility.models.{MutedKeyword => VfMutedKeyword}
|
||||
import java.util.Locale
|
||||
|
||||
class MutedKeywordFeatures(
|
||||
userSource: UserSource,
|
||||
userRelationshipSource: UserRelationshipSource,
|
||||
keywordMatcher: KeywordMatcher.Matcher = KeywordMatcher.TestMatcher,
|
||||
statsReceiver: StatsReceiver,
|
||||
enableFollowCheckInMutedKeyword: Gate[Unit] = Gate.False) {
|
||||
|
||||
private[this] val scopedStatsReceiver: StatsReceiver =
|
||||
statsReceiver.scope("muted_keyword_features")
|
||||
|
||||
private[this] val requests = scopedStatsReceiver.counter("requests")
|
||||
|
||||
private[this] val viewerMutesKeywordInTweetForHomeTimeline =
|
||||
scopedStatsReceiver.scope(ViewerMutesKeywordInTweetForHomeTimeline.name).counter("requests")
|
||||
private[this] val viewerMutesKeywordInTweetForTweetReplies =
|
||||
scopedStatsReceiver.scope(ViewerMutesKeywordInTweetForTweetReplies.name).counter("requests")
|
||||
private[this] val viewerMutesKeywordInTweetForNotifications =
|
||||
scopedStatsReceiver.scope(ViewerMutesKeywordInTweetForNotifications.name).counter("requests")
|
||||
private[this] val excludeFollowingForMutedKeywordsRequests =
|
||||
scopedStatsReceiver.scope("exclude_following").counter("requests")
|
||||
private[this] val viewerMutesKeywordInTweetForAllSurfaces =
|
||||
scopedStatsReceiver.scope(ViewerMutesKeywordInTweetForAllSurfaces.name).counter("requests")
|
||||
|
||||
def forTweet(
|
||||
tweet: Tweet,
|
||||
viewerId: Option[Long],
|
||||
authorId: Long
|
||||
): FeatureMapBuilder => FeatureMapBuilder = { featureMapBuilder =>
|
||||
requests.incr()
|
||||
viewerMutesKeywordInTweetForHomeTimeline.incr()
|
||||
viewerMutesKeywordInTweetForTweetReplies.incr()
|
||||
viewerMutesKeywordInTweetForNotifications.incr()
|
||||
viewerMutesKeywordInTweetForAllSurfaces.incr()
|
||||
|
||||
val keywordsBySurface = allMutedKeywords(viewerId)
|
||||
|
||||
val keywordsWithoutDefinedSurface = allMutedKeywordsWithoutDefinedSurface(viewerId)
|
||||
|
||||
featureMapBuilder
|
||||
.withFeature(
|
||||
ViewerMutesKeywordInTweetForHomeTimeline,
|
||||
tweetContainsMutedKeyword(
|
||||
tweet,
|
||||
keywordsBySurface,
|
||||
MuteSurface.HomeTimeline,
|
||||
viewerId,
|
||||
authorId
|
||||
)
|
||||
)
|
||||
.withFeature(
|
||||
ViewerMutesKeywordInTweetForTweetReplies,
|
||||
tweetContainsMutedKeyword(
|
||||
tweet,
|
||||
keywordsBySurface,
|
||||
MuteSurface.TweetReplies,
|
||||
viewerId,
|
||||
authorId
|
||||
)
|
||||
)
|
||||
.withFeature(
|
||||
ViewerMutesKeywordInTweetForNotifications,
|
||||
tweetContainsMutedKeyword(
|
||||
tweet,
|
||||
keywordsBySurface,
|
||||
MuteSurface.Notifications,
|
||||
viewerId,
|
||||
authorId
|
||||
)
|
||||
)
|
||||
.withFeature(
|
||||
ViewerMutesKeywordInTweetForAllSurfaces,
|
||||
tweetContainsMutedKeywordWithoutDefinedSurface(
|
||||
tweet,
|
||||
keywordsWithoutDefinedSurface,
|
||||
viewerId,
|
||||
authorId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def allMutedKeywords(viewerId: Option[Long]): Stitch[Map[MuteSurface, Seq[GdMutedKeyword]]] =
|
||||
viewerId
|
||||
.map { id => userSource.getAllMutedKeywords(id) }.getOrElse(Stitch.value(Map.empty))
|
||||
|
||||
def allMutedKeywordsWithoutDefinedSurface(viewerId: Option[Long]): Stitch[Seq[GdMutedKeyword]] =
|
||||
viewerId
|
||||
.map { id => userSource.getAllMutedKeywordsWithoutDefinedSurface(id) }.getOrElse(
|
||||
Stitch.value(Seq.empty))
|
||||
|
||||
private def mutingKeywordsText(
|
||||
mutedKeywords: Seq[GdMutedKeyword],
|
||||
muteSurface: MuteSurface,
|
||||
viewerIdOpt: Option[Long],
|
||||
authorId: Long
|
||||
): Stitch[Option[String]] = {
|
||||
if (muteSurface == MuteSurface.HomeTimeline && mutedKeywords.nonEmpty) {
|
||||
Stitch.value(Some(mutedKeywords.map(_.keyword).mkString(",")))
|
||||
} else {
|
||||
mutedKeywords.partition(kw =>
|
||||
kw.muteOptions.contains(MuteOption.ExcludeFollowingAccounts)) match {
|
||||
case (_, mutedKeywordsFromAnyone) if mutedKeywordsFromAnyone.nonEmpty =>
|
||||
Stitch.value(Some(mutedKeywordsFromAnyone.map(_.keyword).mkString(",")))
|
||||
case (mutedKeywordsExcludeFollowing, _)
|
||||
if mutedKeywordsExcludeFollowing.nonEmpty && enableFollowCheckInMutedKeyword() =>
|
||||
excludeFollowingForMutedKeywordsRequests.incr()
|
||||
viewerIdOpt match {
|
||||
case Some(viewerId) =>
|
||||
userRelationshipSource.follows(viewerId, authorId).map {
|
||||
case true =>
|
||||
case false => Some(mutedKeywordsExcludeFollowing.map(_.keyword).mkString(","))
|
||||
}
|
||||
case _ => Stitch.None
|
||||
}
|
||||
case (_, _) => Stitch.None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def mutingKeywordsTextWithoutDefinedSurface(
|
||||
mutedKeywords: Seq[GdMutedKeyword],
|
||||
viewerIdOpt: Option[Long],
|
||||
authorId: Long
|
||||
): Stitch[Option[String]] = {
|
||||
mutedKeywords.partition(kw =>
|
||||
kw.muteOptions.contains(MuteOption.ExcludeFollowingAccounts)) match {
|
||||
case (_, mutedKeywordsFromAnyone) if mutedKeywordsFromAnyone.nonEmpty =>
|
||||
Stitch.value(Some(mutedKeywordsFromAnyone.map(_.keyword).mkString(",")))
|
||||
case (mutedKeywordsExcludeFollowing, _)
|
||||
if mutedKeywordsExcludeFollowing.nonEmpty && enableFollowCheckInMutedKeyword() =>
|
||||
excludeFollowingForMutedKeywordsRequests.incr()
|
||||
viewerIdOpt match {
|
||||
case Some(viewerId) =>
|
||||
userRelationshipSource.follows(viewerId, authorId).map {
|
||||
case true =>
|
||||
case false => Some(mutedKeywordsExcludeFollowing.map(_.keyword).mkString(","))
|
||||
}
|
||||
case _ => Stitch.None
|
||||
}
|
||||
case (_, _) => Stitch.None
|
||||
}
|
||||
}
|
||||
|
||||
def tweetContainsMutedKeyword(
|
||||
tweet: Tweet,
|
||||
mutedKeywordMap: Stitch[Map[MuteSurface, Seq[GdMutedKeyword]]],
|
||||
muteSurface: MuteSurface,
|
||||
viewerIdOpt: Option[Long],
|
||||
authorId: Long
|
||||
): Stitch[VfMutedKeyword] = {
|
||||
mutedKeywordMap.flatMap { keywordMap =>
|
||||
if (keywordMap.isEmpty) {
|
||||
Stitch.value(VfMutedKeyword(None))
|
||||
} else {
|
||||
val mutedKeywords = keywordMap.getOrElse(muteSurface, Nil)
|
||||
val matchTweetFn: KeywordMatcher.MatchTweet = keywordMatcher(mutedKeywords)
|
||||
val locale = tweet.language.map(l => Locale.forLanguageTag(l.language))
|
||||
val text = tweet.coreData.get.text
|
||||
|
||||
matchTweetFn(locale, text).flatMap { results =>
|
||||
mutingKeywordsText(results, muteSurface, viewerIdOpt, authorId).map(VfMutedKeyword)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def tweetContainsMutedKeywordWithoutDefinedSurface(
|
||||
tweet: Tweet,
|
||||
mutedKeywordSeq: Stitch[Seq[GdMutedKeyword]],
|
||||
viewerIdOpt: Option[Long],
|
||||
authorId: Long
|
||||
): Stitch[VfMutedKeyword] = {
|
||||
mutedKeywordSeq.flatMap { mutedKeyword =>
|
||||
if (mutedKeyword.isEmpty) {
|
||||
Stitch.value(VfMutedKeyword(None))
|
||||
} else {
|
||||
val matchTweetFn: KeywordMatcher.MatchTweet = keywordMatcher(mutedKeyword)
|
||||
val locale = tweet.language.map(l => Locale.forLanguageTag(l.language))
|
||||
val text = tweet.coreData.get.text
|
||||
|
||||
matchTweetFn(locale, text).flatMap { results =>
|
||||
mutingKeywordsTextWithoutDefinedSurface(results, viewerIdOpt, authorId).map(
|
||||
VfMutedKeyword
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
def spaceTitleContainsMutedKeyword(
|
||||
spaceTitle: String,
|
||||
spaceLanguageOpt: Option[String],
|
||||
mutedKeywordMap: Stitch[Map[MuteSurface, Seq[GdMutedKeyword]]],
|
||||
muteSurface: MuteSurface,
|
||||
): Stitch[VfMutedKeyword] = {
|
||||
mutedKeywordMap.flatMap { keywordMap =>
|
||||
if (keywordMap.isEmpty) {
|
||||
Stitch.value(VfMutedKeyword(None))
|
||||
} else {
|
||||
val mutedKeywords = keywordMap.getOrElse(muteSurface, Nil)
|
||||
val matchTweetFn: KeywordMatcher.MatchTweet = keywordMatcher(mutedKeywords)
|
||||
|
||||
val locale = spaceLanguageOpt.map(l => Locale.forLanguageTag(l))
|
||||
matchTweetFn(locale, spaceTitle).flatMap { results =>
|
||||
if (results.nonEmpty) {
|
||||
Stitch.value(Some(results.map(_.keyword).mkString(","))).map(VfMutedKeyword)
|
||||
} else {
|
||||
Stitch.None.map(VfMutedKeyword)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
scala_library(
|
||||
sources = ["*.scala"],
|
||||
compiler_option_sets = ["fatal_warnings"],
|
||||
platform = "java8",
|
||||
strict_deps = True,
|
||||
tags = ["bazel-compatible"],
|
||||
dependencies = [
|
||||
"src/thrift/com/twitter/convosvc:convosvc-scala",
|
||||
"src/thrift/com/twitter/convosvc/internal:internal-scala",
|
||||
"src/thrift/com/twitter/spam/rtf:safety-level-scala",
|
||||
"stitch/stitch-core",
|
||||
"stitch/stitch-core/src/main/scala/com/twitter/stitch",
|
||||
"visibility/common/src/main/scala/com/twitter/visibility/common",
|
||||
"visibility/common/src/main/scala/com/twitter/visibility/common/dm_sources",
|
||||
"visibility/lib/src/main/resources/config",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/builder",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/builder/users",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/features",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/util",
|
||||
"visibility/lib/src/main/thrift/com/twitter/visibility/safety_label_store:safety-label-store-scala",
|
||||
],
|
||||
)
|
@ -0,0 +1,196 @@
|
||||
package com.twitter.visibility.builder.dms
|
||||
|
||||
import com.twitter.convosvc.thriftscala.ConversationQuery
|
||||
import com.twitter.convosvc.thriftscala.ConversationQueryOptions
|
||||
import com.twitter.convosvc.thriftscala.ConversationType
|
||||
import com.twitter.convosvc.thriftscala.TimelineLookupState
|
||||
import com.twitter.stitch.NotFound
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.builder.users.AuthorFeatures
|
||||
import com.twitter.visibility.common.DmConversationId
|
||||
import com.twitter.visibility.common.UserId
|
||||
import com.twitter.visibility.common.dm_sources.DmConversationSource
|
||||
import com.twitter.visibility.features._
|
||||
|
||||
case class InvalidDmConversationFeatureException(message: String) extends Exception(message)
|
||||
|
||||
class DmConversationFeatures(
|
||||
dmConversationSource: DmConversationSource,
|
||||
authorFeatures: AuthorFeatures) {
|
||||
|
||||
def forDmConversationId(
|
||||
dmConversationId: DmConversationId,
|
||||
viewerIdOpt: Option[UserId]
|
||||
): FeatureMapBuilder => FeatureMapBuilder =
|
||||
_.withFeature(
|
||||
DmConversationIsOneToOneConversation,
|
||||
dmConversationIsOneToOneConversation(dmConversationId, viewerIdOpt))
|
||||
.withFeature(
|
||||
DmConversationHasEmptyTimeline,
|
||||
dmConversationHasEmptyTimeline(dmConversationId, viewerIdOpt))
|
||||
.withFeature(
|
||||
DmConversationHasValidLastReadableEventId,
|
||||
dmConversationHasValidLastReadableEventId(dmConversationId, viewerIdOpt))
|
||||
.withFeature(
|
||||
DmConversationInfoExists,
|
||||
dmConversationInfoExists(dmConversationId, viewerIdOpt))
|
||||
.withFeature(
|
||||
DmConversationTimelineExists,
|
||||
dmConversationTimelineExists(dmConversationId, viewerIdOpt))
|
||||
.withFeature(
|
||||
AuthorIsSuspended,
|
||||
dmConversationHasSuspendedParticipant(dmConversationId, viewerIdOpt))
|
||||
.withFeature(
|
||||
AuthorIsDeactivated,
|
||||
dmConversationHasDeactivatedParticipant(dmConversationId, viewerIdOpt))
|
||||
.withFeature(
|
||||
AuthorIsErased,
|
||||
dmConversationHasErasedParticipant(dmConversationId, viewerIdOpt))
|
||||
.withFeature(
|
||||
ViewerIsDmConversationParticipant,
|
||||
viewerIsDmConversationParticipant(dmConversationId, viewerIdOpt))
|
||||
|
||||
def dmConversationIsOneToOneConversation(
|
||||
dmConversationId: DmConversationId,
|
||||
viewerIdOpt: Option[UserId]
|
||||
): Stitch[Boolean] =
|
||||
viewerIdOpt match {
|
||||
case Some(viewerId) =>
|
||||
dmConversationSource.getConversationType(dmConversationId, viewerId).flatMap {
|
||||
case Some(ConversationType.OneToOneDm | ConversationType.SecretOneToOneDm) =>
|
||||
Stitch.True
|
||||
case None =>
|
||||
Stitch.exception(InvalidDmConversationFeatureException("Conversation type not found"))
|
||||
case _ => Stitch.False
|
||||
}
|
||||
case _ => Stitch.exception(InvalidDmConversationFeatureException("Viewer id missing"))
|
||||
}
|
||||
|
||||
private[dms] def dmConversationHasEmptyTimeline(
|
||||
dmConversationId: DmConversationId,
|
||||
viewerIdOpt: Option[UserId]
|
||||
): Stitch[Boolean] =
|
||||
dmConversationSource
|
||||
.getConversationTimelineEntries(
|
||||
dmConversationId,
|
||||
ConversationQuery(
|
||||
conversationId = Some(dmConversationId),
|
||||
options = Some(
|
||||
ConversationQueryOptions(
|
||||
perspectivalUserId = viewerIdOpt,
|
||||
hydrateEvents = Some(false),
|
||||
supportsReactions = Some(true)
|
||||
)
|
||||
),
|
||||
maxCount = 10
|
||||
)
|
||||
).map(_.forall(entries => entries.isEmpty))
|
||||
|
||||
private[dms] def dmConversationHasValidLastReadableEventId(
|
||||
dmConversationId: DmConversationId,
|
||||
viewerIdOpt: Option[UserId]
|
||||
): Stitch[Boolean] =
|
||||
viewerIdOpt match {
|
||||
case Some(viewerId) =>
|
||||
dmConversationSource
|
||||
.getConversationLastReadableEventId(dmConversationId, viewerId).map(_.exists(id =>
|
||||
id > 0L))
|
||||
case _ => Stitch.exception(InvalidDmConversationFeatureException("Viewer id missing"))
|
||||
}
|
||||
|
||||
private[dms] def dmConversationInfoExists(
|
||||
dmConversationId: DmConversationId,
|
||||
viewerIdOpt: Option[UserId]
|
||||
): Stitch[Boolean] =
|
||||
viewerIdOpt match {
|
||||
case Some(viewerId) =>
|
||||
dmConversationSource
|
||||
.getDmConversationInfo(dmConversationId, viewerId).map(_.isDefined)
|
||||
case _ => Stitch.exception(InvalidDmConversationFeatureException("Viewer id missing"))
|
||||
}
|
||||
|
||||
private[dms] def dmConversationTimelineExists(
|
||||
dmConversationId: DmConversationId,
|
||||
viewerIdOpt: Option[UserId]
|
||||
): Stitch[Boolean] =
|
||||
dmConversationSource
|
||||
.getConversationTimelineState(
|
||||
dmConversationId,
|
||||
ConversationQuery(
|
||||
conversationId = Some(dmConversationId),
|
||||
options = Some(
|
||||
ConversationQueryOptions(
|
||||
perspectivalUserId = viewerIdOpt,
|
||||
hydrateEvents = Some(false),
|
||||
supportsReactions = Some(true)
|
||||
)
|
||||
),
|
||||
maxCount = 1
|
||||
)
|
||||
).map {
|
||||
case Some(TimelineLookupState.NotFound) | None => false
|
||||
case _ => true
|
||||
}
|
||||
|
||||
private[dms] def anyConversationParticipantMatchesCondition(
|
||||
condition: UserId => Stitch[Boolean],
|
||||
dmConversationId: DmConversationId,
|
||||
viewerIdOpt: Option[UserId]
|
||||
): Stitch[Boolean] =
|
||||
viewerIdOpt match {
|
||||
case Some(viewerId) =>
|
||||
dmConversationSource
|
||||
.getConversationParticipantIds(dmConversationId, viewerId).flatMap {
|
||||
case Some(participants) =>
|
||||
Stitch
|
||||
.collect(participants.map(condition)).map(_.contains(true)).rescue {
|
||||
case NotFound =>
|
||||
Stitch.exception(InvalidDmConversationFeatureException("User not found"))
|
||||
}
|
||||
case _ => Stitch.False
|
||||
}
|
||||
case _ => Stitch.exception(InvalidDmConversationFeatureException("Viewer id missing"))
|
||||
}
|
||||
|
||||
def dmConversationHasSuspendedParticipant(
|
||||
dmConversationId: DmConversationId,
|
||||
viewerIdOpt: Option[UserId]
|
||||
): Stitch[Boolean] =
|
||||
anyConversationParticipantMatchesCondition(
|
||||
participant => authorFeatures.authorIsSuspended(participant),
|
||||
dmConversationId,
|
||||
viewerIdOpt)
|
||||
|
||||
def dmConversationHasDeactivatedParticipant(
|
||||
dmConversationId: DmConversationId,
|
||||
viewerIdOpt: Option[UserId]
|
||||
): Stitch[Boolean] =
|
||||
anyConversationParticipantMatchesCondition(
|
||||
participant => authorFeatures.authorIsDeactivated(participant),
|
||||
dmConversationId,
|
||||
viewerIdOpt)
|
||||
|
||||
def dmConversationHasErasedParticipant(
|
||||
dmConversationId: DmConversationId,
|
||||
viewerIdOpt: Option[UserId]
|
||||
): Stitch[Boolean] =
|
||||
anyConversationParticipantMatchesCondition(
|
||||
participant => authorFeatures.authorIsErased(participant),
|
||||
dmConversationId,
|
||||
viewerIdOpt)
|
||||
|
||||
def viewerIsDmConversationParticipant(
|
||||
dmConversationId: DmConversationId,
|
||||
viewerIdOpt: Option[UserId]
|
||||
): Stitch[Boolean] =
|
||||
viewerIdOpt match {
|
||||
case Some(viewerId) =>
|
||||
dmConversationSource
|
||||
.getConversationParticipantIds(dmConversationId, viewerId).map {
|
||||
case Some(participants) => participants.contains(viewerId)
|
||||
case _ => false
|
||||
}
|
||||
case _ => Stitch.exception(InvalidDmConversationFeatureException("Viewer id missing"))
|
||||
}
|
||||
}
|
@ -0,0 +1,341 @@
|
||||
package com.twitter.visibility.builder.dms
|
||||
|
||||
import com.twitter.convosvc.thriftscala.Event
|
||||
import com.twitter.convosvc.thriftscala.StoredDelete
|
||||
import com.twitter.convosvc.thriftscala.StoredPerspectivalMessageInfo
|
||||
import com.twitter.convosvc.thriftscala.PerspectivalSpamState
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.builder.users.AuthorFeatures
|
||||
import com.twitter.visibility.common.DmEventId
|
||||
import com.twitter.visibility.common.dm_sources.DmEventSource
|
||||
import com.twitter.visibility.common.UserId
|
||||
import com.twitter.convosvc.thriftscala.EventType
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.stitch.NotFound
|
||||
import com.twitter.visibility.common.dm_sources.DmConversationSource
|
||||
import com.twitter.visibility.features._
|
||||
|
||||
case class InvalidDmEventFeatureException(message: String) extends Exception(message)
|
||||
|
||||
class DmEventFeatures(
|
||||
dmEventSource: DmEventSource,
|
||||
dmConversationSource: DmConversationSource,
|
||||
authorFeatures: AuthorFeatures,
|
||||
dmConversationFeatures: DmConversationFeatures,
|
||||
statsReceiver: StatsReceiver) {
|
||||
private[this] val scopedStatsReceiver = statsReceiver.scope("dm_event_features")
|
||||
private[this] val requests = scopedStatsReceiver.counter("requests")
|
||||
|
||||
def forDmEventId(
|
||||
dmEventId: DmEventId,
|
||||
viewerId: UserId
|
||||
): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
requests.incr()
|
||||
|
||||
val dmEventStitchRef: Stitch[Option[Event]] =
|
||||
Stitch.ref(dmEventSource.getDmEvent(dmEventId, viewerId))
|
||||
|
||||
_.withFeature(
|
||||
DmEventIsMessageCreateEvent,
|
||||
isDmEventType(dmEventStitchRef, EventType.MessageCreate))
|
||||
.withFeature(
|
||||
AuthorIsSuspended,
|
||||
messageCreateEventHasInactiveInitiatingUser(
|
||||
dmEventStitchRef,
|
||||
initiatingUser => authorFeatures.authorIsSuspended(initiatingUser))
|
||||
)
|
||||
.withFeature(
|
||||
AuthorIsDeactivated,
|
||||
messageCreateEventHasInactiveInitiatingUser(
|
||||
dmEventStitchRef,
|
||||
initiatingUser => authorFeatures.authorIsDeactivated(initiatingUser))
|
||||
)
|
||||
.withFeature(
|
||||
AuthorIsErased,
|
||||
messageCreateEventHasInactiveInitiatingUser(
|
||||
dmEventStitchRef,
|
||||
initiatingUser => authorFeatures.authorIsErased(initiatingUser))
|
||||
)
|
||||
.withFeature(
|
||||
DmEventOccurredBeforeLastClearedEvent,
|
||||
dmEventOccurredBeforeLastClearedEvent(dmEventStitchRef, dmEventId, viewerId)
|
||||
)
|
||||
.withFeature(
|
||||
DmEventOccurredBeforeJoinConversationEvent,
|
||||
dmEventOccurredBeforeJoinConversationEvent(dmEventStitchRef, dmEventId, viewerId)
|
||||
)
|
||||
.withFeature(
|
||||
ViewerIsDmConversationParticipant,
|
||||
dmEventViewerIsDmConversationParticipant(dmEventStitchRef, viewerId)
|
||||
)
|
||||
.withFeature(
|
||||
DmEventIsDeleted,
|
||||
dmEventIsDeleted(dmEventStitchRef, dmEventId)
|
||||
)
|
||||
.withFeature(
|
||||
DmEventIsHidden,
|
||||
dmEventIsHidden(dmEventStitchRef, dmEventId)
|
||||
)
|
||||
.withFeature(
|
||||
ViewerIsDmEventInitiatingUser,
|
||||
viewerIsDmEventInitiatingUser(dmEventStitchRef, viewerId)
|
||||
)
|
||||
.withFeature(
|
||||
DmEventInOneToOneConversationWithUnavailableUser,
|
||||
dmEventInOneToOneConversationWithUnavailableUser(dmEventStitchRef, viewerId)
|
||||
)
|
||||
.withFeature(
|
||||
DmEventIsLastMessageReadUpdateEvent,
|
||||
isDmEventType(dmEventStitchRef, EventType.LastMessageReadUpdate)
|
||||
)
|
||||
.withFeature(
|
||||
DmEventIsJoinConversationEvent,
|
||||
isDmEventType(dmEventStitchRef, EventType.JoinConversation)
|
||||
)
|
||||
.withFeature(
|
||||
DmEventIsWelcomeMessageCreateEvent,
|
||||
isDmEventType(dmEventStitchRef, EventType.WelcomeMessageCreate)
|
||||
)
|
||||
.withFeature(
|
||||
DmEventIsTrustConversationEvent,
|
||||
isDmEventType(dmEventStitchRef, EventType.TrustConversation)
|
||||
)
|
||||
.withFeature(
|
||||
DmEventIsCsFeedbackSubmitted,
|
||||
isDmEventType(dmEventStitchRef, EventType.CsFeedbackSubmitted)
|
||||
)
|
||||
.withFeature(
|
||||
DmEventIsCsFeedbackDismissed,
|
||||
isDmEventType(dmEventStitchRef, EventType.CsFeedbackDismissed)
|
||||
)
|
||||
.withFeature(
|
||||
DmEventIsConversationCreateEvent,
|
||||
isDmEventType(dmEventStitchRef, EventType.ConversationCreate)
|
||||
)
|
||||
.withFeature(
|
||||
DmEventInOneToOneConversation,
|
||||
dmEventInOneToOneConversation(dmEventStitchRef, viewerId)
|
||||
)
|
||||
.withFeature(
|
||||
DmEventIsPerspectivalJoinConversationEvent,
|
||||
dmEventIsPerspectivalJoinConversationEvent(dmEventStitchRef, dmEventId, viewerId))
|
||||
|
||||
}
|
||||
|
||||
private def isDmEventType(
|
||||
dmEventOptStitch: Stitch[Option[Event]],
|
||||
eventType: EventType
|
||||
): Stitch[Boolean] =
|
||||
dmEventSource.getEventType(dmEventOptStitch).flatMap {
|
||||
case Some(_: eventType.type) =>
|
||||
Stitch.True
|
||||
case None =>
|
||||
Stitch.exception(InvalidDmEventFeatureException(s"$eventType event type not found"))
|
||||
case _ =>
|
||||
Stitch.False
|
||||
}
|
||||
|
||||
private def dmEventIsPerspectivalJoinConversationEvent(
|
||||
dmEventOptStitch: Stitch[Option[Event]],
|
||||
dmEventId: DmEventId,
|
||||
viewerId: UserId
|
||||
): Stitch[Boolean] =
|
||||
Stitch
|
||||
.join(
|
||||
dmEventSource.getEventType(dmEventOptStitch),
|
||||
dmEventSource.getConversationId(dmEventOptStitch)).flatMap {
|
||||
case (Some(EventType.JoinConversation), conversationIdOpt) =>
|
||||
conversationIdOpt match {
|
||||
case Some(conversationId) =>
|
||||
dmConversationSource
|
||||
.getParticipantJoinConversationEventId(conversationId, viewerId, viewerId)
|
||||
.flatMap {
|
||||
case Some(joinConversationEventId) =>
|
||||
Stitch.value(joinConversationEventId == dmEventId)
|
||||
case _ => Stitch.False
|
||||
}
|
||||
case _ =>
|
||||
Stitch.exception(InvalidDmEventFeatureException("Conversation id not found"))
|
||||
}
|
||||
case (None, _) =>
|
||||
Stitch.exception(InvalidDmEventFeatureException("Event type not found"))
|
||||
case _ => Stitch.False
|
||||
}
|
||||
|
||||
private def messageCreateEventHasInactiveInitiatingUser(
|
||||
dmEventOptStitch: Stitch[Option[Event]],
|
||||
condition: UserId => Stitch[Boolean],
|
||||
): Stitch[Boolean] =
|
||||
Stitch
|
||||
.join(
|
||||
dmEventSource.getEventType(dmEventOptStitch),
|
||||
dmEventSource.getInitiatingUserId(dmEventOptStitch)).flatMap {
|
||||
case (Some(EventType.MessageCreate), Some(userId)) =>
|
||||
condition(userId).rescue {
|
||||
case NotFound =>
|
||||
Stitch.exception(InvalidDmEventFeatureException("initiating user not found"))
|
||||
}
|
||||
case (None, _) =>
|
||||
Stitch.exception(InvalidDmEventFeatureException("DmEvent type is missing"))
|
||||
case (Some(EventType.MessageCreate), _) =>
|
||||
Stitch.exception(InvalidDmEventFeatureException("initiating user id is missing"))
|
||||
case _ => Stitch.False
|
||||
}
|
||||
|
||||
private def dmEventOccurredBeforeLastClearedEvent(
|
||||
dmEventOptStitch: Stitch[Option[Event]],
|
||||
dmEventId: DmEventId,
|
||||
viewerId: UserId
|
||||
): Stitch[Boolean] = {
|
||||
dmEventSource.getConversationId(dmEventOptStitch).flatMap {
|
||||
case Some(convoId) =>
|
||||
val lastClearedEventIdStitch =
|
||||
dmConversationSource.getParticipantLastClearedEventId(convoId, viewerId, viewerId)
|
||||
lastClearedEventIdStitch.flatMap {
|
||||
case Some(lastClearedEventId) => Stitch(dmEventId <= lastClearedEventId)
|
||||
case _ =>
|
||||
Stitch.False
|
||||
}
|
||||
case _ => Stitch.False
|
||||
}
|
||||
}
|
||||
|
||||
private def dmEventOccurredBeforeJoinConversationEvent(
|
||||
dmEventOptStitch: Stitch[Option[Event]],
|
||||
dmEventId: DmEventId,
|
||||
viewerId: UserId
|
||||
): Stitch[Boolean] = {
|
||||
dmEventSource.getConversationId(dmEventOptStitch).flatMap {
|
||||
case Some(convoId) =>
|
||||
val joinConversationEventIdStitch =
|
||||
dmConversationSource
|
||||
.getParticipantJoinConversationEventId(convoId, viewerId, viewerId)
|
||||
joinConversationEventIdStitch.flatMap {
|
||||
case Some(joinConversationEventId) => Stitch(dmEventId < joinConversationEventId)
|
||||
case _ => Stitch.False
|
||||
}
|
||||
case _ => Stitch.False
|
||||
}
|
||||
}
|
||||
|
||||
private def dmEventViewerIsDmConversationParticipant(
|
||||
dmEventOptStitch: Stitch[Option[Event]],
|
||||
viewerId: UserId
|
||||
): Stitch[Boolean] = {
|
||||
dmEventSource.getConversationId(dmEventOptStitch).flatMap {
|
||||
case Some(convoId) =>
|
||||
dmConversationFeatures.viewerIsDmConversationParticipant(convoId, Some(viewerId))
|
||||
case _ => Stitch.True
|
||||
}
|
||||
}
|
||||
|
||||
private def dmEventIsDeleted(
|
||||
dmEventOptStitch: Stitch[Option[Event]],
|
||||
dmEventId: DmEventId
|
||||
): Stitch[Boolean] =
|
||||
dmEventSource.getConversationId(dmEventOptStitch).flatMap {
|
||||
case Some(convoId) =>
|
||||
dmConversationSource
|
||||
.getDeleteInfo(convoId, dmEventId).rescue {
|
||||
case e: java.lang.IllegalArgumentException =>
|
||||
Stitch.exception(InvalidDmEventFeatureException("Invalid conversation id"))
|
||||
}.flatMap {
|
||||
case Some(StoredDelete(None)) => Stitch.True
|
||||
case _ => Stitch.False
|
||||
}
|
||||
case _ => Stitch.False
|
||||
}
|
||||
|
||||
private def dmEventIsHidden(
|
||||
dmEventOptStitch: Stitch[Option[Event]],
|
||||
dmEventId: DmEventId
|
||||
): Stitch[Boolean] =
|
||||
dmEventSource.getConversationId(dmEventOptStitch).flatMap {
|
||||
case Some(convoId) =>
|
||||
dmConversationSource
|
||||
.getPerspectivalMessageInfo(convoId, dmEventId).rescue {
|
||||
case e: java.lang.IllegalArgumentException =>
|
||||
Stitch.exception(InvalidDmEventFeatureException("Invalid conversation id"))
|
||||
}.flatMap {
|
||||
case Some(StoredPerspectivalMessageInfo(Some(hidden), _)) if hidden =>
|
||||
Stitch.True
|
||||
case Some(StoredPerspectivalMessageInfo(_, Some(spamState)))
|
||||
if spamState == PerspectivalSpamState.Spam =>
|
||||
Stitch.True
|
||||
case _ => Stitch.False
|
||||
}
|
||||
case _ => Stitch.False
|
||||
}
|
||||
|
||||
private def viewerIsDmEventInitiatingUser(
|
||||
dmEventOptStitch: Stitch[Option[Event]],
|
||||
viewerId: UserId
|
||||
): Stitch[Boolean] =
|
||||
Stitch
|
||||
.join(
|
||||
dmEventSource.getEventType(dmEventOptStitch),
|
||||
dmEventSource.getInitiatingUserId(dmEventOptStitch)).flatMap {
|
||||
case (
|
||||
Some(
|
||||
EventType.TrustConversation | EventType.CsFeedbackSubmitted |
|
||||
EventType.CsFeedbackDismissed | EventType.WelcomeMessageCreate |
|
||||
EventType.JoinConversation),
|
||||
Some(userId)) =>
|
||||
Stitch(viewerId == userId)
|
||||
case (
|
||||
Some(
|
||||
EventType.TrustConversation | EventType.CsFeedbackSubmitted |
|
||||
EventType.CsFeedbackDismissed | EventType.WelcomeMessageCreate |
|
||||
EventType.JoinConversation),
|
||||
None) =>
|
||||
Stitch.exception(InvalidDmEventFeatureException("Initiating user id is missing"))
|
||||
case (None, _) =>
|
||||
Stitch.exception(InvalidDmEventFeatureException("DmEvent type is missing"))
|
||||
case _ => Stitch.True
|
||||
}
|
||||
|
||||
private def dmEventInOneToOneConversationWithUnavailableUser(
|
||||
dmEventOptStitch: Stitch[Option[Event]],
|
||||
viewerId: UserId
|
||||
): Stitch[Boolean] =
|
||||
dmEventSource.getConversationId(dmEventOptStitch).flatMap {
|
||||
case Some(conversationId) =>
|
||||
dmConversationFeatures
|
||||
.dmConversationIsOneToOneConversation(conversationId, Some(viewerId)).flatMap {
|
||||
isOneToOne =>
|
||||
if (isOneToOne) {
|
||||
Stitch
|
||||
.join(
|
||||
dmConversationFeatures
|
||||
.dmConversationHasSuspendedParticipant(conversationId, Some(viewerId)),
|
||||
dmConversationFeatures
|
||||
.dmConversationHasDeactivatedParticipant(conversationId, Some(viewerId)),
|
||||
dmConversationFeatures
|
||||
.dmConversationHasErasedParticipant(conversationId, Some(viewerId))
|
||||
).flatMap {
|
||||
case (
|
||||
convoParticipantIsSuspended,
|
||||
convoParticipantIsDeactivated,
|
||||
convoParticipantIsErased) =>
|
||||
Stitch.value(
|
||||
convoParticipantIsSuspended || convoParticipantIsDeactivated || convoParticipantIsErased)
|
||||
}
|
||||
} else {
|
||||
Stitch.False
|
||||
}
|
||||
}
|
||||
case _ => Stitch.False
|
||||
}
|
||||
|
||||
private def dmEventInOneToOneConversation(
|
||||
dmEventOptStitch: Stitch[Option[Event]],
|
||||
viewerId: UserId
|
||||
): Stitch[Boolean] =
|
||||
dmEventSource.getConversationId(dmEventOptStitch).flatMap {
|
||||
case Some(conversationId) =>
|
||||
dmConversationFeatures
|
||||
.dmConversationIsOneToOneConversation(conversationId, Some(viewerId))
|
||||
case _ => Stitch.False
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
scala_library(
|
||||
sources = ["*.scala"],
|
||||
compiler_option_sets = ["fatal_warnings"],
|
||||
platform = "java8",
|
||||
strict_deps = True,
|
||||
tags = ["bazel-compatible"],
|
||||
dependencies = [
|
||||
"3rdparty/jvm/com/google/guava",
|
||||
"mediaservices/commons/src/main/thrift:thrift-scala",
|
||||
"mediaservices/media-util/src/main/scala",
|
||||
"snowflake/src/main/scala/com/twitter/snowflake/id",
|
||||
"src/thrift/com/twitter/context:twitter-context-scala",
|
||||
"src/thrift/com/twitter/gizmoduck:user-thrift-scala",
|
||||
"src/thrift/com/twitter/spam/rtf:safety-level-scala",
|
||||
"src/thrift/com/twitter/spam/rtf:tweet-rtf-event-scala",
|
||||
"src/thrift/com/twitter/tweetypie:service-scala",
|
||||
"src/thrift/com/twitter/tweetypie:tweet-scala",
|
||||
"stitch/stitch-core",
|
||||
"tweetypie/src/scala/com/twitter/tweetypie/additionalfields",
|
||||
"twitter-context/src/main/scala",
|
||||
"visibility/common/src/main/scala/com/twitter/visibility/common",
|
||||
"visibility/lib/src/main/resources/config",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/builder/tweets",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/builder/users",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/features",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/interfaces/common/tweets",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/util",
|
||||
"visibility/lib/src/main/thrift/com/twitter/visibility/safety_label_store:safety-label-store-scala",
|
||||
],
|
||||
)
|
@ -0,0 +1,90 @@
|
||||
package com.twitter.visibility.builder.media
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.mediaservices.media_util.GenericMediaKey
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.common.MediaSafetyLabelMapSource
|
||||
import com.twitter.visibility.features.MediaSafetyLabels
|
||||
import com.twitter.visibility.models.MediaSafetyLabel
|
||||
import com.twitter.visibility.models.MediaSafetyLabelType
|
||||
import com.twitter.visibility.models.SafetyLabel
|
||||
|
||||
class MediaFeatures(
|
||||
mediaSafetyLabelMap: StratoMediaLabelMaps,
|
||||
statsReceiver: StatsReceiver) {
|
||||
|
||||
private[this] val scopedStatsReceiver = statsReceiver.scope("media_features")
|
||||
|
||||
private[this] val requests =
|
||||
scopedStatsReceiver
|
||||
.counter("requests")
|
||||
|
||||
private[this] val mediaSafetyLabelsStats =
|
||||
scopedStatsReceiver
|
||||
.scope(MediaSafetyLabels.name)
|
||||
.counter("requests")
|
||||
|
||||
private[this] val nonEmptyMediaStats = scopedStatsReceiver.scope("non_empty_media")
|
||||
private[this] val nonEmptyMediaRequests = nonEmptyMediaStats.counter("requests")
|
||||
private[this] val nonEmptyMediaKeysCount = nonEmptyMediaStats.counter("keys")
|
||||
private[this] val nonEmptyMediaKeysLength = nonEmptyMediaStats.stat("keys_length")
|
||||
|
||||
def forMediaKeys(
|
||||
mediaKeys: Seq[GenericMediaKey],
|
||||
): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
requests.incr()
|
||||
nonEmptyMediaKeysCount.incr(mediaKeys.size)
|
||||
mediaSafetyLabelsStats.incr()
|
||||
|
||||
if (mediaKeys.nonEmpty) {
|
||||
nonEmptyMediaRequests.incr()
|
||||
nonEmptyMediaKeysLength.add(mediaKeys.size)
|
||||
}
|
||||
|
||||
_.withFeature(MediaSafetyLabels, mediaSafetyLabelMap.forGenericMediaKeys(mediaKeys))
|
||||
}
|
||||
|
||||
def forGenericMediaKey(
|
||||
genericMediaKey: GenericMediaKey
|
||||
): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
requests.incr()
|
||||
nonEmptyMediaKeysCount.incr()
|
||||
mediaSafetyLabelsStats.incr()
|
||||
nonEmptyMediaRequests.incr()
|
||||
nonEmptyMediaKeysLength.add(1L)
|
||||
|
||||
_.withFeature(MediaSafetyLabels, mediaSafetyLabelMap.forGenericMediaKey(genericMediaKey))
|
||||
}
|
||||
}
|
||||
|
||||
class StratoMediaLabelMaps(source: MediaSafetyLabelMapSource) {
|
||||
|
||||
def forGenericMediaKeys(
|
||||
mediaKeys: Seq[GenericMediaKey],
|
||||
): Stitch[Seq[MediaSafetyLabel]] = {
|
||||
Stitch
|
||||
.collect(
|
||||
mediaKeys
|
||||
.map(getFilteredSafetyLabels)
|
||||
).map(_.flatten)
|
||||
}
|
||||
|
||||
def forGenericMediaKey(
|
||||
genericMediaKey: GenericMediaKey
|
||||
): Stitch[Seq[MediaSafetyLabel]] = {
|
||||
getFilteredSafetyLabels(genericMediaKey)
|
||||
}
|
||||
|
||||
private def getFilteredSafetyLabels(
|
||||
genericMediaKey: GenericMediaKey,
|
||||
): Stitch[Seq[MediaSafetyLabel]] =
|
||||
source
|
||||
.fetch(genericMediaKey).map(_.flatMap(_.labels.map { stratoSafetyLabelMap =>
|
||||
stratoSafetyLabelMap
|
||||
.map(label =>
|
||||
MediaSafetyLabel(
|
||||
MediaSafetyLabelType.fromThrift(label._1),
|
||||
SafetyLabel.fromThrift(label._2)))
|
||||
}).toSeq.flatten)
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package com.twitter.visibility.builder.media
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.mediaservices.media_util.GenericMediaKey
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.common.MediaMetadataSource
|
||||
import com.twitter.visibility.features.HasDmcaMediaFeature
|
||||
import com.twitter.visibility.features.MediaGeoRestrictionsAllowList
|
||||
import com.twitter.visibility.features.MediaGeoRestrictionsDenyList
|
||||
import com.twitter.visibility.features.AuthorId
|
||||
|
||||
class MediaMetadataFeatures(
|
||||
mediaMetadataSource: MediaMetadataSource,
|
||||
statsReceiver: StatsReceiver) {
|
||||
|
||||
private[this] val scopedStatsReceiver = statsReceiver.scope("media_metadata_features")
|
||||
private[this] val requests = scopedStatsReceiver.counter("requests")
|
||||
|
||||
private[this] val hasDmcaMedia =
|
||||
scopedStatsReceiver.scope(HasDmcaMediaFeature.name).counter("requests")
|
||||
private[this] val mediaGeoAllowList =
|
||||
scopedStatsReceiver.scope(MediaGeoRestrictionsAllowList.name).counter("requests")
|
||||
private[this] val mediaGeoDenyList =
|
||||
scopedStatsReceiver.scope(MediaGeoRestrictionsDenyList.name).counter("requests")
|
||||
private[this] val uploaderId =
|
||||
scopedStatsReceiver.scope(AuthorId.name).counter("requests")
|
||||
|
||||
def forGenericMediaKey(
|
||||
genericMediaKey: GenericMediaKey
|
||||
): FeatureMapBuilder => FeatureMapBuilder = { featureMapBuilder =>
|
||||
requests.incr()
|
||||
|
||||
featureMapBuilder.withFeature(
|
||||
HasDmcaMediaFeature,
|
||||
mediaIsDmca(genericMediaKey)
|
||||
)
|
||||
|
||||
featureMapBuilder.withFeature(
|
||||
MediaGeoRestrictionsAllowList,
|
||||
geoRestrictionsAllowList(genericMediaKey)
|
||||
)
|
||||
|
||||
featureMapBuilder.withFeature(
|
||||
MediaGeoRestrictionsDenyList,
|
||||
geoRestrictionsDenyList(genericMediaKey)
|
||||
)
|
||||
|
||||
featureMapBuilder.withFeature(
|
||||
AuthorId,
|
||||
mediaUploaderId(genericMediaKey)
|
||||
)
|
||||
}
|
||||
|
||||
private def mediaIsDmca(genericMediaKey: GenericMediaKey) = {
|
||||
hasDmcaMedia.incr()
|
||||
mediaMetadataSource.getMediaIsDmca(genericMediaKey)
|
||||
}
|
||||
|
||||
private def geoRestrictionsAllowList(genericMediaKey: GenericMediaKey) = {
|
||||
mediaGeoAllowList.incr()
|
||||
mediaMetadataSource.getGeoRestrictionsAllowList(genericMediaKey).map { allowListOpt =>
|
||||
allowListOpt.getOrElse(Nil)
|
||||
}
|
||||
}
|
||||
|
||||
private def geoRestrictionsDenyList(genericMediaKey: GenericMediaKey) = {
|
||||
mediaGeoDenyList.incr()
|
||||
mediaMetadataSource.getGeoRestrictionsDenyList(genericMediaKey).map { denyListOpt =>
|
||||
denyListOpt.getOrElse(Nil)
|
||||
}
|
||||
}
|
||||
|
||||
private def mediaUploaderId(genericMediaKey: GenericMediaKey) = {
|
||||
uploaderId.incr()
|
||||
mediaMetadataSource.getMediaUploaderId(genericMediaKey).map { uploaderIdOpt =>
|
||||
uploaderIdOpt.map(Set(_)).getOrElse(Set.empty)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
scala_library(
|
||||
sources = ["*.scala"],
|
||||
compiler_option_sets = ["fatal_warnings"],
|
||||
platform = "java8",
|
||||
strict_deps = True,
|
||||
tags = ["bazel-compatible"],
|
||||
dependencies = [
|
||||
"3rdparty/jvm/com/google/guava",
|
||||
"snowflake/src/main/scala/com/twitter/snowflake/id",
|
||||
"src/thrift/com/twitter/context:twitter-context-scala",
|
||||
"src/thrift/com/twitter/gizmoduck:user-thrift-scala",
|
||||
"src/thrift/com/twitter/spam/rtf:safety-level-scala",
|
||||
"src/thrift/com/twitter/spam/rtf:tweet-rtf-event-scala",
|
||||
"stitch/stitch-core",
|
||||
"twitter-context/src/main/scala",
|
||||
"visibility/common/src/main/scala/com/twitter/visibility/common",
|
||||
"visibility/lib/src/main/resources/config",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/builder/common",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/builder/users",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/features",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/util",
|
||||
"visibility/lib/src/main/thrift/com/twitter/visibility/safety_label_store:safety-label-store-scala",
|
||||
],
|
||||
)
|
@ -0,0 +1,131 @@
|
||||
package com.twitter.visibility.builder.spaces
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.gizmoduck.thriftscala.Label
|
||||
import com.twitter.gizmoduck.thriftscala.MuteSurface
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.builder.common.MutedKeywordFeatures
|
||||
import com.twitter.visibility.builder.users.AuthorFeatures
|
||||
import com.twitter.visibility.builder.users.RelationshipFeatures
|
||||
import com.twitter.visibility.common.AudioSpaceSource
|
||||
import com.twitter.visibility.common.SpaceId
|
||||
import com.twitter.visibility.common.SpaceSafetyLabelMapSource
|
||||
import com.twitter.visibility.common.UserId
|
||||
import com.twitter.visibility.features._
|
||||
import com.twitter.visibility.models.{MutedKeyword => VfMutedKeyword}
|
||||
import com.twitter.visibility.models.SafetyLabel
|
||||
import com.twitter.visibility.models.SpaceSafetyLabel
|
||||
import com.twitter.visibility.models.SpaceSafetyLabelType
|
||||
|
||||
class SpaceFeatures(
|
||||
spaceSafetyLabelMap: StratoSpaceLabelMaps,
|
||||
authorFeatures: AuthorFeatures,
|
||||
relationshipFeatures: RelationshipFeatures,
|
||||
mutedKeywordFeatures: MutedKeywordFeatures,
|
||||
audioSpaceSource: AudioSpaceSource) {
|
||||
|
||||
def forSpaceAndAuthorIds(
|
||||
spaceId: SpaceId,
|
||||
viewerId: Option[UserId],
|
||||
authorIds: Option[Seq[UserId]]
|
||||
): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
|
||||
_.withFeature(SpaceSafetyLabels, spaceSafetyLabelMap.forSpaceId(spaceId))
|
||||
.withFeature(AuthorId, getSpaceAuthors(spaceId, authorIds).map(_.toSet))
|
||||
.withFeature(AuthorUserLabels, allSpaceAuthorLabels(spaceId, authorIds))
|
||||
.withFeature(ViewerFollowsAuthor, viewerFollowsAnySpaceAuthor(spaceId, authorIds, viewerId))
|
||||
.withFeature(ViewerMutesAuthor, viewerMutesAnySpaceAuthor(spaceId, authorIds, viewerId))
|
||||
.withFeature(ViewerBlocksAuthor, viewerBlocksAnySpaceAuthor(spaceId, authorIds, viewerId))
|
||||
.withFeature(AuthorBlocksViewer, anySpaceAuthorBlocksViewer(spaceId, authorIds, viewerId))
|
||||
.withFeature(
|
||||
ViewerMutesKeywordInSpaceTitleForNotifications,
|
||||
titleContainsMutedKeyword(
|
||||
audioSpaceSource.getSpaceTitle(spaceId),
|
||||
audioSpaceSource.getSpaceLanguage(spaceId),
|
||||
viewerId)
|
||||
)
|
||||
}
|
||||
|
||||
def titleContainsMutedKeyword(
|
||||
titleOptStitch: Stitch[Option[String]],
|
||||
languageOptStitch: Stitch[Option[String]],
|
||||
viewerId: Option[UserId],
|
||||
): Stitch[VfMutedKeyword] = {
|
||||
titleOptStitch.flatMap {
|
||||
case None => Stitch.value(VfMutedKeyword(None))
|
||||
case Some(spaceTitle) =>
|
||||
languageOptStitch.flatMap { languageOpt =>
|
||||
mutedKeywordFeatures.spaceTitleContainsMutedKeyword(
|
||||
spaceTitle,
|
||||
languageOpt,
|
||||
mutedKeywordFeatures.allMutedKeywords(viewerId),
|
||||
MuteSurface.Notifications)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def getSpaceAuthors(
|
||||
spaceId: SpaceId,
|
||||
authorIdsFromRequest: Option[Seq[UserId]]
|
||||
): Stitch[Seq[UserId]] = {
|
||||
authorIdsFromRequest match {
|
||||
case Some(authorIds) => Stitch.apply(authorIds)
|
||||
case _ => audioSpaceSource.getAdminIds(spaceId)
|
||||
}
|
||||
}
|
||||
|
||||
def allSpaceAuthorLabels(
|
||||
spaceId: SpaceId,
|
||||
authorIdsFromRequest: Option[Seq[UserId]]
|
||||
): Stitch[Seq[Label]] = {
|
||||
getSpaceAuthors(spaceId, authorIdsFromRequest)
|
||||
.flatMap(authorIds =>
|
||||
Stitch.collect(authorIds.map(authorId => authorFeatures.authorUserLabels(authorId)))).map(
|
||||
_.flatten)
|
||||
}
|
||||
|
||||
def viewerMutesAnySpaceAuthor(
|
||||
spaceId: SpaceId,
|
||||
authorIdsFromRequest: Option[Seq[UserId]],
|
||||
viewerId: Option[UserId]
|
||||
): Stitch[Boolean] = {
|
||||
getSpaceAuthors(spaceId, authorIdsFromRequest)
|
||||
.flatMap(authorIds =>
|
||||
Stitch.collect(authorIds.map(authorId =>
|
||||
relationshipFeatures.viewerMutesAuthor(authorId, viewerId)))).map(_.contains(true))
|
||||
}
|
||||
|
||||
def anySpaceAuthorBlocksViewer(
|
||||
spaceId: SpaceId,
|
||||
authorIdsFromRequest: Option[Seq[UserId]],
|
||||
viewerId: Option[UserId]
|
||||
): Stitch[Boolean] = {
|
||||
getSpaceAuthors(spaceId, authorIdsFromRequest)
|
||||
.flatMap(authorIds =>
|
||||
Stitch.collect(authorIds.map(authorId =>
|
||||
relationshipFeatures.authorBlocksViewer(authorId, viewerId)))).map(_.contains(true))
|
||||
}
|
||||
}
|
||||
|
||||
class StratoSpaceLabelMaps(
|
||||
spaceSafetyLabelSource: SpaceSafetyLabelMapSource,
|
||||
statsReceiver: StatsReceiver) {
|
||||
|
||||
private[this] val scopedStatsReceiver = statsReceiver.scope("space_features")
|
||||
private[this] val spaceSafetyLabelsStats =
|
||||
scopedStatsReceiver.scope(SpaceSafetyLabels.name).counter("requests")
|
||||
|
||||
def forSpaceId(
|
||||
spaceId: SpaceId,
|
||||
): Stitch[Seq[SpaceSafetyLabel]] = {
|
||||
spaceSafetyLabelSource
|
||||
.fetch(spaceId).map(_.flatMap(_.labels.map { stratoSafetyLabelMap =>
|
||||
stratoSafetyLabelMap
|
||||
.map(label =>
|
||||
SpaceSafetyLabel(
|
||||
SpaceSafetyLabelType.fromThrift(label._1),
|
||||
SafetyLabel.fromThrift(label._2)))
|
||||
}).toSeq.flatten).ensure(spaceSafetyLabelsStats.incr)
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
scala_library(
|
||||
sources = ["*.scala"],
|
||||
compiler_option_sets = ["fatal_warnings"],
|
||||
platform = "java8",
|
||||
strict_deps = True,
|
||||
tags = ["bazel-compatible"],
|
||||
dependencies = [
|
||||
"3rdparty/jvm/com/google/guava",
|
||||
"communities/thrift/src/main/thrift/com/twitter/communities:thrift-scala",
|
||||
"communities/thrift/src/main/thrift/com/twitter/communities/moderation:thrift-scala",
|
||||
"communities/thrift/src/main/thrift/com/twitter/communities/visibility:thrift-scala",
|
||||
"escherbird/src/thrift/com/twitter/escherbird/softintervention:softintervention_thrift-scala",
|
||||
"mediaservices/media-util/src/main/scala",
|
||||
"notificationservice/common/src/main/scala/com/twitter/notificationservice/model:alias",
|
||||
"notificationservice/common/src/main/scala/com/twitter/notificationservice/model/notification",
|
||||
"snowflake/src/main/scala/com/twitter/snowflake/id",
|
||||
"src/thrift/com/twitter/context:twitter-context-scala",
|
||||
"src/thrift/com/twitter/escherbird/common:common-scala",
|
||||
"src/thrift/com/twitter/gizmoduck:user-thrift-scala",
|
||||
"src/thrift/com/twitter/search/common:constants-scala",
|
||||
"src/thrift/com/twitter/spam/rtf:safety-level-scala",
|
||||
"src/thrift/com/twitter/spam/rtf:tweet-rtf-event-scala",
|
||||
"src/thrift/com/twitter/tweetypie:tweet-scala",
|
||||
"stitch/stitch-core",
|
||||
# "tweetypie/src/scala/com/twitter/tweetypie/additionalfields",
|
||||
"twitter-context/src/main/scala",
|
||||
"visibility/common/src/main/scala/com/twitter/visibility/common",
|
||||
"visibility/common/src/main/scala/com/twitter/visibility/common/stitch",
|
||||
"visibility/lib/src/main/resources/config",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/builder/users",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/features",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/interfaces/common/blender",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/interfaces/common/search",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/interfaces/common/tweets",
|
||||
"visibility/lib/src/main/thrift/com/twitter/visibility/strato:vf-strato-scala",
|
||||
],
|
||||
)
|
@ -0,0 +1,45 @@
|
||||
package com.twitter.visibility.builder.tweets
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.search.common.constants.thriftscala.ThriftQuerySource
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.features.SearchCandidateCount
|
||||
import com.twitter.visibility.features.SearchQueryHasUser
|
||||
import com.twitter.visibility.features.SearchQuerySource
|
||||
import com.twitter.visibility.features.SearchResultsPageNumber
|
||||
import com.twitter.visibility.interfaces.common.blender.BlenderVFRequestContext
|
||||
|
||||
@Deprecated
|
||||
class BlenderContextFeatures(
|
||||
statsReceiver: StatsReceiver) {
|
||||
private[this] val scopedStatsReceiver = statsReceiver.scope("blender_context_features")
|
||||
private[this] val requests = scopedStatsReceiver.counter("requests")
|
||||
private[this] val searchResultsPageNumber =
|
||||
scopedStatsReceiver.scope(SearchResultsPageNumber.name).counter("requests")
|
||||
private[this] val searchCandidateCount =
|
||||
scopedStatsReceiver.scope(SearchCandidateCount.name).counter("requests")
|
||||
private[this] val searchQuerySource =
|
||||
scopedStatsReceiver.scope(SearchQuerySource.name).counter("requests")
|
||||
private[this] val searchQueryHasUser =
|
||||
scopedStatsReceiver.scope(SearchQueryHasUser.name).counter("requests")
|
||||
|
||||
def forBlenderContext(
|
||||
blenderContext: BlenderVFRequestContext
|
||||
): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
requests.incr()
|
||||
searchResultsPageNumber.incr()
|
||||
searchCandidateCount.incr()
|
||||
searchQuerySource.incr()
|
||||
searchQueryHasUser.incr()
|
||||
|
||||
_.withConstantFeature(SearchResultsPageNumber, blenderContext.resultsPageNumber)
|
||||
.withConstantFeature(SearchCandidateCount, blenderContext.candidateCount)
|
||||
.withConstantFeature(
|
||||
SearchQuerySource,
|
||||
blenderContext.querySourceOption match {
|
||||
case Some(querySource) => querySource
|
||||
case _ => ThriftQuerySource.Unknown
|
||||
})
|
||||
.withConstantFeature(SearchQueryHasUser, blenderContext.queryHasUser)
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package com.twitter.visibility.builder.tweets
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.notificationservice.model.notification.ActivityNotification
|
||||
import com.twitter.notificationservice.model.notification.MentionNotification
|
||||
import com.twitter.notificationservice.model.notification.MentionQuoteNotification
|
||||
import com.twitter.notificationservice.model.notification.Notification
|
||||
import com.twitter.notificationservice.model.notification.QuoteTweetNotification
|
||||
import com.twitter.servo.util.Gate
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.common.TweetSource
|
||||
import com.twitter.visibility.features.NotificationIsOnCommunityTweet
|
||||
import com.twitter.visibility.models.CommunityTweet
|
||||
|
||||
object CommunityNotificationFeatures {
|
||||
def ForNonCommunityTweetNotification: FeatureMapBuilder => FeatureMapBuilder = {
|
||||
_.withConstantFeature(NotificationIsOnCommunityTweet, false)
|
||||
}
|
||||
}
|
||||
|
||||
class CommunityNotificationFeatures(
|
||||
tweetSource: TweetSource,
|
||||
enableCommunityTweetHydration: Gate[Long],
|
||||
statsReceiver: StatsReceiver) {
|
||||
|
||||
private[this] val scopedStatsReceiver = statsReceiver.scope("community_notification_features")
|
||||
private[this] val requestsCounter = scopedStatsReceiver.counter("requests")
|
||||
private[this] val hydrationsCounter = scopedStatsReceiver.counter("hydrations")
|
||||
private[this] val notificationIsOnCommunityTweetCounter =
|
||||
scopedStatsReceiver.scope(NotificationIsOnCommunityTweet.name).counter("true")
|
||||
private[this] val notificationIsNotOnCommunityTweetCounter =
|
||||
scopedStatsReceiver.scope(NotificationIsOnCommunityTweet.name).counter("false")
|
||||
|
||||
def forNotification(notification: Notification): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
requestsCounter.incr()
|
||||
val isCommunityTweetResult = getTweetIdOption(notification) match {
|
||||
case Some(tweetId) if enableCommunityTweetHydration(notification.target) =>
|
||||
hydrationsCounter.incr()
|
||||
tweetSource
|
||||
.getTweet(tweetId)
|
||||
.map {
|
||||
case Some(tweet) if CommunityTweet(tweet).nonEmpty =>
|
||||
notificationIsOnCommunityTweetCounter.incr()
|
||||
true
|
||||
case _ =>
|
||||
notificationIsNotOnCommunityTweetCounter.incr()
|
||||
false
|
||||
}
|
||||
case _ => Stitch.False
|
||||
}
|
||||
_.withFeature(NotificationIsOnCommunityTweet, isCommunityTweetResult)
|
||||
}
|
||||
|
||||
private[this] def getTweetIdOption(notification: Notification): Option[Long] = {
|
||||
notification match {
|
||||
case n: MentionNotification => Some(n.mentioningTweetId)
|
||||
case n: MentionQuoteNotification => Some(n.mentioningTweetId)
|
||||
case n: QuoteTweetNotification => Some(n.quotedTweetId)
|
||||
case n: ActivityNotification[_] if n.visibilityTweets.contains(n.objectId) => Some(n.objectId)
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
package com.twitter.visibility.builder.tweets
|
||||
|
||||
import com.twitter.tweetypie.thriftscala.Tweet
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.features.CommunityTweetAuthorIsRemoved
|
||||
import com.twitter.visibility.features.CommunityTweetCommunityNotFound
|
||||
import com.twitter.visibility.features.CommunityTweetCommunityDeleted
|
||||
import com.twitter.visibility.features.CommunityTweetCommunitySuspended
|
||||
import com.twitter.visibility.features.CommunityTweetCommunityVisible
|
||||
import com.twitter.visibility.features.CommunityTweetIsHidden
|
||||
import com.twitter.visibility.features.TweetIsCommunityTweet
|
||||
import com.twitter.visibility.features.ViewerIsCommunityAdmin
|
||||
import com.twitter.visibility.features.ViewerIsCommunityMember
|
||||
import com.twitter.visibility.features.ViewerIsCommunityModerator
|
||||
import com.twitter.visibility.features.ViewerIsInternalCommunitiesAdmin
|
||||
import com.twitter.visibility.models.CommunityTweet
|
||||
import com.twitter.visibility.models.ViewerContext
|
||||
|
||||
trait CommunityTweetFeatures {
|
||||
|
||||
def forTweet(
|
||||
tweet: Tweet,
|
||||
viewerContext: ViewerContext
|
||||
): FeatureMapBuilder => FeatureMapBuilder
|
||||
|
||||
def forTweetOnly(tweet: Tweet): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
_.withConstantFeature(
|
||||
TweetIsCommunityTweet,
|
||||
CommunityTweet(tweet).isDefined
|
||||
)
|
||||
}
|
||||
|
||||
protected def forNonCommunityTweet(): FeatureMapBuilder => FeatureMapBuilder = { builder =>
|
||||
builder
|
||||
.withConstantFeature(
|
||||
TweetIsCommunityTweet,
|
||||
false
|
||||
).withConstantFeature(
|
||||
CommunityTweetCommunityNotFound,
|
||||
false
|
||||
).withConstantFeature(
|
||||
CommunityTweetCommunitySuspended,
|
||||
false
|
||||
).withConstantFeature(
|
||||
CommunityTweetCommunityDeleted,
|
||||
false
|
||||
).withConstantFeature(
|
||||
CommunityTweetCommunityVisible,
|
||||
false
|
||||
).withConstantFeature(
|
||||
ViewerIsInternalCommunitiesAdmin,
|
||||
false
|
||||
).withConstantFeature(
|
||||
ViewerIsCommunityAdmin,
|
||||
false
|
||||
).withConstantFeature(
|
||||
ViewerIsCommunityModerator,
|
||||
false
|
||||
).withConstantFeature(
|
||||
ViewerIsCommunityMember,
|
||||
false
|
||||
).withConstantFeature(
|
||||
CommunityTweetIsHidden,
|
||||
false
|
||||
).withConstantFeature(
|
||||
CommunityTweetAuthorIsRemoved,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package com.twitter.visibility.builder.tweets
|
||||
|
||||
import com.twitter.servo.util.Gate
|
||||
import com.twitter.tweetypie.thriftscala.Tweet
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.models.ViewerContext
|
||||
|
||||
class CommunityTweetFeaturesPartitioned(
|
||||
a: CommunityTweetFeatures,
|
||||
b: CommunityTweetFeatures,
|
||||
bEnabled: Gate[Unit],
|
||||
) extends CommunityTweetFeatures {
|
||||
override def forTweet(
|
||||
tweet: Tweet,
|
||||
viewerContext: ViewerContext
|
||||
): FeatureMapBuilder => FeatureMapBuilder =
|
||||
bEnabled.pick(
|
||||
b.forTweet(tweet, viewerContext),
|
||||
a.forTweet(tweet, viewerContext),
|
||||
)
|
||||
|
||||
override def forTweetOnly(tweet: Tweet): FeatureMapBuilder => FeatureMapBuilder = bEnabled.pick(
|
||||
b.forTweetOnly(tweet),
|
||||
a.forTweetOnly(tweet),
|
||||
)
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
package com.twitter.visibility.builder.tweets
|
||||
|
||||
import com.twitter.communities.moderation.thriftscala.CommunityTweetModerationState
|
||||
import com.twitter.communities.moderation.thriftscala.CommunityUserModerationState
|
||||
import com.twitter.communities.visibility.thriftscala.CommunityVisibilityFeatures
|
||||
import com.twitter.communities.visibility.thriftscala.CommunityVisibilityFeaturesV1
|
||||
import com.twitter.communities.visibility.thriftscala.CommunityVisibilityResult
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.tweetypie.thriftscala.Tweet
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.common.CommunitiesSource
|
||||
import com.twitter.visibility.features.CommunityTweetAuthorIsRemoved
|
||||
import com.twitter.visibility.features.CommunityTweetCommunityNotFound
|
||||
import com.twitter.visibility.features.CommunityTweetCommunityDeleted
|
||||
import com.twitter.visibility.features.CommunityTweetCommunitySuspended
|
||||
import com.twitter.visibility.features.CommunityTweetCommunityVisible
|
||||
import com.twitter.visibility.features.CommunityTweetIsHidden
|
||||
import com.twitter.visibility.features.TweetIsCommunityTweet
|
||||
import com.twitter.visibility.features.ViewerIsCommunityAdmin
|
||||
import com.twitter.visibility.features.ViewerIsCommunityMember
|
||||
import com.twitter.visibility.features.ViewerIsCommunityModerator
|
||||
import com.twitter.visibility.features.ViewerIsInternalCommunitiesAdmin
|
||||
import com.twitter.visibility.models.CommunityTweet
|
||||
import com.twitter.visibility.models.ViewerContext
|
||||
|
||||
class CommunityTweetFeaturesV2(communitiesSource: CommunitiesSource)
|
||||
extends CommunityTweetFeatures {
|
||||
private[this] def forCommunityTweet(
|
||||
communityTweet: CommunityTweet
|
||||
): FeatureMapBuilder => FeatureMapBuilder = { builder: FeatureMapBuilder =>
|
||||
{
|
||||
val communityVisibilityFeaturesStitch =
|
||||
communitiesSource.getCommunityVisibilityFeatures(communityTweet.communityId)
|
||||
val communityTweetModerationStateStitch =
|
||||
communitiesSource.getTweetModerationState(communityTweet.tweet.id)
|
||||
val communityTweetAuthorModerationStateStitch =
|
||||
communitiesSource.getUserModerationState(
|
||||
communityTweet.authorId,
|
||||
communityTweet.communityId
|
||||
)
|
||||
|
||||
def getFlagFromFeatures(f: CommunityVisibilityFeaturesV1 => Boolean): Stitch[Boolean] =
|
||||
communityVisibilityFeaturesStitch.map {
|
||||
case Some(CommunityVisibilityFeatures.V1(v1)) => f(v1)
|
||||
case _ => false
|
||||
}
|
||||
|
||||
def getFlagFromCommunityVisibilityResult(
|
||||
f: CommunityVisibilityResult => Boolean
|
||||
): Stitch[Boolean] = getFlagFromFeatures { v =>
|
||||
f(v.communityVisibilityResult)
|
||||
}
|
||||
|
||||
builder
|
||||
.withConstantFeature(
|
||||
TweetIsCommunityTweet,
|
||||
true
|
||||
)
|
||||
.withFeature(
|
||||
CommunityTweetCommunityNotFound,
|
||||
getFlagFromCommunityVisibilityResult {
|
||||
case CommunityVisibilityResult.NotFound => true
|
||||
case _ => false
|
||||
}
|
||||
)
|
||||
.withFeature(
|
||||
CommunityTweetCommunitySuspended,
|
||||
getFlagFromCommunityVisibilityResult {
|
||||
case CommunityVisibilityResult.Suspended => true
|
||||
case _ => false
|
||||
}
|
||||
)
|
||||
.withFeature(
|
||||
CommunityTweetCommunityDeleted,
|
||||
getFlagFromCommunityVisibilityResult {
|
||||
case CommunityVisibilityResult.Deleted => true
|
||||
case _ => false
|
||||
}
|
||||
)
|
||||
.withFeature(
|
||||
CommunityTweetCommunityVisible,
|
||||
getFlagFromCommunityVisibilityResult {
|
||||
case CommunityVisibilityResult.Visible => true
|
||||
case _ => false
|
||||
}
|
||||
)
|
||||
.withFeature(
|
||||
ViewerIsInternalCommunitiesAdmin,
|
||||
getFlagFromFeatures { _.viewerIsInternalAdmin }
|
||||
)
|
||||
.withFeature(
|
||||
ViewerIsCommunityAdmin,
|
||||
getFlagFromFeatures { _.viewerIsCommunityAdmin }
|
||||
)
|
||||
.withFeature(
|
||||
ViewerIsCommunityModerator,
|
||||
getFlagFromFeatures { _.viewerIsCommunityModerator }
|
||||
)
|
||||
.withFeature(
|
||||
ViewerIsCommunityMember,
|
||||
getFlagFromFeatures { _.viewerIsCommunityMember }
|
||||
)
|
||||
.withFeature(
|
||||
CommunityTweetIsHidden,
|
||||
communityTweetModerationStateStitch.map {
|
||||
case Some(CommunityTweetModerationState.Hidden(_)) => true
|
||||
case _ => false
|
||||
}
|
||||
)
|
||||
.withFeature(
|
||||
CommunityTweetAuthorIsRemoved,
|
||||
communityTweetAuthorModerationStateStitch.map {
|
||||
case Some(CommunityUserModerationState.Removed(_)) => true
|
||||
case _ => false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def forTweet(
|
||||
tweet: Tweet,
|
||||
viewerContext: ViewerContext
|
||||
): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
CommunityTweet(tweet) match {
|
||||
case None => forNonCommunityTweet()
|
||||
case Some(communityTweet) => forCommunityTweet(communityTweet)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,178 @@
|
||||
package com.twitter.visibility.builder.tweets
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.tweetypie.thriftscala.Tweet
|
||||
import com.twitter.tweetypie.thriftscala.ConversationControl
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.builder.users.RelationshipFeatures
|
||||
import com.twitter.visibility.common.InvitedToConversationRepo
|
||||
import com.twitter.visibility.features.ConversationRootAuthorFollowsViewer
|
||||
import com.twitter.visibility.features.TweetConversationViewerIsInvited
|
||||
import com.twitter.visibility.features.TweetConversationViewerIsInvitedViaReplyMention
|
||||
import com.twitter.visibility.features.TweetConversationViewerIsRootAuthor
|
||||
import com.twitter.visibility.features.TweetHasByInvitationConversationControl
|
||||
import com.twitter.visibility.features.TweetHasCommunityConversationControl
|
||||
import com.twitter.visibility.features.TweetHasFollowersConversationControl
|
||||
import com.twitter.visibility.features.ViewerFollowsConversationRootAuthor
|
||||
|
||||
class ConversationControlFeatures(
|
||||
relationshipFeatures: RelationshipFeatures,
|
||||
isInvitedToConversationRepository: InvitedToConversationRepo,
|
||||
statsReceiver: StatsReceiver) {
|
||||
|
||||
private[this] val scopedStatsReceiver = statsReceiver.scope("conversation_control_features")
|
||||
|
||||
private[this] val requests = scopedStatsReceiver.counter("requests")
|
||||
|
||||
private[this] val tweetCommunityConversationRequest =
|
||||
scopedStatsReceiver.scope(TweetHasCommunityConversationControl.name).counter("requests")
|
||||
private[this] val tweetByInvitationConversationRequest =
|
||||
scopedStatsReceiver.scope(TweetHasByInvitationConversationControl.name).counter("requests")
|
||||
private[this] val tweetFollowersConversationRequest =
|
||||
scopedStatsReceiver.scope(TweetHasFollowersConversationControl.name).counter("requests")
|
||||
private[this] val rootAuthorFollowsViewer =
|
||||
scopedStatsReceiver.scope(ConversationRootAuthorFollowsViewer.name).counter("requests")
|
||||
private[this] val viewerFollowsRootAuthor =
|
||||
scopedStatsReceiver.scope(ViewerFollowsConversationRootAuthor.name).counter("requests")
|
||||
|
||||
def isCommunityConversation(conversationControl: Option[ConversationControl]): Boolean =
|
||||
conversationControl
|
||||
.collect {
|
||||
case _: ConversationControl.Community =>
|
||||
tweetCommunityConversationRequest.incr()
|
||||
true
|
||||
}.getOrElse(false)
|
||||
|
||||
def isByInvitationConversation(conversationControl: Option[ConversationControl]): Boolean =
|
||||
conversationControl
|
||||
.collect {
|
||||
case _: ConversationControl.ByInvitation =>
|
||||
tweetByInvitationConversationRequest.incr()
|
||||
true
|
||||
}.getOrElse(false)
|
||||
|
||||
def isFollowersConversation(conversationControl: Option[ConversationControl]): Boolean =
|
||||
conversationControl
|
||||
.collect {
|
||||
case _: ConversationControl.Followers =>
|
||||
tweetFollowersConversationRequest.incr()
|
||||
true
|
||||
}.getOrElse(false)
|
||||
|
||||
def conversationRootAuthorId(
|
||||
conversationControl: Option[ConversationControl]
|
||||
): Option[Long] =
|
||||
conversationControl match {
|
||||
case Some(ConversationControl.Community(community)) =>
|
||||
Some(community.conversationTweetAuthorId)
|
||||
case Some(ConversationControl.ByInvitation(byInvitation)) =>
|
||||
Some(byInvitation.conversationTweetAuthorId)
|
||||
case Some(ConversationControl.Followers(followers)) =>
|
||||
Some(followers.conversationTweetAuthorId)
|
||||
case _ => None
|
||||
}
|
||||
|
||||
def viewerIsRootAuthor(
|
||||
conversationControl: Option[ConversationControl],
|
||||
viewerIdOpt: Option[Long]
|
||||
): Boolean =
|
||||
(conversationRootAuthorId(conversationControl), viewerIdOpt) match {
|
||||
case (Some(rootAuthorId), Some(viewerId)) if rootAuthorId == viewerId => true
|
||||
case _ => false
|
||||
}
|
||||
|
||||
def viewerIsInvited(
|
||||
conversationControl: Option[ConversationControl],
|
||||
viewerId: Option[Long]
|
||||
): Boolean = {
|
||||
val invitedUserIds = conversationControl match {
|
||||
case Some(ConversationControl.Community(community)) =>
|
||||
community.invitedUserIds
|
||||
case Some(ConversationControl.ByInvitation(byInvitation)) =>
|
||||
byInvitation.invitedUserIds
|
||||
case Some(ConversationControl.Followers(followers)) =>
|
||||
followers.invitedUserIds
|
||||
case _ => Seq()
|
||||
}
|
||||
|
||||
viewerId.exists(invitedUserIds.contains(_))
|
||||
}
|
||||
|
||||
def conversationAuthorFollows(
|
||||
conversationControl: Option[ConversationControl],
|
||||
viewerId: Option[Long]
|
||||
): Stitch[Boolean] = {
|
||||
val conversationAuthorId = conversationControl.collect {
|
||||
case ConversationControl.Community(community) =>
|
||||
community.conversationTweetAuthorId
|
||||
}
|
||||
|
||||
conversationAuthorId match {
|
||||
case Some(authorId) =>
|
||||
rootAuthorFollowsViewer.incr()
|
||||
relationshipFeatures.authorFollowsViewer(authorId, viewerId)
|
||||
case None =>
|
||||
Stitch.False
|
||||
}
|
||||
}
|
||||
|
||||
def followsConversationAuthor(
|
||||
conversationControl: Option[ConversationControl],
|
||||
viewerId: Option[Long]
|
||||
): Stitch[Boolean] = {
|
||||
val conversationAuthorId = conversationControl.collect {
|
||||
case ConversationControl.Followers(followers) =>
|
||||
followers.conversationTweetAuthorId
|
||||
}
|
||||
|
||||
conversationAuthorId match {
|
||||
case Some(authorId) =>
|
||||
viewerFollowsRootAuthor.incr()
|
||||
relationshipFeatures.viewerFollowsAuthor(authorId, viewerId)
|
||||
case None =>
|
||||
Stitch.False
|
||||
}
|
||||
}
|
||||
|
||||
def viewerIsInvitedViaReplyMention(
|
||||
tweet: Tweet,
|
||||
viewerIdOpt: Option[Long]
|
||||
): Stitch[Boolean] = {
|
||||
val conversationIdOpt: Option[Long] = tweet.conversationControl match {
|
||||
case Some(ConversationControl.Community(community))
|
||||
if community.inviteViaMention.contains(true) =>
|
||||
tweet.coreData.flatMap(_.conversationId)
|
||||
case Some(ConversationControl.ByInvitation(invitation))
|
||||
if invitation.inviteViaMention.contains(true) =>
|
||||
tweet.coreData.flatMap(_.conversationId)
|
||||
case Some(ConversationControl.Followers(followers))
|
||||
if followers.inviteViaMention.contains(true) =>
|
||||
tweet.coreData.flatMap(_.conversationId)
|
||||
case _ => None
|
||||
}
|
||||
|
||||
(conversationIdOpt, viewerIdOpt) match {
|
||||
case (Some(conversationId), Some(viewerId)) =>
|
||||
isInvitedToConversationRepository(conversationId, viewerId)
|
||||
case _ => Stitch.False
|
||||
}
|
||||
}
|
||||
|
||||
def forTweet(tweet: Tweet, viewerId: Option[Long]): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
requests.incr()
|
||||
val cc = tweet.conversationControl
|
||||
|
||||
_.withConstantFeature(TweetHasCommunityConversationControl, isCommunityConversation(cc))
|
||||
.withConstantFeature(TweetHasByInvitationConversationControl, isByInvitationConversation(cc))
|
||||
.withConstantFeature(TweetHasFollowersConversationControl, isFollowersConversation(cc))
|
||||
.withConstantFeature(TweetConversationViewerIsRootAuthor, viewerIsRootAuthor(cc, viewerId))
|
||||
.withConstantFeature(TweetConversationViewerIsInvited, viewerIsInvited(cc, viewerId))
|
||||
.withFeature(ConversationRootAuthorFollowsViewer, conversationAuthorFollows(cc, viewerId))
|
||||
.withFeature(ViewerFollowsConversationRootAuthor, followsConversationAuthor(cc, viewerId))
|
||||
.withFeature(
|
||||
TweetConversationViewerIsInvitedViaReplyMention,
|
||||
viewerIsInvitedViaReplyMention(tweet, viewerId))
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
package com.twitter.visibility.builder.tweets
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.tweetypie.thriftscala.EditControl
|
||||
import com.twitter.tweetypie.thriftscala.Tweet
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.features.TweetIsEditTweet
|
||||
import com.twitter.visibility.features.TweetIsInitialTweet
|
||||
import com.twitter.visibility.features.TweetIsLatestTweet
|
||||
import com.twitter.visibility.features.TweetIsStaleTweet
|
||||
|
||||
class EditTweetFeatures(
|
||||
statsReceiver: StatsReceiver) {
|
||||
|
||||
private[this] val scopedStatsReceiver = statsReceiver.scope("edit_tweet_features")
|
||||
private[this] val tweetIsEditTweet =
|
||||
scopedStatsReceiver.scope(TweetIsEditTweet.name).counter("requests")
|
||||
private[this] val tweetIsStaleTweet =
|
||||
scopedStatsReceiver.scope(TweetIsStaleTweet.name).counter("requests")
|
||||
private[this] val tweetIsLatestTweet =
|
||||
scopedStatsReceiver.scope(TweetIsLatestTweet.name).counter("requests")
|
||||
private[this] val tweetIsInitialTweet =
|
||||
scopedStatsReceiver.scope(TweetIsInitialTweet.name).counter("requests")
|
||||
|
||||
def forTweet(
|
||||
tweet: Tweet
|
||||
): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
_.withConstantFeature(TweetIsEditTweet, tweetIsEditTweet(tweet))
|
||||
.withConstantFeature(TweetIsStaleTweet, tweetIsStaleTweet(tweet))
|
||||
.withConstantFeature(TweetIsLatestTweet, tweetIsLatestTweet(tweet))
|
||||
.withConstantFeature(TweetIsInitialTweet, tweetIsInitialTweet(tweet))
|
||||
}
|
||||
|
||||
def tweetIsStaleTweet(tweet: Tweet, incrementMetric: Boolean = true): Boolean = {
|
||||
if (incrementMetric) tweetIsStaleTweet.incr()
|
||||
|
||||
tweet.editControl match {
|
||||
case None => false
|
||||
case Some(ec) =>
|
||||
ec match {
|
||||
case eci: EditControl.Initial => eci.initial.editTweetIds.last != tweet.id
|
||||
case ece: EditControl.Edit =>
|
||||
ece.edit.editControlInitial.exists(_.editTweetIds.last != tweet.id)
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def tweetIsEditTweet(tweet: Tweet, incrementMetric: Boolean = true): Boolean = {
|
||||
if (incrementMetric) tweetIsEditTweet.incr()
|
||||
|
||||
tweet.editControl match {
|
||||
case None => false
|
||||
case Some(ec) =>
|
||||
ec match {
|
||||
case _: EditControl.Initial => false
|
||||
case _ => true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def tweetIsLatestTweet(tweet: Tweet): Boolean = {
|
||||
tweetIsLatestTweet.incr()
|
||||
!tweetIsStaleTweet(tweet = tweet, incrementMetric = false)
|
||||
}
|
||||
|
||||
def tweetIsInitialTweet(tweet: Tweet): Boolean = {
|
||||
tweetIsInitialTweet.incr()
|
||||
!tweetIsEditTweet(tweet = tweet, incrementMetric = false)
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package com.twitter.visibility.builder.tweets
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.tweetypie.thriftscala.Tweet
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.builder.users.ViewerVerbsAuthor
|
||||
import com.twitter.visibility.common.UserRelationshipSource
|
||||
import com.twitter.visibility.features.TweetIsExclusiveTweet
|
||||
import com.twitter.visibility.features.ViewerIsExclusiveTweetRootAuthor
|
||||
import com.twitter.visibility.features.ViewerSuperFollowsExclusiveTweetRootAuthor
|
||||
import com.twitter.visibility.models.ViewerContext
|
||||
|
||||
class ExclusiveTweetFeatures(
|
||||
userRelationshipSource: UserRelationshipSource,
|
||||
statsReceiver: StatsReceiver) {
|
||||
|
||||
private[this] val scopedStatsReceiver = statsReceiver.scope("exclusive_tweet_features")
|
||||
private[this] val viewerSuperFollowsAuthor =
|
||||
scopedStatsReceiver.scope(ViewerSuperFollowsExclusiveTweetRootAuthor.name).counter("requests")
|
||||
|
||||
def rootAuthorId(tweet: Tweet): Option[Long] =
|
||||
tweet.exclusiveTweetControl.map(_.conversationAuthorId)
|
||||
|
||||
def viewerIsRootAuthor(
|
||||
tweet: Tweet,
|
||||
viewerIdOpt: Option[Long]
|
||||
): Boolean =
|
||||
(rootAuthorId(tweet), viewerIdOpt) match {
|
||||
case (Some(rootAuthorId), Some(viewerId)) if rootAuthorId == viewerId => true
|
||||
case _ => false
|
||||
}
|
||||
|
||||
def viewerSuperFollowsRootAuthor(
|
||||
tweet: Tweet,
|
||||
viewerId: Option[Long]
|
||||
): Stitch[Boolean] =
|
||||
rootAuthorId(tweet) match {
|
||||
case Some(authorId) =>
|
||||
ViewerVerbsAuthor(
|
||||
authorId,
|
||||
viewerId,
|
||||
userRelationshipSource.superFollows,
|
||||
viewerSuperFollowsAuthor)
|
||||
case None =>
|
||||
Stitch.False
|
||||
}
|
||||
|
||||
def forTweet(
|
||||
tweet: Tweet,
|
||||
viewerContext: ViewerContext
|
||||
): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
val viewerId = viewerContext.userId
|
||||
|
||||
_.withConstantFeature(TweetIsExclusiveTweet, tweet.exclusiveTweetControl.isDefined)
|
||||
.withConstantFeature(ViewerIsExclusiveTweetRootAuthor, viewerIsRootAuthor(tweet, viewerId))
|
||||
.withFeature(
|
||||
ViewerSuperFollowsExclusiveTweetRootAuthor,
|
||||
viewerSuperFollowsRootAuthor(tweet, viewerId))
|
||||
}
|
||||
|
||||
def forTweetOnly(tweet: Tweet): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
_.withConstantFeature(TweetIsExclusiveTweet, tweet.exclusiveTweetControl.isDefined)
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package com.twitter.visibility.builder.tweets
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.builder.users.ViewerVerbsAuthor
|
||||
import com.twitter.visibility.common.UserId
|
||||
import com.twitter.visibility.common.UserRelationshipSource
|
||||
import com.twitter.visibility.features._
|
||||
import com.twitter.visibility.models.TweetSafetyLabel
|
||||
import com.twitter.visibility.models.ViolationLevel
|
||||
|
||||
class FosnrPefetchedLabelsRelationshipFeatures(
|
||||
userRelationshipSource: UserRelationshipSource,
|
||||
statsReceiver: StatsReceiver) {
|
||||
|
||||
private[this] val scopedStatsReceiver =
|
||||
statsReceiver.scope("fonsr_prefetched_relationship_features")
|
||||
|
||||
private[this] val requests = scopedStatsReceiver.counter("requests")
|
||||
|
||||
private[this] val viewerFollowsAuthorOfViolatingTweet =
|
||||
scopedStatsReceiver.scope(ViewerFollowsAuthorOfViolatingTweet.name).counter("requests")
|
||||
|
||||
private[this] val viewerDoesNotFollowAuthorOfViolatingTweet =
|
||||
scopedStatsReceiver.scope(ViewerDoesNotFollowAuthorOfViolatingTweet.name).counter("requests")
|
||||
|
||||
def forNonFosnr(): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
requests.incr()
|
||||
_.withConstantFeature(ViewerFollowsAuthorOfViolatingTweet, false)
|
||||
.withConstantFeature(ViewerDoesNotFollowAuthorOfViolatingTweet, false)
|
||||
}
|
||||
def forTweetWithSafetyLabelsAndAuthorId(
|
||||
safetyLabels: Seq[TweetSafetyLabel],
|
||||
authorId: Long,
|
||||
viewerId: Option[Long]
|
||||
): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
requests.incr()
|
||||
_.withFeature(
|
||||
ViewerFollowsAuthorOfViolatingTweet,
|
||||
viewerFollowsAuthorOfViolatingTweet(safetyLabels, authorId, viewerId))
|
||||
.withFeature(
|
||||
ViewerDoesNotFollowAuthorOfViolatingTweet,
|
||||
viewerDoesNotFollowAuthorOfViolatingTweet(safetyLabels, authorId, viewerId))
|
||||
}
|
||||
def viewerFollowsAuthorOfViolatingTweet(
|
||||
safetyLabels: Seq[TweetSafetyLabel],
|
||||
authorId: UserId,
|
||||
viewerId: Option[UserId]
|
||||
): Stitch[Boolean] = {
|
||||
if (safetyLabels
|
||||
.map(ViolationLevel.fromTweetSafetyLabelOpt).collect {
|
||||
case Some(level) => level
|
||||
}.isEmpty) {
|
||||
return Stitch.False
|
||||
}
|
||||
ViewerVerbsAuthor(
|
||||
authorId,
|
||||
viewerId,
|
||||
userRelationshipSource.follows,
|
||||
viewerFollowsAuthorOfViolatingTweet)
|
||||
}
|
||||
def viewerDoesNotFollowAuthorOfViolatingTweet(
|
||||
safetyLabels: Seq[TweetSafetyLabel],
|
||||
authorId: UserId,
|
||||
viewerId: Option[UserId]
|
||||
): Stitch[Boolean] = {
|
||||
if (safetyLabels
|
||||
.map(ViolationLevel.fromTweetSafetyLabelOpt).collect {
|
||||
case Some(level) => level
|
||||
}.isEmpty) {
|
||||
return Stitch.False
|
||||
}
|
||||
ViewerVerbsAuthor(
|
||||
authorId,
|
||||
viewerId,
|
||||
userRelationshipSource.follows,
|
||||
viewerDoesNotFollowAuthorOfViolatingTweet).map(following => !following)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
package com.twitter.visibility.builder.tweets
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.tweetypie.thriftscala.Tweet
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.builder.users.ViewerVerbsAuthor
|
||||
import com.twitter.visibility.common.UserId
|
||||
import com.twitter.visibility.common.UserRelationshipSource
|
||||
import com.twitter.visibility.features._
|
||||
import com.twitter.visibility.models.ViolationLevel
|
||||
|
||||
class FosnrRelationshipFeatures(
|
||||
tweetLabels: TweetLabels,
|
||||
userRelationshipSource: UserRelationshipSource,
|
||||
statsReceiver: StatsReceiver) {
|
||||
|
||||
private[this] val scopedStatsReceiver = statsReceiver.scope("fonsr_relationship_features")
|
||||
|
||||
private[this] val requests = scopedStatsReceiver.counter("requests")
|
||||
|
||||
private[this] val viewerFollowsAuthorOfViolatingTweet =
|
||||
scopedStatsReceiver.scope(ViewerFollowsAuthorOfViolatingTweet.name).counter("requests")
|
||||
|
||||
private[this] val viewerDoesNotFollowAuthorOfViolatingTweet =
|
||||
scopedStatsReceiver.scope(ViewerDoesNotFollowAuthorOfViolatingTweet.name).counter("requests")
|
||||
|
||||
def forTweetAndAuthorId(
|
||||
tweet: Tweet,
|
||||
authorId: Long,
|
||||
viewerId: Option[Long]
|
||||
): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
requests.incr()
|
||||
_.withFeature(
|
||||
ViewerFollowsAuthorOfViolatingTweet,
|
||||
viewerFollowsAuthorOfViolatingTweet(tweet, authorId, viewerId))
|
||||
.withFeature(
|
||||
ViewerDoesNotFollowAuthorOfViolatingTweet,
|
||||
viewerDoesNotFollowAuthorOfViolatingTweet(tweet, authorId, viewerId))
|
||||
}
|
||||
|
||||
def viewerFollowsAuthorOfViolatingTweet(
|
||||
tweet: Tweet,
|
||||
authorId: UserId,
|
||||
viewerId: Option[UserId]
|
||||
): Stitch[Boolean] =
|
||||
tweetLabels.forTweet(tweet).flatMap { safetyLabels =>
|
||||
if (safetyLabels
|
||||
.map(ViolationLevel.fromTweetSafetyLabelOpt).collect {
|
||||
case Some(level) => level
|
||||
}.isEmpty) {
|
||||
Stitch.False
|
||||
} else {
|
||||
ViewerVerbsAuthor(
|
||||
authorId,
|
||||
viewerId,
|
||||
userRelationshipSource.follows,
|
||||
viewerFollowsAuthorOfViolatingTweet)
|
||||
}
|
||||
}
|
||||
|
||||
def viewerDoesNotFollowAuthorOfViolatingTweet(
|
||||
tweet: Tweet,
|
||||
authorId: UserId,
|
||||
viewerId: Option[UserId]
|
||||
): Stitch[Boolean] =
|
||||
tweetLabels.forTweet(tweet).flatMap { safetyLabels =>
|
||||
if (safetyLabels
|
||||
.map(ViolationLevel.fromTweetSafetyLabelOpt).collect {
|
||||
case Some(level) => level
|
||||
}.isEmpty) {
|
||||
Stitch.False
|
||||
} else {
|
||||
ViewerVerbsAuthor(
|
||||
authorId,
|
||||
viewerId,
|
||||
userRelationshipSource.follows,
|
||||
viewerDoesNotFollowAuthorOfViolatingTweet).map(following => !following)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
package com.twitter.visibility.builder.tweets
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.tweetypie.thriftscala.EscherbirdEntityAnnotations
|
||||
import com.twitter.tweetypie.thriftscala.Tweet
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.common.MisinformationPolicySource
|
||||
import com.twitter.visibility.features._
|
||||
import com.twitter.visibility.models.MisinformationPolicy
|
||||
import com.twitter.visibility.models.SemanticCoreMisinformation
|
||||
import com.twitter.visibility.models.ViewerContext
|
||||
|
||||
class MisinformationPolicyFeatures(
|
||||
misinformationPolicySource: MisinformationPolicySource,
|
||||
statsReceiver: StatsReceiver) {
|
||||
|
||||
private[this] val scopedStatsReceiver =
|
||||
statsReceiver.scope("misinformation_policy_features")
|
||||
|
||||
private[this] val requests = scopedStatsReceiver.counter("requests")
|
||||
private[this] val tweetMisinformationPolicies =
|
||||
scopedStatsReceiver.scope(TweetMisinformationPolicies.name).counter("requests")
|
||||
|
||||
def forTweet(
|
||||
tweet: Tweet,
|
||||
viewerContext: ViewerContext
|
||||
): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
requests.incr()
|
||||
tweetMisinformationPolicies.incr()
|
||||
|
||||
_.withFeature(
|
||||
TweetMisinformationPolicies,
|
||||
misinformationPolicy(tweet.escherbirdEntityAnnotations, viewerContext))
|
||||
.withFeature(
|
||||
TweetEnglishMisinformationPolicies,
|
||||
misinformationPolicyEnglishOnly(tweet.escherbirdEntityAnnotations))
|
||||
}
|
||||
|
||||
def misinformationPolicyEnglishOnly(
|
||||
escherbirdEntityAnnotations: Option[EscherbirdEntityAnnotations],
|
||||
): Stitch[Seq[MisinformationPolicy]] = {
|
||||
val locale = Some(
|
||||
MisinformationPolicySource.LanguageAndCountry(
|
||||
language = Some("en"),
|
||||
country = Some("us")
|
||||
))
|
||||
fetchMisinformationPolicy(escherbirdEntityAnnotations, locale)
|
||||
}
|
||||
|
||||
def misinformationPolicy(
|
||||
escherbirdEntityAnnotations: Option[EscherbirdEntityAnnotations],
|
||||
viewerContext: ViewerContext
|
||||
): Stitch[Seq[MisinformationPolicy]] = {
|
||||
val locale = viewerContext.requestLanguageCode.map { language =>
|
||||
MisinformationPolicySource.LanguageAndCountry(
|
||||
language = Some(language),
|
||||
country = viewerContext.requestCountryCode
|
||||
)
|
||||
}
|
||||
fetchMisinformationPolicy(escherbirdEntityAnnotations, locale)
|
||||
}
|
||||
|
||||
def fetchMisinformationPolicy(
|
||||
escherbirdEntityAnnotations: Option[EscherbirdEntityAnnotations],
|
||||
locale: Option[MisinformationPolicySource.LanguageAndCountry]
|
||||
): Stitch[Seq[MisinformationPolicy]] = {
|
||||
Stitch.collect(
|
||||
escherbirdEntityAnnotations
|
||||
.map(_.entityAnnotations)
|
||||
.getOrElse(Seq.empty)
|
||||
.filter(_.domainId == SemanticCoreMisinformation.domainId)
|
||||
.map(annotation =>
|
||||
misinformationPolicySource
|
||||
.fetch(
|
||||
annotation,
|
||||
locale
|
||||
)
|
||||
.map(misinformation =>
|
||||
MisinformationPolicy(
|
||||
annotation = annotation,
|
||||
misinformation = misinformation
|
||||
)))
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package com.twitter.visibility.builder.tweets
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.features.TweetIsModerated
|
||||
|
||||
class ModerationFeatures(moderationSource: Long => Boolean, statsReceiver: StatsReceiver) {
|
||||
|
||||
private[this] val scopedStatsReceiver: StatsReceiver =
|
||||
statsReceiver.scope("moderation_features")
|
||||
|
||||
private[this] val requests = scopedStatsReceiver.counter("requests")
|
||||
|
||||
private[this] val tweetIsModerated =
|
||||
scopedStatsReceiver.scope(TweetIsModerated.name).counter("requests")
|
||||
|
||||
def forTweetId(tweetId: Long): FeatureMapBuilder => FeatureMapBuilder = { featureMapBuilder =>
|
||||
requests.incr()
|
||||
tweetIsModerated.incr()
|
||||
|
||||
featureMapBuilder.withConstantFeature(TweetIsModerated, moderationSource(tweetId))
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package com.twitter.visibility.builder.tweets
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.search.common.constants.thriftscala.ThriftQuerySource
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.features.SearchCandidateCount
|
||||
import com.twitter.visibility.features.SearchQueryHasUser
|
||||
import com.twitter.visibility.features.SearchQuerySource
|
||||
import com.twitter.visibility.features.SearchResultsPageNumber
|
||||
import com.twitter.visibility.interfaces.common.search.SearchVFRequestContext
|
||||
|
||||
class SearchContextFeatures(
|
||||
statsReceiver: StatsReceiver) {
|
||||
private[this] val scopedStatsReceiver = statsReceiver.scope("search_context_features")
|
||||
private[this] val requests = scopedStatsReceiver.counter("requests")
|
||||
private[this] val searchResultsPageNumber =
|
||||
scopedStatsReceiver.scope(SearchResultsPageNumber.name).counter("requests")
|
||||
private[this] val searchCandidateCount =
|
||||
scopedStatsReceiver.scope(SearchCandidateCount.name).counter("requests")
|
||||
private[this] val searchQuerySource =
|
||||
scopedStatsReceiver.scope(SearchQuerySource.name).counter("requests")
|
||||
private[this] val searchQueryHasUser =
|
||||
scopedStatsReceiver.scope(SearchQueryHasUser.name).counter("requests")
|
||||
|
||||
def forSearchContext(
|
||||
searchContext: SearchVFRequestContext
|
||||
): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
requests.incr()
|
||||
searchResultsPageNumber.incr()
|
||||
searchCandidateCount.incr()
|
||||
searchQuerySource.incr()
|
||||
searchQueryHasUser.incr()
|
||||
|
||||
_.withConstantFeature(SearchResultsPageNumber, searchContext.resultsPageNumber)
|
||||
.withConstantFeature(SearchCandidateCount, searchContext.candidateCount)
|
||||
.withConstantFeature(
|
||||
SearchQuerySource,
|
||||
searchContext.querySourceOption match {
|
||||
case Some(querySource) => querySource
|
||||
case _ => ThriftQuerySource.Unknown
|
||||
})
|
||||
.withConstantFeature(SearchQueryHasUser, searchContext.queryHasUser)
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package com.twitter.visibility.builder.tweets
|
||||
|
||||
import com.twitter.contenthealth.toxicreplyfilter.thriftscala.FilterState
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.tweetypie.thriftscala.Tweet
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.features.ToxicReplyFilterConversationAuthorIsViewer
|
||||
import com.twitter.visibility.features.ToxicReplyFilterState
|
||||
|
||||
class ToxicReplyFilterFeature(
|
||||
statsReceiver: StatsReceiver) {
|
||||
|
||||
def forTweet(tweet: Tweet, viewerId: Option[Long]): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
builder =>
|
||||
requests.incr()
|
||||
|
||||
builder
|
||||
.withConstantFeature(ToxicReplyFilterState, isTweetFilteredFromAuthor(tweet))
|
||||
.withConstantFeature(
|
||||
ToxicReplyFilterConversationAuthorIsViewer,
|
||||
isRootAuthorViewer(tweet, viewerId))
|
||||
}
|
||||
|
||||
private[this] def isRootAuthorViewer(tweet: Tweet, maybeViewerId: Option[Long]): Boolean = {
|
||||
val maybeAuthorId = tweet.filteredReplyDetails.map(_.conversationAuthorId)
|
||||
|
||||
(maybeViewerId, maybeAuthorId) match {
|
||||
case (Some(viewerId), Some(authorId)) if viewerId == authorId => {
|
||||
rootAuthorViewerStats.incr()
|
||||
true
|
||||
}
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
|
||||
private[this] def isTweetFilteredFromAuthor(
|
||||
tweet: Tweet,
|
||||
): FilterState = {
|
||||
val result = tweet.filteredReplyDetails.map(_.filterState).getOrElse(FilterState.Unfiltered)
|
||||
|
||||
if (result == FilterState.FilteredFromAuthor) {
|
||||
filteredFromAuthorStats.incr()
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
private[this] val scopedStatsReceiver =
|
||||
statsReceiver.scope("toxicreplyfilter")
|
||||
|
||||
private[this] val requests = scopedStatsReceiver.counter("requests")
|
||||
|
||||
private[this] val rootAuthorViewerStats =
|
||||
scopedStatsReceiver.scope(ToxicReplyFilterConversationAuthorIsViewer.name).counter("requests")
|
||||
|
||||
private[this] val filteredFromAuthorStats =
|
||||
scopedStatsReceiver.scope(ToxicReplyFilterState.name).counter("requests")
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package com.twitter.visibility.builder.tweets
|
||||
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.tweetypie.thriftscala.Tweet
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.common.TrustedFriendsListId
|
||||
import com.twitter.visibility.common.TrustedFriendsSource
|
||||
import com.twitter.visibility.features.TweetIsTrustedFriendTweet
|
||||
import com.twitter.visibility.features.ViewerIsTrustedFriendOfTweetAuthor
|
||||
import com.twitter.visibility.features.ViewerIsTrustedFriendTweetAuthor
|
||||
|
||||
class TrustedFriendsFeatures(trustedFriendsSource: TrustedFriendsSource) {
|
||||
|
||||
private[builder] def viewerIsTrustedFriend(
|
||||
tweet: Tweet,
|
||||
viewerId: Option[Long]
|
||||
): Stitch[Boolean] =
|
||||
(trustedFriendsListId(tweet), viewerId) match {
|
||||
case (Some(tfListId), Some(userId)) =>
|
||||
trustedFriendsSource.isTrustedFriend(tfListId, userId)
|
||||
case _ => Stitch.False
|
||||
}
|
||||
|
||||
private[builder] def viewerIsTrustedFriendListOwner(
|
||||
tweet: Tweet,
|
||||
viewerId: Option[Long]
|
||||
): Stitch[Boolean] =
|
||||
(trustedFriendsListId(tweet), viewerId) match {
|
||||
case (Some(tfListId), Some(userId)) =>
|
||||
trustedFriendsSource.isTrustedFriendListOwner(tfListId, userId)
|
||||
case _ => Stitch.False
|
||||
}
|
||||
|
||||
private[builder] def trustedFriendsListId(tweet: Tweet): Option[TrustedFriendsListId] =
|
||||
tweet.trustedFriendsControl.map(_.trustedFriendsListId)
|
||||
|
||||
def forTweet(
|
||||
tweet: Tweet,
|
||||
viewerId: Option[Long]
|
||||
): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
_.withConstantFeature(
|
||||
TweetIsTrustedFriendTweet,
|
||||
tweet.trustedFriendsControl.isDefined
|
||||
).withFeature(
|
||||
ViewerIsTrustedFriendTweetAuthor,
|
||||
viewerIsTrustedFriendListOwner(tweet, viewerId)
|
||||
).withFeature(
|
||||
ViewerIsTrustedFriendOfTweetAuthor,
|
||||
viewerIsTrustedFriend(tweet, viewerId)
|
||||
)
|
||||
}
|
||||
|
||||
def forTweetOnly(tweet: Tweet): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
_.withConstantFeature(TweetIsTrustedFriendTweet, tweet.trustedFriendsControl.isDefined)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,210 @@
|
||||
package com.twitter.visibility.builder.tweets
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.snowflake.id.SnowflakeId
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.tweetypie.thriftscala.CollabControl
|
||||
import com.twitter.tweetypie.thriftscala.Tweet
|
||||
import com.twitter.util.Duration
|
||||
import com.twitter.util.Time
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.common.SafetyLabelMapSource
|
||||
import com.twitter.visibility.common.TweetId
|
||||
import com.twitter.visibility.common.UserId
|
||||
import com.twitter.visibility.features._
|
||||
import com.twitter.visibility.models.SemanticCoreAnnotation
|
||||
import com.twitter.visibility.models.TweetSafetyLabel
|
||||
|
||||
object TweetFeatures {
|
||||
|
||||
def FALLBACK_TIMESTAMP: Time = Time.epoch
|
||||
|
||||
def tweetIsSelfReply(tweet: Tweet): Boolean = {
|
||||
tweet.coreData match {
|
||||
case Some(coreData) =>
|
||||
coreData.reply match {
|
||||
case Some(reply) =>
|
||||
reply.inReplyToUserId == coreData.userId
|
||||
|
||||
case None =>
|
||||
false
|
||||
}
|
||||
|
||||
case None =>
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
def tweetReplyToParentTweetDuration(tweet: Tweet): Option[Duration] = for {
|
||||
coreData <- tweet.coreData
|
||||
reply <- coreData.reply
|
||||
inReplyToStatusId <- reply.inReplyToStatusId
|
||||
replyTime <- SnowflakeId.timeFromIdOpt(tweet.id)
|
||||
repliedToTime <- SnowflakeId.timeFromIdOpt(inReplyToStatusId)
|
||||
} yield {
|
||||
replyTime.diff(repliedToTime)
|
||||
}
|
||||
|
||||
def tweetReplyToRootTweetDuration(tweet: Tweet): Option[Duration] = for {
|
||||
coreData <- tweet.coreData
|
||||
if coreData.reply.isDefined
|
||||
conversationId <- coreData.conversationId
|
||||
replyTime <- SnowflakeId.timeFromIdOpt(tweet.id)
|
||||
rootTime <- SnowflakeId.timeFromIdOpt(conversationId)
|
||||
} yield {
|
||||
replyTime.diff(rootTime)
|
||||
}
|
||||
|
||||
def tweetTimestamp(tweetId: Long): Time =
|
||||
SnowflakeId.timeFromIdOpt(tweetId).getOrElse(FALLBACK_TIMESTAMP)
|
||||
|
||||
def tweetSemanticCoreAnnotations(tweet: Tweet): Seq[SemanticCoreAnnotation] = {
|
||||
tweet.escherbirdEntityAnnotations
|
||||
.map(a =>
|
||||
a.entityAnnotations.map { annotation =>
|
||||
SemanticCoreAnnotation(
|
||||
annotation.groupId,
|
||||
annotation.domainId,
|
||||
annotation.entityId
|
||||
)
|
||||
}).toSeq.flatten
|
||||
}
|
||||
|
||||
def tweetIsNullcast(tweet: Tweet): Boolean = {
|
||||
tweet.coreData match {
|
||||
case Some(coreData) =>
|
||||
coreData.nullcast
|
||||
case None =>
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
def tweetAuthorUserId(tweet: Tweet): Option[UserId] = {
|
||||
tweet.coreData.map(_.userId)
|
||||
}
|
||||
}
|
||||
|
||||
sealed trait TweetLabels {
|
||||
def forTweet(tweet: Tweet): Stitch[Seq[TweetSafetyLabel]]
|
||||
def forTweetId(tweetId: TweetId): Stitch[Seq[TweetSafetyLabel]]
|
||||
}
|
||||
|
||||
class StratoTweetLabelMaps(safetyLabelSource: SafetyLabelMapSource) extends TweetLabels {
|
||||
|
||||
override def forTweet(tweet: Tweet): Stitch[Seq[TweetSafetyLabel]] = {
|
||||
forTweetId(tweet.id)
|
||||
}
|
||||
|
||||
def forTweetId(tweetId: TweetId): Stitch[Seq[TweetSafetyLabel]] = {
|
||||
safetyLabelSource
|
||||
.fetch(tweetId).map(
|
||||
_.map(
|
||||
_.labels
|
||||
.map(
|
||||
_.map(sl => TweetSafetyLabel.fromTuple(sl._1, sl._2)).toSeq
|
||||
).getOrElse(Seq())
|
||||
).getOrElse(Seq()))
|
||||
}
|
||||
}
|
||||
|
||||
object NilTweetLabelMaps extends TweetLabels {
|
||||
override def forTweet(tweet: Tweet): Stitch[Seq[TweetSafetyLabel]] = Stitch.Nil
|
||||
override def forTweetId(tweetId: TweetId): Stitch[Seq[TweetSafetyLabel]] = Stitch.Nil
|
||||
}
|
||||
|
||||
class TweetFeatures(tweetLabels: TweetLabels, statsReceiver: StatsReceiver) {
|
||||
private[this] val scopedStatsReceiver = statsReceiver.scope("tweet_features")
|
||||
|
||||
private[this] val requests = scopedStatsReceiver.counter("requests")
|
||||
private[this] val tweetSafetyLabels =
|
||||
scopedStatsReceiver.scope(TweetSafetyLabels.name).counter("requests")
|
||||
private[this] val tweetTakedownReasons =
|
||||
scopedStatsReceiver.scope(TweetTakedownReasons.name).counter("requests")
|
||||
private[this] val tweetIsSelfReply =
|
||||
scopedStatsReceiver.scope(TweetIsSelfReply.name).counter("requests")
|
||||
private[this] val tweetTimestamp =
|
||||
scopedStatsReceiver.scope(TweetTimestamp.name).counter("requests")
|
||||
private[this] val tweetReplyToParentTweetDuration =
|
||||
scopedStatsReceiver.scope(TweetReplyToParentTweetDuration.name).counter("requests")
|
||||
private[this] val tweetReplyToRootTweetDuration =
|
||||
scopedStatsReceiver.scope(TweetReplyToRootTweetDuration.name).counter("requests")
|
||||
private[this] val tweetSemanticCoreAnnotations =
|
||||
scopedStatsReceiver.scope(TweetSemanticCoreAnnotations.name).counter("requests")
|
||||
private[this] val tweetId =
|
||||
scopedStatsReceiver.scope(TweetId.name).counter("requests")
|
||||
private[this] val tweetHasNsfwUser =
|
||||
scopedStatsReceiver.scope(TweetHasNsfwUser.name).counter("requests")
|
||||
private[this] val tweetHasNsfwAdmin =
|
||||
scopedStatsReceiver.scope(TweetHasNsfwAdmin.name).counter("requests")
|
||||
private[this] val tweetIsNullcast =
|
||||
scopedStatsReceiver.scope(TweetIsNullcast.name).counter("requests")
|
||||
private[this] val tweetHasMedia =
|
||||
scopedStatsReceiver.scope(TweetHasMedia.name).counter("requests")
|
||||
private[this] val tweetIsCommunity =
|
||||
scopedStatsReceiver.scope(TweetIsCommunityTweet.name).counter("requests")
|
||||
private[this] val tweetIsCollabInvitation =
|
||||
scopedStatsReceiver.scope(TweetIsCollabInvitationTweet.name).counter("requests")
|
||||
|
||||
def forTweet(tweet: Tweet): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
forTweetWithoutSafetyLabels(tweet)
|
||||
.andThen(_.withFeature(TweetSafetyLabels, tweetLabels.forTweet(tweet)))
|
||||
}
|
||||
|
||||
def forTweetWithoutSafetyLabels(tweet: Tweet): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
requests.incr()
|
||||
|
||||
tweetTakedownReasons.incr()
|
||||
tweetIsSelfReply.incr()
|
||||
tweetTimestamp.incr()
|
||||
tweetReplyToParentTweetDuration.incr()
|
||||
tweetReplyToRootTweetDuration.incr()
|
||||
tweetSemanticCoreAnnotations.incr()
|
||||
tweetId.incr()
|
||||
tweetHasNsfwUser.incr()
|
||||
tweetHasNsfwAdmin.incr()
|
||||
tweetIsNullcast.incr()
|
||||
tweetHasMedia.incr()
|
||||
tweetIsCommunity.incr()
|
||||
tweetIsCollabInvitation.incr()
|
||||
|
||||
_.withConstantFeature(TweetTakedownReasons, tweet.takedownReasons.getOrElse(Seq.empty))
|
||||
.withConstantFeature(TweetIsSelfReply, TweetFeatures.tweetIsSelfReply(tweet))
|
||||
.withConstantFeature(TweetTimestamp, TweetFeatures.tweetTimestamp(tweet.id))
|
||||
.withConstantFeature(
|
||||
TweetReplyToParentTweetDuration,
|
||||
TweetFeatures.tweetReplyToParentTweetDuration(tweet))
|
||||
.withConstantFeature(
|
||||
TweetReplyToRootTweetDuration,
|
||||
TweetFeatures.tweetReplyToRootTweetDuration(tweet))
|
||||
.withConstantFeature(
|
||||
TweetSemanticCoreAnnotations,
|
||||
TweetFeatures.tweetSemanticCoreAnnotations(tweet))
|
||||
.withConstantFeature(TweetId, tweet.id)
|
||||
.withConstantFeature(TweetHasNsfwUser, tweetHasNsfwUser(tweet))
|
||||
.withConstantFeature(TweetHasNsfwAdmin, tweetHasNsfwAdmin(tweet))
|
||||
.withConstantFeature(TweetIsNullcast, TweetFeatures.tweetIsNullcast(tweet))
|
||||
.withConstantFeature(TweetHasMedia, tweetHasMedia(tweet))
|
||||
.withConstantFeature(TweetIsCommunityTweet, tweetHasCommunity(tweet))
|
||||
.withConstantFeature(TweetIsCollabInvitationTweet, tweetIsCollabInvitation(tweet))
|
||||
}
|
||||
|
||||
def tweetHasNsfwUser(tweet: Tweet): Boolean =
|
||||
tweet.coreData.exists(_.nsfwUser)
|
||||
|
||||
def tweetHasNsfwAdmin(tweet: Tweet): Boolean =
|
||||
tweet.coreData.exists(_.nsfwAdmin)
|
||||
|
||||
def tweetHasMedia(tweet: Tweet): Boolean =
|
||||
tweet.coreData.exists(_.hasMedia.getOrElse(false))
|
||||
|
||||
def tweetHasCommunity(tweet: Tweet): Boolean = {
|
||||
tweet.communities.exists(_.communityIds.nonEmpty)
|
||||
}
|
||||
|
||||
def tweetIsCollabInvitation(tweet: Tweet): Boolean = {
|
||||
tweet.collabControl.exists(_ match {
|
||||
case CollabControl.CollabInvitation(_) => true
|
||||
case _ => false
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
package com.twitter.visibility.builder.tweets
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.servo.util.Gate
|
||||
import com.twitter.spam.rtf.thriftscala.SafetyLabel
|
||||
import com.twitter.spam.rtf.thriftscala.SafetyLabelType
|
||||
import com.twitter.spam.rtf.thriftscala.SafetyLabelValue
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.common.stitch.StitchHelpers
|
||||
import com.twitter.visibility.features.TweetId
|
||||
import com.twitter.visibility.features.TweetSafetyLabels
|
||||
import com.twitter.visibility.features.TweetTimestamp
|
||||
import com.twitter.visibility.models.TweetSafetyLabel
|
||||
|
||||
class TweetIdFeatures(
|
||||
statsReceiver: StatsReceiver,
|
||||
enableStitchProfiling: Gate[Unit]) {
|
||||
private[this] val scopedStatsReceiver: StatsReceiver = statsReceiver.scope("tweet_id_features")
|
||||
|
||||
private[this] val requests = scopedStatsReceiver.counter("requests")
|
||||
private[this] val tweetSafetyLabels =
|
||||
scopedStatsReceiver.scope(TweetSafetyLabels.name).counter("requests")
|
||||
private[this] val tweetTimestamp =
|
||||
scopedStatsReceiver.scope(TweetTimestamp.name).counter("requests")
|
||||
|
||||
private[this] val labelFetchScope: StatsReceiver =
|
||||
scopedStatsReceiver.scope("labelFetch")
|
||||
|
||||
private[this] def getTweetLabels(
|
||||
tweetId: Long,
|
||||
labelFetcher: Long => Stitch[Map[SafetyLabelType, SafetyLabel]]
|
||||
): Stitch[Seq[TweetSafetyLabel]] = {
|
||||
val stitch =
|
||||
labelFetcher(tweetId).map { labelMap =>
|
||||
labelMap
|
||||
.map { case (labelType, label) => SafetyLabelValue(labelType, label) }.toSeq
|
||||
.map(TweetSafetyLabel.fromThrift)
|
||||
}
|
||||
|
||||
if (enableStitchProfiling()) {
|
||||
StitchHelpers.profileStitch(
|
||||
stitch,
|
||||
Seq(labelFetchScope)
|
||||
)
|
||||
} else {
|
||||
stitch
|
||||
}
|
||||
}
|
||||
|
||||
def forTweetId(
|
||||
tweetId: Long,
|
||||
labelFetcher: Long => Stitch[Map[SafetyLabelType, SafetyLabel]]
|
||||
): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
requests.incr()
|
||||
tweetSafetyLabels.incr()
|
||||
tweetTimestamp.incr()
|
||||
|
||||
_.withFeature(TweetSafetyLabels, getTweetLabels(tweetId, labelFetcher))
|
||||
.withConstantFeature(TweetTimestamp, TweetFeatures.tweetTimestamp(tweetId))
|
||||
.withConstantFeature(TweetId, tweetId)
|
||||
}
|
||||
|
||||
def forTweetId(
|
||||
tweetId: Long,
|
||||
constantTweetSafetyLabels: Seq[TweetSafetyLabel]
|
||||
): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
requests.incr()
|
||||
tweetSafetyLabels.incr()
|
||||
tweetTimestamp.incr()
|
||||
|
||||
_.withConstantFeature(TweetSafetyLabels, constantTweetSafetyLabels)
|
||||
.withConstantFeature(TweetTimestamp, TweetFeatures.tweetTimestamp(tweetId))
|
||||
.withConstantFeature(TweetId, tweetId)
|
||||
}
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
package com.twitter.visibility.builder.tweets
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.mediaservices.commons.mediainformation.thriftscala.AdditionalMetadata
|
||||
import com.twitter.mediaservices.media_util.GenericMediaKey
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.tweetypie.thriftscala.Tweet
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.common.TweetMediaMetadataSource
|
||||
import com.twitter.visibility.features.HasDmcaMediaFeature
|
||||
import com.twitter.visibility.features.MediaGeoRestrictionsAllowList
|
||||
import com.twitter.visibility.features.MediaGeoRestrictionsDenyList
|
||||
|
||||
class TweetMediaMetadataFeatures(
|
||||
mediaMetadataSource: TweetMediaMetadataSource,
|
||||
statsReceiver: StatsReceiver) {
|
||||
|
||||
private[this] val scopedStatsReceiver = statsReceiver.scope("tweet_media_metadata_features")
|
||||
private[this] val reportedStats = scopedStatsReceiver.scope("dmcaStats")
|
||||
|
||||
def forTweet(
|
||||
tweet: Tweet,
|
||||
mediaKeys: Seq[GenericMediaKey],
|
||||
enableFetchMediaMetadata: Boolean
|
||||
): FeatureMapBuilder => FeatureMapBuilder = { featureMapBuilder =>
|
||||
featureMapBuilder.withFeature(
|
||||
HasDmcaMediaFeature,
|
||||
mediaIsDmca(tweet, mediaKeys, enableFetchMediaMetadata))
|
||||
featureMapBuilder.withFeature(
|
||||
MediaGeoRestrictionsAllowList,
|
||||
allowlist(tweet, mediaKeys, enableFetchMediaMetadata))
|
||||
featureMapBuilder.withFeature(
|
||||
MediaGeoRestrictionsDenyList,
|
||||
denylist(tweet, mediaKeys, enableFetchMediaMetadata))
|
||||
}
|
||||
|
||||
private def mediaIsDmca(
|
||||
tweet: Tweet,
|
||||
mediaKeys: Seq[GenericMediaKey],
|
||||
enableFetchMediaMetadata: Boolean
|
||||
) = getMediaAdditionalMetadata(tweet, mediaKeys, enableFetchMediaMetadata)
|
||||
.map(_.exists(_.restrictions.exists(_.isDmca)))
|
||||
|
||||
private def allowlist(
|
||||
tweet: Tweet,
|
||||
mediaKeys: Seq[GenericMediaKey],
|
||||
enableFetchMediaMetadata: Boolean
|
||||
) = getMediaGeoRestrictions(tweet, mediaKeys, enableFetchMediaMetadata)
|
||||
.map(_.flatMap(_.whitelistedCountryCodes))
|
||||
|
||||
private def denylist(
|
||||
tweet: Tweet,
|
||||
mediaKeys: Seq[GenericMediaKey],
|
||||
enableFetchMediaMetadata: Boolean
|
||||
) = getMediaGeoRestrictions(tweet, mediaKeys, enableFetchMediaMetadata)
|
||||
.map(_.flatMap(_.blacklistedCountryCodes))
|
||||
|
||||
private def getMediaGeoRestrictions(
|
||||
tweet: Tweet,
|
||||
mediaKeys: Seq[GenericMediaKey],
|
||||
enableFetchMediaMetadata: Boolean
|
||||
) = {
|
||||
getMediaAdditionalMetadata(tweet, mediaKeys, enableFetchMediaMetadata)
|
||||
.map(additionalMetadatasSeq => {
|
||||
for {
|
||||
additionalMetadata <- additionalMetadatasSeq
|
||||
restrictions <- additionalMetadata.restrictions
|
||||
geoRestrictions <- restrictions.geoRestrictions
|
||||
} yield {
|
||||
geoRestrictions
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private def getMediaAdditionalMetadata(
|
||||
tweet: Tweet,
|
||||
mediaKeys: Seq[GenericMediaKey],
|
||||
enableFetchMediaMetadata: Boolean
|
||||
): Stitch[Seq[AdditionalMetadata]] = {
|
||||
if (mediaKeys.isEmpty) {
|
||||
reportedStats.counter("empty").incr()
|
||||
Stitch.value(Seq.empty)
|
||||
} else {
|
||||
tweet.media.flatMap { mediaEntities =>
|
||||
val alreadyHydratedMetadata = mediaEntities
|
||||
.filter(_.mediaKey.isDefined)
|
||||
.flatMap(_.additionalMetadata)
|
||||
|
||||
if (alreadyHydratedMetadata.nonEmpty) {
|
||||
Some(alreadyHydratedMetadata)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} match {
|
||||
case Some(additionalMetadata) =>
|
||||
reportedStats.counter("already_hydrated").incr()
|
||||
Stitch.value(additionalMetadata)
|
||||
case None =>
|
||||
Stitch
|
||||
.collect(
|
||||
mediaKeys.map(fetchAdditionalMetadata(tweet.id, _, enableFetchMediaMetadata))
|
||||
).map(maybeMetadatas => {
|
||||
maybeMetadatas
|
||||
.filter(_.isDefined)
|
||||
.map(_.get)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def fetchAdditionalMetadata(
|
||||
tweetId: Long,
|
||||
genericMediaKey: GenericMediaKey,
|
||||
enableFetchMediaMetadata: Boolean
|
||||
): Stitch[Option[AdditionalMetadata]] =
|
||||
if (enableFetchMediaMetadata) {
|
||||
genericMediaKey.toThriftMediaKey() match {
|
||||
case Some(mediaKey) =>
|
||||
reportedStats.counter("request").incr()
|
||||
mediaMetadataSource.fetch(tweetId, mediaKey)
|
||||
case None =>
|
||||
reportedStats.counter("empty_key").incr()
|
||||
Stitch.None
|
||||
}
|
||||
} else {
|
||||
reportedStats.counter("light_request").incr()
|
||||
Stitch.None
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package com.twitter.visibility.builder.tweets
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.tweetypie.thriftscala.Tweet
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.common.TweetPerspectiveSource
|
||||
import com.twitter.visibility.features.ViewerReportedTweet
|
||||
|
||||
class TweetPerspectiveFeatures(
|
||||
tweetPerspectiveSource: TweetPerspectiveSource,
|
||||
statsReceiver: StatsReceiver) {
|
||||
|
||||
private[this] val scopedStatsReceiver = statsReceiver.scope("tweet_perspective_features")
|
||||
private[this] val reportedStats = scopedStatsReceiver.scope("reported")
|
||||
|
||||
def forTweet(
|
||||
tweet: Tweet,
|
||||
viewerId: Option[Long],
|
||||
enableFetchReportedPerspective: Boolean
|
||||
): FeatureMapBuilder => FeatureMapBuilder =
|
||||
_.withFeature(
|
||||
ViewerReportedTweet,
|
||||
tweetIsReported(tweet, viewerId, enableFetchReportedPerspective))
|
||||
|
||||
private[builder] def tweetIsReported(
|
||||
tweet: Tweet,
|
||||
viewerId: Option[Long],
|
||||
enableFetchReportedPerspective: Boolean = true
|
||||
): Stitch[Boolean] = {
|
||||
((tweet.perspective, viewerId) match {
|
||||
case (Some(perspective), _) =>
|
||||
Stitch.value(perspective.reported).onSuccess { _ =>
|
||||
reportedStats.counter("already_hydrated").incr()
|
||||
}
|
||||
case (None, Some(viewerId)) =>
|
||||
if (enableFetchReportedPerspective) {
|
||||
tweetPerspectiveSource.reported(tweet.id, viewerId).onSuccess { _ =>
|
||||
reportedStats.counter("request").incr()
|
||||
}
|
||||
} else {
|
||||
Stitch.False.onSuccess { _ =>
|
||||
reportedStats.counter("light_request").incr()
|
||||
}
|
||||
}
|
||||
case _ =>
|
||||
Stitch.False.onSuccess { _ =>
|
||||
reportedStats.counter("empty").incr()
|
||||
}
|
||||
}).onSuccess { _ =>
|
||||
reportedStats.counter("").incr()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package com.twitter.visibility.builder.tweets
|
||||
|
||||
import com.twitter.spam.rtf.thriftscala.SafetyLabelType
|
||||
import com.twitter.spam.rtf.thriftscala.SafetyLabelType.ExperimentalNudge
|
||||
import com.twitter.spam.rtf.thriftscala.SafetyLabelType.SemanticCoreMisinformation
|
||||
import com.twitter.spam.rtf.thriftscala.SafetyLabelType.UnsafeUrl
|
||||
import com.twitter.visibility.common.LocalizedNudgeSource
|
||||
import com.twitter.visibility.common.actions.TweetVisibilityNudgeReason
|
||||
import com.twitter.visibility.common.actions.TweetVisibilityNudgeReason.ExperimentalNudgeSafetyLabelReason
|
||||
import com.twitter.visibility.common.actions.TweetVisibilityNudgeReason.SemanticCoreMisinformationLabelReason
|
||||
import com.twitter.visibility.common.actions.TweetVisibilityNudgeReason.UnsafeURLLabelReason
|
||||
import com.twitter.visibility.rules.LocalizedNudge
|
||||
|
||||
class TweetVisibilityNudgeSourceWrapper(localizedNudgeSource: LocalizedNudgeSource) {
|
||||
|
||||
def getLocalizedNudge(
|
||||
reason: TweetVisibilityNudgeReason,
|
||||
languageCode: String,
|
||||
countryCode: Option[String]
|
||||
): Option[LocalizedNudge] =
|
||||
reason match {
|
||||
case ExperimentalNudgeSafetyLabelReason =>
|
||||
fetchNudge(ExperimentalNudge, languageCode, countryCode)
|
||||
case SemanticCoreMisinformationLabelReason =>
|
||||
fetchNudge(SemanticCoreMisinformation, languageCode, countryCode)
|
||||
case UnsafeURLLabelReason =>
|
||||
fetchNudge(UnsafeUrl, languageCode, countryCode)
|
||||
}
|
||||
|
||||
private def fetchNudge(
|
||||
safetyLabel: SafetyLabelType,
|
||||
languageCode: String,
|
||||
countryCode: Option[String]
|
||||
): Option[LocalizedNudge] = {
|
||||
localizedNudgeSource
|
||||
.fetch(safetyLabel, languageCode, countryCode)
|
||||
.map(LocalizedNudge.fromStratoThrift)
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
package com.twitter.visibility.builder.tweets
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.notificationservice.model.notification._
|
||||
import com.twitter.servo.util.Gate
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.tweetypie.thriftscala.SettingsUnmentions
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.common.TweetSource
|
||||
import com.twitter.visibility.features.NotificationIsOnUnmentionedViewer
|
||||
|
||||
object UnmentionNotificationFeatures {
|
||||
def ForNonUnmentionNotificationFeatures: FeatureMapBuilder => FeatureMapBuilder = {
|
||||
_.withConstantFeature(NotificationIsOnUnmentionedViewer, false)
|
||||
}
|
||||
}
|
||||
|
||||
class UnmentionNotificationFeatures(
|
||||
tweetSource: TweetSource,
|
||||
enableUnmentionHydration: Gate[Long],
|
||||
statsReceiver: StatsReceiver) {
|
||||
|
||||
private[this] val scopedStatsReceiver =
|
||||
statsReceiver.scope("unmention_notification_features")
|
||||
private[this] val requestsCounter = scopedStatsReceiver.counter("requests")
|
||||
private[this] val hydrationsCounter = scopedStatsReceiver.counter("hydrations")
|
||||
private[this] val notificationsUnmentionUserCounter =
|
||||
scopedStatsReceiver
|
||||
.scope(NotificationIsOnUnmentionedViewer.name).counter("unmentioned_users")
|
||||
|
||||
def forNotification(notification: Notification): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
requestsCounter.incr()
|
||||
|
||||
val isUnmentionNotification = tweetId(notification) match {
|
||||
case Some(tweetId) if enableUnmentionHydration(notification.target) =>
|
||||
hydrationsCounter.incr()
|
||||
tweetSource
|
||||
.getTweet(tweetId)
|
||||
.map {
|
||||
case Some(tweet) =>
|
||||
tweet.settingsUnmentions match {
|
||||
case Some(SettingsUnmentions(Some(unmentionedUserIds))) =>
|
||||
if (unmentionedUserIds.contains(notification.target)) {
|
||||
notificationsUnmentionUserCounter.incr()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
case _ => false
|
||||
}
|
||||
case _ => false
|
||||
}
|
||||
case _ => Stitch.False
|
||||
}
|
||||
_.withFeature(NotificationIsOnUnmentionedViewer, isUnmentionNotification)
|
||||
}
|
||||
|
||||
private[this] def tweetId(notification: Notification): Option[Long] = {
|
||||
notification match {
|
||||
case n: MentionNotification => Some(n.mentioningTweetId)
|
||||
case n: FavoritedMentioningTweetNotification => Some(n.mentioningTweetId)
|
||||
case n: FavoritedReplyToYourTweetNotification => Some(n.replyTweetId)
|
||||
case n: MentionQuoteNotification => Some(n.mentioningTweetId)
|
||||
case n: ReactionMentioningTweetNotification => Some(n.mentioningTweetId)
|
||||
case n: ReplyNotification => Some(n.replyingTweetId)
|
||||
case n: RetweetedMentionNotification => Some(n.mentioningTweetId)
|
||||
case n: RetweetedReplyToYourTweetNotification => Some(n.replyTweetId)
|
||||
case n: ReplyToConversationNotification => Some(n.replyingTweetId)
|
||||
case n: ReactionReplyToYourTweetNotification => Some(n.replyTweetId)
|
||||
case _ => None
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package com.twitter.visibility.builder.users
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.gizmoduck.thriftscala.User
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.common.UserDeviceSource
|
||||
import com.twitter.visibility.features.AuthorHasConfirmedEmail
|
||||
import com.twitter.visibility.features.AuthorHasVerifiedPhone
|
||||
|
||||
class AuthorDeviceFeatures(userDeviceSource: UserDeviceSource, statsReceiver: StatsReceiver) {
|
||||
private[this] val scopedStatsReceiver = statsReceiver.scope("author_device_features")
|
||||
|
||||
private[this] val requests = scopedStatsReceiver.counter("requests")
|
||||
|
||||
private[this] val authorHasConfirmedEmailRequests =
|
||||
scopedStatsReceiver.scope(AuthorHasConfirmedEmail.name).counter("requests")
|
||||
private[this] val authorHasVerifiedPhoneRequests =
|
||||
scopedStatsReceiver.scope(AuthorHasVerifiedPhone.name).counter("requests")
|
||||
|
||||
def forAuthor(author: User): FeatureMapBuilder => FeatureMapBuilder = forAuthorId(author.id)
|
||||
|
||||
def forAuthorId(authorId: Long): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
requests.incr()
|
||||
|
||||
_.withFeature(AuthorHasConfirmedEmail, authorHasConfirmedEmail(authorId))
|
||||
.withFeature(AuthorHasVerifiedPhone, authorHasVerifiedPhone(authorId))
|
||||
}
|
||||
|
||||
def authorHasConfirmedEmail(authorId: Long): Stitch[Boolean] = {
|
||||
authorHasConfirmedEmailRequests.incr()
|
||||
userDeviceSource.hasConfirmedEmail(authorId)
|
||||
}
|
||||
|
||||
def authorHasVerifiedPhone(authorId: Long): Stitch[Boolean] = {
|
||||
authorHasVerifiedPhoneRequests.incr()
|
||||
userDeviceSource.hasConfirmedPhone(authorId)
|
||||
}
|
||||
}
|
@ -0,0 +1,221 @@
|
||||
package com.twitter.visibility.builder.users
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.gizmoduck.thriftscala.Label
|
||||
import com.twitter.gizmoduck.thriftscala.Labels
|
||||
import com.twitter.gizmoduck.thriftscala.Profile
|
||||
import com.twitter.gizmoduck.thriftscala.Safety
|
||||
import com.twitter.gizmoduck.thriftscala.User
|
||||
import com.twitter.stitch.NotFound
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.tseng.withholding.thriftscala.TakedownReason
|
||||
import com.twitter.util.Duration
|
||||
import com.twitter.util.Time
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.common.UserSource
|
||||
import com.twitter.visibility.features._
|
||||
|
||||
class AuthorFeatures(userSource: UserSource, statsReceiver: StatsReceiver) {
|
||||
private[this] val scopedStatsReceiver = statsReceiver.scope("author_features")
|
||||
|
||||
private[this] val requests = scopedStatsReceiver.counter("requests")
|
||||
|
||||
private[this] val authorUserLabels =
|
||||
scopedStatsReceiver.scope(AuthorUserLabels.name).counter("requests")
|
||||
private[this] val authorIsSuspended =
|
||||
scopedStatsReceiver.scope(AuthorIsSuspended.name).counter("requests")
|
||||
private[this] val authorIsProtected =
|
||||
scopedStatsReceiver.scope(AuthorIsProtected.name).counter("requests")
|
||||
private[this] val authorIsDeactivated =
|
||||
scopedStatsReceiver.scope(AuthorIsDeactivated.name).counter("requests")
|
||||
private[this] val authorIsErased =
|
||||
scopedStatsReceiver.scope(AuthorIsErased.name).counter("requests")
|
||||
private[this] val authorIsOffboarded =
|
||||
scopedStatsReceiver.scope(AuthorIsOffboarded.name).counter("requests")
|
||||
private[this] val authorIsNsfwUser =
|
||||
scopedStatsReceiver.scope(AuthorIsNsfwUser.name).counter("requests")
|
||||
private[this] val authorIsNsfwAdmin =
|
||||
scopedStatsReceiver.scope(AuthorIsNsfwAdmin.name).counter("requests")
|
||||
private[this] val authorTakedownReasons =
|
||||
scopedStatsReceiver.scope(AuthorTakedownReasons.name).counter("requests")
|
||||
private[this] val authorHasDefaultProfileImage =
|
||||
scopedStatsReceiver.scope(AuthorHasDefaultProfileImage.name).counter("requests")
|
||||
private[this] val authorAccountAge =
|
||||
scopedStatsReceiver.scope(AuthorAccountAge.name).counter("requests")
|
||||
private[this] val authorIsVerified =
|
||||
scopedStatsReceiver.scope(AuthorIsVerified.name).counter("requests")
|
||||
private[this] val authorScreenName =
|
||||
scopedStatsReceiver.scope(AuthorScreenName.name).counter("requests")
|
||||
private[this] val authorIsBlueVerified =
|
||||
scopedStatsReceiver.scope(AuthorIsBlueVerified.name).counter("requests")
|
||||
|
||||
def forAuthor(author: User): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
requests.incr()
|
||||
|
||||
_.withConstantFeature(AuthorId, Set(author.id))
|
||||
.withConstantFeature(AuthorUserLabels, authorUserLabels(author))
|
||||
.withConstantFeature(AuthorIsProtected, authorIsProtected(author))
|
||||
.withConstantFeature(AuthorIsSuspended, authorIsSuspended(author))
|
||||
.withConstantFeature(AuthorIsDeactivated, authorIsDeactivated(author))
|
||||
.withConstantFeature(AuthorIsErased, authorIsErased(author))
|
||||
.withConstantFeature(AuthorIsOffboarded, authorIsOffboarded(author))
|
||||
.withConstantFeature(AuthorTakedownReasons, authorTakedownReasons(author))
|
||||
.withConstantFeature(AuthorHasDefaultProfileImage, authorHasDefaultProfileImage(author))
|
||||
.withConstantFeature(AuthorAccountAge, authorAccountAge(author))
|
||||
.withConstantFeature(AuthorIsNsfwUser, authorIsNsfwUser(author))
|
||||
.withConstantFeature(AuthorIsNsfwAdmin, authorIsNsfwAdmin(author))
|
||||
.withConstantFeature(AuthorIsVerified, authorIsVerified(author))
|
||||
.withConstantFeature(AuthorScreenName, authorScreenName(author))
|
||||
.withConstantFeature(AuthorIsBlueVerified, authorIsBlueVerified(author))
|
||||
}
|
||||
|
||||
def forAuthorNoDefaults(author: User): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
requests.incr()
|
||||
|
||||
_.withConstantFeature(AuthorId, Set(author.id))
|
||||
.withConstantFeature(AuthorUserLabels, authorUserLabelsOpt(author))
|
||||
.withConstantFeature(AuthorIsProtected, authorIsProtectedOpt(author))
|
||||
.withConstantFeature(AuthorIsSuspended, authorIsSuspendedOpt(author))
|
||||
.withConstantFeature(AuthorIsDeactivated, authorIsDeactivatedOpt(author))
|
||||
.withConstantFeature(AuthorIsErased, authorIsErasedOpt(author))
|
||||
.withConstantFeature(AuthorIsOffboarded, authorIsOffboarded(author))
|
||||
.withConstantFeature(AuthorTakedownReasons, authorTakedownReasons(author))
|
||||
.withConstantFeature(AuthorHasDefaultProfileImage, authorHasDefaultProfileImage(author))
|
||||
.withConstantFeature(AuthorAccountAge, authorAccountAge(author))
|
||||
.withConstantFeature(AuthorIsNsfwUser, authorIsNsfwUserOpt(author))
|
||||
.withConstantFeature(AuthorIsNsfwAdmin, authorIsNsfwAdminOpt(author))
|
||||
.withConstantFeature(AuthorIsVerified, authorIsVerifiedOpt(author))
|
||||
.withConstantFeature(AuthorScreenName, authorScreenName(author))
|
||||
.withConstantFeature(AuthorIsBlueVerified, authorIsBlueVerified(author))
|
||||
}
|
||||
|
||||
def forAuthorId(authorId: Long): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
requests.incr()
|
||||
|
||||
_.withConstantFeature(AuthorId, Set(authorId))
|
||||
.withFeature(AuthorUserLabels, authorUserLabels(authorId))
|
||||
.withFeature(AuthorIsProtected, authorIsProtected(authorId))
|
||||
.withFeature(AuthorIsSuspended, authorIsSuspended(authorId))
|
||||
.withFeature(AuthorIsDeactivated, authorIsDeactivated(authorId))
|
||||
.withFeature(AuthorIsErased, authorIsErased(authorId))
|
||||
.withFeature(AuthorIsOffboarded, authorIsOffboarded(authorId))
|
||||
.withFeature(AuthorTakedownReasons, authorTakedownReasons(authorId))
|
||||
.withFeature(AuthorHasDefaultProfileImage, authorHasDefaultProfileImage(authorId))
|
||||
.withFeature(AuthorAccountAge, authorAccountAge(authorId))
|
||||
.withFeature(AuthorIsNsfwUser, authorIsNsfwUser(authorId))
|
||||
.withFeature(AuthorIsNsfwAdmin, authorIsNsfwAdmin(authorId))
|
||||
.withFeature(AuthorIsVerified, authorIsVerified(authorId))
|
||||
.withFeature(AuthorScreenName, authorScreenName(authorId))
|
||||
.withFeature(AuthorIsBlueVerified, authorIsBlueVerified(authorId))
|
||||
}
|
||||
|
||||
def forNoAuthor(): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
_.withConstantFeature(AuthorId, Set.empty[Long])
|
||||
.withConstantFeature(AuthorUserLabels, Seq.empty)
|
||||
.withConstantFeature(AuthorIsProtected, false)
|
||||
.withConstantFeature(AuthorIsSuspended, false)
|
||||
.withConstantFeature(AuthorIsDeactivated, false)
|
||||
.withConstantFeature(AuthorIsErased, false)
|
||||
.withConstantFeature(AuthorIsOffboarded, false)
|
||||
.withConstantFeature(AuthorTakedownReasons, Seq.empty)
|
||||
.withConstantFeature(AuthorHasDefaultProfileImage, false)
|
||||
.withConstantFeature(AuthorAccountAge, Duration.Zero)
|
||||
.withConstantFeature(AuthorIsNsfwUser, false)
|
||||
.withConstantFeature(AuthorIsNsfwAdmin, false)
|
||||
.withConstantFeature(AuthorIsVerified, false)
|
||||
.withConstantFeature(AuthorIsBlueVerified, false)
|
||||
}
|
||||
|
||||
def authorUserLabels(author: User): Seq[Label] =
|
||||
authorUserLabels(author.labels)
|
||||
|
||||
def authorIsSuspended(authorId: Long): Stitch[Boolean] =
|
||||
userSource.getSafety(authorId).map(safety => authorIsSuspended(Some(safety)))
|
||||
|
||||
def authorIsSuspendedOpt(author: User): Option[Boolean] = {
|
||||
authorIsSuspended.incr()
|
||||
author.safety.map(_.suspended)
|
||||
}
|
||||
|
||||
private def authorIsSuspended(safety: Option[Safety]): Boolean = {
|
||||
authorIsSuspended.incr()
|
||||
safety.exists(_.suspended)
|
||||
}
|
||||
|
||||
def authorIsProtected(author: User): Boolean =
|
||||
authorIsProtected(author.safety)
|
||||
|
||||
def authorIsDeactivated(authorId: Long): Stitch[Boolean] =
|
||||
userSource.getSafety(authorId).map(safety => authorIsDeactivated(Some(safety)))
|
||||
|
||||
def authorIsDeactivatedOpt(author: User): Option[Boolean] = {
|
||||
authorIsDeactivated.incr()
|
||||
author.safety.map(_.deactivated)
|
||||
}
|
||||
|
||||
private def authorIsDeactivated(safety: Option[Safety]): Boolean = {
|
||||
authorIsDeactivated.incr()
|
||||
safety.exists(_.deactivated)
|
||||
}
|
||||
|
||||
def authorIsErased(author: User): Boolean = {
|
||||
authorIsErased.incr()
|
||||
author.safety.exists(_.erased)
|
||||
}
|
||||
|
||||
def authorIsOffboarded(authorId: Long): Stitch[Boolean] = {
|
||||
userSource.getSafety(authorId).map(safety => authorIsOffboarded(Some(safety)))
|
||||
}
|
||||
|
||||
def authorIsNsfwUser(author: User): Boolean = {
|
||||
authorIsNsfwUser(author.safety)
|
||||
}
|
||||
|
||||
def authorIsNsfwUser(authorId: Long): Stitch[Boolean] = {
|
||||
userSource.getSafety(authorId).map(safety => authorIsNsfwUser(Some(safety)))
|
||||
}
|
||||
|
||||
def authorIsNsfwUser(safety: Option[Safety]): Boolean = {
|
||||
authorIsNsfwUser.incr()
|
||||
safety.exists(_.nsfwUser)
|
||||
}
|
||||
|
||||
def authorIsNsfwAdminOpt(author: User): Option[Boolean] = {
|
||||
authorIsNsfwAdmin.incr()
|
||||
author.safety.map(_.nsfwAdmin)
|
||||
}
|
||||
|
||||
def authorTakedownReasons(authorId: Long): Stitch[Seq[TakedownReason]] = {
|
||||
authorTakedownReasons.incr()
|
||||
userSource.getTakedownReasons(authorId)
|
||||
}
|
||||
|
||||
def authorHasDefaultProfileImage(authorId: Long): Stitch[Boolean] =
|
||||
userSource.getProfile(authorId).map(profile => authorHasDefaultProfileImage(Some(profile)))
|
||||
|
||||
def authorAccountAge(authorId: Long): Stitch[Duration] =
|
||||
userSource.getCreatedAtMsec(authorId).map(authorAccountAgeFromTimestamp)
|
||||
|
||||
def authorIsVerified(authorId: Long): Stitch[Boolean] =
|
||||
userSource.getSafety(authorId).map(safety => authorIsVerified(Some(safety)))
|
||||
|
||||
def authorIsVerifiedOpt(author: User): Option[Boolean] = {
|
||||
authorIsVerified.incr()
|
||||
author.safety.map(_.verified)
|
||||
}
|
||||
|
||||
private def authorIsVerified(safety: Option[Safety]): Boolean = {
|
||||
authorIsVerified.incr()
|
||||
safety.exists(_.verified)
|
||||
}
|
||||
|
||||
def authorScreenName(author: User): Option[String] = {
|
||||
authorScreenName.incr()
|
||||
author.profile.map(_.screenName)
|
||||
}
|
||||
|
||||
def authorScreenName(authorId: Long): Stitch[String] = {
|
||||
authorScreenName.incr()
|
||||
userSource.getProfile(authorId).map(profile => profile.screenName)
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
scala_library(
|
||||
sources = ["*.scala"],
|
||||
compiler_option_sets = ["fatal_warnings"],
|
||||
platform = "java8",
|
||||
strict_deps = True,
|
||||
tags = ["bazel-compatible"],
|
||||
dependencies = [
|
||||
"src/scala/com/twitter/search/blender/services/strato",
|
||||
"src/thrift/com/twitter/content-health/sensitivemediasettings:sensitivemediasettings-scala",
|
||||
"src/thrift/com/twitter/gizmoduck:user-thrift-scala",
|
||||
"stitch/stitch-core",
|
||||
"visibility/common/src/main/scala/com/twitter/visibility/common",
|
||||
"visibility/common/src/main/scala/com/twitter/visibility/common/user_result",
|
||||
"visibility/common/src/main/thrift/com/twitter/visibility:action-scala",
|
||||
"visibility/lib/src/main/resources/config",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/features",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/interfaces/common/blender",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/interfaces/common/search",
|
||||
"visibility/lib/src/main/thrift/com/twitter/visibility/context:vf-context-scala",
|
||||
],
|
||||
)
|
@ -0,0 +1,52 @@
|
||||
package com.twitter.visibility.builder.users
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.features.AuthorBlocksOuterAuthor
|
||||
import com.twitter.visibility.features.OuterAuthorFollowsAuthor
|
||||
import com.twitter.visibility.features.OuterAuthorId
|
||||
import com.twitter.visibility.features.OuterAuthorIsInnerAuthor
|
||||
|
||||
class QuotedTweetFeatures(
|
||||
relationshipFeatures: RelationshipFeatures,
|
||||
statsReceiver: StatsReceiver) {
|
||||
|
||||
private[this] val scopedStatsReceiver = statsReceiver.scope("quoted_tweet_features")
|
||||
|
||||
private[this] val requests = scopedStatsReceiver.counter("requests")
|
||||
|
||||
private[this] val outerAuthorIdStat =
|
||||
scopedStatsReceiver.scope(OuterAuthorId.name).counter("requests")
|
||||
private[this] val authorBlocksOuterAuthor =
|
||||
scopedStatsReceiver.scope(AuthorBlocksOuterAuthor.name).counter("requests")
|
||||
private[this] val outerAuthorFollowsAuthor =
|
||||
scopedStatsReceiver.scope(OuterAuthorFollowsAuthor.name).counter("requests")
|
||||
private[this] val outerAuthorIsInnerAuthor =
|
||||
scopedStatsReceiver.scope(OuterAuthorIsInnerAuthor.name).counter("requests")
|
||||
|
||||
def forOuterAuthor(
|
||||
outerAuthorId: Long,
|
||||
innerAuthorId: Long
|
||||
): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
requests.incr()
|
||||
|
||||
outerAuthorIdStat.incr()
|
||||
authorBlocksOuterAuthor.incr()
|
||||
outerAuthorFollowsAuthor.incr()
|
||||
outerAuthorIsInnerAuthor.incr()
|
||||
|
||||
val viewer = Some(outerAuthorId)
|
||||
|
||||
_.withConstantFeature(OuterAuthorId, outerAuthorId)
|
||||
.withFeature(
|
||||
AuthorBlocksOuterAuthor,
|
||||
relationshipFeatures.authorBlocksViewer(innerAuthorId, viewer))
|
||||
.withFeature(
|
||||
OuterAuthorFollowsAuthor,
|
||||
relationshipFeatures.viewerFollowsAuthor(innerAuthorId, viewer))
|
||||
.withConstantFeature(
|
||||
OuterAuthorIsInnerAuthor,
|
||||
innerAuthorId == outerAuthorId
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,176 @@
|
||||
package com.twitter.visibility.builder.users
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.gizmoduck.thriftscala.User
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.common.UserId
|
||||
import com.twitter.visibility.common.UserRelationshipSource
|
||||
import com.twitter.visibility.features._
|
||||
|
||||
class RelationshipFeatures(
|
||||
userRelationshipSource: UserRelationshipSource,
|
||||
statsReceiver: StatsReceiver) {
|
||||
|
||||
private[this] val scopedStatsReceiver = statsReceiver.scope("relationship_features")
|
||||
|
||||
private[this] val requests = scopedStatsReceiver.counter("requests")
|
||||
|
||||
private[this] val authorFollowsViewer =
|
||||
scopedStatsReceiver.scope(AuthorFollowsViewer.name).counter("requests")
|
||||
private[this] val viewerFollowsAuthor =
|
||||
scopedStatsReceiver.scope(ViewerFollowsAuthor.name).counter("requests")
|
||||
private[this] val authorBlocksViewer =
|
||||
scopedStatsReceiver.scope(AuthorBlocksViewer.name).counter("requests")
|
||||
private[this] val viewerBlocksAuthor =
|
||||
scopedStatsReceiver.scope(ViewerBlocksAuthor.name).counter("requests")
|
||||
private[this] val authorMutesViewer =
|
||||
scopedStatsReceiver.scope(AuthorMutesViewer.name).counter("requests")
|
||||
private[this] val viewerMutesAuthor =
|
||||
scopedStatsReceiver.scope(ViewerMutesAuthor.name).counter("requests")
|
||||
private[this] val authorHasReportedViewer =
|
||||
scopedStatsReceiver.scope(AuthorReportsViewerAsSpam.name).counter("requests")
|
||||
private[this] val viewerHasReportedAuthor =
|
||||
scopedStatsReceiver.scope(ViewerReportsAuthorAsSpam.name).counter("requests")
|
||||
private[this] val viewerMutesRetweetsFromAuthor =
|
||||
scopedStatsReceiver.scope(ViewerMutesRetweetsFromAuthor.name).counter("requests")
|
||||
|
||||
def forAuthorId(
|
||||
authorId: Long,
|
||||
viewerId: Option[Long]
|
||||
): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
requests.incr()
|
||||
|
||||
_.withFeature(AuthorFollowsViewer, authorFollowsViewer(authorId, viewerId))
|
||||
.withFeature(ViewerFollowsAuthor, viewerFollowsAuthor(authorId, viewerId))
|
||||
.withFeature(AuthorBlocksViewer, authorBlocksViewer(authorId, viewerId))
|
||||
.withFeature(ViewerBlocksAuthor, viewerBlocksAuthor(authorId, viewerId))
|
||||
.withFeature(AuthorMutesViewer, authorMutesViewer(authorId, viewerId))
|
||||
.withFeature(ViewerMutesAuthor, viewerMutesAuthor(authorId, viewerId))
|
||||
.withFeature(AuthorReportsViewerAsSpam, authorHasReportedViewer(authorId, viewerId))
|
||||
.withFeature(ViewerReportsAuthorAsSpam, viewerHasReportedAuthor(authorId, viewerId))
|
||||
.withFeature(ViewerMutesRetweetsFromAuthor, viewerMutesRetweetsFromAuthor(authorId, viewerId))
|
||||
}
|
||||
|
||||
def forNoAuthor(): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
_.withConstantFeature(AuthorFollowsViewer, false)
|
||||
.withConstantFeature(ViewerFollowsAuthor, false)
|
||||
.withConstantFeature(AuthorBlocksViewer, false)
|
||||
.withConstantFeature(ViewerBlocksAuthor, false)
|
||||
.withConstantFeature(AuthorMutesViewer, false)
|
||||
.withConstantFeature(ViewerMutesAuthor, false)
|
||||
.withConstantFeature(AuthorReportsViewerAsSpam, false)
|
||||
.withConstantFeature(ViewerReportsAuthorAsSpam, false)
|
||||
.withConstantFeature(ViewerMutesRetweetsFromAuthor, false)
|
||||
}
|
||||
|
||||
def forAuthor(author: User, viewerId: Option[Long]): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
requests.incr()
|
||||
|
||||
|
||||
_.withFeature(AuthorFollowsViewer, authorFollowsViewer(author, viewerId))
|
||||
.withFeature(ViewerFollowsAuthor, viewerFollowsAuthor(author, viewerId))
|
||||
.withFeature(AuthorBlocksViewer, authorBlocksViewer(author, viewerId))
|
||||
.withFeature(ViewerBlocksAuthor, viewerBlocksAuthor(author, viewerId))
|
||||
.withFeature(AuthorMutesViewer, authorMutesViewer(author, viewerId))
|
||||
.withFeature(ViewerMutesAuthor, viewerMutesAuthor(author, viewerId))
|
||||
.withFeature(AuthorReportsViewerAsSpam, authorHasReportedViewer(author.id, viewerId))
|
||||
.withFeature(ViewerReportsAuthorAsSpam, viewerHasReportedAuthor(author.id, viewerId))
|
||||
.withFeature(ViewerMutesRetweetsFromAuthor, viewerMutesRetweetsFromAuthor(author, viewerId))
|
||||
}
|
||||
|
||||
def viewerFollowsAuthor(authorId: UserId, viewerId: Option[UserId]): Stitch[Boolean] =
|
||||
ViewerVerbsAuthor(authorId, viewerId, userRelationshipSource.follows, viewerFollowsAuthor)
|
||||
|
||||
def viewerFollowsAuthor(author: User, viewerId: Option[UserId]): Stitch[Boolean] =
|
||||
ViewerVerbsAuthor(
|
||||
author,
|
||||
viewerId,
|
||||
p => p.following,
|
||||
userRelationshipSource.follows,
|
||||
viewerFollowsAuthor)
|
||||
|
||||
def authorFollowsViewer(authorId: UserId, viewerId: Option[UserId]): Stitch[Boolean] =
|
||||
AuthorVerbsViewer(authorId, viewerId, userRelationshipSource.follows, authorFollowsViewer)
|
||||
|
||||
def authorFollowsViewer(author: User, viewerId: Option[UserId]): Stitch[Boolean] =
|
||||
AuthorVerbsViewer(
|
||||
author,
|
||||
viewerId,
|
||||
p => p.followedBy,
|
||||
userRelationshipSource.follows,
|
||||
authorFollowsViewer)
|
||||
|
||||
def viewerBlocksAuthor(authorId: UserId, viewerId: Option[UserId]): Stitch[Boolean] =
|
||||
ViewerVerbsAuthor(authorId, viewerId, userRelationshipSource.blocks, viewerBlocksAuthor)
|
||||
|
||||
def viewerBlocksAuthor(author: User, viewerId: Option[UserId]): Stitch[Boolean] =
|
||||
ViewerVerbsAuthor(
|
||||
author,
|
||||
viewerId,
|
||||
p => p.blocking,
|
||||
userRelationshipSource.blocks,
|
||||
viewerBlocksAuthor)
|
||||
|
||||
def authorBlocksViewer(authorId: UserId, viewerId: Option[UserId]): Stitch[Boolean] =
|
||||
ViewerVerbsAuthor(authorId, viewerId, userRelationshipSource.blockedBy, authorBlocksViewer)
|
||||
|
||||
def authorBlocksViewer(author: User, viewerId: Option[UserId]): Stitch[Boolean] =
|
||||
ViewerVerbsAuthor(
|
||||
author,
|
||||
viewerId,
|
||||
p => p.blockedBy,
|
||||
userRelationshipSource.blockedBy,
|
||||
authorBlocksViewer)
|
||||
|
||||
def viewerMutesAuthor(authorId: UserId, viewerId: Option[UserId]): Stitch[Boolean] =
|
||||
ViewerVerbsAuthor(authorId, viewerId, userRelationshipSource.mutes, viewerMutesAuthor)
|
||||
|
||||
def viewerMutesAuthor(author: User, viewerId: Option[UserId]): Stitch[Boolean] =
|
||||
ViewerVerbsAuthor(
|
||||
author,
|
||||
viewerId,
|
||||
p => p.muting,
|
||||
userRelationshipSource.mutes,
|
||||
viewerMutesAuthor)
|
||||
|
||||
def authorMutesViewer(authorId: UserId, viewerId: Option[UserId]): Stitch[Boolean] =
|
||||
ViewerVerbsAuthor(authorId, viewerId, userRelationshipSource.mutedBy, authorMutesViewer)
|
||||
|
||||
def authorMutesViewer(author: User, viewerId: Option[UserId]): Stitch[Boolean] =
|
||||
ViewerVerbsAuthor(
|
||||
author,
|
||||
viewerId,
|
||||
p => p.mutedBy,
|
||||
userRelationshipSource.mutedBy,
|
||||
authorMutesViewer)
|
||||
|
||||
def viewerHasReportedAuthor(authorId: UserId, viewerId: Option[UserId]): Stitch[Boolean] =
|
||||
ViewerVerbsAuthor(
|
||||
authorId,
|
||||
viewerId,
|
||||
userRelationshipSource.reportsAsSpam,
|
||||
viewerHasReportedAuthor)
|
||||
|
||||
def authorHasReportedViewer(authorId: UserId, viewerId: Option[UserId]): Stitch[Boolean] =
|
||||
ViewerVerbsAuthor(
|
||||
authorId,
|
||||
viewerId,
|
||||
userRelationshipSource.reportedAsSpamBy,
|
||||
authorHasReportedViewer)
|
||||
|
||||
def viewerMutesRetweetsFromAuthor(authorId: UserId, viewerId: Option[UserId]): Stitch[Boolean] =
|
||||
ViewerVerbsAuthor(
|
||||
authorId,
|
||||
viewerId,
|
||||
userRelationshipSource.noRetweetsFrom,
|
||||
viewerMutesRetweetsFromAuthor)
|
||||
|
||||
def viewerMutesRetweetsFromAuthor(author: User, viewerId: Option[UserId]): Stitch[Boolean] =
|
||||
ViewerVerbsAuthor(
|
||||
author,
|
||||
viewerId,
|
||||
p => p.noRetweetsFrom,
|
||||
userRelationshipSource.noRetweetsFrom,
|
||||
viewerMutesRetweetsFromAuthor)
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package com.twitter.visibility.builder.users
|
||||
|
||||
import com.twitter.finagle.stats.Counter
|
||||
import com.twitter.gizmoduck.thriftscala.Perspective
|
||||
import com.twitter.gizmoduck.thriftscala.User
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.visibility.common.UserId
|
||||
|
||||
case object ViewerVerbsAuthor {
|
||||
def apply(
|
||||
authorId: UserId,
|
||||
viewerIdOpt: Option[UserId],
|
||||
relationship: (UserId, UserId) => Stitch[Boolean],
|
||||
relationshipCounter: Counter
|
||||
): Stitch[Boolean] = {
|
||||
relationshipCounter.incr()
|
||||
|
||||
viewerIdOpt match {
|
||||
case Some(viewerId) => relationship(viewerId, authorId)
|
||||
case _ => Stitch.False
|
||||
}
|
||||
}
|
||||
|
||||
def apply(
|
||||
author: User,
|
||||
viewerId: Option[UserId],
|
||||
checkPerspective: Perspective => Option[Boolean],
|
||||
relationship: (UserId, UserId) => Stitch[Boolean],
|
||||
relationshipCounter: Counter
|
||||
): Stitch[Boolean] = {
|
||||
author.perspective match {
|
||||
case Some(perspective) =>
|
||||
checkPerspective(perspective) match {
|
||||
case Some(status) =>
|
||||
relationshipCounter.incr()
|
||||
Stitch.value(status)
|
||||
case None =>
|
||||
ViewerVerbsAuthor(author.id, viewerId, relationship, relationshipCounter)
|
||||
}
|
||||
case None => ViewerVerbsAuthor(author.id, viewerId, relationship, relationshipCounter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case object AuthorVerbsViewer {
|
||||
|
||||
def apply(
|
||||
authorId: UserId,
|
||||
viewerIdOpt: Option[UserId],
|
||||
relationship: (UserId, UserId) => Stitch[Boolean],
|
||||
relationshipCounter: Counter
|
||||
): Stitch[Boolean] = {
|
||||
relationshipCounter.incr()
|
||||
|
||||
viewerIdOpt match {
|
||||
case Some(viewerId) => relationship(authorId, viewerId)
|
||||
case _ => Stitch.False
|
||||
}
|
||||
}
|
||||
def apply(
|
||||
author: User,
|
||||
viewerId: Option[UserId],
|
||||
checkPerspective: Perspective => Option[Boolean],
|
||||
relationship: (UserId, UserId) => Stitch[Boolean],
|
||||
relationshipCounter: Counter
|
||||
): Stitch[Boolean] = {
|
||||
author.perspective match {
|
||||
case Some(perspective) =>
|
||||
checkPerspective(perspective) match {
|
||||
case Some(status) =>
|
||||
relationshipCounter.incr()
|
||||
Stitch.value(status)
|
||||
case None =>
|
||||
AuthorVerbsViewer(author.id, viewerId, relationship, relationshipCounter)
|
||||
}
|
||||
case None => AuthorVerbsViewer(author.id, viewerId, relationship, relationshipCounter)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package com.twitter.visibility.builder.users
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.features._
|
||||
import com.twitter.visibility.context.thriftscala.SearchContext
|
||||
|
||||
class SearchFeatures(statsReceiver: StatsReceiver) {
|
||||
private[this] val scopedStatsReceiver = statsReceiver.scope("search_features")
|
||||
private[this] val requests = scopedStatsReceiver.counter("requests")
|
||||
private[this] val rawQueryCounter =
|
||||
scopedStatsReceiver.scope(RawQuery.name).counter("requests")
|
||||
|
||||
def forSearchContext(
|
||||
searchContext: Option[SearchContext]
|
||||
): FeatureMapBuilder => FeatureMapBuilder = { builder =>
|
||||
requests.incr()
|
||||
searchContext match {
|
||||
case Some(context: SearchContext) =>
|
||||
rawQueryCounter.incr()
|
||||
builder
|
||||
.withConstantFeature(RawQuery, context.rawQuery)
|
||||
case _ => builder
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
package com.twitter.visibility.builder.users
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.common.user_result.UserVisibilityResultHelper
|
||||
import com.twitter.visibility.features.AuthorBlocksViewer
|
||||
import com.twitter.visibility.features.AuthorIsDeactivated
|
||||
import com.twitter.visibility.features.AuthorIsErased
|
||||
import com.twitter.visibility.features.AuthorIsOffboarded
|
||||
import com.twitter.visibility.features.AuthorIsProtected
|
||||
import com.twitter.visibility.features.AuthorIsSuspended
|
||||
import com.twitter.visibility.features.AuthorIsUnavailable
|
||||
import com.twitter.visibility.features.ViewerBlocksAuthor
|
||||
import com.twitter.visibility.features.ViewerMutesAuthor
|
||||
import com.twitter.visibility.models.UserUnavailableStateEnum
|
||||
|
||||
case class UserUnavailableFeatures(statsReceiver: StatsReceiver) {
|
||||
|
||||
private[this] val scopedStatsReceiver = statsReceiver.scope("user_unavailable_features")
|
||||
private[this] val suspendedAuthorStats = scopedStatsReceiver.scope("suspended_author")
|
||||
private[this] val deactivatedAuthorStats = scopedStatsReceiver.scope("deactivated_author")
|
||||
private[this] val offboardedAuthorStats = scopedStatsReceiver.scope("offboarded_author")
|
||||
private[this] val erasedAuthorStats = scopedStatsReceiver.scope("erased_author")
|
||||
private[this] val protectedAuthorStats = scopedStatsReceiver.scope("protected_author")
|
||||
private[this] val authorBlocksViewerStats = scopedStatsReceiver.scope("author_blocks_viewer")
|
||||
private[this] val viewerBlocksAuthorStats = scopedStatsReceiver.scope("viewer_blocks_author")
|
||||
private[this] val viewerMutesAuthorStats = scopedStatsReceiver.scope("viewer_mutes_author")
|
||||
private[this] val unavailableStats = scopedStatsReceiver.scope("unavailable")
|
||||
|
||||
def forState(state: UserUnavailableStateEnum): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
builder =>
|
||||
builder
|
||||
.withConstantFeature(AuthorIsSuspended, isSuspended(state))
|
||||
.withConstantFeature(AuthorIsDeactivated, isDeactivated(state))
|
||||
.withConstantFeature(AuthorIsOffboarded, isOffboarded(state))
|
||||
.withConstantFeature(AuthorIsErased, isErased(state))
|
||||
.withConstantFeature(AuthorIsProtected, isProtected(state))
|
||||
.withConstantFeature(AuthorBlocksViewer, authorBlocksViewer(state))
|
||||
.withConstantFeature(ViewerBlocksAuthor, viewerBlocksAuthor(state))
|
||||
.withConstantFeature(ViewerMutesAuthor, viewerMutesAuthor(state))
|
||||
.withConstantFeature(AuthorIsUnavailable, isUnavailable(state))
|
||||
}
|
||||
|
||||
private[this] def isSuspended(state: UserUnavailableStateEnum): Boolean =
|
||||
state match {
|
||||
case UserUnavailableStateEnum.Suspended =>
|
||||
suspendedAuthorStats.counter().incr()
|
||||
true
|
||||
case UserUnavailableStateEnum.Filtered(result)
|
||||
if UserVisibilityResultHelper.isDropSuspendedAuthor(result) =>
|
||||
suspendedAuthorStats.counter().incr()
|
||||
suspendedAuthorStats.counter("filtered").incr()
|
||||
true
|
||||
case _ => false
|
||||
}
|
||||
|
||||
private[this] def isDeactivated(state: UserUnavailableStateEnum): Boolean =
|
||||
state match {
|
||||
case UserUnavailableStateEnum.Deactivated =>
|
||||
deactivatedAuthorStats.counter().incr()
|
||||
true
|
||||
case _ => false
|
||||
}
|
||||
|
||||
private[this] def isOffboarded(state: UserUnavailableStateEnum): Boolean =
|
||||
state match {
|
||||
case UserUnavailableStateEnum.Offboarded =>
|
||||
offboardedAuthorStats.counter().incr()
|
||||
true
|
||||
case _ => false
|
||||
}
|
||||
|
||||
private[this] def isErased(state: UserUnavailableStateEnum): Boolean =
|
||||
state match {
|
||||
case UserUnavailableStateEnum.Erased =>
|
||||
erasedAuthorStats.counter().incr()
|
||||
true
|
||||
case _ => false
|
||||
}
|
||||
|
||||
private[this] def isProtected(state: UserUnavailableStateEnum): Boolean =
|
||||
state match {
|
||||
case UserUnavailableStateEnum.Protected =>
|
||||
protectedAuthorStats.counter().incr()
|
||||
true
|
||||
case UserUnavailableStateEnum.Filtered(result)
|
||||
if UserVisibilityResultHelper.isDropProtectedAuthor(result) =>
|
||||
protectedAuthorStats.counter().incr()
|
||||
protectedAuthorStats.counter("filtered").incr()
|
||||
true
|
||||
case _ => false
|
||||
}
|
||||
|
||||
private[this] def authorBlocksViewer(state: UserUnavailableStateEnum): Boolean =
|
||||
state match {
|
||||
case UserUnavailableStateEnum.AuthorBlocksViewer =>
|
||||
authorBlocksViewerStats.counter().incr()
|
||||
true
|
||||
case UserUnavailableStateEnum.Filtered(result)
|
||||
if UserVisibilityResultHelper.isDropAuthorBlocksViewer(result) =>
|
||||
authorBlocksViewerStats.counter().incr()
|
||||
authorBlocksViewerStats.counter("filtered").incr()
|
||||
true
|
||||
case _ => false
|
||||
}
|
||||
|
||||
private[this] def viewerBlocksAuthor(state: UserUnavailableStateEnum): Boolean =
|
||||
state match {
|
||||
case UserUnavailableStateEnum.ViewerBlocksAuthor =>
|
||||
viewerBlocksAuthorStats.counter().incr()
|
||||
true
|
||||
case UserUnavailableStateEnum.Filtered(result)
|
||||
if UserVisibilityResultHelper.isDropViewerBlocksAuthor(result) =>
|
||||
viewerBlocksAuthorStats.counter().incr()
|
||||
viewerBlocksAuthorStats.counter("filtered").incr()
|
||||
true
|
||||
case _ => false
|
||||
}
|
||||
|
||||
private[this] def viewerMutesAuthor(state: UserUnavailableStateEnum): Boolean =
|
||||
state match {
|
||||
case UserUnavailableStateEnum.ViewerMutesAuthor =>
|
||||
viewerMutesAuthorStats.counter().incr()
|
||||
true
|
||||
case UserUnavailableStateEnum.Filtered(result)
|
||||
if UserVisibilityResultHelper.isDropViewerMutesAuthor(result) =>
|
||||
viewerMutesAuthorStats.counter().incr()
|
||||
viewerMutesAuthorStats.counter("filtered").incr()
|
||||
true
|
||||
case _ => false
|
||||
}
|
||||
|
||||
private[this] def isUnavailable(state: UserUnavailableStateEnum): Boolean =
|
||||
state match {
|
||||
case UserUnavailableStateEnum.Unavailable =>
|
||||
unavailableStats.counter().incr()
|
||||
true
|
||||
case UserUnavailableStateEnum.Filtered(result)
|
||||
if UserVisibilityResultHelper.isDropUnspecifiedAuthor(result) =>
|
||||
unavailableStats.counter().incr()
|
||||
unavailableStats.counter("filtered").incr()
|
||||
true
|
||||
case _ => false
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
package com.twitter.visibility.builder.users
|
||||
|
||||
import com.twitter.finagle.stats.Counter
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.gizmoduck.thriftscala.AdvancedFilters
|
||||
import com.twitter.gizmoduck.thriftscala.MentionFilter
|
||||
import com.twitter.stitch.NotFound
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.common.UserSource
|
||||
import com.twitter.visibility.features.ViewerFiltersDefaultProfileImage
|
||||
import com.twitter.visibility.features.ViewerFiltersNewUsers
|
||||
import com.twitter.visibility.features.ViewerFiltersNoConfirmedEmail
|
||||
import com.twitter.visibility.features.ViewerFiltersNoConfirmedPhone
|
||||
import com.twitter.visibility.features.ViewerFiltersNotFollowedBy
|
||||
import com.twitter.visibility.features.ViewerMentionFilter
|
||||
|
||||
class ViewerAdvancedFilteringFeatures(userSource: UserSource, statsReceiver: StatsReceiver) {
|
||||
private[this] val scopedStatsReceiver = statsReceiver.scope("viewer_advanced_filtering_features")
|
||||
|
||||
private[this] val requests = scopedStatsReceiver.counter("requests")
|
||||
|
||||
private[this] val viewerFiltersNoConfirmedEmail =
|
||||
scopedStatsReceiver.scope(ViewerFiltersNoConfirmedEmail.name).counter("requests")
|
||||
private[this] val viewerFiltersNoConfirmedPhone =
|
||||
scopedStatsReceiver.scope(ViewerFiltersNoConfirmedPhone.name).counter("requests")
|
||||
private[this] val viewerFiltersDefaultProfileImage =
|
||||
scopedStatsReceiver.scope(ViewerFiltersDefaultProfileImage.name).counter("requests")
|
||||
private[this] val viewerFiltersNewUsers =
|
||||
scopedStatsReceiver.scope(ViewerFiltersNewUsers.name).counter("requests")
|
||||
private[this] val viewerFiltersNotFollowedBy =
|
||||
scopedStatsReceiver.scope(ViewerFiltersNotFollowedBy.name).counter("requests")
|
||||
private[this] val viewerMentionFilter =
|
||||
scopedStatsReceiver.scope(ViewerMentionFilter.name).counter("requests")
|
||||
|
||||
def forViewerId(viewerId: Option[Long]): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
requests.incr()
|
||||
|
||||
_.withFeature(ViewerFiltersNoConfirmedEmail, viewerFiltersNoConfirmedEmail(viewerId))
|
||||
.withFeature(ViewerFiltersNoConfirmedPhone, viewerFiltersNoConfirmedPhone(viewerId))
|
||||
.withFeature(ViewerFiltersDefaultProfileImage, viewerFiltersDefaultProfileImage(viewerId))
|
||||
.withFeature(ViewerFiltersNewUsers, viewerFiltersNewUsers(viewerId))
|
||||
.withFeature(ViewerFiltersNotFollowedBy, viewerFiltersNotFollowedBy(viewerId))
|
||||
.withFeature(ViewerMentionFilter, viewerMentionFilter(viewerId))
|
||||
}
|
||||
|
||||
def viewerFiltersNoConfirmedEmail(viewerId: Option[Long]): Stitch[Boolean] =
|
||||
viewerAdvancedFilters(viewerId, af => af.filterNoConfirmedEmail, viewerFiltersNoConfirmedEmail)
|
||||
|
||||
def viewerFiltersNoConfirmedPhone(viewerId: Option[Long]): Stitch[Boolean] =
|
||||
viewerAdvancedFilters(viewerId, af => af.filterNoConfirmedPhone, viewerFiltersNoConfirmedPhone)
|
||||
|
||||
def viewerFiltersDefaultProfileImage(viewerId: Option[Long]): Stitch[Boolean] =
|
||||
viewerAdvancedFilters(
|
||||
viewerId,
|
||||
af => af.filterDefaultProfileImage,
|
||||
viewerFiltersDefaultProfileImage
|
||||
)
|
||||
|
||||
def viewerFiltersNewUsers(viewerId: Option[Long]): Stitch[Boolean] =
|
||||
viewerAdvancedFilters(viewerId, af => af.filterNewUsers, viewerFiltersNewUsers)
|
||||
|
||||
def viewerFiltersNotFollowedBy(viewerId: Option[Long]): Stitch[Boolean] =
|
||||
viewerAdvancedFilters(viewerId, af => af.filterNotFollowedBy, viewerFiltersNotFollowedBy)
|
||||
|
||||
def viewerMentionFilter(viewerId: Option[Long]): Stitch[MentionFilter] = {
|
||||
viewerMentionFilter.incr()
|
||||
viewerId match {
|
||||
case Some(id) =>
|
||||
userSource.getMentionFilter(id).handle {
|
||||
case NotFound =>
|
||||
MentionFilter.Unfiltered
|
||||
}
|
||||
case _ => Stitch.value(MentionFilter.Unfiltered)
|
||||
}
|
||||
}
|
||||
|
||||
private[this] def viewerAdvancedFilters(
|
||||
viewerId: Option[Long],
|
||||
advancedFilterCheck: AdvancedFilters => Boolean,
|
||||
featureCounter: Counter
|
||||
): Stitch[Boolean] = {
|
||||
featureCounter.incr()
|
||||
|
||||
val advancedFilters = viewerId match {
|
||||
case Some(id) => userSource.getAdvancedFilters(id)
|
||||
case _ => Stitch.value(AdvancedFilters())
|
||||
}
|
||||
|
||||
advancedFilters.map(advancedFilterCheck)
|
||||
}
|
||||
}
|
@ -0,0 +1,245 @@
|
||||
package com.twitter.visibility.builder.users
|
||||
|
||||
import com.twitter.finagle.stats.Counter
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.gizmoduck.thriftscala.Label
|
||||
import com.twitter.gizmoduck.thriftscala.Safety
|
||||
import com.twitter.gizmoduck.thriftscala.UniversalQualityFiltering
|
||||
import com.twitter.gizmoduck.thriftscala.User
|
||||
import com.twitter.gizmoduck.thriftscala.UserType
|
||||
import com.twitter.stitch.NotFound
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.common.UserId
|
||||
import com.twitter.visibility.common.UserSource
|
||||
import com.twitter.visibility.features._
|
||||
import com.twitter.visibility.interfaces.common.blender.BlenderVFRequestContext
|
||||
import com.twitter.visibility.interfaces.common.search.SearchVFRequestContext
|
||||
import com.twitter.visibility.models.UserAge
|
||||
import com.twitter.visibility.models.ViewerContext
|
||||
|
||||
class ViewerFeatures(userSource: UserSource, statsReceiver: StatsReceiver) {
|
||||
private[this] val scopedStatsReceiver = statsReceiver.scope("viewer_features")
|
||||
|
||||
private[this] val requests = scopedStatsReceiver.counter("requests")
|
||||
|
||||
private[this] val viewerIdCount =
|
||||
scopedStatsReceiver.scope(ViewerId.name).counter("requests")
|
||||
private[this] val requestCountryCode =
|
||||
scopedStatsReceiver.scope(RequestCountryCode.name).counter("requests")
|
||||
private[this] val requestIsVerifiedCrawler =
|
||||
scopedStatsReceiver.scope(RequestIsVerifiedCrawler.name).counter("requests")
|
||||
private[this] val viewerUserLabels =
|
||||
scopedStatsReceiver.scope(ViewerUserLabels.name).counter("requests")
|
||||
private[this] val viewerIsDeactivated =
|
||||
scopedStatsReceiver.scope(ViewerIsDeactivated.name).counter("requests")
|
||||
private[this] val viewerIsProtected =
|
||||
scopedStatsReceiver.scope(ViewerIsProtected.name).counter("requests")
|
||||
private[this] val viewerIsSuspended =
|
||||
scopedStatsReceiver.scope(ViewerIsSuspended.name).counter("requests")
|
||||
private[this] val viewerRoles =
|
||||
scopedStatsReceiver.scope(ViewerRoles.name).counter("requests")
|
||||
private[this] val viewerCountryCode =
|
||||
scopedStatsReceiver.scope(ViewerCountryCode.name).counter("requests")
|
||||
private[this] val viewerAge =
|
||||
scopedStatsReceiver.scope(ViewerAge.name).counter("requests")
|
||||
private[this] val viewerHasUniversalQualityFilterEnabled =
|
||||
scopedStatsReceiver.scope(ViewerHasUniversalQualityFilterEnabled.name).counter("requests")
|
||||
private[this] val viewerIsSoftUserCtr =
|
||||
scopedStatsReceiver.scope(ViewerIsSoftUser.name).counter("requests")
|
||||
|
||||
def forViewerBlenderContext(
|
||||
blenderContext: BlenderVFRequestContext,
|
||||
viewerContext: ViewerContext
|
||||
): FeatureMapBuilder => FeatureMapBuilder =
|
||||
forViewerContext(viewerContext)
|
||||
.andThen(
|
||||
_.withConstantFeature(
|
||||
ViewerOptInBlocking,
|
||||
blenderContext.userSearchSafetySettings.optInBlocking)
|
||||
.withConstantFeature(
|
||||
ViewerOptInFiltering,
|
||||
blenderContext.userSearchSafetySettings.optInFiltering)
|
||||
)
|
||||
|
||||
def forViewerSearchContext(
|
||||
searchContext: SearchVFRequestContext,
|
||||
viewerContext: ViewerContext
|
||||
): FeatureMapBuilder => FeatureMapBuilder =
|
||||
forViewerContext(viewerContext)
|
||||
.andThen(
|
||||
_.withConstantFeature(
|
||||
ViewerOptInBlocking,
|
||||
searchContext.userSearchSafetySettings.optInBlocking)
|
||||
.withConstantFeature(
|
||||
ViewerOptInFiltering,
|
||||
searchContext.userSearchSafetySettings.optInFiltering)
|
||||
)
|
||||
|
||||
def forViewerContext(viewerContext: ViewerContext): FeatureMapBuilder => FeatureMapBuilder =
|
||||
forViewerId(viewerContext.userId)
|
||||
.andThen(
|
||||
_.withConstantFeature(RequestCountryCode, requestCountryCode(viewerContext))
|
||||
).andThen(
|
||||
_.withConstantFeature(RequestIsVerifiedCrawler, requestIsVerifiedCrawler(viewerContext))
|
||||
)
|
||||
|
||||
def forViewerId(viewerId: Option[UserId]): FeatureMapBuilder => FeatureMapBuilder = { builder =>
|
||||
requests.incr()
|
||||
|
||||
val builderWithFeatures = builder
|
||||
.withConstantFeature(ViewerId, viewerId)
|
||||
.withFeature(ViewerIsProtected, viewerIsProtected(viewerId))
|
||||
.withFeature(
|
||||
ViewerHasUniversalQualityFilterEnabled,
|
||||
viewerHasUniversalQualityFilterEnabled(viewerId)
|
||||
)
|
||||
.withFeature(ViewerIsSuspended, viewerIsSuspended(viewerId))
|
||||
.withFeature(ViewerIsDeactivated, viewerIsDeactivated(viewerId))
|
||||
.withFeature(ViewerUserLabels, viewerUserLabels(viewerId))
|
||||
.withFeature(ViewerRoles, viewerRoles(viewerId))
|
||||
.withFeature(ViewerAge, viewerAgeInYears(viewerId))
|
||||
.withFeature(ViewerIsSoftUser, viewerIsSoftUser(viewerId))
|
||||
|
||||
viewerId match {
|
||||
case Some(_) =>
|
||||
viewerIdCount.incr()
|
||||
builderWithFeatures
|
||||
.withFeature(ViewerCountryCode, viewerCountryCode(viewerId))
|
||||
|
||||
case _ =>
|
||||
builderWithFeatures
|
||||
}
|
||||
}
|
||||
|
||||
def forViewerNoDefaults(viewerOpt: Option[User]): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
builder =>
|
||||
requests.incr()
|
||||
|
||||
viewerOpt match {
|
||||
case Some(viewer) =>
|
||||
builder
|
||||
.withConstantFeature(ViewerId, viewer.id)
|
||||
.withConstantFeature(ViewerIsProtected, viewerIsProtectedOpt(viewer))
|
||||
.withConstantFeature(ViewerIsSuspended, viewerIsSuspendedOpt(viewer))
|
||||
.withConstantFeature(ViewerIsDeactivated, viewerIsDeactivatedOpt(viewer))
|
||||
.withConstantFeature(ViewerCountryCode, viewerCountryCode(viewer))
|
||||
case None =>
|
||||
builder
|
||||
.withConstantFeature(ViewerIsProtected, false)
|
||||
.withConstantFeature(ViewerIsSuspended, false)
|
||||
.withConstantFeature(ViewerIsDeactivated, false)
|
||||
}
|
||||
}
|
||||
|
||||
private def checkSafetyValue(
|
||||
viewerId: Option[UserId],
|
||||
safetyCheck: Safety => Boolean,
|
||||
featureCounter: Counter
|
||||
): Stitch[Boolean] =
|
||||
viewerId match {
|
||||
case Some(id) =>
|
||||
userSource.getSafety(id).map(safetyCheck).ensure {
|
||||
featureCounter.incr()
|
||||
}
|
||||
case None => Stitch.False
|
||||
}
|
||||
|
||||
private def checkSafetyValue(
|
||||
viewer: User,
|
||||
safetyCheck: Safety => Boolean
|
||||
): Boolean = {
|
||||
viewer.safety.exists(safetyCheck)
|
||||
}
|
||||
|
||||
def viewerIsProtected(viewerId: Option[UserId]): Stitch[Boolean] =
|
||||
checkSafetyValue(viewerId, s => s.isProtected, viewerIsProtected)
|
||||
|
||||
def viewerIsProtected(viewer: User): Boolean =
|
||||
checkSafetyValue(viewer, s => s.isProtected)
|
||||
|
||||
def viewerIsProtectedOpt(viewer: User): Option[Boolean] =
|
||||
viewer.safety.map(_.isProtected)
|
||||
|
||||
def viewerIsDeactivated(viewerId: Option[UserId]): Stitch[Boolean] =
|
||||
checkSafetyValue(viewerId, s => s.deactivated, viewerIsDeactivated)
|
||||
|
||||
def viewerIsDeactivated(viewer: User): Boolean =
|
||||
checkSafetyValue(viewer, s => s.deactivated)
|
||||
|
||||
def viewerIsDeactivatedOpt(viewer: User): Option[Boolean] =
|
||||
viewer.safety.map(_.deactivated)
|
||||
|
||||
def viewerHasUniversalQualityFilterEnabled(viewerId: Option[UserId]): Stitch[Boolean] =
|
||||
checkSafetyValue(
|
||||
viewerId,
|
||||
s => s.universalQualityFiltering == UniversalQualityFiltering.Enabled,
|
||||
viewerHasUniversalQualityFilterEnabled
|
||||
)
|
||||
|
||||
def viewerUserLabels(viewerIdOpt: Option[UserId]): Stitch[Seq[Label]] =
|
||||
viewerIdOpt match {
|
||||
case Some(viewerId) =>
|
||||
userSource
|
||||
.getLabels(viewerId).map(_.labels)
|
||||
.handle {
|
||||
case NotFound => Seq.empty
|
||||
}.ensure {
|
||||
viewerUserLabels.incr()
|
||||
}
|
||||
case _ => Stitch.value(Seq.empty)
|
||||
}
|
||||
|
||||
def viewerAgeInYears(viewerId: Option[UserId]): Stitch[UserAge] =
|
||||
(viewerId match {
|
||||
case Some(id) =>
|
||||
userSource
|
||||
.getExtendedProfile(id).map(_.ageInYears)
|
||||
.handle {
|
||||
case NotFound => None
|
||||
}.ensure {
|
||||
viewerAge.incr()
|
||||
}
|
||||
case _ => Stitch.value(None)
|
||||
}).map(UserAge)
|
||||
|
||||
def viewerIsSoftUser(viewerId: Option[UserId]): Stitch[Boolean] = {
|
||||
viewerId match {
|
||||
case Some(id) =>
|
||||
userSource
|
||||
.getUserType(id).map { userType =>
|
||||
userType == UserType.Soft
|
||||
}.ensure {
|
||||
viewerIsSoftUserCtr.incr()
|
||||
}
|
||||
case _ => Stitch.False
|
||||
}
|
||||
}
|
||||
|
||||
def requestCountryCode(viewerContext: ViewerContext): Option[String] = {
|
||||
requestCountryCode.incr()
|
||||
viewerContext.requestCountryCode
|
||||
}
|
||||
|
||||
def requestIsVerifiedCrawler(viewerContext: ViewerContext): Boolean = {
|
||||
requestIsVerifiedCrawler.incr()
|
||||
viewerContext.isVerifiedCrawler
|
||||
}
|
||||
|
||||
def viewerCountryCode(viewerId: Option[UserId]): Stitch[String] =
|
||||
viewerId match {
|
||||
case Some(id) =>
|
||||
userSource
|
||||
.getAccount(id).map(_.countryCode).flatMap {
|
||||
case Some(countryCode) => Stitch.value(countryCode.toLowerCase)
|
||||
case _ => Stitch.NotFound
|
||||
}.ensure {
|
||||
viewerCountryCode.incr()
|
||||
}
|
||||
|
||||
case _ => Stitch.NotFound
|
||||
}
|
||||
|
||||
def viewerCountryCode(viewer: User): Option[String] =
|
||||
viewer.account.flatMap(_.countryCode)
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package com.twitter.visibility.builder.users
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.common.UserId
|
||||
import com.twitter.visibility.common.UserSearchSafetySource
|
||||
import com.twitter.visibility.features.ViewerId
|
||||
import com.twitter.visibility.features.ViewerOptInBlocking
|
||||
import com.twitter.visibility.features.ViewerOptInFiltering
|
||||
|
||||
class ViewerSearchSafetyFeatures(
|
||||
userSearchSafetySource: UserSearchSafetySource,
|
||||
statsReceiver: StatsReceiver) {
|
||||
private[this] val scopedStatsReceiver = statsReceiver.scope("viewer_search_safety_features")
|
||||
|
||||
private[this] val requests = scopedStatsReceiver.counter("requests")
|
||||
|
||||
private[this] val viewerOptInBlockingRequests =
|
||||
scopedStatsReceiver.scope(ViewerOptInBlocking.name).counter("requests")
|
||||
|
||||
private[this] val viewerOptInFilteringRequests =
|
||||
scopedStatsReceiver.scope(ViewerOptInFiltering.name).counter("requests")
|
||||
|
||||
def forViewerId(viewerId: Option[UserId]): FeatureMapBuilder => FeatureMapBuilder = { builder =>
|
||||
requests.incr()
|
||||
|
||||
builder
|
||||
.withConstantFeature(ViewerId, viewerId)
|
||||
.withFeature(ViewerOptInBlocking, viewerOptInBlocking(viewerId))
|
||||
.withFeature(ViewerOptInFiltering, viewerOptInFiltering(viewerId))
|
||||
}
|
||||
|
||||
def viewerOptInBlocking(viewerId: Option[UserId]): Stitch[Boolean] = {
|
||||
viewerOptInBlockingRequests.incr()
|
||||
viewerId match {
|
||||
case Some(userId) => userSearchSafetySource.optInBlocking(userId)
|
||||
case _ => Stitch.False
|
||||
}
|
||||
}
|
||||
|
||||
def viewerOptInFiltering(viewerId: Option[UserId]): Stitch[Boolean] = {
|
||||
viewerOptInFilteringRequests.incr()
|
||||
viewerId match {
|
||||
case Some(userId) => userSearchSafetySource.optInFiltering(userId)
|
||||
case _ => Stitch.False
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package com.twitter.visibility.builder.users
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.stitch.NotFound
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.common.UserId
|
||||
import com.twitter.visibility.common.UserSensitiveMediaSettingsSource
|
||||
import com.twitter.visibility.features.ViewerId
|
||||
import com.twitter.visibility.features.ViewerSensitiveMediaSettings
|
||||
import com.twitter.visibility.models.UserSensitiveMediaSettings
|
||||
|
||||
|
||||
class ViewerSensitiveMediaSettingsFeatures(
|
||||
userSensitiveMediaSettingsSource: UserSensitiveMediaSettingsSource,
|
||||
statsReceiver: StatsReceiver) {
|
||||
private[this] val scopedStatsReceiver =
|
||||
statsReceiver.scope("viewer_sensitive_media_settings_features")
|
||||
|
||||
private[this] val requests = scopedStatsReceiver.counter("requests")
|
||||
|
||||
def forViewerId(viewerId: Option[UserId]): FeatureMapBuilder => FeatureMapBuilder = { builder =>
|
||||
requests.incr()
|
||||
|
||||
builder
|
||||
.withConstantFeature(ViewerId, viewerId)
|
||||
.withFeature(ViewerSensitiveMediaSettings, viewerSensitiveMediaSettings(viewerId))
|
||||
}
|
||||
|
||||
def viewerSensitiveMediaSettings(viewerId: Option[UserId]): Stitch[UserSensitiveMediaSettings] = {
|
||||
(viewerId match {
|
||||
case Some(userId) =>
|
||||
userSensitiveMediaSettingsSource
|
||||
.userSensitiveMediaSettings(userId)
|
||||
.handle {
|
||||
case NotFound => None
|
||||
}
|
||||
case _ => Stitch.value(None)
|
||||
}).map(UserSensitiveMediaSettings)
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
scala_library(
|
||||
sources = ["*.scala"],
|
||||
compiler_option_sets = ["fatal_warnings"],
|
||||
platform = "java8",
|
||||
strict_deps = True,
|
||||
tags = ["bazel-compatible"],
|
||||
dependencies = [
|
||||
"abdecider/src/main/scala",
|
||||
"configapi/configapi-abdecider",
|
||||
"configapi/configapi-core",
|
||||
"configapi/configapi-featureswitches:v2",
|
||||
"decider",
|
||||
"featureswitches/featureswitches-core/src/main/scala",
|
||||
"finagle/finagle-stats",
|
||||
"servo/decider/src/main/scala",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/configapi/configs",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/models",
|
||||
],
|
||||
)
|
@ -0,0 +1,43 @@
|
||||
package com.twitter.visibility.configapi
|
||||
|
||||
import com.twitter.decider.Decider
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.logging.Logger
|
||||
import com.twitter.servo.decider.DeciderGateBuilder
|
||||
import com.twitter.timelines.configapi.CompositeConfig
|
||||
import com.twitter.timelines.configapi.Config
|
||||
import com.twitter.util.Memoize
|
||||
import com.twitter.visibility.configapi.configs.VisibilityDeciders
|
||||
import com.twitter.visibility.configapi.configs.VisibilityExperimentsConfig
|
||||
import com.twitter.visibility.configapi.configs.VisibilityFeatureSwitches
|
||||
import com.twitter.visibility.models.SafetyLevel
|
||||
|
||||
object ConfigBuilder {
|
||||
|
||||
def apply(statsReceiver: StatsReceiver, decider: Decider, logger: Logger): ConfigBuilder = {
|
||||
val deciderGateBuilder: DeciderGateBuilder =
|
||||
new DeciderGateBuilder(decider)
|
||||
|
||||
new ConfigBuilder(
|
||||
deciderGateBuilder,
|
||||
statsReceiver,
|
||||
logger
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigBuilder(
|
||||
deciderGateBuilder: DeciderGateBuilder,
|
||||
statsReceiver: StatsReceiver,
|
||||
logger: Logger) {
|
||||
|
||||
def buildMemoized: SafetyLevel => Config = Memoize(build)
|
||||
|
||||
def build(safetyLevel: SafetyLevel): Config = {
|
||||
new CompositeConfig(
|
||||
VisibilityExperimentsConfig.config(safetyLevel) :+
|
||||
VisibilityDeciders.config(deciderGateBuilder, logger, statsReceiver, safetyLevel) :+
|
||||
VisibilityFeatureSwitches.config(statsReceiver, logger)
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package com.twitter.visibility.configapi
|
||||
|
||||
import com.twitter.abdecider.LoggingABDecider
|
||||
import com.twitter.decider.Decider
|
||||
import com.twitter.featureswitches.v2.FeatureSwitches
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.logging.Logger
|
||||
import com.twitter.servo.util.MemoizingStatsReceiver
|
||||
import com.twitter.timelines.configapi.Params
|
||||
import com.twitter.visibility.models.SafetyLevel
|
||||
import com.twitter.visibility.models.UnitOfDiversion
|
||||
import com.twitter.visibility.models.ViewerContext
|
||||
|
||||
object VisibilityParams {
|
||||
def apply(
|
||||
log: Logger,
|
||||
statsReceiver: StatsReceiver,
|
||||
decider: Decider,
|
||||
abDecider: LoggingABDecider,
|
||||
featureSwitches: FeatureSwitches
|
||||
): VisibilityParams =
|
||||
new VisibilityParams(log, statsReceiver, decider, abDecider, featureSwitches)
|
||||
}
|
||||
|
||||
class VisibilityParams(
|
||||
log: Logger,
|
||||
statsReceiver: StatsReceiver,
|
||||
decider: Decider,
|
||||
abDecider: LoggingABDecider,
|
||||
featureSwitches: FeatureSwitches) {
|
||||
|
||||
private[this] val contextFactory = new VisibilityRequestContextFactory(
|
||||
abDecider,
|
||||
featureSwitches
|
||||
)
|
||||
|
||||
private[this] val configBuilder = ConfigBuilder(statsReceiver.scope("config"), decider, log)
|
||||
|
||||
private[this] val paramStats: MemoizingStatsReceiver = new MemoizingStatsReceiver(
|
||||
statsReceiver.scope("configapi_params"))
|
||||
|
||||
def apply(
|
||||
viewerContext: ViewerContext,
|
||||
safetyLevel: SafetyLevel,
|
||||
unitsOfDiversion: Seq[UnitOfDiversion] = Seq.empty
|
||||
): Params = {
|
||||
val config = configBuilder.build(safetyLevel)
|
||||
val requestContext = contextFactory(viewerContext, safetyLevel, unitsOfDiversion)
|
||||
config.apply(requestContext, paramStats)
|
||||
}
|
||||
|
||||
def memoized(
|
||||
viewerContext: ViewerContext,
|
||||
safetyLevel: SafetyLevel,
|
||||
unitsOfDiversion: Seq[UnitOfDiversion] = Seq.empty
|
||||
): Params = {
|
||||
val config = configBuilder.buildMemoized(safetyLevel)
|
||||
val requestContext = contextFactory(viewerContext, safetyLevel, unitsOfDiversion)
|
||||
config.apply(requestContext, paramStats)
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package com.twitter.visibility.configapi
|
||||
|
||||
import com.twitter.timelines.configapi._
|
||||
|
||||
case class VisibilityRequestContext(
|
||||
userId: Option[Long],
|
||||
guestId: Option[Long],
|
||||
experimentContext: ExperimentContext = NullExperimentContext,
|
||||
featureContext: FeatureContext = NullFeatureContext)
|
||||
extends BaseRequestContext
|
||||
with WithUserId
|
||||
with WithGuestId
|
||||
with WithExperimentContext
|
||||
with WithFeatureContext
|
@ -0,0 +1,64 @@
|
||||
package com.twitter.visibility.configapi
|
||||
|
||||
import com.twitter.abdecider.LoggingABDecider
|
||||
import com.twitter.featureswitches.FSRecipient
|
||||
import com.twitter.featureswitches.v2.FeatureSwitches
|
||||
import com.twitter.timelines.configapi.abdecider.UserRecipientExperimentContextFactory
|
||||
import com.twitter.timelines.configapi.featureswitches.v2.FeatureSwitchResultsFeatureContext
|
||||
import com.twitter.timelines.configapi.FeatureContext
|
||||
import com.twitter.timelines.configapi.NullExperimentContext
|
||||
import com.twitter.timelines.configapi.UseFeatureContextExperimentContext
|
||||
import com.twitter.visibility.models.SafetyLevel
|
||||
import com.twitter.visibility.models.UnitOfDiversion
|
||||
import com.twitter.visibility.models.ViewerContext
|
||||
|
||||
class VisibilityRequestContextFactory(
|
||||
loggingABDecider: LoggingABDecider,
|
||||
featureSwitches: FeatureSwitches) {
|
||||
private val userExperimentContextFactory = new UserRecipientExperimentContextFactory(
|
||||
loggingABDecider
|
||||
)
|
||||
private[this] def getFeatureContext(
|
||||
context: ViewerContext,
|
||||
safetyLevel: SafetyLevel,
|
||||
unitsOfDiversion: Seq[UnitOfDiversion]
|
||||
): FeatureContext = {
|
||||
val uodCustomFields = unitsOfDiversion.map(_.apply)
|
||||
val recipient = FSRecipient(
|
||||
userId = context.userId,
|
||||
guestId = context.guestId,
|
||||
userAgent = context.fsUserAgent,
|
||||
clientApplicationId = context.clientApplicationId,
|
||||
countryCode = context.requestCountryCode,
|
||||
deviceId = context.deviceId,
|
||||
languageCode = context.requestLanguageCode,
|
||||
isTwoffice = Some(context.isTwOffice),
|
||||
userRoles = context.userRoles,
|
||||
).withCustomFields(("safety_level", safetyLevel.name), uodCustomFields: _*)
|
||||
|
||||
val results = featureSwitches.matchRecipient(recipient)
|
||||
new FeatureSwitchResultsFeatureContext(results)
|
||||
}
|
||||
|
||||
def apply(
|
||||
context: ViewerContext,
|
||||
safetyLevel: SafetyLevel,
|
||||
unitsOfDiversion: Seq[UnitOfDiversion] = Seq.empty
|
||||
): VisibilityRequestContext = {
|
||||
val experimentContextBase =
|
||||
context.userId
|
||||
.map(userId => userExperimentContextFactory.apply(userId)).getOrElse(NullExperimentContext)
|
||||
|
||||
val featureContext = getFeatureContext(context, safetyLevel, unitsOfDiversion)
|
||||
|
||||
val experimentContext =
|
||||
UseFeatureContextExperimentContext(experimentContextBase, featureContext)
|
||||
|
||||
VisibilityRequestContext(
|
||||
context.userId,
|
||||
context.guestId,
|
||||
experimentContext,
|
||||
featureContext
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
scala_library(
|
||||
sources = ["*.scala"],
|
||||
compiler_option_sets = ["fatal_warnings"],
|
||||
platform = "java8",
|
||||
strict_deps = True,
|
||||
tags = ["bazel-compatible"],
|
||||
dependencies = [
|
||||
"configapi/configapi-core",
|
||||
"configapi/configapi-decider",
|
||||
"decider",
|
||||
"finagle/finagle-stats",
|
||||
"servo/decider",
|
||||
"servo/decider/src/main/scala",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/configapi/params",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/models",
|
||||
],
|
||||
)
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,26 @@
|
||||
package com.twitter.visibility.configapi.configs
|
||||
|
||||
import com.twitter.timelines.configapi.Config
|
||||
import com.twitter.timelines.configapi.ExperimentConfigBuilder
|
||||
import com.twitter.timelines.configapi.Param
|
||||
import com.twitter.visibility.configapi.params.VisibilityExperiment
|
||||
import com.twitter.visibility.models.SafetyLevel
|
||||
|
||||
object ExperimentsHelper {
|
||||
|
||||
def mkABExperimentConfig(experiment: VisibilityExperiment, param: Param[Boolean]): Config = {
|
||||
ExperimentConfigBuilder(experiment)
|
||||
.addBucket(
|
||||
experiment.ControlBucket,
|
||||
param := true
|
||||
)
|
||||
.addBucket(
|
||||
experiment.TreatmentBucket,
|
||||
param := false
|
||||
)
|
||||
.build
|
||||
}
|
||||
|
||||
def mkABExperimentConfig(experiment: VisibilityExperiment, safetyLevel: SafetyLevel): Config =
|
||||
mkABExperimentConfig(experiment, safetyLevel.enabledParam)
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
package com.twitter.visibility.configapi.configs
|
||||
|
||||
import com.twitter.decider.Decider
|
||||
import com.twitter.servo.gate.DeciderGate
|
||||
import com.twitter.servo.util.Gate
|
||||
|
||||
case class VisibilityDeciderGates(decider: Decider) {
|
||||
import DeciderKey._
|
||||
|
||||
private[this] def feature(deciderKey: Value) = decider.feature(deciderKey.toString)
|
||||
|
||||
val enableFetchTweetReportedPerspective: Gate[Unit] =
|
||||
DeciderGate.linear(feature(DeciderKey.EnableFetchTweetReportedPerspective))
|
||||
val enableFetchTweetMediaMetadata: Gate[Unit] =
|
||||
DeciderGate.linear(feature(DeciderKey.EnableFetchTweetMediaMetadata))
|
||||
val enableFollowCheckInMutedKeyword: Gate[Unit] =
|
||||
DeciderGate.linear(feature(DeciderKey.VisibilityLibraryEnableFollowCheckInMutedKeyword))
|
||||
val enableMediaInterstitialComposition: Gate[Unit] =
|
||||
DeciderGate.linear(feature(DeciderKey.VisibilityLibraryEnableMediaInterstitialComposition))
|
||||
val enableExperimentalRuleEngine: Gate[Unit] =
|
||||
DeciderGate.linear(feature(DeciderKey.EnableExperimentalRuleEngine))
|
||||
|
||||
val enableLocalizedInterstitialGenerator: Gate[Unit] =
|
||||
DeciderGate.linear(feature(DeciderKey.EnableLocalizedInterstitialGenerator))
|
||||
|
||||
val enableShortCircuitingTVL: Gate[Unit] =
|
||||
DeciderGate.linear(feature(EnableShortCircuitingFromTweetVisibilityLibrary))
|
||||
val enableVerdictLoggerTVL = DeciderGate.linear(
|
||||
feature(DeciderKey.EnableVerdictLoggerEventPublisherInstantiationFromTweetVisibilityLibrary))
|
||||
val enableVerdictScribingTVL =
|
||||
DeciderGate.linear(feature(DeciderKey.EnableVerdictScribingFromTweetVisibilityLibrary))
|
||||
val enableBackendLimitedActions =
|
||||
DeciderGate.linear(feature(DeciderKey.EnableBackendLimitedActions))
|
||||
val enableMemoizeSafetyLevelParams: Gate[Unit] =
|
||||
DeciderGate.linear(feature(DeciderKey.EnableMemoizeSafetyLevelParams))
|
||||
|
||||
val enableShortCircuitingBVL: Gate[Unit] =
|
||||
DeciderGate.linear(feature(EnableShortCircuitingFromBlenderVisibilityLibrary))
|
||||
val enableVerdictLoggerBVL = DeciderGate.linear(
|
||||
feature(DeciderKey.EnableVerdictLoggerEventPublisherInstantiationFromBlenderVisibilityLibrary))
|
||||
val enableVerdictScribingBVL =
|
||||
DeciderGate.linear(feature(DeciderKey.EnableVerdictScribingFromBlenderVisibilityLibrary))
|
||||
|
||||
val enableShortCircuitingSVL: Gate[Unit] =
|
||||
DeciderGate.linear(feature(EnableShortCircuitingFromSearchVisibilityLibrary))
|
||||
val enableVerdictLoggerSVL = DeciderGate.linear(
|
||||
feature(DeciderKey.EnableVerdictLoggerEventPublisherInstantiationFromSearchVisibilityLibrary))
|
||||
val enableVerdictScribingSVL =
|
||||
DeciderGate.linear(feature(DeciderKey.EnableVerdictScribingFromSearchVisibilityLibrary))
|
||||
|
||||
val enableShortCircuitingTCVL: Gate[Unit] =
|
||||
DeciderGate.linear(feature(EnableShortCircuitingFromTimelineConversationsVisibilityLibrary))
|
||||
val enableVerdictLoggerTCVL = DeciderGate.linear(feature(
|
||||
DeciderKey.EnableVerdictLoggerEventPublisherInstantiationFromTimelineConversationsVisibilityLibrary))
|
||||
val enableVerdictScribingTCVL =
|
||||
DeciderGate.linear(
|
||||
feature(DeciderKey.EnableVerdictScribingFromTimelineConversationsVisibilityLibrary))
|
||||
|
||||
val enableCardVisibilityLibraryCardUriParsing =
|
||||
DeciderGate.linear(feature(DeciderKey.EnableCardVisibilityLibraryCardUriParsing))
|
||||
|
||||
val enableConvosLocalizedInterstitial: Gate[Unit] =
|
||||
DeciderGate.linear(feature(DeciderKey.EnableConvosLocalizedInterstitial))
|
||||
|
||||
val enableConvosLegacyInterstitial: Gate[Unit] =
|
||||
DeciderGate.linear(feature(DeciderKey.EnableConvosLegacyInterstitial))
|
||||
|
||||
val disableLegacyInterstitialFilteredReason: Gate[Unit] =
|
||||
DeciderGate.linear(feature(DeciderKey.DisableLegacyInterstitialFilteredReason))
|
||||
|
||||
val enableLocalizedInterstitialInUserStateLibrary: Gate[Unit] =
|
||||
DeciderGate.linear(feature(DeciderKey.EnableLocalizedInterstitialInUserStateLib))
|
||||
}
|
@ -0,0 +1,389 @@
|
||||
package com.twitter.visibility.configapi.configs
|
||||
|
||||
import com.twitter.decider.Recipient
|
||||
import com.twitter.decider.SimpleRecipient
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.logging.Logger
|
||||
import com.twitter.servo.decider.DeciderGateBuilder
|
||||
import com.twitter.timelines.configapi.BaseConfigBuilder
|
||||
import com.twitter.timelines.configapi.BaseRequestContext
|
||||
import com.twitter.timelines.configapi.Config
|
||||
import com.twitter.timelines.configapi.Param
|
||||
import com.twitter.timelines.configapi.WithGuestId
|
||||
import com.twitter.timelines.configapi.WithUserId
|
||||
import com.twitter.timelines.configapi.decider.DeciderSwitchOverrideValue
|
||||
import com.twitter.timelines.configapi.decider.GuestRecipient
|
||||
import com.twitter.timelines.configapi.decider.RecipientBuilder
|
||||
import com.twitter.visibility.configapi.params.RuleParams
|
||||
import com.twitter.visibility.configapi.params.TimelineConversationsDownrankingSpecificParams
|
||||
import com.twitter.visibility.models.SafetyLevel
|
||||
import com.twitter.visibility.models.SafetyLevel._
|
||||
|
||||
private[visibility] object VisibilityDeciders {
|
||||
val SafetyLevelToDeciderMap: Map[SafetyLevel, DeciderKey.Value] = Map(
|
||||
AllSubscribedLists -> DeciderKey.EnableAllSubscribedListsSafetyLevel,
|
||||
AccessInternalPromotedContent -> DeciderKey.EnableAccessInternalPromotedContentSafetyLevel,
|
||||
AdsBusinessSettings -> DeciderKey.EnableAdsBusinessSettingsSafetyLevel,
|
||||
AdsCampaign -> DeciderKey.EnableAdsCampaignSafetyLevel,
|
||||
AdsManager -> DeciderKey.EnableAdsManagerSafetyLevel,
|
||||
AdsReportingDashboard -> DeciderKey.EnableAdsReportingDashboardSafetyLevel,
|
||||
Appeals -> DeciderKey.EnableAppealsSafetyLevel,
|
||||
ArticleTweetTimeline -> DeciderKey.EnableArticleTweetTimelineSafetyLevel,
|
||||
BaseQig -> DeciderKey.EnableBaseQig,
|
||||
BirdwatchNoteAuthor -> DeciderKey.EnableBirdwatchNoteAuthorSafetyLevel,
|
||||
BirdwatchNoteTweetsTimeline -> DeciderKey.EnableBirdwatchNoteTweetsTimelineSafetyLevel,
|
||||
BirdwatchNeedsYourHelpNotifications -> DeciderKey.EnableBirdwatchNeedsYourHelpNotificationsSafetyLevel,
|
||||
BlockMuteUsersTimeline -> DeciderKey.EnableBlockMuteUsersTimelineSafetyLevel,
|
||||
BrandSafety -> DeciderKey.EnableBrandSafetySafetyLevel,
|
||||
CardPollVoting -> DeciderKey.EnableCardPollVotingSafetyLevel,
|
||||
CardsService -> DeciderKey.EnableCardsServiceSafetyLevel,
|
||||
Communities -> DeciderKey.EnableCommunitiesSafetyLevel,
|
||||
ContentControlToolInstall -> DeciderKey.EnableContentControlToolInstallSafetyLevel,
|
||||
ConversationFocalPrehydration -> DeciderKey.EnableConversationFocalPrehydrationSafetyLevel,
|
||||
ConversationFocalTweet -> DeciderKey.EnableConversationFocalTweetSafetyLevel,
|
||||
ConversationInjectedTweet -> DeciderKey.EnableConversationInjectedTweetSafetyLevel,
|
||||
ConversationReply -> DeciderKey.EnableConversationReplySafetyLevel,
|
||||
CuratedTrendsRepresentativeTweet -> DeciderKey.EnableCuratedTrendsRepresentativeTweet,
|
||||
CurationPolicyViolations -> DeciderKey.EnableCurationPolicyViolations,
|
||||
DeprecatedSafetyLevel -> DeciderKey.EnableDeprecatedSafetyLevelSafetyLevel,
|
||||
DevPlatformGetListTweets -> DeciderKey.EnableDevPlatformGetListTweetsSafetyLevel,
|
||||
DesFollowingAndFollowersUserList -> DeciderKey.EnableDesFollowingAndFollowersUserListSafetyLevel,
|
||||
DesHomeTimeline -> DeciderKey.EnableDesHomeTimelineSafetyLevel,
|
||||
DesQuoteTweetTimeline -> DeciderKey.EnableDesQuoteTweetTimelineSafetyLevel,
|
||||
DesRealtime -> DeciderKey.EnableDesRealtimeSafetyLevel,
|
||||
DesRealtimeSpamEnrichment -> DeciderKey.EnableDesRealtimeSpamEnrichmentSafetyLevel,
|
||||
DesRealtimeTweetFilter -> DeciderKey.EnableDesRealtimeTweetFilterSafetyLevel,
|
||||
DesRetweetingUsers -> DeciderKey.EnableDesRetweetingUsersSafetyLevel,
|
||||
DesTweetDetail -> DeciderKey.EnableDesTweetDetailSafetyLevel,
|
||||
DesTweetLikingUsers -> DeciderKey.EnableDesTweetLikingUsersSafetyLevel,
|
||||
DesUserBookmarks -> DeciderKey.EnableDesUserBookmarksSafetyLevel,
|
||||
DesUserLikedTweets -> DeciderKey.EnableDesUserLikedTweetsSafetyLevel,
|
||||
DesUserMentions -> DeciderKey.EnableDesUserMentionsSafetyLevel,
|
||||
DesUserTweets -> DeciderKey.EnableDesUserTweetsSafetyLevel,
|
||||
DevPlatformComplianceStream -> DeciderKey.EnableDevPlatformComplianceStreamSafetyLevel,
|
||||
DirectMessages -> DeciderKey.EnableDirectMessagesSafetyLevel,
|
||||
DirectMessagesConversationList -> DeciderKey.EnableDirectMessagesConversationListSafetyLevel,
|
||||
DirectMessagesConversationTimeline -> DeciderKey.EnableDirectMessagesConversationTimelineSafetyLevel,
|
||||
DirectMessagesInbox -> DeciderKey.EnableDirectMessagesInboxSafetyLevel,
|
||||
DirectMessagesMutedUsers -> DeciderKey.EnableDirectMessagesMutedUsersSafetyLevel,
|
||||
DirectMessagesPinned -> DeciderKey.EnableDirectMessagesPinnedSafetyLevel,
|
||||
DirectMessagesSearch -> DeciderKey.EnableDirectMessagesSearchSafetyLevel,
|
||||
EditHistoryTimeline -> DeciderKey.EnableEditHistoryTimelineSafetyLevel,
|
||||
ElevatedQuoteTweetTimeline -> DeciderKey.EnableElevatedQuoteTweetTimelineSafetyLevel,
|
||||
EmbeddedTweet -> DeciderKey.EnableEmbeddedTweetSafetyLevel,
|
||||
EmbedsPublicInterestNotice -> DeciderKey.EnableEmbedsPublicInterestNoticeSafetyLevel,
|
||||
EmbedTweetMarkup -> DeciderKey.EnableEmbedTweetMarkupSafetyLevel,
|
||||
FilterAll -> DeciderKey.EnableFilterAllSafetyLevel,
|
||||
FilterAllPlaceholder -> DeciderKey.EnableFilterAllPlaceholderSafetyLevel,
|
||||
FilterNone -> DeciderKey.EnableFilterNoneSafetyLevel,
|
||||
FilterDefault -> DeciderKey.EnableFilterDefaultSafetyLevel,
|
||||
FollowedTopicsTimeline -> DeciderKey.EnableFollowedTopicsTimelineSafetyLevel,
|
||||
FollowerConnections -> DeciderKey.EnableFollowerConnectionsSafetyLevel,
|
||||
FollowingAndFollowersUserList -> DeciderKey.EnableFollowingAndFollowersUserListSafetyLevel,
|
||||
ForDevelopmentOnly -> DeciderKey.EnableForDevelopmentOnlySafetyLevel,
|
||||
FriendsFollowingList -> DeciderKey.EnableFriendsFollowingListSafetyLevel,
|
||||
GraphqlDefault -> DeciderKey.EnableGraphqlDefaultSafetyLevel,
|
||||
GryphonDecksAndColumns -> DeciderKey.EnableGryphonDecksAndColumnsSafetyLevel,
|
||||
HumanizationNudge -> DeciderKey.EnableHumanizationNudgeSafetyLevel,
|
||||
KitchenSinkDevelopment -> DeciderKey.EnableKitchenSinkDevelopmentSafetyLevel,
|
||||
ListHeader -> DeciderKey.EnableListHeaderSafetyLevel,
|
||||
ListMemberships -> DeciderKey.EnableListMembershipsSafetyLevel,
|
||||
ListOwnerships -> DeciderKey.EnableListOwnershipsSafetyLevel,
|
||||
ListRecommendations -> DeciderKey.EnableListRecommendationsSafetyLevel,
|
||||
ListSearch -> DeciderKey.EnableListSearchSafetyLevel,
|
||||
ListSubscriptions -> DeciderKey.EnableListSubscriptionsSafetyLevel,
|
||||
LiveVideoTimeline -> DeciderKey.EnableLiveVideoTimelineSafetyLevel,
|
||||
LivePipelineEngagementCounts -> DeciderKey.EnableLivePipelineEngagementCountsSafetyLevel,
|
||||
MagicRecs -> DeciderKey.EnableMagicRecsSafetyLevel,
|
||||
MagicRecsAggressive -> DeciderKey.EnableMagicRecsAggressiveSafetyLevel,
|
||||
MagicRecsAggressiveV2 -> DeciderKey.EnableMagicRecsAggressiveV2SafetyLevel,
|
||||
MagicRecsV2 -> DeciderKey.EnableMagicRecsV2SafetyLevel,
|
||||
Minimal -> DeciderKey.EnableMinimalSafetyLevel,
|
||||
ModeratedTweetsTimeline -> DeciderKey.EnableModeratedTweetsTimelineSafetyLevel,
|
||||
Moments -> DeciderKey.EnableMomentsSafetyLevel,
|
||||
NearbyTimeline -> DeciderKey.EnableNearbyTimelineSafetyLevel,
|
||||
NewUserExperience -> DeciderKey.EnableNewUserExperienceSafetyLevel,
|
||||
NotificationsIbis -> DeciderKey.EnableNotificationsIbisSafetyLevel,
|
||||
NotificationsPlatform -> DeciderKey.EnableNotificationsPlatformSafetyLevel,
|
||||
NotificationsPlatformPush -> DeciderKey.EnableNotificationsPlatformPushSafetyLevel,
|
||||
NotificationsQig -> DeciderKey.EnableNotificationsQig,
|
||||
NotificationsRead -> DeciderKey.EnableNotificationsReadSafetyLevel,
|
||||
NotificationsTimelineDeviceFollow -> DeciderKey.EnableNotificationsTimelineDeviceFollowSafetyLevel,
|
||||
NotificationsWrite -> DeciderKey.EnableNotificationsWriteSafetyLevel,
|
||||
NotificationsWriterV2 -> DeciderKey.EnableNotificationsWriterV2SafetyLevel,
|
||||
NotificationsWriterTweetHydrator -> DeciderKey.EnableNotificationsWriterTweetHydratorSafetyLevel,
|
||||
ProfileMixerMedia -> DeciderKey.EnableProfileMixeMediaSafetyLevel,
|
||||
ProfileMixerFavorites -> DeciderKey.EnableProfileMixerFavoritesSafetyLevel,
|
||||
QuickPromoteTweetEligibility -> DeciderKey.EnableQuickPromoteTweetEligibilitySafetyLevel,
|
||||
QuoteTweetTimeline -> DeciderKey.EnableQuoteTweetTimelineSafetyLevel,
|
||||
QuotedTweetRules -> DeciderKey.EnableQuotedTweetRulesSafetyLevel,
|
||||
Recommendations -> DeciderKey.EnableRecommendationsSafetyLevel,
|
||||
RecosVideo -> DeciderKey.EnableRecosVideoSafetyLevel,
|
||||
RecosWritePath -> DeciderKey.EnableRecosWritePathSafetyLevel,
|
||||
RepliesGrouping -> DeciderKey.EnableRepliesGroupingSafetyLevel,
|
||||
ReportCenter -> DeciderKey.EnableReportCenterSafetyLevel,
|
||||
ReturningUserExperience -> DeciderKey.EnableReturningUserExperienceSafetyLevel,
|
||||
ReturningUserExperienceFocalTweet -> DeciderKey.EnableReturningUserExperienceFocalTweetSafetyLevel,
|
||||
Revenue -> DeciderKey.EnableRevenueSafetyLevel,
|
||||
RitoActionedTweetTimeline -> DeciderKey.EnableRitoActionedTweetTimelineSafetyLevel,
|
||||
SafeSearchMinimal -> DeciderKey.EnableSafeSearchMinimalSafetyLevel,
|
||||
SafeSearchStrict -> DeciderKey.EnableSafeSearchStrictSafetyLevel,
|
||||
SearchMixerSrpMinimal -> DeciderKey.EnableSearchMixerSrpMinimalSafetyLevel,
|
||||
SearchMixerSrpStrict -> DeciderKey.EnableSearchMixerSrpStrictSafetyLevel,
|
||||
SearchHydration -> DeciderKey.EnableSearchHydration,
|
||||
SearchLatest -> DeciderKey.EnableSearchLatest,
|
||||
SearchPeopleSrp -> DeciderKey.EnableSearchPeopleSrp,
|
||||
SearchPeopleTypeahead -> DeciderKey.EnableSearchPeopleTypeahead,
|
||||
SearchPhoto -> DeciderKey.EnableSearchPhoto,
|
||||
SearchTrendTakeoverPromotedTweet -> DeciderKey.EnableSearchTrendTakeoverPromotedTweet,
|
||||
SearchTop -> DeciderKey.EnableSearchTop,
|
||||
SearchTopQig -> DeciderKey.EnableSearchTopQig,
|
||||
SearchVideo -> DeciderKey.EnableSearchVideo,
|
||||
SearchBlenderUserRules -> DeciderKey.EnableSearchLatestUserRules,
|
||||
SearchLatestUserRules -> DeciderKey.EnableSearchLatestUserRules,
|
||||
ShoppingManagerSpyMode -> DeciderKey.EnableShoppingManagerSpyModeSafetyLevel,
|
||||
SignalsReactions -> DeciderKey.EnableSignalsReactions,
|
||||
SignalsTweetReactingUsers -> DeciderKey.EnableSignalsTweetReactingUsers,
|
||||
SocialProof -> DeciderKey.EnableSocialProof,
|
||||
SoftInterventionPivot -> DeciderKey.EnableSoftInterventionPivot,
|
||||
SpaceFleetline -> DeciderKey.EnableSpaceFleetlineSafetyLevel,
|
||||
SpaceHomeTimelineUpranking -> DeciderKey.EnableSpaceHomeTimelineUprankingSafetyLevel,
|
||||
SpaceJoinScreen -> DeciderKey.EnableSpaceJoinScreenSafetyLevel,
|
||||
SpaceNotifications -> DeciderKey.EnableSpaceNotificationsSafetyLevel,
|
||||
Spaces -> DeciderKey.EnableSpacesSafetyLevel,
|
||||
SpacesParticipants -> DeciderKey.EnableSpacesParticipantsSafetyLevel,
|
||||
SpacesSellerApplicationStatus -> DeciderKey.EnableSpacesSellerApplicationStatus,
|
||||
SpacesSharing -> DeciderKey.EnableSpacesSharingSafetyLevel,
|
||||
SpaceTweetAvatarHomeTimeline -> DeciderKey.EnableSpaceTweetAvatarHomeTimelineSafetyLevel,
|
||||
StickersTimeline -> DeciderKey.EnableStickersTimelineSafetyLevel,
|
||||
StratoExtLimitedEngagements -> DeciderKey.EnableStratoExtLimitedEngagementsSafetyLevel,
|
||||
StreamServices -> DeciderKey.EnableStreamServicesSafetyLevel,
|
||||
SuperFollowerConnections -> DeciderKey.EnableSuperFollowerConnectionsSafetyLevel,
|
||||
SuperLike -> DeciderKey.EnableSuperLikeSafetyLevel,
|
||||
Test -> DeciderKey.EnableTestSafetyLevel,
|
||||
TimelineContentControls -> DeciderKey.EnableTimelineContentControlsSafetyLevel,
|
||||
TimelineConversations -> DeciderKey.EnableTimelineConversationsSafetyLevel,
|
||||
TimelineConversationsDownranking -> DeciderKey.EnableTimelineConversationsDownrankingSafetyLevel,
|
||||
TimelineConversationsDownrankingMinimal -> DeciderKey.EnableTimelineConversationsDownrankingMinimalSafetyLevel,
|
||||
TimelineFollowingActivity -> DeciderKey.EnableTimelineFollowingActivitySafetyLevel,
|
||||
TimelineHome -> DeciderKey.EnableTimelineHomeSafetyLevel,
|
||||
TimelineHomeCommunities -> DeciderKey.EnableTimelineHomeCommunitiesSafetyLevel,
|
||||
TimelineHomeHydration -> DeciderKey.EnableTimelineHomeHydrationSafetyLevel,
|
||||
TimelineHomePromotedHydration -> DeciderKey.EnableTimelineHomePromotedHydrationSafetyLevel,
|
||||
TimelineHomeRecommendations -> DeciderKey.EnableTimelineHomeRecommendationsSafetyLevel,
|
||||
TimelineHomeTopicFollowRecommendations -> DeciderKey.EnableTimelineHomeTopicFollowRecommendationsSafetyLevel,
|
||||
TimelineScorer -> DeciderKey.EnableTimelineScorerSafetyLevel,
|
||||
TopicsLandingPageTopicRecommendations -> DeciderKey.EnableTopicsLandingPageTopicRecommendationsSafetyLevel,
|
||||
ExploreRecommendations -> DeciderKey.EnableExploreRecommendationsSafetyLevel,
|
||||
TimelineInjection -> DeciderKey.EnableTimelineInjectionSafetyLevel,
|
||||
TimelineMentions -> DeciderKey.EnableTimelineMentionsSafetyLevel,
|
||||
TimelineModeratedTweetsHydration -> DeciderKey.EnableTimelineModeratedTweetsHydrationSafetyLevel,
|
||||
TimelineHomeLatest -> DeciderKey.EnableTimelineHomeLatestSafetyLevel,
|
||||
TimelineLikedBy -> DeciderKey.EnableTimelineLikedBySafetyLevel,
|
||||
TimelineRetweetedBy -> DeciderKey.EnableTimelineRetweetedBySafetyLevel,
|
||||
TimelineSuperLikedBy -> DeciderKey.EnableTimelineSuperLikedBySafetyLevel,
|
||||
TimelineBookmark -> DeciderKey.EnableTimelineBookmarkSafetyLevel,
|
||||
TimelineMedia -> DeciderKey.EnableTimelineMediaSafetyLevel,
|
||||
TimelineReactiveBlending -> DeciderKey.EnableTimelineReactiveBlendingSafetyLevel,
|
||||
TimelineFavorites -> DeciderKey.EnableTimelineFavoritesSafetyLevel,
|
||||
TimelineFavoritesSelfView -> DeciderKey.EnableSelfViewTimelineFavoritesSafetyLevel,
|
||||
TimelineLists -> DeciderKey.EnableTimelineListsSafetyLevel,
|
||||
TimelineProfile -> DeciderKey.EnableTimelineProfileSafetyLevel,
|
||||
TimelineProfileAll -> DeciderKey.EnableTimelineProfileAllSafetyLevel,
|
||||
TimelineProfileSpaces -> DeciderKey.EnableTimelineProfileSpacesSafetyLevel,
|
||||
TimelineProfileSuperFollows -> DeciderKey.EnableTimelineProfileSuperFollowsSafetyLevel,
|
||||
TimelineFocalTweet -> DeciderKey.EnableTweetTimelineFocalTweetSafetyLevel,
|
||||
TweetDetailWithInjectionsHydration -> DeciderKey.EnableTweetDetailWithInjectionsHydrationSafetyLevel,
|
||||
Tombstoning -> DeciderKey.EnableTombstoningSafetyLevel,
|
||||
TopicRecommendations -> DeciderKey.EnableTopicRecommendationsSafetyLevel,
|
||||
TrendsRepresentativeTweet -> DeciderKey.EnableTrendsRepresentativeTweetSafetyLevel,
|
||||
TrustedFriendsUserList -> DeciderKey.EnableTrustedFriendsUserListSafetyLevel,
|
||||
TweetDetail -> DeciderKey.EnableTweetDetailSafetyLevel,
|
||||
TweetDetailNonToo -> DeciderKey.EnableTweetDetailNonTooSafetyLevel,
|
||||
TweetEngagers -> DeciderKey.EnableTweetEngagersSafetyLevel,
|
||||
TweetReplyNudge -> DeciderKey.EnableTweetReplyNudgeSafetyLevel,
|
||||
TweetScopedTimeline -> DeciderKey.EnableTweetScopedTimelineSafetyLevel,
|
||||
TweetWritesApi -> DeciderKey.EnableTweetWritesApiSafetyLevel,
|
||||
TwitterArticleCompose -> DeciderKey.EnableTwitterArticleComposeSafetyLevel,
|
||||
TwitterArticleProfileTab -> DeciderKey.EnableTwitterArticleProfileTabSafetyLevel,
|
||||
TwitterArticleRead -> DeciderKey.EnableTwitterArticleReadSafetyLevel,
|
||||
UserProfileHeader -> DeciderKey.EnableUserProfileHeaderSafetyLevel,
|
||||
UserMilestoneRecommendation -> DeciderKey.EnableUserMilestoneRecommendationSafetyLevel,
|
||||
UserScopedTimeline -> DeciderKey.EnableUserScopedTimelineSafetyLevel,
|
||||
UserSearchSrp -> DeciderKey.EnableUserSearchSrpSafetyLevel,
|
||||
UserSearchTypeahead -> DeciderKey.EnableUserSearchTypeaheadSafetyLevel,
|
||||
UserSelfViewOnly -> DeciderKey.EnableUserSelfViewOnlySafetyLevel,
|
||||
UserSettings -> DeciderKey.EnableUserSettingsSafetyLevel,
|
||||
VideoAds -> DeciderKey.EnableVideoAdsSafetyLevel,
|
||||
WritePathLimitedActionsEnforcement -> DeciderKey.EnableWritePathLimitedActionsEnforcementSafetyLevel,
|
||||
ZipbirdConsumerArchives -> DeciderKey.EnableZipbirdConsumerArchivesSafetyLevel,
|
||||
TweetAward -> DeciderKey.EnableTweetAwardSafetyLevel,
|
||||
)
|
||||
|
||||
val BoolToDeciderMap: Map[Param[Boolean], DeciderKey.Value] = Map(
|
||||
RuleParams.TweetConversationControlEnabledParam ->
|
||||
DeciderKey.EnableTweetConversationControlRules,
|
||||
RuleParams.CommunityTweetsEnabledParam ->
|
||||
DeciderKey.EnableCommunityTweetsControlRules,
|
||||
RuleParams.DropCommunityTweetWithUndefinedCommunityRuleEnabledParam ->
|
||||
DeciderKey.EnableDropCommunityTweetWithUndefinedCommunityRule,
|
||||
TimelineConversationsDownrankingSpecificParams.EnablePSpammyTweetDownrankConvosLowQualityParam ->
|
||||
DeciderKey.EnablePSpammyTweetDownrankConvosLowQuality,
|
||||
RuleParams.EnableHighPSpammyTweetScoreSearchTweetLabelDropRuleParam ->
|
||||
DeciderKey.EnableHighPSpammyTweetScoreSearchTweetLabelDropRule,
|
||||
TimelineConversationsDownrankingSpecificParams.EnableRitoActionedTweetDownrankConvosLowQualityParam ->
|
||||
DeciderKey.EnableRitoActionedTweetDownrankConvosLowQuality,
|
||||
RuleParams.EnableSmyteSpamTweetRuleParam ->
|
||||
DeciderKey.EnableSmyteSpamTweetRule,
|
||||
RuleParams.EnableHighSpammyTweetContentScoreSearchLatestTweetLabelDropRuleParam ->
|
||||
DeciderKey.EnableHighSpammyTweetContentScoreSearchLatestTweetLabelDropRule,
|
||||
RuleParams.EnableHighSpammyTweetContentScoreSearchTopTweetLabelDropRuleParam ->
|
||||
DeciderKey.EnableHighSpammyTweetContentScoreSearchTopTweetLabelDropRule,
|
||||
RuleParams.EnableHighSpammyTweetContentScoreTrendsTopTweetLabelDropRuleParam ->
|
||||
DeciderKey.EnableHighSpammyTweetContentScoreTrendsTopTweetLabelDropRule,
|
||||
RuleParams.EnableHighSpammyTweetContentScoreTrendsLatestTweetLabelDropRuleParam ->
|
||||
DeciderKey.EnableHighSpammyTweetContentScoreTrendsLatestTweetLabelDropRule,
|
||||
TimelineConversationsDownrankingSpecificParams.EnableHighSpammyTweetContentScoreConvoDownrankAbusiveQualityRuleParam ->
|
||||
DeciderKey.EnableHighSpammyTweetContentScoreConvoDownrankAbusiveQualityRule,
|
||||
TimelineConversationsDownrankingSpecificParams.EnableHighCryptospamScoreConvoDownrankAbusiveQualityRuleParam ->
|
||||
DeciderKey.EnableHighCryptospamScoreConvoDownrankAbusiveQualityRule,
|
||||
RuleParams.EnableGoreAndViolenceTopicHighRecallTweetLabelRule ->
|
||||
DeciderKey.EnableGoreAndViolenceTopicHighRecallTweetLabelRule,
|
||||
RuleParams.EnableLimitRepliesFollowersConversationRule ->
|
||||
DeciderKey.EnableLimitRepliesFollowersConversationRule,
|
||||
RuleParams.EnableSearchBasicBlockMuteRulesParam -> DeciderKey.EnableSearchBasicBlockMuteRules,
|
||||
RuleParams.EnableBlinkBadDownrankingRuleParam ->
|
||||
DeciderKey.EnableBlinkBadDownrankingRule,
|
||||
RuleParams.EnableBlinkWorstDownrankingRuleParam ->
|
||||
DeciderKey.EnableBlinkWorstDownrankingRule,
|
||||
RuleParams.EnableCopypastaSpamDownrankConvosAbusiveQualityRule ->
|
||||
DeciderKey.EnableCopypastaSpamDownrankConvosAbusiveQualityRule,
|
||||
RuleParams.EnableCopypastaSpamSearchDropRule ->
|
||||
DeciderKey.EnableCopypastaSpamSearchDropRule,
|
||||
RuleParams.EnableSpammyUserModelTweetDropRuleParam ->
|
||||
DeciderKey.EnableSpammyUserModelHighPrecisionDropTweetRule,
|
||||
RuleParams.EnableAvoidNsfwRulesParam ->
|
||||
DeciderKey.EnableAvoidNsfwRules,
|
||||
RuleParams.EnableReportedTweetInterstitialRule ->
|
||||
DeciderKey.EnableReportedTweetInterstitialRule,
|
||||
RuleParams.EnableReportedTweetInterstitialSearchRule ->
|
||||
DeciderKey.EnableReportedTweetInterstitialSearchRule,
|
||||
RuleParams.EnableDropExclusiveTweetContentRule ->
|
||||
DeciderKey.EnableDropExclusiveTweetContentRule,
|
||||
RuleParams.EnableDropExclusiveTweetContentRuleFailClosed ->
|
||||
DeciderKey.EnableDropExclusiveTweetContentRuleFailClosed,
|
||||
RuleParams.EnableTombstoneExclusiveQtProfileTimelineParam ->
|
||||
DeciderKey.EnableTombstoneExclusiveQtProfileTimelineParam,
|
||||
RuleParams.EnableDropAllExclusiveTweetsRuleParam -> DeciderKey.EnableDropAllExclusiveTweetsRule,
|
||||
RuleParams.EnableDropAllExclusiveTweetsRuleFailClosedParam -> DeciderKey.EnableDropAllExclusiveTweetsRuleFailClosed,
|
||||
RuleParams.EnableDownrankSpamReplySectioningRuleParam ->
|
||||
DeciderKey.EnableDownrankSpamReplySectioningRule,
|
||||
RuleParams.EnableNsfwTextSectioningRuleParam ->
|
||||
DeciderKey.EnableNsfwTextSectioningRule,
|
||||
RuleParams.EnableSearchIpiSafeSearchWithoutUserInQueryDropRule -> DeciderKey.EnableSearchIpiSafeSearchWithoutUserInQueryDropRule,
|
||||
RuleParams.EnableTimelineHomePromotedTweetHealthEnforcementRules -> DeciderKey.EnableTimelineHomePromotedTweetHealthEnforcementRules,
|
||||
RuleParams.EnableMutedKeywordFilteringSpaceTitleNotificationsRuleParam -> DeciderKey.EnableMutedKeywordFilteringSpaceTitleNotificationsRule,
|
||||
RuleParams.EnableDropTweetsWithGeoRestrictedMediaRuleParam -> DeciderKey.EnableDropTweetsWithGeoRestrictedMediaRule,
|
||||
RuleParams.EnableDropAllTrustedFriendsTweetsRuleParam -> DeciderKey.EnableDropAllTrustedFriendsTweetsRule,
|
||||
RuleParams.EnableDropTrustedFriendsTweetContentRuleParam -> DeciderKey.EnableDropTrustedFriendsTweetContentRule,
|
||||
RuleParams.EnableDropAllCollabInvitationTweetsRuleParam -> DeciderKey.EnableDropCollabInvitationTweetsRule,
|
||||
RuleParams.EnableNsfwTextTopicsDropRuleParam -> DeciderKey.EnableNsfwTextTopicsDropRule,
|
||||
RuleParams.EnableLikelyIvsUserLabelDropRule -> DeciderKey.EnableLikelyIvsUserLabelDropRule,
|
||||
RuleParams.EnableCardUriRootDomainCardDenylistRule -> DeciderKey.EnableCardUriRootDomainDenylistRule,
|
||||
RuleParams.EnableCommunityNonMemberPollCardRule -> DeciderKey.EnableCommunityNonMemberPollCardRule,
|
||||
RuleParams.EnableCommunityNonMemberPollCardRuleFailClosed -> DeciderKey.EnableCommunityNonMemberPollCardRuleFailClosed,
|
||||
RuleParams.EnableExperimentalNudgeEnabledParam -> DeciderKey.EnableExperimentalNudgeLabelRule,
|
||||
RuleParams.NsfwHighPrecisionUserLabelAvoidTweetRuleEnabledParam -> DeciderKey.NsfwHighPrecisionUserLabelAvoidTweetRuleEnabledParam,
|
||||
RuleParams.EnableNewAdAvoidanceRulesParam -> DeciderKey.EnableNewAdAvoidanceRules,
|
||||
RuleParams.EnableNsfaHighRecallAdAvoidanceParam -> DeciderKey.EnableNsfaHighRecallAdAvoidanceParam,
|
||||
RuleParams.EnableNsfaKeywordsHighPrecisionAdAvoidanceParam -> DeciderKey.EnableNsfaKeywordsHighPrecisionAdAvoidanceParam,
|
||||
RuleParams.EnableStaleTweetDropRuleParam -> DeciderKey.EnableStaleTweetDropRuleParam,
|
||||
RuleParams.EnableStaleTweetDropRuleFailClosedParam -> DeciderKey.EnableStaleTweetDropRuleFailClosedParam,
|
||||
RuleParams.EnableDeleteStateTweetRulesParam -> DeciderKey.EnableDeleteStateTweetRules,
|
||||
RuleParams.EnableSpacesSharingNsfwDropRulesParam -> DeciderKey.EnableSpacesSharingNsfwDropRulesParam,
|
||||
RuleParams.EnableViewerIsSoftUserDropRuleParam -> DeciderKey.EnableViewerIsSoftUserDropRuleParam,
|
||||
RuleParams.EnablePdnaQuotedTweetTombstoneRuleParam -> DeciderKey.EnablePdnaQuotedTweetTombstoneRule,
|
||||
RuleParams.EnableSpamQuotedTweetTombstoneRuleParam -> DeciderKey.EnableSpamQuotedTweetTombstoneRule,
|
||||
RuleParams.EnableNsfwHpQuotedTweetDropRuleParam -> DeciderKey.EnableNsfwHpQuotedTweetDropRule,
|
||||
RuleParams.EnableNsfwHpQuotedTweetTombstoneRuleParam -> DeciderKey.EnableNsfwHpQuotedTweetTombstoneRule,
|
||||
RuleParams.EnableInnerQuotedTweetViewerBlocksAuthorInterstitialRuleParam -> DeciderKey.EnableInnerQuotedTweetViewerBlocksAuthorInterstitialRule,
|
||||
RuleParams.EnableInnerQuotedTweetViewerMutesAuthorInterstitialRuleParam -> DeciderKey.EnableInnerQuotedTweetViewerMutesAuthorInterstitialRule,
|
||||
RuleParams.EnableToxicReplyFilteringConversationRulesParam -> DeciderKey.VisibilityLibraryEnableToxicReplyFilterConversation,
|
||||
RuleParams.EnableToxicReplyFilteringNotificationsRulesParam -> DeciderKey.VisibilityLibraryEnableToxicReplyFilterNotifications,
|
||||
RuleParams.EnableLegacySensitiveMediaHomeTimelineRulesParam -> DeciderKey.EnableLegacySensitiveMediaRulesHomeTimeline,
|
||||
RuleParams.EnableNewSensitiveMediaSettingsInterstitialsHomeTimelineRulesParam -> DeciderKey.EnableNewSensitiveMediaSettingsInterstitialRulesHomeTimeline,
|
||||
RuleParams.EnableLegacySensitiveMediaConversationRulesParam -> DeciderKey.EnableLegacySensitiveMediaRulesConversation,
|
||||
RuleParams.EnableNewSensitiveMediaSettingsInterstitialsConversationRulesParam -> DeciderKey.EnableNewSensitiveMediaSettingsInterstitialRulesConversation,
|
||||
RuleParams.EnableLegacySensitiveMediaProfileTimelineRulesParam -> DeciderKey.EnableLegacySensitiveMediaRulesProfileTimeline,
|
||||
RuleParams.EnableNewSensitiveMediaSettingsInterstitialsProfileTimelineRulesParam -> DeciderKey.EnableNewSensitiveMediaSettingsInterstitialRulesProfileTimeline,
|
||||
RuleParams.EnableLegacySensitiveMediaTweetDetailRulesParam -> DeciderKey.EnableLegacySensitiveMediaRulesTweetDetail,
|
||||
RuleParams.EnableNewSensitiveMediaSettingsInterstitialsTweetDetailRulesParam -> DeciderKey.EnableNewSensitiveMediaSettingsInterstitialRulesTweetDetail,
|
||||
RuleParams.EnableLegacySensitiveMediaDirectMessagesRulesParam -> DeciderKey.EnableLegacySensitiveMediaRulesDirectMessages,
|
||||
RuleParams.EnableAbusiveBehaviorDropRuleParam -> DeciderKey.EnableAbusiveBehaviorDropRule,
|
||||
RuleParams.EnableAbusiveBehaviorInterstitialRuleParam -> DeciderKey.EnableAbusiveBehaviorInterstitialRule,
|
||||
RuleParams.EnableAbusiveBehaviorLimitedEngagementsRuleParam -> DeciderKey.EnableAbusiveBehaviorLimitedEngagementsRule,
|
||||
RuleParams.EnableNotGraduatedDownrankConvosAbusiveQualityRuleParam -> DeciderKey.EnableNotGraduatedDownrankConvosAbusiveQualityRule,
|
||||
RuleParams.EnableNotGraduatedSearchDropRuleParam -> DeciderKey.EnableNotGraduatedSearchDropRule,
|
||||
RuleParams.EnableNotGraduatedDropRuleParam -> DeciderKey.EnableNotGraduatedDropRule,
|
||||
RuleParams.EnableFosnrRuleParam -> DeciderKey.EnableFosnrRules,
|
||||
RuleParams.EnableAuthorBlocksViewerDropRuleParam -> DeciderKey.EnableAuthorBlocksViewerDropRule
|
||||
)
|
||||
|
||||
def config(
|
||||
deciderGateBuilder: DeciderGateBuilder,
|
||||
logger: Logger,
|
||||
statsReceiver: StatsReceiver,
|
||||
SafetyLevel: SafetyLevel
|
||||
): Config = {
|
||||
|
||||
object UserOrGuestOrRequest extends RecipientBuilder {
|
||||
private val scopedStats = statsReceiver.scope("decider_recipient")
|
||||
private val userIdDefinedCounter = scopedStats.counter("user_id_defined")
|
||||
private val userIdNotDefinedCounter = scopedStats.counter("user_id_undefined")
|
||||
private val guestIdDefinedCounter = scopedStats.counter("guest_id_defined")
|
||||
private val guestIdNotDefinedCounter = scopedStats.counter("guest_id_undefined")
|
||||
private val noIdCounter = scopedStats.counter("no_id_defined")
|
||||
|
||||
def apply(requestContext: BaseRequestContext): Option[Recipient] = requestContext match {
|
||||
case c: WithUserId if c.userId.isDefined =>
|
||||
userIdDefinedCounter.incr()
|
||||
c.userId.map(SimpleRecipient)
|
||||
case c: WithGuestId if c.guestId.isDefined =>
|
||||
guestIdDefinedCounter.incr()
|
||||
c.guestId.map(GuestRecipient)
|
||||
case c: WithGuestId =>
|
||||
guestIdNotDefinedCounter.incr()
|
||||
RecipientBuilder.Request(c)
|
||||
case _: WithUserId =>
|
||||
userIdNotDefinedCounter.incr()
|
||||
None
|
||||
case _ =>
|
||||
logger.warning("Request Context with no user or guest id trait found: " + requestContext)
|
||||
noIdCounter.incr()
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
val boolOverrides = BoolToDeciderMap.map {
|
||||
case (param, deciderKey) =>
|
||||
param.optionalOverrideValue(
|
||||
DeciderSwitchOverrideValue(
|
||||
feature = deciderGateBuilder.keyToFeature(deciderKey),
|
||||
enabledValue = true,
|
||||
disabledValueOption = Some(false),
|
||||
recipientBuilder = UserOrGuestOrRequest
|
||||
)
|
||||
)
|
||||
}.toSeq
|
||||
|
||||
val safetyLevelOverride = SafetyLevel.enabledParam.optionalOverrideValue(
|
||||
DeciderSwitchOverrideValue(
|
||||
feature = deciderGateBuilder.keyToFeature(SafetyLevelToDeciderMap(SafetyLevel)),
|
||||
enabledValue = true,
|
||||
recipientBuilder = UserOrGuestOrRequest
|
||||
)
|
||||
)
|
||||
|
||||
BaseConfigBuilder(boolOverrides :+ safetyLevelOverride).build("VisibilityDeciders")
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package com.twitter.visibility.configapi.configs
|
||||
|
||||
import com.twitter.timelines.configapi.Config
|
||||
import com.twitter.visibility.configapi.params.RuleParams._
|
||||
import com.twitter.visibility.configapi.params.VisibilityExperiments._
|
||||
import com.twitter.visibility.models.SafetyLevel
|
||||
import com.twitter.visibility.models.SafetyLevel._
|
||||
|
||||
private[visibility] object VisibilityExperimentsConfig {
|
||||
import ExperimentsHelper._
|
||||
|
||||
val TestExperimentConfig: Config = mkABExperimentConfig(TestExperiment, TestHoldbackParam)
|
||||
|
||||
val NotGraduatedUserLabelRuleHoldbackExperimentConfig: Config =
|
||||
mkABExperimentConfig(
|
||||
NotGraduatedUserLabelRuleExperiment,
|
||||
NotGraduatedUserLabelRuleHoldbackExperimentParam
|
||||
)
|
||||
|
||||
def config(safetyLevel: SafetyLevel): Seq[Config] = {
|
||||
|
||||
val experimentConfigs = safetyLevel match {
|
||||
|
||||
case Test =>
|
||||
Seq(TestExperimentConfig)
|
||||
|
||||
case _ => Seq(NotGraduatedUserLabelRuleHoldbackExperimentConfig)
|
||||
}
|
||||
|
||||
experimentConfigs
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
package com.twitter.visibility.configapi.configs
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.logging.Logger
|
||||
import com.twitter.timelines.configapi._
|
||||
import com.twitter.util.Time
|
||||
import com.twitter.visibility.configapi.params.FSEnumRuleParam
|
||||
import com.twitter.visibility.configapi.params.FSRuleParams._
|
||||
|
||||
private[visibility] object VisibilityFeatureSwitches {
|
||||
|
||||
val booleanFsOverrides: Seq[OptionalOverride[Boolean]] =
|
||||
FeatureSwitchOverrideUtil.getBooleanFSOverrides(
|
||||
AgeGatingAdultContentExperimentRuleEnabledParam,
|
||||
CommunityTweetCommunityUnavailableLimitedActionsRulesEnabledParam,
|
||||
CommunityTweetDropProtectedRuleEnabledParam,
|
||||
CommunityTweetDropRuleEnabledParam,
|
||||
CommunityTweetLimitedActionsRulesEnabledParam,
|
||||
CommunityTweetMemberRemovedLimitedActionsRulesEnabledParam,
|
||||
CommunityTweetNonMemberLimitedActionsRuleEnabledParam,
|
||||
NsfwAgeBasedDropRulesHoldbackParam,
|
||||
SkipTweetDetailLimitedEngagementRuleEnabledParam,
|
||||
StaleTweetLimitedActionsRulesEnabledParam,
|
||||
TrustedFriendsTweetLimitedEngagementsRuleEnabledParam,
|
||||
FosnrFallbackDropRulesEnabledParam,
|
||||
FosnrRulesEnabledParam
|
||||
)
|
||||
|
||||
val doubleFsOverrides: Seq[OptionalOverride[Double]] =
|
||||
FeatureSwitchOverrideUtil.getBoundedDoubleFSOverrides(
|
||||
HighSpammyTweetContentScoreSearchTopProdTweetLabelDropRuleThresholdParam,
|
||||
HighSpammyTweetContentScoreSearchLatestProdTweetLabelDropRuleThresholdParam,
|
||||
HighSpammyTweetContentScoreTrendTopTweetLabelDropRuleThresholdParam,
|
||||
HighSpammyTweetContentScoreTrendLatestTweetLabelDropRuleThresholdParam,
|
||||
HighSpammyTweetContentScoreConvoDownrankAbusiveQualityThresholdParam,
|
||||
HighToxicityModelScoreSpaceThresholdParam,
|
||||
AdAvoidanceHighToxicityModelScoreThresholdParam,
|
||||
AdAvoidanceReportedTweetModelScoreThresholdParam,
|
||||
)
|
||||
|
||||
val timeFsOverrides: Seq[OptionalOverride[Time]] =
|
||||
FeatureSwitchOverrideUtil.getTimeFromStringFSOverrides()
|
||||
|
||||
val stringSeqFeatureSwitchOverrides: Seq[OptionalOverride[Seq[String]]] =
|
||||
FeatureSwitchOverrideUtil.getStringSeqFSOverrides(
|
||||
CountrySpecificNsfwContentGatingCountriesParam,
|
||||
AgeGatingAdultContentExperimentCountriesParam,
|
||||
CardUriRootDomainDenyListParam
|
||||
)
|
||||
|
||||
val enumFsParams: Seq[FSEnumRuleParam[_ <: Enumeration]] = Seq()
|
||||
|
||||
val mkOptionalEnumFsOverrides: (StatsReceiver, Logger) => Seq[OptionalOverride[_]] = {
|
||||
(statsReceiver: StatsReceiver, logger: Logger) =>
|
||||
FeatureSwitchOverrideUtil.getEnumFSOverrides(
|
||||
statsReceiver,
|
||||
logger,
|
||||
enumFsParams: _*
|
||||
)
|
||||
}
|
||||
|
||||
def overrides(statsReceiver: StatsReceiver, logger: Logger): Seq[OptionalOverride[_]] = {
|
||||
val enumOverrides = mkOptionalEnumFsOverrides(statsReceiver, logger)
|
||||
booleanFsOverrides ++
|
||||
doubleFsOverrides ++
|
||||
timeFsOverrides ++
|
||||
stringSeqFeatureSwitchOverrides ++
|
||||
enumOverrides
|
||||
}
|
||||
|
||||
def config(statsReceiver: StatsReceiver, logger: Logger): BaseConfig =
|
||||
BaseConfigBuilder(overrides(statsReceiver.scope("features_switches"), logger))
|
||||
.build("VisibilityFeatureSwitches")
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
scala_library(
|
||||
sources = ["*.scala"],
|
||||
compiler_option_sets = ["fatal_warnings"],
|
||||
strict_deps = True,
|
||||
tags = ["bazel-compatible"],
|
||||
dependencies = [
|
||||
"decider",
|
||||
],
|
||||
)
|
@ -0,0 +1,24 @@
|
||||
package com.twitter.visibility.configapi.configs.overrides
|
||||
|
||||
import com.twitter.decider.LocalOverrides
|
||||
|
||||
object VisibilityLibraryDeciderOverrides
|
||||
extends LocalOverrides.Namespace("visibility-library", "") {
|
||||
|
||||
val EnableLocalizedTombstoneOnVisibilityResults = feature(
|
||||
"visibility_library_enable_localized_tombstones_on_visibility_results")
|
||||
|
||||
val EnableLocalizedInterstitialGenerator: LocalOverrides.Override =
|
||||
feature("visibility_library_enable_localized_interstitial_generator")
|
||||
|
||||
val EnableInnerQuotedTweetViewerBlocksAuthorInterstitialRule: LocalOverrides.Override =
|
||||
feature("visibility_library_enable_inner_quoted_tweet_viewer_blocks_author_interstitial_rule")
|
||||
val EnableInnerQuotedTweetViewerMutesAuthorInterstitialRule: LocalOverrides.Override =
|
||||
feature("visibility_library_enable_inner_quoted_tweet_viewer_mutes_author_interstitial_rule")
|
||||
|
||||
val EnableBackendLimitedActions: LocalOverrides.Override =
|
||||
feature("visibility_library_enable_backend_limited_actions")
|
||||
|
||||
val disableLegacyInterstitialFilteredReason: LocalOverrides.Override = feature(
|
||||
"visibility_library_disable_legacy_interstitial_filtered_reason")
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
scala_library(
|
||||
sources = ["*.scala"],
|
||||
compiler_option_sets = ["fatal_warnings"],
|
||||
platform = "java8",
|
||||
strict_deps = True,
|
||||
tags = ["bazel-compatible"],
|
||||
dependencies = [
|
||||
"configapi/configapi-core",
|
||||
"finagle/finagle-stats",
|
||||
"visibility/common/src/main/scala/com/twitter/visibility/common:model_thresholds",
|
||||
],
|
||||
exports = [
|
||||
"visibility/common/src/main/scala/com/twitter/visibility/common:model_thresholds",
|
||||
],
|
||||
)
|
@ -0,0 +1,213 @@
|
||||
package com.twitter.visibility.configapi.params
|
||||
|
||||
import com.twitter.timelines.configapi.Bounded
|
||||
import com.twitter.timelines.configapi.FSBoundedParam
|
||||
import com.twitter.timelines.configapi.FSName
|
||||
import com.twitter.timelines.configapi.FeatureName
|
||||
import com.twitter.timelines.configapi.HasTimeConversion
|
||||
import com.twitter.timelines.configapi.TimeConversion
|
||||
import com.twitter.util.Time
|
||||
import com.twitter.visibility.common.ModelScoreThresholds
|
||||
|
||||
private[visibility] object FeatureSwitchKey extends Enumeration {
|
||||
type FeatureSwitchKey = String
|
||||
|
||||
final val HighSpammyTweetContentScoreSearchTopProdTweetLabelDropFuleThreshold =
|
||||
"high_spammy_tweet_content_score_search_top_prod_tweet_label_drop_rule_threshold"
|
||||
final val HighSpammyTweetContentScoreSearchLatestProdTweetLabelDropRuleThreshold =
|
||||
"high_spammy_tweet_content_score_search_latest_prod_tweet_label_drop_rule_threshold"
|
||||
final val HighSpammyTweetContentScoreTrendTopTweetLabelDropRuleThreshold =
|
||||
"high_spammy_tweet_content_score_trend_top_tweet_label_drop_rule_threshold"
|
||||
final val HighSpammyTweetContentScoreTrendLatestTweetLabelDropRuleThreshold =
|
||||
"high_spammy_tweet_content_score_trend_latest_tweet_label_drop_rule_threshold"
|
||||
final val HighSpammyTweetContentScoreConvoDownrankAbusiveQualityThreshold =
|
||||
"high_spammy_tweet_content_score_convos_downranking_abusive_quality_threshold"
|
||||
|
||||
final val NsfwAgeBasedDropRulesHoldbackParam =
|
||||
"nsfw_age_based_drop_rules_holdback"
|
||||
|
||||
final val CommunityTweetDropRuleEnabled =
|
||||
"community_tweet_drop_rule_enabled"
|
||||
final val CommunityTweetDropProtectedRuleEnabled =
|
||||
"community_tweet_drop_protected_rule_enabled"
|
||||
final val CommunityTweetLimitedActionsRulesEnabled =
|
||||
"community_tweet_limited_actions_rules_enabled"
|
||||
final val CommunityTweetMemberRemovedLimitedActionsRulesEnabled =
|
||||
"community_tweet_member_removed_limited_actions_rules_enabled"
|
||||
final val CommunityTweetCommunityUnavailableLimitedActionsRulesEnabled =
|
||||
"community_tweet_community_unavailable_limited_actions_rules_enabled"
|
||||
final val CommunityTweetNonMemberLimitedActionsRuleEnabled =
|
||||
"community_tweet_non_member_limited_actions_rule_enabled"
|
||||
|
||||
final val TrustedFriendsTweetLimitedEngagementsRuleEnabled =
|
||||
"trusted_friends_tweet_limited_engagements_rule_enabled"
|
||||
|
||||
final val CountrySpecificNsfwContentGatingCountries =
|
||||
"country_specific_nsfw_content_gating_countries"
|
||||
|
||||
final val AgeGatingAdultContentExperimentCountries =
|
||||
"age_gating_adult_content_experiment_countries"
|
||||
final val AgeGatingAdultContentExperimentEnabled =
|
||||
"age_gating_adult_content_experiment_enabled"
|
||||
|
||||
final val HighToxicityModelScoreSpaceThreshold =
|
||||
"high_toxicity_model_score_space_threshold"
|
||||
|
||||
final val CardUriRootDomainDenyList = "card_uri_root_domain_deny_list"
|
||||
|
||||
final val SkipTweetDetailLimitedEngagementsRuleEnabled =
|
||||
"skip_tweet_detail_limited_engagements_rule_enabled"
|
||||
|
||||
final val AdAvoidanceHighToxicityModelScoreThreshold =
|
||||
"ad_avoidance_model_thresholds_high_toxicity_model"
|
||||
final val AdAvoidanceReportedTweetModelScoreThreshold =
|
||||
"ad_avoidance_model_thresholds_reported_tweet_model"
|
||||
|
||||
final val StaleTweetLimitedActionsRulesEnabled =
|
||||
"stale_tweet_limited_actions_rules_enabled"
|
||||
|
||||
final val FosnrFallbackDropRulesEnabled =
|
||||
"freedom_of_speech_not_reach_fallback_drop_rules_enabled"
|
||||
final val FosnrRulesEnabled =
|
||||
"freedom_of_speech_not_reach_rules_enabled"
|
||||
}
|
||||
|
||||
abstract class FSRuleParam[T](override val name: FeatureName, override val default: T)
|
||||
extends RuleParam(default)
|
||||
with FSName
|
||||
|
||||
abstract class FSBoundedRuleParam[T](
|
||||
override val name: FeatureName,
|
||||
override val default: T,
|
||||
override val min: T,
|
||||
override val max: T
|
||||
)(
|
||||
implicit override val ordering: Ordering[T])
|
||||
extends RuleParam(default)
|
||||
with Bounded[T]
|
||||
with FSName
|
||||
|
||||
abstract class FSTimeRuleParam[T](
|
||||
override val name: FeatureName,
|
||||
override val default: Time,
|
||||
override val timeConversion: TimeConversion[T])
|
||||
extends RuleParam(default)
|
||||
with HasTimeConversion[T]
|
||||
with FSName
|
||||
|
||||
abstract class FSEnumRuleParam[T <: Enumeration](
|
||||
override val name: FeatureName,
|
||||
override val default: T#Value,
|
||||
override val enum: T)
|
||||
extends EnumRuleParam(default, enum)
|
||||
with FSName
|
||||
|
||||
private[visibility] object FSRuleParams {
|
||||
object HighSpammyTweetContentScoreSearchTopProdTweetLabelDropRuleThresholdParam
|
||||
extends FSBoundedParam(
|
||||
FeatureSwitchKey.HighSpammyTweetContentScoreSearchTopProdTweetLabelDropFuleThreshold,
|
||||
default = ModelScoreThresholds.HighSpammyTweetContentScoreDefaultThreshold,
|
||||
min = 0,
|
||||
max = 1)
|
||||
object HighSpammyTweetContentScoreSearchLatestProdTweetLabelDropRuleThresholdParam
|
||||
extends FSBoundedParam(
|
||||
FeatureSwitchKey.HighSpammyTweetContentScoreSearchLatestProdTweetLabelDropRuleThreshold,
|
||||
default = ModelScoreThresholds.HighSpammyTweetContentScoreDefaultThreshold,
|
||||
min = 0,
|
||||
max = 1)
|
||||
object HighSpammyTweetContentScoreTrendTopTweetLabelDropRuleThresholdParam
|
||||
extends FSBoundedParam(
|
||||
FeatureSwitchKey.HighSpammyTweetContentScoreTrendTopTweetLabelDropRuleThreshold,
|
||||
default = ModelScoreThresholds.HighSpammyTweetContentScoreDefaultThreshold,
|
||||
min = 0,
|
||||
max = 1)
|
||||
object HighSpammyTweetContentScoreTrendLatestTweetLabelDropRuleThresholdParam
|
||||
extends FSBoundedParam(
|
||||
FeatureSwitchKey.HighSpammyTweetContentScoreTrendLatestTweetLabelDropRuleThreshold,
|
||||
default = ModelScoreThresholds.HighSpammyTweetContentScoreDefaultThreshold,
|
||||
min = 0,
|
||||
max = 1)
|
||||
object HighSpammyTweetContentScoreConvoDownrankAbusiveQualityThresholdParam
|
||||
extends FSBoundedParam(
|
||||
FeatureSwitchKey.HighSpammyTweetContentScoreConvoDownrankAbusiveQualityThreshold,
|
||||
default = ModelScoreThresholds.HighSpammyTweetContentScoreDefaultThreshold,
|
||||
min = 0,
|
||||
max = 1)
|
||||
|
||||
object CommunityTweetDropRuleEnabledParam
|
||||
extends FSRuleParam(FeatureSwitchKey.CommunityTweetDropRuleEnabled, true)
|
||||
|
||||
object CommunityTweetDropProtectedRuleEnabledParam
|
||||
extends FSRuleParam(FeatureSwitchKey.CommunityTweetDropProtectedRuleEnabled, true)
|
||||
|
||||
object CommunityTweetLimitedActionsRulesEnabledParam
|
||||
extends FSRuleParam(FeatureSwitchKey.CommunityTweetLimitedActionsRulesEnabled, false)
|
||||
|
||||
object CommunityTweetMemberRemovedLimitedActionsRulesEnabledParam
|
||||
extends FSRuleParam(
|
||||
FeatureSwitchKey.CommunityTweetMemberRemovedLimitedActionsRulesEnabled,
|
||||
false)
|
||||
|
||||
object CommunityTweetCommunityUnavailableLimitedActionsRulesEnabledParam
|
||||
extends FSRuleParam(
|
||||
FeatureSwitchKey.CommunityTweetCommunityUnavailableLimitedActionsRulesEnabled,
|
||||
false)
|
||||
|
||||
object CommunityTweetNonMemberLimitedActionsRuleEnabledParam
|
||||
extends FSRuleParam(FeatureSwitchKey.CommunityTweetNonMemberLimitedActionsRuleEnabled, false)
|
||||
|
||||
object TrustedFriendsTweetLimitedEngagementsRuleEnabledParam
|
||||
extends FSRuleParam(FeatureSwitchKey.TrustedFriendsTweetLimitedEngagementsRuleEnabled, false)
|
||||
|
||||
object SkipTweetDetailLimitedEngagementRuleEnabledParam
|
||||
extends FSRuleParam(FeatureSwitchKey.SkipTweetDetailLimitedEngagementsRuleEnabled, false)
|
||||
|
||||
|
||||
object NsfwAgeBasedDropRulesHoldbackParam
|
||||
extends FSRuleParam(FeatureSwitchKey.NsfwAgeBasedDropRulesHoldbackParam, true)
|
||||
|
||||
object CountrySpecificNsfwContentGatingCountriesParam
|
||||
extends FSRuleParam[Seq[String]](
|
||||
FeatureSwitchKey.CountrySpecificNsfwContentGatingCountries,
|
||||
default = Seq("au"))
|
||||
|
||||
object AgeGatingAdultContentExperimentCountriesParam
|
||||
extends FSRuleParam[Seq[String]](
|
||||
FeatureSwitchKey.AgeGatingAdultContentExperimentCountries,
|
||||
default = Seq.empty)
|
||||
object AgeGatingAdultContentExperimentRuleEnabledParam
|
||||
extends FSRuleParam(FeatureSwitchKey.AgeGatingAdultContentExperimentEnabled, default = false)
|
||||
|
||||
object HighToxicityModelScoreSpaceThresholdParam
|
||||
extends FSBoundedParam(
|
||||
FeatureSwitchKey.HighToxicityModelScoreSpaceThreshold,
|
||||
default = ModelScoreThresholds.HighToxicityModelScoreSpaceDefaultThreshold,
|
||||
min = 0,
|
||||
max = 1)
|
||||
|
||||
object CardUriRootDomainDenyListParam
|
||||
extends FSRuleParam[Seq[String]](
|
||||
FeatureSwitchKey.CardUriRootDomainDenyList,
|
||||
default = Seq.empty)
|
||||
|
||||
object AdAvoidanceHighToxicityModelScoreThresholdParam
|
||||
extends FSBoundedParam(
|
||||
FeatureSwitchKey.AdAvoidanceHighToxicityModelScoreThreshold,
|
||||
default = ModelScoreThresholds.AdAvoidanceHighToxicityModelScoreDefaultThreshold,
|
||||
min = 0,
|
||||
max = 1)
|
||||
|
||||
object AdAvoidanceReportedTweetModelScoreThresholdParam
|
||||
extends FSBoundedParam(
|
||||
FeatureSwitchKey.AdAvoidanceReportedTweetModelScoreThreshold,
|
||||
default = ModelScoreThresholds.AdAvoidanceReportedTweetModelScoreDefaultThreshold,
|
||||
min = 0,
|
||||
max = 1)
|
||||
|
||||
object StaleTweetLimitedActionsRulesEnabledParam
|
||||
extends FSRuleParam(FeatureSwitchKey.StaleTweetLimitedActionsRulesEnabled, false)
|
||||
|
||||
object FosnrFallbackDropRulesEnabledParam
|
||||
extends FSRuleParam(FeatureSwitchKey.FosnrFallbackDropRulesEnabled, false)
|
||||
object FosnrRulesEnabledParam extends FSRuleParam(FeatureSwitchKey.FosnrRulesEnabled, true)
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package com.twitter.visibility.configapi.params
|
||||
|
||||
import com.twitter.timelines.configapi.Param
|
||||
|
||||
abstract class GlobalParam[T](override val default: T) extends Param(default) {
|
||||
override val statName: String = s"GlobalParam/${this.getClass.getSimpleName}"
|
||||
}
|
||||
|
||||
private[visibility] object GlobalParams {
|
||||
object EnableFetchingLabelMap extends GlobalParam(false)
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.twitter.visibility.configapi.params
|
||||
|
||||
import com.twitter.timelines.configapi.Param
|
||||
|
||||
abstract class LabelSourceParam(override val default: Boolean) extends Param(default) {
|
||||
override val statName: String = s"LabelSourceParam/${this.getClass.getSimpleName}"
|
||||
}
|
||||
|
||||
private[visibility] object LabelSourceParams {
|
||||
object FilterLabelsFromBot7174Param extends LabelSourceParam(false)
|
||||
|
||||
object FilterTweetsSmyteAutomationParamA extends LabelSourceParam(false)
|
||||
object FilterTweetsSmyteAutomationParamB extends LabelSourceParam(false)
|
||||
object FilterTweetsSmyteAutomationParamAB extends LabelSourceParam(false)
|
||||
}
|
@ -0,0 +1,164 @@
|
||||
package com.twitter.visibility.configapi.params
|
||||
|
||||
import com.twitter.timelines.configapi.EnumParam
|
||||
import com.twitter.timelines.configapi.Param
|
||||
|
||||
abstract class RuleParam[T](override val default: T) extends Param(default) {
|
||||
override val statName: String = s"RuleParam/${this.getClass.getSimpleName}"
|
||||
}
|
||||
|
||||
abstract class EnumRuleParam[T <: Enumeration](override val default: T#Value, override val enum: T)
|
||||
extends EnumParam(default, enum) {
|
||||
override val statName: String = s"RuleParam/${this.getClass.getSimpleName}"
|
||||
}
|
||||
|
||||
private[visibility] object RuleParams {
|
||||
object True extends RuleParam(true)
|
||||
object False extends RuleParam(false)
|
||||
|
||||
object TestHoldbackParam extends RuleParam(true)
|
||||
|
||||
object TweetConversationControlEnabledParam extends RuleParam(default = false)
|
||||
|
||||
object EnableLimitRepliesFollowersConversationRule extends RuleParam(default = false)
|
||||
|
||||
object CommunityTweetsEnabledParam extends RuleParam(default = false)
|
||||
|
||||
object DropCommunityTweetWithUndefinedCommunityRuleEnabledParam extends RuleParam(default = false)
|
||||
|
||||
object EnableHighPSpammyTweetScoreSearchTweetLabelDropRuleParam extends RuleParam(false)
|
||||
|
||||
object EnableSmyteSpamTweetRuleParam extends RuleParam(false)
|
||||
|
||||
object EnableHighSpammyTweetContentScoreSearchLatestTweetLabelDropRuleParam
|
||||
extends RuleParam(false)
|
||||
|
||||
object EnableHighSpammyTweetContentScoreSearchTopTweetLabelDropRuleParam extends RuleParam(false)
|
||||
|
||||
object NotGraduatedUserLabelRuleHoldbackExperimentParam extends RuleParam(default = false)
|
||||
|
||||
object EnableGoreAndViolenceTopicHighRecallTweetLabelRule extends RuleParam(default = false)
|
||||
|
||||
object EnableBlinkBadDownrankingRuleParam extends RuleParam(false)
|
||||
object EnableBlinkWorstDownrankingRuleParam extends RuleParam(false)
|
||||
|
||||
object EnableHighSpammyTweetContentScoreTrendsTopTweetLabelDropRuleParam
|
||||
extends RuleParam(default = false)
|
||||
|
||||
object EnableHighSpammyTweetContentScoreTrendsLatestTweetLabelDropRuleParam
|
||||
extends RuleParam(default = false)
|
||||
|
||||
object EnableCopypastaSpamDownrankConvosAbusiveQualityRule extends RuleParam(default = false)
|
||||
object EnableCopypastaSpamSearchDropRule extends RuleParam(default = false)
|
||||
|
||||
object EnableSpammyUserModelTweetDropRuleParam extends RuleParam(default = false)
|
||||
|
||||
object EnableAvoidNsfwRulesParam extends RuleParam(false)
|
||||
|
||||
object EnableReportedTweetInterstitialRule extends RuleParam(default = false)
|
||||
|
||||
object EnableReportedTweetInterstitialSearchRule extends RuleParam(default = false)
|
||||
|
||||
object EnableDropExclusiveTweetContentRule extends RuleParam(default = false)
|
||||
|
||||
object EnableDropExclusiveTweetContentRuleFailClosed extends RuleParam(default = false)
|
||||
|
||||
object EnableTombstoneExclusiveQtProfileTimelineParam extends RuleParam(default = false)
|
||||
|
||||
object EnableDropAllExclusiveTweetsRuleParam extends RuleParam(false)
|
||||
object EnableDropAllExclusiveTweetsRuleFailClosedParam extends RuleParam(false)
|
||||
|
||||
object EnableDownrankSpamReplySectioningRuleParam extends RuleParam(default = false)
|
||||
object EnableNsfwTextSectioningRuleParam extends RuleParam(default = false)
|
||||
|
||||
object EnableSearchIpiSafeSearchWithoutUserInQueryDropRule extends RuleParam(false)
|
||||
|
||||
object PromotedTweetHealthEnforcementHoldback extends RuleParam(default = true)
|
||||
object EnableTimelineHomePromotedTweetHealthEnforcementRules extends RuleParam(default = false)
|
||||
|
||||
object EnableMutedKeywordFilteringSpaceTitleNotificationsRuleParam extends RuleParam(false)
|
||||
|
||||
object EnableDropTweetsWithGeoRestrictedMediaRuleParam extends RuleParam(default = false)
|
||||
|
||||
object EnableDropAllTrustedFriendsTweetsRuleParam extends RuleParam(false)
|
||||
object EnableDropTrustedFriendsTweetContentRuleParam extends RuleParam(false)
|
||||
|
||||
object EnableDropAllCollabInvitationTweetsRuleParam extends RuleParam(false)
|
||||
|
||||
object EnableNsfwTextTopicsDropRuleParam extends RuleParam(false)
|
||||
|
||||
object EnableLikelyIvsUserLabelDropRule extends RuleParam(false)
|
||||
|
||||
object EnableCardUriRootDomainCardDenylistRule extends RuleParam(false)
|
||||
object EnableCommunityNonMemberPollCardRule extends RuleParam(false)
|
||||
object EnableCommunityNonMemberPollCardRuleFailClosed extends RuleParam(false)
|
||||
|
||||
object EnableExperimentalNudgeEnabledParam extends RuleParam(false)
|
||||
|
||||
object NsfwHighPrecisionUserLabelAvoidTweetRuleEnabledParam extends RuleParam(default = false)
|
||||
|
||||
object EnableNewAdAvoidanceRulesParam extends RuleParam(false)
|
||||
|
||||
object EnableNsfaHighRecallAdAvoidanceParam extends RuleParam(false)
|
||||
|
||||
object EnableNsfaKeywordsHighPrecisionAdAvoidanceParam extends RuleParam(false)
|
||||
|
||||
object EnableStaleTweetDropRuleParam extends RuleParam(false)
|
||||
object EnableStaleTweetDropRuleFailClosedParam extends RuleParam(false)
|
||||
|
||||
object EnableDeleteStateTweetRulesParam extends RuleParam(default = false)
|
||||
|
||||
object EnableSpacesSharingNsfwDropRulesParam extends RuleParam(default = true)
|
||||
|
||||
object EnableViewerIsSoftUserDropRuleParam extends RuleParam(default = false)
|
||||
|
||||
object EnablePdnaQuotedTweetTombstoneRuleParam extends RuleParam(default = true)
|
||||
object EnableSpamQuotedTweetTombstoneRuleParam extends RuleParam(default = true)
|
||||
|
||||
object EnableNsfwHpQuotedTweetDropRuleParam extends RuleParam(default = false)
|
||||
object EnableNsfwHpQuotedTweetTombstoneRuleParam extends RuleParam(default = false)
|
||||
|
||||
object EnableInnerQuotedTweetViewerBlocksAuthorInterstitialRuleParam
|
||||
extends RuleParam(default = false)
|
||||
object EnableInnerQuotedTweetViewerMutesAuthorInterstitialRuleParam
|
||||
extends RuleParam(default = false)
|
||||
|
||||
|
||||
object EnableNewSensitiveMediaSettingsInterstitialsHomeTimelineRulesParam extends RuleParam(false)
|
||||
|
||||
object EnableNewSensitiveMediaSettingsInterstitialsConversationRulesParam extends RuleParam(false)
|
||||
|
||||
object EnableNewSensitiveMediaSettingsInterstitialsProfileTimelineRulesParam
|
||||
extends RuleParam(false)
|
||||
|
||||
object EnableNewSensitiveMediaSettingsInterstitialsTweetDetailRulesParam extends RuleParam(false)
|
||||
|
||||
object EnableLegacySensitiveMediaHomeTimelineRulesParam extends RuleParam(true)
|
||||
|
||||
object EnableLegacySensitiveMediaConversationRulesParam extends RuleParam(true)
|
||||
|
||||
object EnableLegacySensitiveMediaProfileTimelineRulesParam extends RuleParam(true)
|
||||
|
||||
object EnableLegacySensitiveMediaTweetDetailRulesParam extends RuleParam(true)
|
||||
|
||||
object EnableLegacySensitiveMediaDirectMessagesRulesParam extends RuleParam(true)
|
||||
|
||||
object EnableToxicReplyFilteringConversationRulesParam extends RuleParam(false)
|
||||
object EnableToxicReplyFilteringNotificationsRulesParam extends RuleParam(false)
|
||||
|
||||
object EnableSearchQueryMatchesTweetAuthorConditionParam extends RuleParam(default = false)
|
||||
|
||||
object EnableSearchBasicBlockMuteRulesParam extends RuleParam(default = false)
|
||||
|
||||
object EnableAbusiveBehaviorDropRuleParam extends RuleParam(default = false)
|
||||
object EnableAbusiveBehaviorInterstitialRuleParam extends RuleParam(default = false)
|
||||
object EnableAbusiveBehaviorLimitedEngagementsRuleParam extends RuleParam(default = false)
|
||||
|
||||
object EnableNotGraduatedDownrankConvosAbusiveQualityRuleParam extends RuleParam(default = false)
|
||||
object EnableNotGraduatedSearchDropRuleParam extends RuleParam(default = false)
|
||||
object EnableNotGraduatedDropRuleParam extends RuleParam(default = false)
|
||||
|
||||
object EnableFosnrRuleParam extends RuleParam(default = false)
|
||||
|
||||
object EnableAuthorBlocksViewerDropRuleParam extends RuleParam(default = false)
|
||||
}
|
@ -0,0 +1,214 @@
|
||||
package com.twitter.visibility.configapi.params
|
||||
|
||||
import com.twitter.timelines.configapi.Param
|
||||
|
||||
abstract class SafetyLevelParam(override val default: Boolean) extends Param(default) {
|
||||
override val statName: String = s"SafetyLevelParam/${this.getClass.getSimpleName}"
|
||||
}
|
||||
|
||||
private[visibility] object SafetyLevelParams {
|
||||
object EnableAccessInternalPromotedContentSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableAdsBusinessSettingsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableAdsCampaignSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableAdsManagerSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableAdsReportingDashboardSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableAllSubscribedListsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableAppealsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableArticleTweetTimelineSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableBaseQigSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableBirdwatchNoteAuthorSafetyLevel extends SafetyLevelParam(false)
|
||||
object EnableBirdwatchNoteTweetsTimelineSafetyLevel extends SafetyLevelParam(false)
|
||||
object EnableBirdwatchNeedsYourHelpNotificationsSafetyLevel extends SafetyLevelParam(false)
|
||||
object EnableBlockMuteUsersTimelineSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableBrandSafetySafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableCardPollVotingSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableCardsServiceSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableCommunitiesSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableContentControlToolInstallSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableConversationFocalPrehydrationSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableConversationFocalTweetSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableConversationInjectedTweetSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableConversationReplySafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableCuratedTrendsRepresentativeTweet extends SafetyLevelParam(default = false)
|
||||
object EnableCurationPolicyViolations extends SafetyLevelParam(default = false)
|
||||
object EnableDevPlatformGetListTweetsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableDESFollowingAndFollowersUserListSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableDESHomeTimelineSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableDESQuoteTweetTimelineSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableDESRealtimeSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableDESRealtimeSpamEnrichmentSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableDESRealtimeTweetFilterSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableDESRetweetingUsersSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableDesTweetDetailSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableDESTweetLikingUsersSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableDESUserBookmarksSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableDESUserLikedTweetSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableDESUserMentionsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableDESUserTweetsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableDevPlatformComplianceStreamSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableDirectMessagesSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableDirectMessagesConversationListSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableDirectMessagesConversationTimelineSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableDirectMessagesInboxSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableDirectMessagesMutedUsersSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableDirectMessagesPinnedSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableDirectMessagesSearchSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableEditHistoryTimelineSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableElevatedQuoteTweetTimelineSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableEmbeddedTweetSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableEmbedsPublicInterestNoticeSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableEmbedTweetMarkupSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableWritePathLimitedActionsEnforcementSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableFilterAllSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableFilterAllPlaceholderSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableFilterDefaultSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableFilterNoneSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableFollowedTopicsTimelineSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableFollowerConnectionsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableFollowingAndFollowersUserListSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableForDevelopmentOnlySafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableFriendsFollowingListSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableGraphqlDefaultSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableGryphonDecksAndColumnsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableHumanizationNudgeSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableKitchenSinkDevelopmentSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableListHeaderSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableListMembershipsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableListOwnershipsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableListRecommendationsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableListSearchSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableListSubscriptionsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableLivePipelineEngagementCountsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableLiveVideoTimelineSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableMagicRecsAggressiveSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableMagicRecsAggressiveV2SafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableMagicRecsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableMagicRecsV2SafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableMinimalSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableModeratedTweetsTimelineSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableMomentsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableNearbySafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableNewUserExperienceSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableNotificationsIbisSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableNotificationsPlatformSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableNotificationsPlatformPushSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableNotificationsQigSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableNotificationsReadSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableNotificationsTimelineDeviceFollowSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableNotificationsWriteSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableNotificationsWriterTweetHydratorSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableNotificationsWriterV2SafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableQuickPromoteTweetEligibilitySafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableQuoteTweetTimelineSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableRecommendationsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableRecommendationsWithoutNsfaSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableRecosVideoSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableRecosWritePathSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableRepliesGroupingSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableReportCenterSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableReturningUserExperienceFocalTweetSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableReturningUserExperienceSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableRevenueSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableRitoActionedTweetTimelineParam extends SafetyLevelParam(false)
|
||||
object EnableSafeSearchMinimalSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSafeSearchStrictSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSearchHydrationSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSearchLatestSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSearchTopSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSearchTopQigSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSearchPhotoSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object SearchTrendTakeoverPromotedTweetSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSearchVideoSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSearchBlenderUserRulesSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSearchLatestUserRulesSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSearchPeopleSearchResultPageSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSearchPeopleTypeaheadSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableUserSearchSrpSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableUserSearchTypeaheadSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSearchMixerSrpMinimalSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSearchMixerSrpStrictSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableShoppingManagerSpyModeSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSignalsReactionsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSignalsTweetReactingUsersSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSocialProofSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSoftInterventionPivotSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSpaceFleetlineSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSpaceHomeTimelineUprankingSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSpaceJoinScreenSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSpaceNotificationsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSpacesSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSpacesParticipantsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSpaceNotificationSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSpacesSellerApplicationStatusSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSpacesSharingSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSpaceTweetAvatarHomeTimelineSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableStickersTimelineSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableStratoExtLimitedEngagementsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableStreamServicesSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSuperFollowerConnectionsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableSuperLikeSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTestSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineBookmarkSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineConversationsDownrankingSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineContentControlsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineConversationsDownrankingMinimalSafetyLevelParam
|
||||
extends SafetyLevelParam(false)
|
||||
object EnableTimelineConversationsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineFavoritesSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineFavoritesSelfViewSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineFocalTweetSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineFollowingActivitySafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineHomeCommunitiesSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineHomeHydrationSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineHomeLatestSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineHomePromotedHydrationSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineHomeRecommendationsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineHomeSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineHomeTopicFollowRecommendationsSafetyLevelParam
|
||||
extends SafetyLevelParam(false)
|
||||
object EnableTimelineScorerSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTopicsLandingPageTopicRecommendationsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableExploreRecommendationsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineInjectionSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineLikedBySafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineListsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineMediaSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineMentionsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineModeratedTweetsHydrationSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineProfileSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineProfileAllSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineProfileSpacesSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineProfileSuperFollowsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineReactiveBlendingSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineRetweetedBySafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTimelineSuperLikedBySafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTombstoningSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTopicRecommendationsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTrendsRepresentativeTweetSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTrustedFriendsUserListSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTweetDetailSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTweetDetailNonTooSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTweetDetailWithInjectionsHydrationSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTweetEngagersSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTweetReplyNudgeParam extends SafetyLevelParam(false)
|
||||
object EnableTweetScopedTimelineSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTweetWritesApiSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTwitterArticleComposeSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTwitterArticleProfileTabSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTwitterArticleReadSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableUserProfileHeaderSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableProfileMixerMediaSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableProfileMixerFavoritesSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableUserMilestoneRecommendationSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableUserScopedTimelineSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableUserSelfViewOnlySafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableUserSettingsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableVideoAdsSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableZipbirdConsumerArchivesSafetyLevelParam extends SafetyLevelParam(false)
|
||||
object EnableTweetAwardSafetyLevelParam extends SafetyLevelParam(false)
|
||||
|
||||
object EnableDeprecatedSafetyLevel extends SafetyLevelParam(true)
|
||||
object EnableQuotedTweetRulesParam extends SafetyLevelParam(true)
|
||||
object EnableUnsupportedSafetyLevel extends SafetyLevelParam(true)
|
||||
object EnableUnknownSafetyLevel$ extends SafetyLevelParam(true)
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package com.twitter.visibility.configapi.params
|
||||
|
||||
private[visibility] object TimelineConversationsDownrankingSpecificParams {
|
||||
|
||||
object EnablePSpammyTweetDownrankConvosLowQualityParam extends RuleParam(false)
|
||||
|
||||
object EnableRitoActionedTweetDownrankConvosLowQualityParam extends RuleParam(false)
|
||||
|
||||
object EnableHighSpammyTweetContentScoreConvoDownrankAbusiveQualityRuleParam
|
||||
extends RuleParam(false)
|
||||
|
||||
object EnableHighCryptospamScoreConvoDownrankAbusiveQualityRuleParam extends RuleParam(false)
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package com.twitter.visibility.configapi.params
|
||||
|
||||
import com.twitter.timelines.configapi.BucketName
|
||||
import com.twitter.timelines.configapi.Experiment
|
||||
import com.twitter.timelines.configapi.UseFeatureContext
|
||||
|
||||
object VisibilityExperiment {
|
||||
val Control = "control"
|
||||
val Treatment = "treatment"
|
||||
}
|
||||
|
||||
abstract class VisibilityExperiment(experimentKey: String)
|
||||
extends Experiment(experimentKey)
|
||||
with UseFeatureContext {
|
||||
val TreatmentBucket: String = VisibilityExperiment.Treatment
|
||||
override def experimentBuckets: Set[BucketName] = Set(TreatmentBucket)
|
||||
val ControlBucket: String = VisibilityExperiment.Control
|
||||
override def controlBuckets: Set[BucketName] = Set(ControlBucket)
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package com.twitter.visibility.configapi.params
|
||||
|
||||
private[visibility] object VisibilityExperiments {
|
||||
|
||||
case object TestExperiment extends VisibilityExperiment("vf_test_ddg_7727")
|
||||
|
||||
object CommonBucketId extends Enumeration {
|
||||
type CommonBucketId = Value
|
||||
val Control = Value("control")
|
||||
val Treatment = Value("treatment")
|
||||
val None = Value("none")
|
||||
}
|
||||
|
||||
case object NotGraduatedUserLabelRuleExperiment
|
||||
extends VisibilityExperiment("not_graduated_user_holdback_16332")
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
scala_library(
|
||||
sources = ["*.scala"],
|
||||
compiler_option_sets = ["fatal_warnings"],
|
||||
platform = "java8",
|
||||
strict_deps = True,
|
||||
tags = ["bazel-compatible"],
|
||||
dependencies = [
|
||||
"abdecider/src/main/scala",
|
||||
"configapi/configapi-core",
|
||||
"servo/util/src/main/scala",
|
||||
"src/thrift/com/twitter/search/common:constants-scala",
|
||||
"src/thrift/com/twitter/spam/rtf:safety-level-scala",
|
||||
"src/thrift/com/twitter/tweetypie:tweet-scala",
|
||||
"stitch/stitch-core",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/builder",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/configapi/params",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/features",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/models",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/rules",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/rules/providers",
|
||||
],
|
||||
)
|
@ -0,0 +1,26 @@
|
||||
package com.twitter.visibility.engine
|
||||
|
||||
import com.twitter.servo.util.Gate
|
||||
import com.twitter.spam.rtf.thriftscala.{SafetyLevel => ThriftSafetyLevel}
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.visibility.builder.VisibilityResult
|
||||
import com.twitter.visibility.builder.VisibilityResultBuilder
|
||||
import com.twitter.visibility.models.SafetyLevel
|
||||
import com.twitter.visibility.rules.EvaluationContext
|
||||
import com.twitter.visibility.rules.Rule
|
||||
|
||||
trait DeciderableVisibilityRuleEngine {
|
||||
def apply(
|
||||
evaluationContext: EvaluationContext,
|
||||
safetyLevel: SafetyLevel,
|
||||
visibilityResultBuilder: VisibilityResultBuilder,
|
||||
enableShortCircuiting: Gate[Unit] = Gate.True,
|
||||
preprocessedRules: Option[Seq[Rule]] = None
|
||||
): Stitch[VisibilityResult]
|
||||
|
||||
def apply(
|
||||
evaluationContext: EvaluationContext,
|
||||
thriftSafetyLevel: ThriftSafetyLevel,
|
||||
visibilityResultBuilder: VisibilityResultBuilder
|
||||
): Stitch[VisibilityResult]
|
||||
}
|
@ -0,0 +1,179 @@
|
||||
package com.twitter.visibility.engine
|
||||
|
||||
import com.twitter.finagle.stats.NullStatsReceiver
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.finagle.stats.Verbosity
|
||||
import com.twitter.servo.util.Gate
|
||||
import com.twitter.servo.util.MemoizingStatsReceiver
|
||||
import com.twitter.visibility.builder.VisibilityResult
|
||||
import com.twitter.visibility.features.Feature
|
||||
import com.twitter.visibility.models.SafetyLevel
|
||||
import com.twitter.visibility.rules.NotEvaluated
|
||||
import com.twitter.visibility.rules.RuleResult
|
||||
import com.twitter.visibility.rules.State
|
||||
import com.twitter.visibility.rules.State.Disabled
|
||||
import com.twitter.visibility.rules.State.FeatureFailed
|
||||
import com.twitter.visibility.rules.State.MissingFeature
|
||||
import com.twitter.visibility.rules.State.RuleFailed
|
||||
import com.twitter.visibility.rules.Action
|
||||
|
||||
|
||||
case class VisibilityResultsMetricRecorder(
|
||||
statsReceiver: StatsReceiver,
|
||||
captureDebugStats: Gate[Unit]) {
|
||||
|
||||
private val scopedStatsReceiver = new MemoizingStatsReceiver(
|
||||
statsReceiver.scope("visibility_rule_engine")
|
||||
)
|
||||
private val actionStats: StatsReceiver = scopedStatsReceiver.scope("by_action")
|
||||
private val featureFailureReceiver: StatsReceiver =
|
||||
scopedStatsReceiver.scope("feature_failed")
|
||||
private val safetyLevelStatsReceiver: StatsReceiver =
|
||||
scopedStatsReceiver.scope("from_safety_level")
|
||||
private val ruleStatsReceiver: StatsReceiver = scopedStatsReceiver.scope("for_rule")
|
||||
private val ruleFailureReceiver: StatsReceiver =
|
||||
scopedStatsReceiver.scope("rule_failures")
|
||||
private val failClosedReceiver: StatsReceiver =
|
||||
scopedStatsReceiver.scope("fail_closed")
|
||||
private val ruleStatsBySafetyLevelReceiver: StatsReceiver =
|
||||
scopedStatsReceiver.scope("for_rule_by_safety_level")
|
||||
|
||||
def recordSuccess(
|
||||
safetyLevel: SafetyLevel,
|
||||
result: VisibilityResult
|
||||
): Unit = {
|
||||
recordAction(safetyLevel, result.verdict.fullName)
|
||||
|
||||
val isFeatureFailure = result.ruleResultMap.values
|
||||
.collectFirst {
|
||||
case RuleResult(_, FeatureFailed(_)) =>
|
||||
ruleFailureReceiver.counter("feature_failed").incr()
|
||||
true
|
||||
}.getOrElse(false)
|
||||
|
||||
val isMissingFeature = result.ruleResultMap.values
|
||||
.collectFirst {
|
||||
case RuleResult(_, MissingFeature(_)) =>
|
||||
ruleFailureReceiver.counter("missing_feature").incr()
|
||||
true
|
||||
}.getOrElse(false)
|
||||
|
||||
val isRuleFailed = result.ruleResultMap.values
|
||||
.collectFirst {
|
||||
case RuleResult(_, RuleFailed(_)) =>
|
||||
ruleFailureReceiver.counter("rule_failed").incr()
|
||||
true
|
||||
}.getOrElse(false)
|
||||
|
||||
if (isFeatureFailure || isMissingFeature || isRuleFailed) {
|
||||
ruleFailureReceiver.counter().incr()
|
||||
}
|
||||
|
||||
if (captureDebugStats()) {
|
||||
val ruleBySafetyLevelStat =
|
||||
ruleStatsBySafetyLevelReceiver.scope(safetyLevel.name)
|
||||
result.ruleResultMap.foreach {
|
||||
case (rule, ruleResult) => {
|
||||
ruleBySafetyLevelStat
|
||||
.scope(rule.name)
|
||||
.scope("action")
|
||||
.counter(Verbosity.Debug, ruleResult.action.fullName).incr()
|
||||
ruleBySafetyLevelStat
|
||||
.scope(rule.name)
|
||||
.scope("state")
|
||||
.counter(Verbosity.Debug, ruleResult.state.name).incr()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def recordFailedFeature(
|
||||
failedFeature: Feature[_],
|
||||
exception: Throwable
|
||||
): Unit = {
|
||||
featureFailureReceiver.counter().incr()
|
||||
|
||||
val featureStat = featureFailureReceiver.scope(failedFeature.name)
|
||||
featureStat.counter().incr()
|
||||
featureStat.counter(exception.getClass.getName).incr()
|
||||
}
|
||||
|
||||
def recordAction(
|
||||
safetyLevel: SafetyLevel,
|
||||
action: String
|
||||
): Unit = {
|
||||
safetyLevelStatsReceiver.scope(safetyLevel.name).counter(action).incr()
|
||||
actionStats.counter(action).incr()
|
||||
}
|
||||
|
||||
def recordUnknownSafetyLevel(
|
||||
safetyLevel: SafetyLevel
|
||||
): Unit = {
|
||||
safetyLevelStatsReceiver
|
||||
.scope("unknown_safety_level")
|
||||
.counter(safetyLevel.name.toLowerCase).incr()
|
||||
}
|
||||
|
||||
def recordRuleMissingFeatures(
|
||||
ruleName: String,
|
||||
missingFeatures: Set[Feature[_]]
|
||||
): Unit = {
|
||||
val ruleStat = ruleStatsReceiver.scope(ruleName)
|
||||
missingFeatures.foreach { featureId =>
|
||||
ruleStat.scope("missing_feature").counter(featureId.name).incr()
|
||||
}
|
||||
ruleStat.scope("action").counter(NotEvaluated.fullName).incr()
|
||||
ruleStat.scope("state").counter(MissingFeature(missingFeatures).name).incr()
|
||||
}
|
||||
|
||||
def recordRuleFailedFeatures(
|
||||
ruleName: String,
|
||||
failedFeatures: Map[Feature[_], Throwable]
|
||||
): Unit = {
|
||||
val ruleStat = ruleStatsReceiver.scope(ruleName)
|
||||
|
||||
ruleStat.scope("action").counter(NotEvaluated.fullName).incr()
|
||||
ruleStat.scope("state").counter(FeatureFailed(failedFeatures).name).incr()
|
||||
}
|
||||
|
||||
def recordFailClosed(rule: String, state: State) {
|
||||
failClosedReceiver.scope(state.name).counter(rule).incr();
|
||||
}
|
||||
|
||||
def recordRuleEvaluation(
|
||||
ruleName: String,
|
||||
action: Action,
|
||||
state: State
|
||||
): Unit = {
|
||||
val ruleStat = ruleStatsReceiver.scope(ruleName)
|
||||
ruleStat.scope("action").counter(action.fullName).incr()
|
||||
ruleStat.scope("state").counter(state.name).incr()
|
||||
}
|
||||
|
||||
|
||||
def recordRuleFallbackAction(
|
||||
ruleName: String
|
||||
): Unit = {
|
||||
val ruleStat = ruleStatsReceiver.scope(ruleName)
|
||||
ruleStat.counter("fallback_action").incr()
|
||||
}
|
||||
|
||||
def recordRuleHoldBack(
|
||||
ruleName: String
|
||||
): Unit = {
|
||||
ruleStatsReceiver.scope(ruleName).counter("heldback").incr()
|
||||
}
|
||||
|
||||
def recordRuleFailed(
|
||||
ruleName: String
|
||||
): Unit = {
|
||||
ruleStatsReceiver.scope(ruleName).counter("failed").incr()
|
||||
}
|
||||
|
||||
def recordDisabledRule(
|
||||
ruleName: String
|
||||
): Unit = recordRuleEvaluation(ruleName, NotEvaluated, Disabled)
|
||||
}
|
||||
|
||||
object NullVisibilityResultsMetricsRecorder
|
||||
extends VisibilityResultsMetricRecorder(NullStatsReceiver, Gate.False)
|
@ -0,0 +1,266 @@
|
||||
package com.twitter.visibility.engine
|
||||
|
||||
import com.twitter.servo.util.Gate
|
||||
import com.twitter.spam.rtf.thriftscala.{SafetyLevel => ThriftSafetyLevel}
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.visibility.builder.VisibilityResult
|
||||
import com.twitter.visibility.builder.VisibilityResultBuilder
|
||||
import com.twitter.visibility.features._
|
||||
import com.twitter.visibility.models.SafetyLevel
|
||||
import com.twitter.visibility.models.SafetyLevel.DeprecatedSafetyLevel
|
||||
import com.twitter.visibility.rules.EvaluationContext
|
||||
import com.twitter.visibility.rules.State._
|
||||
import com.twitter.visibility.rules._
|
||||
import com.twitter.visibility.rules.providers.ProvidedEvaluationContext
|
||||
import com.twitter.visibility.rules.providers.PolicyProvider
|
||||
|
||||
class VisibilityRuleEngine private[VisibilityRuleEngine] (
|
||||
rulePreprocessor: VisibilityRulePreprocessor,
|
||||
metricsRecorder: VisibilityResultsMetricRecorder,
|
||||
enableComposableActions: Gate[Unit],
|
||||
enableFailClosed: Gate[Unit],
|
||||
policyProviderOpt: Option[PolicyProvider] = None)
|
||||
extends DeciderableVisibilityRuleEngine {
|
||||
|
||||
private[visibility] def apply(
|
||||
evaluationContext: ProvidedEvaluationContext,
|
||||
visibilityPolicy: VisibilityPolicy,
|
||||
visibilityResultBuilder: VisibilityResultBuilder,
|
||||
enableShortCircuiting: Gate[Unit],
|
||||
preprocessedRules: Option[Seq[Rule]]
|
||||
): Stitch[VisibilityResult] = {
|
||||
val (resultBuilder, rules) = preprocessedRules match {
|
||||
case Some(r) =>
|
||||
(visibilityResultBuilder, r)
|
||||
case None =>
|
||||
rulePreprocessor.evaluate(evaluationContext, visibilityPolicy, visibilityResultBuilder)
|
||||
}
|
||||
evaluate(evaluationContext, resultBuilder, rules, enableShortCircuiting)
|
||||
}
|
||||
|
||||
def apply(
|
||||
evaluationContext: EvaluationContext,
|
||||
safetyLevel: SafetyLevel,
|
||||
visibilityResultBuilder: VisibilityResultBuilder,
|
||||
enableShortCircuiting: Gate[Unit] = Gate.True,
|
||||
preprocessedRules: Option[Seq[Rule]] = None
|
||||
): Stitch[VisibilityResult] = {
|
||||
val visibilityPolicy = policyProviderOpt match {
|
||||
case Some(policyProvider) =>
|
||||
policyProvider.policyForSurface(safetyLevel)
|
||||
case None => RuleBase.RuleMap(safetyLevel)
|
||||
}
|
||||
if (evaluationContext.params(safetyLevel.enabledParam)) {
|
||||
apply(
|
||||
ProvidedEvaluationContext.injectRuntimeRulesIntoEvaluationContext(
|
||||
evaluationContext = evaluationContext,
|
||||
safetyLevel = Some(safetyLevel),
|
||||
policyProviderOpt = policyProviderOpt
|
||||
),
|
||||
visibilityPolicy,
|
||||
visibilityResultBuilder,
|
||||
enableShortCircuiting,
|
||||
preprocessedRules
|
||||
).onSuccess { result =>
|
||||
metricsRecorder.recordSuccess(safetyLevel, result)
|
||||
}
|
||||
.onFailure { _ =>
|
||||
metricsRecorder.recordAction(safetyLevel, "failure")
|
||||
}
|
||||
} else {
|
||||
metricsRecorder.recordAction(safetyLevel, "disabled")
|
||||
val rules: Seq[Rule] = visibilityPolicy.forContentId(visibilityResultBuilder.contentId)
|
||||
Stitch.value(
|
||||
visibilityResultBuilder
|
||||
.withRuleResultMap(rules.map(r => r -> RuleResult(Allow, Skipped)).toMap)
|
||||
.withVerdict(verdict = Allow)
|
||||
.withFinished(finished = true)
|
||||
.build
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def apply(
|
||||
evaluationContext: EvaluationContext,
|
||||
thriftSafetyLevel: ThriftSafetyLevel,
|
||||
visibilityResultBuilder: VisibilityResultBuilder
|
||||
): Stitch[VisibilityResult] = {
|
||||
val safetyLevel: SafetyLevel = SafetyLevel.fromThrift(thriftSafetyLevel)
|
||||
safetyLevel match {
|
||||
case DeprecatedSafetyLevel =>
|
||||
metricsRecorder.recordUnknownSafetyLevel(safetyLevel)
|
||||
Stitch.value(
|
||||
visibilityResultBuilder
|
||||
.withVerdict(verdict = Allow)
|
||||
.withFinished(finished = true)
|
||||
.build
|
||||
)
|
||||
|
||||
case thriftSafetyLevel: SafetyLevel =>
|
||||
this(
|
||||
ProvidedEvaluationContext.injectRuntimeRulesIntoEvaluationContext(
|
||||
evaluationContext = evaluationContext,
|
||||
safetyLevel = Some(safetyLevel),
|
||||
policyProviderOpt = policyProviderOpt
|
||||
),
|
||||
thriftSafetyLevel,
|
||||
visibilityResultBuilder
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private[visibility] def evaluateRules(
|
||||
evaluationContext: ProvidedEvaluationContext,
|
||||
resolvedFeatureMap: Map[Feature[_], Any],
|
||||
failedFeatures: Map[Feature[_], Throwable],
|
||||
resultBuilderWithoutFailedFeatures: VisibilityResultBuilder,
|
||||
preprocessedRules: Seq[Rule],
|
||||
enableShortCircuiting: Gate[Unit]
|
||||
): VisibilityResultBuilder = {
|
||||
preprocessedRules
|
||||
.foldLeft(resultBuilderWithoutFailedFeatures) { (builder, rule) =>
|
||||
builder.ruleResults.get(rule) match {
|
||||
case Some(RuleResult(_, state)) if state == Evaluated || state == ShortCircuited =>
|
||||
builder
|
||||
|
||||
case _ =>
|
||||
val failedFeatureDependencies: Map[Feature[_], Throwable] =
|
||||
failedFeatures.filterKeys(key => rule.featureDependencies.contains(key))
|
||||
|
||||
val shortCircuit =
|
||||
builder.finished && enableShortCircuiting() &&
|
||||
!(enableComposableActions() && builder.isVerdictComposable())
|
||||
|
||||
if (failedFeatureDependencies.nonEmpty && rule.fallbackActionBuilder.isEmpty) {
|
||||
metricsRecorder.recordRuleFailedFeatures(rule.name, failedFeatureDependencies)
|
||||
builder.withRuleResult(
|
||||
rule,
|
||||
RuleResult(NotEvaluated, FeatureFailed(failedFeatureDependencies)))
|
||||
|
||||
} else if (shortCircuit) {
|
||||
|
||||
metricsRecorder.recordRuleEvaluation(rule.name, NotEvaluated, ShortCircuited)
|
||||
builder.withRuleResult(rule, RuleResult(builder.verdict, ShortCircuited))
|
||||
} else {
|
||||
|
||||
if (rule.fallbackActionBuilder.nonEmpty) {
|
||||
metricsRecorder.recordRuleFallbackAction(rule.name)
|
||||
}
|
||||
|
||||
|
||||
val ruleResult =
|
||||
rule.evaluate(evaluationContext, resolvedFeatureMap)
|
||||
metricsRecorder
|
||||
.recordRuleEvaluation(rule.name, ruleResult.action, ruleResult.state)
|
||||
val nextBuilder = (ruleResult.action, builder.finished) match {
|
||||
case (NotEvaluated | Allow, _) =>
|
||||
ruleResult.state match {
|
||||
case Heldback =>
|
||||
metricsRecorder.recordRuleHoldBack(rule.name)
|
||||
case RuleFailed(_) =>
|
||||
metricsRecorder.recordRuleFailed(rule.name)
|
||||
case _ =>
|
||||
}
|
||||
builder.withRuleResult(rule, ruleResult)
|
||||
|
||||
case (_, true) =>
|
||||
builder
|
||||
.withRuleResult(rule, ruleResult)
|
||||
.withSecondaryVerdict(ruleResult.action, rule)
|
||||
|
||||
case _ =>
|
||||
builder
|
||||
.withRuleResult(rule, ruleResult)
|
||||
.withVerdict(ruleResult.action, Some(rule))
|
||||
.withFinished(true)
|
||||
}
|
||||
|
||||
nextBuilder
|
||||
}
|
||||
}
|
||||
}.withResolvedFeatureMap(resolvedFeatureMap)
|
||||
}
|
||||
|
||||
private[visibility] def evaluateFailClosed(
|
||||
evaluationContext: ProvidedEvaluationContext
|
||||
): VisibilityResultBuilder => Stitch[VisibilityResultBuilder] = { builder =>
|
||||
builder.failClosedException(evaluationContext) match {
|
||||
case Some(e: FailClosedException) if enableFailClosed() =>
|
||||
metricsRecorder.recordFailClosed(e.getRuleName, e.getState);
|
||||
Stitch.exception(e)
|
||||
case _ => Stitch.value(builder)
|
||||
}
|
||||
}
|
||||
|
||||
private[visibility] def checkMarkFinished(
|
||||
builder: VisibilityResultBuilder
|
||||
): VisibilityResult = {
|
||||
val allRulesEvaluated: Boolean = builder.ruleResults.values.forall {
|
||||
case RuleResult(_, state) =>
|
||||
state == Evaluated || state == Disabled || state == Skipped
|
||||
case _ =>
|
||||
false
|
||||
}
|
||||
|
||||
if (allRulesEvaluated) {
|
||||
builder.withFinished(true).build
|
||||
} else {
|
||||
builder.build
|
||||
}
|
||||
}
|
||||
|
||||
private[visibility] def evaluate(
|
||||
evaluationContext: ProvidedEvaluationContext,
|
||||
visibilityResultBuilder: VisibilityResultBuilder,
|
||||
preprocessedRules: Seq[Rule],
|
||||
enableShortCircuiting: Gate[Unit] = Gate.True
|
||||
): Stitch[VisibilityResult] = {
|
||||
|
||||
val finalBuilder =
|
||||
FeatureMap.resolve(visibilityResultBuilder.features, evaluationContext.statsReceiver).map {
|
||||
resolvedFeatureMap =>
|
||||
val (failedFeatureMap, successfulFeatureMap) = resolvedFeatureMap.constantMap.partition({
|
||||
case (_, _: FeatureFailedPlaceholderObject) => true
|
||||
case _ => false
|
||||
})
|
||||
|
||||
val failedFeatures: Map[Feature[_], Throwable] =
|
||||
failedFeatureMap.mapValues({
|
||||
case failurePlaceholder: FeatureFailedPlaceholderObject =>
|
||||
failurePlaceholder.throwable
|
||||
})
|
||||
|
||||
val resultBuilderWithoutFailedFeatures =
|
||||
visibilityResultBuilder.withFeatureMap(ResolvedFeatureMap(successfulFeatureMap))
|
||||
|
||||
evaluateRules(
|
||||
evaluationContext,
|
||||
successfulFeatureMap,
|
||||
failedFeatures,
|
||||
resultBuilderWithoutFailedFeatures,
|
||||
preprocessedRules,
|
||||
enableShortCircuiting
|
||||
)
|
||||
}
|
||||
|
||||
finalBuilder.flatMap(evaluateFailClosed(evaluationContext)).map(checkMarkFinished)
|
||||
}
|
||||
}
|
||||
|
||||
object VisibilityRuleEngine {
|
||||
|
||||
def apply(
|
||||
rulePreprocessor: Option[VisibilityRulePreprocessor] = None,
|
||||
metricsRecorder: VisibilityResultsMetricRecorder = NullVisibilityResultsMetricsRecorder,
|
||||
enableComposableActions: Gate[Unit] = Gate.False,
|
||||
enableFailClosed: Gate[Unit] = Gate.False,
|
||||
policyProviderOpt: Option[PolicyProvider] = None,
|
||||
): VisibilityRuleEngine = {
|
||||
new VisibilityRuleEngine(
|
||||
rulePreprocessor.getOrElse(VisibilityRulePreprocessor(metricsRecorder)),
|
||||
metricsRecorder,
|
||||
enableComposableActions,
|
||||
enableFailClosed,
|
||||
policyProviderOpt = policyProviderOpt)
|
||||
}
|
||||
}
|
@ -0,0 +1,156 @@
|
||||
package com.twitter.visibility.engine
|
||||
|
||||
import com.twitter.abdecider.NullABDecider
|
||||
import com.twitter.util.Return
|
||||
import com.twitter.util.Throw
|
||||
import com.twitter.util.Try
|
||||
import com.twitter.visibility.builder.VisibilityResultBuilder
|
||||
import com.twitter.visibility.features._
|
||||
import com.twitter.visibility.models.SafetyLevel
|
||||
import com.twitter.visibility.rules.Rule.DisabledRuleResult
|
||||
import com.twitter.visibility.rules.Rule.EvaluatedRuleResult
|
||||
import com.twitter.visibility.rules.State._
|
||||
import com.twitter.visibility.rules._
|
||||
import com.twitter.visibility.rules.providers.ProvidedEvaluationContext
|
||||
import com.twitter.visibility.rules.providers.PolicyProvider
|
||||
|
||||
class VisibilityRulePreprocessor private (
|
||||
metricsRecorder: VisibilityResultsMetricRecorder,
|
||||
policyProviderOpt: Option[PolicyProvider] = None) {
|
||||
|
||||
private[engine] def filterEvaluableRules(
|
||||
evaluationContext: ProvidedEvaluationContext,
|
||||
resultBuilder: VisibilityResultBuilder,
|
||||
rules: Seq[Rule]
|
||||
): (VisibilityResultBuilder, Seq[Rule]) = {
|
||||
val (builder, ruleList) = rules.foldLeft((resultBuilder, Seq.empty[Rule])) {
|
||||
case ((builder, nextPassRules), rule) =>
|
||||
if (evaluationContext.ruleEnabledInContext(rule)) {
|
||||
val missingFeatures: Set[Feature[_]] = rule.featureDependencies.collect {
|
||||
case feature: Feature[_] if !builder.featureMap.contains(feature) => feature
|
||||
}
|
||||
|
||||
if (missingFeatures.isEmpty) {
|
||||
(builder, nextPassRules :+ rule)
|
||||
} else {
|
||||
metricsRecorder.recordRuleMissingFeatures(rule.name, missingFeatures)
|
||||
(
|
||||
builder.withRuleResult(
|
||||
rule,
|
||||
RuleResult(NotEvaluated, MissingFeature(missingFeatures))
|
||||
),
|
||||
nextPassRules
|
||||
)
|
||||
}
|
||||
} else {
|
||||
(builder.withRuleResult(rule, DisabledRuleResult), nextPassRules)
|
||||
}
|
||||
}
|
||||
(builder, ruleList)
|
||||
}
|
||||
|
||||
private[visibility] def preFilterRules(
|
||||
evaluationContext: ProvidedEvaluationContext,
|
||||
resolvedFeatureMap: Map[Feature[_], Any],
|
||||
resultBuilder: VisibilityResultBuilder,
|
||||
rules: Seq[Rule]
|
||||
): (VisibilityResultBuilder, Seq[Rule]) = {
|
||||
val isResolvedFeatureMap = resultBuilder.featureMap.isInstanceOf[ResolvedFeatureMap]
|
||||
val (builder, ruleList) = rules.foldLeft((resultBuilder, Seq.empty[Rule])) {
|
||||
case ((builder, nextPassRules), rule) =>
|
||||
rule.preFilter(evaluationContext, resolvedFeatureMap, NullABDecider) match {
|
||||
case NeedsFullEvaluation =>
|
||||
(builder, nextPassRules :+ rule)
|
||||
case NotFiltered =>
|
||||
(builder, nextPassRules :+ rule)
|
||||
case Filtered if isResolvedFeatureMap =>
|
||||
(builder, nextPassRules :+ rule)
|
||||
case Filtered =>
|
||||
(builder.withRuleResult(rule, EvaluatedRuleResult), nextPassRules)
|
||||
}
|
||||
}
|
||||
(builder, ruleList)
|
||||
}
|
||||
|
||||
private[visibility] def evaluate(
|
||||
evaluationContext: ProvidedEvaluationContext,
|
||||
safetyLevel: SafetyLevel,
|
||||
resultBuilder: VisibilityResultBuilder
|
||||
): (VisibilityResultBuilder, Seq[Rule]) = {
|
||||
val visibilityPolicy = policyProviderOpt match {
|
||||
case Some(policyProvider) =>
|
||||
policyProvider.policyForSurface(safetyLevel)
|
||||
case None => RuleBase.RuleMap(safetyLevel)
|
||||
}
|
||||
|
||||
if (evaluationContext.params(safetyLevel.enabledParam)) {
|
||||
evaluate(evaluationContext, visibilityPolicy, resultBuilder)
|
||||
} else {
|
||||
metricsRecorder.recordAction(safetyLevel, "disabled")
|
||||
|
||||
val rules: Seq[Rule] = visibilityPolicy.forContentId(resultBuilder.contentId)
|
||||
val skippedResultBuilder = resultBuilder
|
||||
.withRuleResultMap(rules.map(r => r -> RuleResult(Allow, Skipped)).toMap)
|
||||
.withVerdict(verdict = Allow)
|
||||
.withFinished(finished = true)
|
||||
|
||||
(skippedResultBuilder, rules)
|
||||
}
|
||||
}
|
||||
|
||||
private[visibility] def evaluate(
|
||||
evaluationContext: ProvidedEvaluationContext,
|
||||
visibilityPolicy: VisibilityPolicy,
|
||||
resultBuilder: VisibilityResultBuilder,
|
||||
): (VisibilityResultBuilder, Seq[Rule]) = {
|
||||
|
||||
val rules: Seq[Rule] = visibilityPolicy.forContentId(resultBuilder.contentId)
|
||||
|
||||
val (secondPassBuilder, secondPassRules) =
|
||||
filterEvaluableRules(evaluationContext, resultBuilder, rules)
|
||||
|
||||
val secondPassFeatureMap = secondPassBuilder.featureMap
|
||||
|
||||
val secondPassConstantFeatures: Set[Feature[_]] = RuleBase
|
||||
.getFeaturesForRules(secondPassRules)
|
||||
.filter(secondPassFeatureMap.containsConstant(_))
|
||||
|
||||
val secondPassFeatureValues: Set[(Feature[_], Any)] = secondPassConstantFeatures.map {
|
||||
feature =>
|
||||
Try(secondPassFeatureMap.getConstant(feature)) match {
|
||||
case Return(value) => (feature, value)
|
||||
case Throw(ex) =>
|
||||
metricsRecorder.recordFailedFeature(feature, ex)
|
||||
(feature, FeatureFailedPlaceholderObject(ex))
|
||||
}
|
||||
}
|
||||
|
||||
val resolvedFeatureMap: Map[Feature[_], Any] =
|
||||
secondPassFeatureValues.filterNot {
|
||||
case (_, value) => value.isInstanceOf[FeatureFailedPlaceholderObject]
|
||||
}.toMap
|
||||
|
||||
val (preFilteredResultBuilder, preFilteredRules) = preFilterRules(
|
||||
evaluationContext,
|
||||
resolvedFeatureMap,
|
||||
secondPassBuilder,
|
||||
secondPassRules
|
||||
)
|
||||
|
||||
val preFilteredFeatureMap =
|
||||
RuleBase.removeUnusedFeaturesFromFeatureMap(
|
||||
preFilteredResultBuilder.featureMap,
|
||||
preFilteredRules)
|
||||
|
||||
(preFilteredResultBuilder.withFeatureMap(preFilteredFeatureMap), preFilteredRules)
|
||||
}
|
||||
}
|
||||
|
||||
object VisibilityRulePreprocessor {
|
||||
def apply(
|
||||
metricsRecorder: VisibilityResultsMetricRecorder,
|
||||
policyProviderOpt: Option[PolicyProvider] = None
|
||||
): VisibilityRulePreprocessor = {
|
||||
new VisibilityRulePreprocessor(metricsRecorder, policyProviderOpt)
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package com.twitter.visibility.features
|
||||
|
||||
import com.twitter.gizmoduck.thriftscala.MentionFilter
|
||||
import com.twitter.util.Duration
|
||||
|
||||
case object ViewerFiltersNoConfirmedEmail extends Feature[Boolean]
|
||||
|
||||
case object ViewerFiltersNoConfirmedPhone extends Feature[Boolean]
|
||||
|
||||
case object ViewerFiltersDefaultProfileImage extends Feature[Boolean]
|
||||
|
||||
case object ViewerFiltersNewUsers extends Feature[Boolean]
|
||||
|
||||
case object ViewerFiltersNotFollowedBy extends Feature[Boolean]
|
||||
|
||||
case object ViewerMentionFilter extends Feature[MentionFilter]
|
||||
|
||||
case object AuthorHasConfirmedEmail extends Feature[Boolean]
|
||||
|
||||
case object AuthorHasVerifiedPhone extends Feature[Boolean]
|
||||
|
||||
case object AuthorHasDefaultProfileImage extends Feature[Boolean]
|
||||
|
||||
case object AuthorAccountAge extends Feature[Duration]
|
@ -0,0 +1,17 @@
|
||||
scala_library(
|
||||
sources = ["*.scala"],
|
||||
compiler_option_sets = ["fatal_warnings"],
|
||||
platform = "java8",
|
||||
strict_deps = True,
|
||||
tags = ["bazel-compatible"],
|
||||
dependencies = [
|
||||
"3rdparty/jvm/com/squareup/okhttp:okhttp3",
|
||||
"finagle/finagle-mux/src/main/scala",
|
||||
"src/thrift/com/twitter/gizmoduck:user-thrift-scala",
|
||||
"src/thrift/com/twitter/search/common:constants-scala",
|
||||
"src/thrift/com/twitter/tweetypie:tweet-scala",
|
||||
"stitch/stitch-core",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/models",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/util",
|
||||
],
|
||||
)
|
@ -0,0 +1,11 @@
|
||||
package com.twitter.visibility.features
|
||||
|
||||
import com.twitter.visibility.util.NamingUtils
|
||||
|
||||
abstract class Feature[T] protected ()(implicit val manifest: Manifest[T]) {
|
||||
|
||||
lazy val name: String = NamingUtils.getFriendlyName(this)
|
||||
|
||||
override lazy val toString: String =
|
||||
"Feature[%s](name=%s)".format(manifest, getClass.getSimpleName)
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
package com.twitter.visibility.features
|
||||
|
||||
import com.twitter.finagle.mux.ClientDiscardedRequestException
|
||||
import com.twitter.finagle.stats.NullStatsReceiver
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.stitch.Stitch
|
||||
import scala.language.existentials
|
||||
|
||||
class MissingFeatureException(feature: Feature[_]) extends Exception("Missing value for " + feature)
|
||||
|
||||
case class FeatureFailedException(feature: Feature[_], exception: Throwable) extends Exception
|
||||
|
||||
private[visibility] case class FeatureFailedPlaceholderObject(throwable: Throwable)
|
||||
|
||||
class FeatureMap(
|
||||
val map: Map[Feature[_], Stitch[_]],
|
||||
val constantMap: Map[Feature[_], Any]) {
|
||||
|
||||
def contains[T](feature: Feature[T]): Boolean =
|
||||
constantMap.contains(feature) || map.contains(feature)
|
||||
|
||||
def containsConstant[T](feature: Feature[T]): Boolean = constantMap.contains(feature)
|
||||
|
||||
lazy val size: Int = keys.size
|
||||
|
||||
lazy val keys: Set[Feature[_]] = constantMap.keySet ++ map.keySet
|
||||
|
||||
def get[T](feature: Feature[T]): Stitch[T] = {
|
||||
map.get(feature) match {
|
||||
case _ if constantMap.contains(feature) =>
|
||||
Stitch.value(getConstant(feature))
|
||||
case Some(x) =>
|
||||
x.asInstanceOf[Stitch[T]]
|
||||
case _ =>
|
||||
Stitch.exception(new MissingFeatureException(feature))
|
||||
}
|
||||
}
|
||||
|
||||
def getConstant[T](feature: Feature[T]): T = {
|
||||
constantMap.get(feature) match {
|
||||
case Some(x) =>
|
||||
x.asInstanceOf[T]
|
||||
case _ =>
|
||||
throw new MissingFeatureException(feature)
|
||||
}
|
||||
}
|
||||
|
||||
def -[T](key: Feature[T]): FeatureMap = new FeatureMap(map - key, constantMap - key)
|
||||
|
||||
override def toString: String = "FeatureMap(%s, %s)".format(map, constantMap)
|
||||
}
|
||||
|
||||
object FeatureMap {
|
||||
|
||||
def empty: FeatureMap = new FeatureMap(Map.empty, Map.empty)
|
||||
|
||||
def resolve(
|
||||
featureMap: FeatureMap,
|
||||
statsReceiver: StatsReceiver = NullStatsReceiver
|
||||
): Stitch[ResolvedFeatureMap] = {
|
||||
val featureMapHydrationStatsReceiver = statsReceiver.scope("feature_map_hydration")
|
||||
|
||||
Stitch
|
||||
.traverse(featureMap.map.toSeq) {
|
||||
case (feature, value: Stitch[_]) =>
|
||||
val featureStatsReceiver = featureMapHydrationStatsReceiver.scope(feature.name)
|
||||
lazy val featureFailureStat = featureStatsReceiver.scope("failures")
|
||||
val featureStitch: Stitch[(Feature[_], Any)] = value
|
||||
.map { resolvedValue =>
|
||||
featureStatsReceiver.counter("success").incr()
|
||||
(feature, resolvedValue)
|
||||
}
|
||||
|
||||
featureStitch
|
||||
.handle {
|
||||
case ffe: FeatureFailedException =>
|
||||
featureFailureStat.counter().incr()
|
||||
featureFailureStat.counter(ffe.exception.getClass.getName).incr()
|
||||
(feature, FeatureFailedPlaceholderObject(ffe.exception))
|
||||
}
|
||||
.ensure {
|
||||
featureStatsReceiver.counter("requests").incr()
|
||||
}
|
||||
}
|
||||
.map { resolvedFeatures: Seq[(Feature[_], Any)] =>
|
||||
new ResolvedFeatureMap(resolvedFeatures.toMap ++ featureMap.constantMap)
|
||||
}
|
||||
}
|
||||
|
||||
def rescueFeatureTuple(kv: (Feature[_], Stitch[_])): (Feature[_], Stitch[_]) = {
|
||||
val (k, v) = kv
|
||||
|
||||
val rescueValue = v.rescue {
|
||||
case e =>
|
||||
e match {
|
||||
case cdre: ClientDiscardedRequestException => Stitch.exception(cdre)
|
||||
case _ => Stitch.exception(FeatureFailedException(k, e))
|
||||
}
|
||||
}
|
||||
|
||||
(k, rescueValue)
|
||||
}
|
||||
}
|
||||
|
||||
class ResolvedFeatureMap(private[visibility] val resolvedMap: Map[Feature[_], Any])
|
||||
extends FeatureMap(Map.empty, resolvedMap) {
|
||||
|
||||
override def equals(other: Any): Boolean = other match {
|
||||
case otherResolvedFeatureMap: ResolvedFeatureMap =>
|
||||
this.resolvedMap.equals(otherResolvedFeatureMap.resolvedMap)
|
||||
case _ => false
|
||||
}
|
||||
|
||||
override def toString: String = "ResolvedFeatureMap(%s)".format(resolvedMap)
|
||||
}
|
||||
|
||||
object ResolvedFeatureMap {
|
||||
def apply(resolvedMap: Map[Feature[_], Any]): ResolvedFeatureMap = {
|
||||
new ResolvedFeatureMap(resolvedMap)
|
||||
}
|
||||
}
|
@ -0,0 +1,269 @@
|
||||
package com.twitter.visibility.features
|
||||
|
||||
import com.twitter.contenthealth.toxicreplyfilter.thriftscala.FilterState
|
||||
import com.twitter.gizmoduck.thriftscala.Label
|
||||
import com.twitter.search.common.constants.thriftscala.ThriftQuerySource
|
||||
import com.twitter.tseng.withholding.thriftscala.TakedownReason
|
||||
import com.twitter.util.Duration
|
||||
import com.twitter.util.Time
|
||||
import com.twitter.visibility.models.TweetDeleteReason.TweetDeleteReason
|
||||
import com.twitter.visibility.models._
|
||||
|
||||
case object AuthorId extends Feature[Set[Long]]
|
||||
|
||||
case object ViewerId extends Feature[Long]
|
||||
|
||||
case object AuthorIsProtected extends Feature[Boolean]
|
||||
|
||||
case object AuthorIsSuspended extends Feature[Boolean]
|
||||
|
||||
case object AuthorIsUnavailable extends Feature[Boolean]
|
||||
|
||||
case object AuthorIsDeactivated extends Feature[Boolean]
|
||||
|
||||
case object AuthorIsErased extends Feature[Boolean]
|
||||
|
||||
case object AuthorIsOffboarded extends Feature[Boolean]
|
||||
|
||||
case object AuthorIsVerified extends Feature[Boolean]
|
||||
|
||||
case object AuthorIsBlueVerified extends Feature[Boolean]
|
||||
|
||||
case object ViewerIsSuspended extends Feature[Boolean]
|
||||
|
||||
case object ViewerIsDeactivated extends Feature[Boolean]
|
||||
|
||||
case object AuthorFollowsViewer extends Feature[Boolean]
|
||||
|
||||
case object AuthorUserLabels extends Feature[Seq[Label]]
|
||||
|
||||
case object ViewerFollowsAuthorOfViolatingTweet extends Feature[Boolean]
|
||||
|
||||
case object ViewerDoesNotFollowAuthorOfViolatingTweet extends Feature[Boolean]
|
||||
|
||||
case object ViewerFollowsAuthor extends Feature[Boolean]
|
||||
|
||||
case object ViewerBlocksAuthor extends Feature[Boolean]
|
||||
|
||||
case object AuthorBlocksViewer extends Feature[Boolean]
|
||||
|
||||
case object AuthorMutesViewer extends Feature[Boolean]
|
||||
|
||||
case object ViewerMutesAuthor extends Feature[Boolean]
|
||||
|
||||
case object AuthorReportsViewerAsSpam extends Feature[Boolean]
|
||||
|
||||
case object ViewerReportsAuthorAsSpam extends Feature[Boolean]
|
||||
|
||||
case object ViewerReportedTweet extends Feature[Boolean]
|
||||
|
||||
case object ViewerMutesRetweetsFromAuthor extends Feature[Boolean]
|
||||
|
||||
case object ViewerHasUniversalQualityFilterEnabled extends Feature[Boolean]
|
||||
|
||||
case object ViewerIsProtected extends Feature[Boolean]
|
||||
|
||||
case object ViewerIsSoftUser extends Feature[Boolean]
|
||||
|
||||
case object TweetSafetyLabels extends Feature[Seq[TweetSafetyLabel]]
|
||||
|
||||
case object SpaceSafetyLabels extends Feature[Seq[SpaceSafetyLabel]]
|
||||
|
||||
case object MediaSafetyLabels extends Feature[Seq[MediaSafetyLabel]]
|
||||
|
||||
case object TweetTakedownReasons extends Feature[Seq[TakedownReason]]
|
||||
|
||||
case object AuthorTakedownReasons extends Feature[Seq[TakedownReason]]
|
||||
|
||||
case object AuthorIsNsfwUser extends Feature[Boolean]
|
||||
|
||||
case object AuthorIsNsfwAdmin extends Feature[Boolean]
|
||||
|
||||
case object TweetHasNsfwUser extends Feature[Boolean]
|
||||
|
||||
case object TweetHasNsfwAdmin extends Feature[Boolean]
|
||||
|
||||
case object TweetHasMedia extends Feature[Boolean]
|
||||
|
||||
case object CardHasMedia extends Feature[Boolean]
|
||||
|
||||
case object TweetHasCard extends Feature[Boolean]
|
||||
|
||||
case object ViewerMutesKeywordInTweetForHomeTimeline extends Feature[MutedKeyword]
|
||||
|
||||
case object ViewerMutesKeywordInTweetForTweetReplies extends Feature[MutedKeyword]
|
||||
|
||||
case object ViewerMutesKeywordInTweetForNotifications extends Feature[MutedKeyword]
|
||||
|
||||
case object ViewerMutesKeywordInSpaceTitleForNotifications extends Feature[MutedKeyword]
|
||||
|
||||
case object ViewerMutesKeywordInTweetForAllSurfaces extends Feature[MutedKeyword]
|
||||
|
||||
case object ViewerUserLabels extends Feature[Seq[Label]]
|
||||
|
||||
case object RequestCountryCode extends Feature[String]
|
||||
|
||||
case object RequestIsVerifiedCrawler extends Feature[Boolean]
|
||||
|
||||
case object ViewerCountryCode extends Feature[String]
|
||||
|
||||
case object TweetIsSelfReply extends Feature[Boolean]
|
||||
|
||||
case object TweetIsNullcast extends Feature[Boolean]
|
||||
|
||||
case object TweetTimestamp extends Feature[Time]
|
||||
|
||||
case object TweetIsInnerQuotedTweet extends Feature[Boolean]
|
||||
|
||||
case object TweetIsRetweet extends Feature[Boolean]
|
||||
|
||||
case object TweetIsSourceTweet extends Feature[Boolean]
|
||||
|
||||
case object TweetDeleteReason extends Feature[TweetDeleteReason]
|
||||
|
||||
case object TweetReplyToParentTweetDuration extends Feature[Duration]
|
||||
|
||||
case object TweetReplyToRootTweetDuration extends Feature[Duration]
|
||||
|
||||
case object TweetHasCommunityConversationControl extends Feature[Boolean]
|
||||
case object TweetHasByInvitationConversationControl extends Feature[Boolean]
|
||||
case object TweetHasFollowersConversationControl extends Feature[Boolean]
|
||||
case object TweetConversationViewerIsInvited extends Feature[Boolean]
|
||||
case object TweetConversationViewerIsInvitedViaReplyMention extends Feature[Boolean]
|
||||
case object TweetConversationViewerIsRootAuthor extends Feature[Boolean]
|
||||
case object ConversationRootAuthorFollowsViewer extends Feature[Boolean]
|
||||
case object ViewerFollowsConversationRootAuthor extends Feature[Boolean]
|
||||
|
||||
case object TweetIsExclusiveTweet extends Feature[Boolean]
|
||||
case object ViewerIsExclusiveTweetRootAuthor extends Feature[Boolean]
|
||||
case object ViewerSuperFollowsExclusiveTweetRootAuthor extends Feature[Boolean]
|
||||
|
||||
case object TweetIsCommunityTweet extends Feature[Boolean]
|
||||
|
||||
case object CommunityTweetCommunityNotFound extends Feature[Boolean]
|
||||
|
||||
case object CommunityTweetCommunityDeleted extends Feature[Boolean]
|
||||
|
||||
case object CommunityTweetCommunitySuspended extends Feature[Boolean]
|
||||
|
||||
case object CommunityTweetCommunityVisible extends Feature[Boolean]
|
||||
|
||||
case object CommunityTweetIsHidden extends Feature[Boolean]
|
||||
|
||||
case object ViewerIsInternalCommunitiesAdmin extends Feature[Boolean]
|
||||
|
||||
case object ViewerIsCommunityAdmin extends Feature[Boolean]
|
||||
|
||||
case object ViewerIsCommunityModerator extends Feature[Boolean]
|
||||
|
||||
case object ViewerIsCommunityMember extends Feature[Boolean]
|
||||
|
||||
case object CommunityTweetAuthorIsRemoved extends Feature[Boolean]
|
||||
|
||||
case object NotificationIsOnCommunityTweet extends Feature[Boolean]
|
||||
|
||||
case object NotificationIsOnUnmentionedViewer extends Feature[Boolean]
|
||||
|
||||
case object SearchResultsPageNumber extends Feature[Int]
|
||||
|
||||
case object SearchCandidateCount extends Feature[Int]
|
||||
|
||||
case object SearchQuerySource extends Feature[ThriftQuerySource]
|
||||
|
||||
case object SearchQueryHasUser extends Feature[Boolean]
|
||||
|
||||
case object TweetSemanticCoreAnnotations extends Feature[Seq[SemanticCoreAnnotation]]
|
||||
|
||||
case object OuterAuthorId extends Feature[Long]
|
||||
|
||||
case object AuthorBlocksOuterAuthor extends Feature[Boolean]
|
||||
|
||||
case object OuterAuthorFollowsAuthor extends Feature[Boolean]
|
||||
|
||||
case object OuterAuthorIsInnerAuthor extends Feature[Boolean]
|
||||
|
||||
case object TweetIsModerated extends Feature[Boolean]
|
||||
case object FocalTweetId extends Feature[Long]
|
||||
|
||||
case object TweetId extends Feature[Long]
|
||||
|
||||
case object TweetConversationId extends Feature[Long]
|
||||
case object TweetParentId extends Feature[Long]
|
||||
case object ConversationRootAuthorIsVerified extends Feature[Boolean]
|
||||
|
||||
case object ViewerOptInBlocking extends Feature[Boolean]
|
||||
|
||||
case object ViewerOptInFiltering extends Feature[Boolean]
|
||||
|
||||
case object ViewerRoles extends Feature[Seq[String]] {
|
||||
val EmployeeRole = "employee"
|
||||
}
|
||||
|
||||
case object TweetMisinformationPolicies extends Feature[Seq[MisinformationPolicy]]
|
||||
|
||||
case object TweetEnglishMisinformationPolicies extends Feature[Seq[MisinformationPolicy]]
|
||||
|
||||
case object HasInnerCircleOfFriendsRelationship extends Feature[Boolean]
|
||||
|
||||
case object ViewerAge extends Feature[UserAge]
|
||||
|
||||
case object HasDmcaMediaFeature extends Feature[Boolean]
|
||||
|
||||
case object MediaGeoRestrictionsAllowList extends Feature[Seq[String]]
|
||||
case object MediaGeoRestrictionsDenyList extends Feature[Seq[String]]
|
||||
|
||||
case object TweetIsTrustedFriendTweet extends Feature[Boolean]
|
||||
case object ViewerIsTrustedFriendTweetAuthor extends Feature[Boolean]
|
||||
case object ViewerIsTrustedFriendOfTweetAuthor extends Feature[Boolean]
|
||||
|
||||
case object DmConversationIsOneToOneConversation extends Feature[Boolean]
|
||||
case object DmConversationHasEmptyTimeline extends Feature[Boolean]
|
||||
case object DmConversationHasValidLastReadableEventId extends Feature[Boolean]
|
||||
case object DmConversationInfoExists extends Feature[Boolean]
|
||||
case object DmConversationTimelineExists extends Feature[Boolean]
|
||||
case object ViewerIsDmConversationParticipant extends Feature[Boolean]
|
||||
|
||||
case object DmEventIsMessageCreateEvent extends Feature[Boolean]
|
||||
case object DmEventIsWelcomeMessageCreateEvent extends Feature[Boolean]
|
||||
case object DmEventIsLastMessageReadUpdateEvent extends Feature[Boolean]
|
||||
case object DmEventIsDeleted extends Feature[Boolean]
|
||||
case object DmEventIsHidden extends Feature[Boolean]
|
||||
case object ViewerIsDmEventInitiatingUser extends Feature[Boolean]
|
||||
case object DmEventInOneToOneConversationWithUnavailableUser extends Feature[Boolean]
|
||||
case object DmEventIsJoinConversationEvent extends Feature[Boolean]
|
||||
case object DmEventIsConversationCreateEvent extends Feature[Boolean]
|
||||
case object DmEventInOneToOneConversation extends Feature[Boolean]
|
||||
case object DmEventIsTrustConversationEvent extends Feature[Boolean]
|
||||
case object DmEventIsCsFeedbackSubmitted extends Feature[Boolean]
|
||||
case object DmEventIsCsFeedbackDismissed extends Feature[Boolean]
|
||||
case object DmEventIsPerspectivalJoinConversationEvent extends Feature[Boolean]
|
||||
|
||||
case object DmEventOccurredBeforeLastClearedEvent extends Feature[Boolean]
|
||||
case object DmEventOccurredBeforeJoinConversationEvent extends Feature[Boolean]
|
||||
|
||||
case object CardUriHost extends Feature[String]
|
||||
case object CardIsPoll extends Feature[Boolean]
|
||||
|
||||
case object TweetIsStaleTweet extends Feature[Boolean]
|
||||
|
||||
case object TweetIsEditTweet extends Feature[Boolean]
|
||||
|
||||
case object TweetIsLatestTweet extends Feature[Boolean]
|
||||
|
||||
case object TweetIsInitialTweet extends Feature[Boolean]
|
||||
|
||||
case object TweetIsCollabInvitationTweet extends Feature[Boolean]
|
||||
|
||||
case object ViewerSensitiveMediaSettings extends Feature[UserSensitiveMediaSettings]
|
||||
|
||||
|
||||
case object ToxicReplyFilterState extends Feature[FilterState]
|
||||
|
||||
|
||||
case object ToxicReplyFilterConversationAuthorIsViewer extends Feature[Boolean]
|
||||
|
||||
case object RawQuery extends Feature[String]
|
||||
|
||||
case object AuthorScreenName extends Feature[String]
|
||||
|
||||
case object TweetIsInternalPromotedContent extends Feature[Boolean]
|
@ -0,0 +1,30 @@
|
||||
scala_library(
|
||||
sources = ["*.scala"],
|
||||
compiler_option_sets = ["fatal_warnings"],
|
||||
platform = "java8",
|
||||
strict_deps = True,
|
||||
tags = ["bazel-compatible"],
|
||||
dependencies = [
|
||||
"3rdparty/jvm/com/ibm/icu:icu4j",
|
||||
"configapi/configapi-core",
|
||||
"decider/src/main/scala",
|
||||
"src/thrift/com/twitter/gizmoduck:thrift-scala",
|
||||
"src/thrift/com/twitter/gizmoduck:user-thrift-scala",
|
||||
"src/thrift/com/twitter/spam/rtf:safety-result-scala",
|
||||
"stitch/stitch-core",
|
||||
"strato/src/main/scala/com/twitter/strato/client",
|
||||
"twitter-config/yaml",
|
||||
"visibility/common/src/main/scala/com/twitter/visibility/common",
|
||||
"visibility/common/src/main/scala/com/twitter/visibility/common/actions",
|
||||
"visibility/common/src/main/scala/com/twitter/visibility/common/user_result",
|
||||
"visibility/common/src/main/thrift/com/twitter/visibility:action-scala",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/builder",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/configapi",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/configapi/configs",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/features",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/models",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/rules",
|
||||
"visibility/results/src/main/scala/com/twitter/visibility/results/richtext",
|
||||
"visibility/results/src/main/scala/com/twitter/visibility/results/translation",
|
||||
],
|
||||
)
|
@ -0,0 +1,58 @@
|
||||
package com.twitter.visibility.generators
|
||||
|
||||
import com.ibm.icu.util.ULocale
|
||||
import com.twitter.config.yaml.YamlMap
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
|
||||
object CountryNameGenerator {
|
||||
|
||||
private val AuroraFilesystemPath = "/usr/local/twitter-config/twitter/config/"
|
||||
|
||||
private val ContentBlockingSupportedCountryList = "takedown_countries.yml"
|
||||
|
||||
def providesFromConfigBus(statsReceiver: StatsReceiver): CountryNameGenerator = {
|
||||
fromFile(AuroraFilesystemPath + ContentBlockingSupportedCountryList, statsReceiver)
|
||||
}
|
||||
|
||||
def providesWithCustomMap(countryCodeMap: Map[String, String], statsReceiver: StatsReceiver) = {
|
||||
new CountryNameGenerator(countryCodeMap, statsReceiver)
|
||||
}
|
||||
|
||||
private def fromFile(fileName: String, statsReceiver: StatsReceiver) = {
|
||||
val yamlConfig = YamlMap.load(fileName)
|
||||
val countryCodeMap: Map[String, String] = yamlConfig.keySet.map { countryCode: String =>
|
||||
val normalizedCode = countryCode.toUpperCase
|
||||
val countryName: Option[String] =
|
||||
yamlConfig.get(Seq(countryCode, "name")).asInstanceOf[Option[String]]
|
||||
(normalizedCode, countryName.getOrElse(normalizedCode))
|
||||
}.toMap
|
||||
new CountryNameGenerator(countryCodeMap, statsReceiver)
|
||||
}
|
||||
}
|
||||
|
||||
class CountryNameGenerator(countryCodeMap: Map[String, String], statsReceiver: StatsReceiver) {
|
||||
|
||||
private val scopedStatsReceiver = statsReceiver.scope("country_name_generator")
|
||||
private val foundCountryReceiver = scopedStatsReceiver.counter("found")
|
||||
private val missingCountryReceiver = scopedStatsReceiver.counter("missing")
|
||||
|
||||
def getCountryName(code: String): String = {
|
||||
val normalizedCode = code.toUpperCase
|
||||
countryCodeMap.get(normalizedCode) match {
|
||||
case Some(retrievedName) => {
|
||||
foundCountryReceiver.incr()
|
||||
retrievedName
|
||||
}
|
||||
case _ => {
|
||||
missingCountryReceiver.incr()
|
||||
val fallbackName =
|
||||
new ULocale("", normalizedCode).getDisplayCountry(ULocale.forLanguageTag("en"))
|
||||
|
||||
if (fallbackName == "")
|
||||
normalizedCode
|
||||
else
|
||||
fallbackName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package com.twitter.visibility.generators
|
||||
|
||||
import com.twitter.visibility.common.actions.LocalizedMessage
|
||||
import com.twitter.visibility.common.actions.MessageLink
|
||||
import com.twitter.visibility.results.translation.Translator
|
||||
import com.twitter.visibility.results.richtext.EpitaphToRichText
|
||||
import com.twitter.visibility.results.translation.Resource
|
||||
import com.twitter.visibility.results.translation.LearnMoreLink
|
||||
import com.twitter.visibility.rules.Epitaph
|
||||
import com.twitter.visibility.results.richtext.EpitaphToRichText.Copy
|
||||
|
||||
object EpitaphToLocalizedMessage {
|
||||
def apply(
|
||||
epitaph: Epitaph,
|
||||
languageTag: String,
|
||||
): LocalizedMessage = {
|
||||
val copy =
|
||||
EpitaphToRichText.EpitaphToPolicyMap.getOrElse(epitaph, EpitaphToRichText.FallbackPolicy)
|
||||
val text = Translator.translate(
|
||||
copy.resource,
|
||||
languageTag
|
||||
)
|
||||
localizeWithCopyAndText(copy, languageTag, text)
|
||||
}
|
||||
|
||||
def apply(
|
||||
epitaph: Epitaph,
|
||||
languageTag: String,
|
||||
applicableCountries: Seq[String],
|
||||
): LocalizedMessage = {
|
||||
val copy =
|
||||
EpitaphToRichText.EpitaphToPolicyMap.getOrElse(epitaph, EpitaphToRichText.FallbackPolicy)
|
||||
val text = Translator.translateWithSimplePlaceholderReplacement(
|
||||
copy.resource,
|
||||
languageTag,
|
||||
Map((Resource.ApplicableCountriesPlaceholder -> applicableCountries.mkString(", ")))
|
||||
)
|
||||
localizeWithCopyAndText(copy, languageTag, text)
|
||||
}
|
||||
|
||||
private def localizeWithCopyAndText(
|
||||
copy: Copy,
|
||||
languageTag: String,
|
||||
text: String
|
||||
): LocalizedMessage = {
|
||||
val learnMore = Translator.translate(LearnMoreLink, languageTag)
|
||||
|
||||
val links = copy.additionalLinks match {
|
||||
case links if links.nonEmpty =>
|
||||
MessageLink(Resource.LearnMorePlaceholder, learnMore, copy.link) +:
|
||||
links.map {
|
||||
case EpitaphToRichText.Link(placeholder, copyResource, link) =>
|
||||
val copyText = Translator.translate(copyResource, languageTag)
|
||||
MessageLink(placeholder, copyText, link)
|
||||
}
|
||||
case _ =>
|
||||
Seq(
|
||||
MessageLink(
|
||||
key = Resource.LearnMorePlaceholder,
|
||||
displayText = learnMore,
|
||||
uri = copy.link))
|
||||
}
|
||||
|
||||
LocalizedMessage(message = text, language = languageTag, links = links)
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package com.twitter.visibility.generators
|
||||
|
||||
import com.twitter.visibility.common.actions.InterstitialReason
|
||||
import com.twitter.visibility.common.actions.LocalizedMessage
|
||||
import com.twitter.visibility.common.actions.MessageLink
|
||||
import com.twitter.visibility.results.richtext.InterstitialReasonToRichText
|
||||
import com.twitter.visibility.results.richtext.InterstitialReasonToRichText.InterstitialCopy
|
||||
import com.twitter.visibility.results.richtext.InterstitialReasonToRichText.InterstitialLink
|
||||
import com.twitter.visibility.results.translation.LearnMoreLink
|
||||
import com.twitter.visibility.results.translation.Resource
|
||||
import com.twitter.visibility.results.translation.Translator
|
||||
|
||||
object InterstitialReasonToLocalizedMessage {
|
||||
def apply(
|
||||
reason: InterstitialReason,
|
||||
languageTag: String,
|
||||
): Option[LocalizedMessage] = {
|
||||
InterstitialReasonToRichText.reasonToCopy(reason).map { copy =>
|
||||
val text = Translator.translate(
|
||||
copy.resource,
|
||||
languageTag
|
||||
)
|
||||
localizeWithCopyAndText(copy, languageTag, text)
|
||||
}
|
||||
}
|
||||
|
||||
private def localizeWithCopyAndText(
|
||||
copy: InterstitialCopy,
|
||||
languageTag: String,
|
||||
text: String
|
||||
): LocalizedMessage = {
|
||||
val learnMore = Translator.translate(LearnMoreLink, languageTag)
|
||||
|
||||
val learnMoreLinkOpt =
|
||||
copy.link.map { link =>
|
||||
MessageLink(key = Resource.LearnMorePlaceholder, displayText = learnMore, uri = link)
|
||||
}
|
||||
val additionalLinks = copy.additionalLinks.map {
|
||||
case InterstitialLink(placeholder, copyResource, link) =>
|
||||
val copyText = Translator.translate(copyResource, languageTag)
|
||||
MessageLink(key = placeholder, displayText = copyText, uri = link)
|
||||
}
|
||||
|
||||
val links = learnMoreLinkOpt.toSeq ++ additionalLinks
|
||||
LocalizedMessage(message = text, language = languageTag, links = links)
|
||||
}
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
package com.twitter.visibility.generators
|
||||
|
||||
import com.twitter.decider.Decider
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.visibility.builder.VisibilityResult
|
||||
import com.twitter.visibility.common.actions.LocalizedMessage
|
||||
import com.twitter.visibility.common.actions.MessageLink
|
||||
import com.twitter.visibility.configapi.configs.VisibilityDeciderGates
|
||||
import com.twitter.visibility.results.richtext.PublicInterestReasonToRichText
|
||||
import com.twitter.visibility.results.translation.LearnMoreLink
|
||||
import com.twitter.visibility.results.translation.Resource
|
||||
import com.twitter.visibility.results.translation.SafetyResultReasonToResource
|
||||
import com.twitter.visibility.results.translation.Translator
|
||||
import com.twitter.visibility.rules.EmergencyDynamicInterstitial
|
||||
import com.twitter.visibility.rules.Interstitial
|
||||
import com.twitter.visibility.rules.InterstitialLimitedEngagements
|
||||
import com.twitter.visibility.rules.PublicInterest
|
||||
import com.twitter.visibility.rules.Reason
|
||||
import com.twitter.visibility.rules.TweetInterstitial
|
||||
|
||||
object LocalizedInterstitialGenerator {
|
||||
def apply(
|
||||
visibilityDecider: Decider,
|
||||
baseStatsReceiver: StatsReceiver,
|
||||
): LocalizedInterstitialGenerator = {
|
||||
new LocalizedInterstitialGenerator(visibilityDecider, baseStatsReceiver)
|
||||
}
|
||||
}
|
||||
|
||||
class LocalizedInterstitialGenerator private (
|
||||
val visibilityDecider: Decider,
|
||||
val baseStatsReceiver: StatsReceiver) {
|
||||
|
||||
private val visibilityDeciderGates = VisibilityDeciderGates(visibilityDecider)
|
||||
private val localizationStatsReceiver = baseStatsReceiver.scope("interstitial_localization")
|
||||
private val publicInterestInterstitialStats =
|
||||
localizationStatsReceiver.scope("public_interest_copy")
|
||||
private val emergencyDynamicInterstitialStats =
|
||||
localizationStatsReceiver.scope("emergency_dynamic_copy")
|
||||
private val regularInterstitialStats = localizationStatsReceiver.scope("interstitial_copy")
|
||||
|
||||
def apply(visibilityResult: VisibilityResult, languageTag: String): VisibilityResult = {
|
||||
if (!visibilityDeciderGates.enableLocalizedInterstitialGenerator()) {
|
||||
return visibilityResult
|
||||
}
|
||||
|
||||
visibilityResult.verdict match {
|
||||
case ipi: InterstitialLimitedEngagements if PublicInterest.Reasons.contains(ipi.reason) =>
|
||||
visibilityResult.copy(
|
||||
verdict = ipi.copy(
|
||||
localizedMessage = Some(localizePublicInterestCopyInResult(ipi, languageTag))
|
||||
))
|
||||
case edi: EmergencyDynamicInterstitial =>
|
||||
visibilityResult.copy(
|
||||
verdict = EmergencyDynamicInterstitial(
|
||||
edi.copy,
|
||||
edi.linkOpt,
|
||||
Some(localizeEmergencyDynamicCopyInResult(edi, languageTag))
|
||||
))
|
||||
case interstitial: Interstitial =>
|
||||
visibilityResult.copy(
|
||||
verdict = interstitial.copy(
|
||||
localizedMessage = localizeInterstitialCopyInResult(interstitial, languageTag)
|
||||
))
|
||||
case tweetInterstitial: TweetInterstitial if tweetInterstitial.interstitial.isDefined =>
|
||||
tweetInterstitial.interstitial.get match {
|
||||
case ipi: InterstitialLimitedEngagements if PublicInterest.Reasons.contains(ipi.reason) =>
|
||||
visibilityResult.copy(
|
||||
verdict = tweetInterstitial.copy(
|
||||
interstitial = Some(
|
||||
ipi.copy(
|
||||
localizedMessage = Some(localizePublicInterestCopyInResult(ipi, languageTag))
|
||||
))
|
||||
))
|
||||
case edi: EmergencyDynamicInterstitial =>
|
||||
visibilityResult.copy(
|
||||
verdict = tweetInterstitial.copy(
|
||||
interstitial = Some(
|
||||
EmergencyDynamicInterstitial(
|
||||
edi.copy,
|
||||
edi.linkOpt,
|
||||
Some(localizeEmergencyDynamicCopyInResult(edi, languageTag))
|
||||
))
|
||||
))
|
||||
case interstitial: Interstitial =>
|
||||
visibilityResult.copy(
|
||||
verdict = tweetInterstitial.copy(
|
||||
interstitial = Some(
|
||||
interstitial.copy(
|
||||
localizedMessage = localizeInterstitialCopyInResult(interstitial, languageTag)
|
||||
))
|
||||
))
|
||||
case _ => visibilityResult
|
||||
}
|
||||
case _ => visibilityResult
|
||||
}
|
||||
}
|
||||
|
||||
private def localizeEmergencyDynamicCopyInResult(
|
||||
edi: EmergencyDynamicInterstitial,
|
||||
languageTag: String
|
||||
): LocalizedMessage = {
|
||||
val text = edi.linkOpt
|
||||
.map(_ => s"${edi.copy} {${Resource.LearnMorePlaceholder}}")
|
||||
.getOrElse(edi.copy)
|
||||
|
||||
val messageLinks = edi.linkOpt
|
||||
.map { link =>
|
||||
val learnMoreText = Translator.translate(LearnMoreLink, languageTag)
|
||||
Seq(MessageLink(Resource.LearnMorePlaceholder, learnMoreText, link))
|
||||
}.getOrElse(Seq.empty)
|
||||
|
||||
emergencyDynamicInterstitialStats.counter("localized").incr()
|
||||
LocalizedMessage(text, languageTag, messageLinks)
|
||||
}
|
||||
|
||||
private def localizePublicInterestCopyInResult(
|
||||
ipi: InterstitialLimitedEngagements,
|
||||
languageTag: String
|
||||
): LocalizedMessage = {
|
||||
val safetyResultReason = PublicInterest.ReasonToSafetyResultReason(ipi.reason)
|
||||
val text = Translator.translate(
|
||||
SafetyResultReasonToResource.resource(safetyResultReason),
|
||||
languageTag,
|
||||
)
|
||||
|
||||
val learnMoreLink = PublicInterestReasonToRichText.toLearnMoreLink(safetyResultReason)
|
||||
val learnMoreText = Translator.translate(LearnMoreLink, languageTag)
|
||||
val messageLinks = Seq(MessageLink(Resource.LearnMorePlaceholder, learnMoreText, learnMoreLink))
|
||||
|
||||
publicInterestInterstitialStats.counter("localized").incr()
|
||||
LocalizedMessage(text, languageTag, messageLinks)
|
||||
}
|
||||
|
||||
private def localizeInterstitialCopyInResult(
|
||||
interstitial: Interstitial,
|
||||
languageTag: String
|
||||
): Option[LocalizedMessage] = {
|
||||
val localizedMessageOpt = Reason
|
||||
.toInterstitialReason(interstitial.reason)
|
||||
.flatMap(InterstitialReasonToLocalizedMessage(_, languageTag))
|
||||
|
||||
if (localizedMessageOpt.isDefined) {
|
||||
regularInterstitialStats.counter("localized").incr()
|
||||
localizedMessageOpt
|
||||
} else {
|
||||
regularInterstitialStats.counter("empty").incr()
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
package com.twitter.visibility.generators
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.servo.util.MemoizingStatsReceiver
|
||||
import com.twitter.visibility.builder.VisibilityResult
|
||||
import com.twitter.visibility.common.actions.TombstoneReason
|
||||
import com.twitter.visibility.configapi.VisibilityParams
|
||||
import com.twitter.visibility.rules.Epitaph
|
||||
import com.twitter.visibility.rules.LocalizedTombstone
|
||||
import com.twitter.visibility.rules.Tombstone
|
||||
|
||||
object TombstoneGenerator {
|
||||
def apply(
|
||||
visibilityParams: VisibilityParams,
|
||||
countryNameGenerator: CountryNameGenerator,
|
||||
statsReceiver: StatsReceiver
|
||||
): TombstoneGenerator = {
|
||||
new TombstoneGenerator(visibilityParams, countryNameGenerator, statsReceiver)
|
||||
}
|
||||
}
|
||||
|
||||
class TombstoneGenerator(
|
||||
paramsFactory: VisibilityParams,
|
||||
countryNameGenerator: CountryNameGenerator,
|
||||
baseStatsReceiver: StatsReceiver) {
|
||||
|
||||
private[this] val statsReceiver = new MemoizingStatsReceiver(
|
||||
baseStatsReceiver.scope("tombstone_generator"))
|
||||
private[this] val deletedReceiver = statsReceiver.scope("deleted_state")
|
||||
private[this] val authorStateReceiver = statsReceiver.scope("tweet_author_state")
|
||||
private[this] val visResultReceiver = statsReceiver.scope("visibility_result")
|
||||
|
||||
def apply(
|
||||
result: VisibilityResult,
|
||||
language: String
|
||||
): VisibilityResult = {
|
||||
|
||||
result.verdict match {
|
||||
case tombstone: Tombstone =>
|
||||
val epitaph = tombstone.epitaph
|
||||
visResultReceiver.scope("tombstone").counter(epitaph.name.toLowerCase())
|
||||
|
||||
val overriddenLanguage = epitaph match {
|
||||
case Epitaph.LegalDemandsWithheldMedia | Epitaph.LocalLawsWithheldMedia => "en"
|
||||
case _ => language
|
||||
}
|
||||
|
||||
tombstone.applicableCountryCodes match {
|
||||
case Some(countryCodes) => {
|
||||
val countryNames = countryCodes.map(countryNameGenerator.getCountryName(_))
|
||||
|
||||
result.copy(verdict = LocalizedTombstone(
|
||||
reason = epitaphToTombstoneReason(epitaph),
|
||||
message = EpitaphToLocalizedMessage(epitaph, overriddenLanguage, countryNames)))
|
||||
}
|
||||
case _ => {
|
||||
result.copy(verdict = LocalizedTombstone(
|
||||
reason = epitaphToTombstoneReason(epitaph),
|
||||
message = EpitaphToLocalizedMessage(epitaph, overriddenLanguage)))
|
||||
}
|
||||
}
|
||||
case _ =>
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
private def epitaphToTombstoneReason(epitaph: Epitaph): TombstoneReason = {
|
||||
epitaph match {
|
||||
case Epitaph.Deleted => TombstoneReason.Deleted
|
||||
case Epitaph.Bounced => TombstoneReason.Bounced
|
||||
case Epitaph.BounceDeleted => TombstoneReason.BounceDeleted
|
||||
case Epitaph.Protected => TombstoneReason.ProtectedAuthor
|
||||
case Epitaph.Suspended => TombstoneReason.SuspendedAuthor
|
||||
case Epitaph.BlockedBy => TombstoneReason.AuthorBlocksViewer
|
||||
case Epitaph.SuperFollowsContent => TombstoneReason.ExclusiveTweet
|
||||
case Epitaph.Underage => TombstoneReason.NsfwViewerIsUnderage
|
||||
case Epitaph.NoStatedAge => TombstoneReason.NsfwViewerHasNoStatedAge
|
||||
case Epitaph.LoggedOutAge => TombstoneReason.NsfwLoggedOut
|
||||
case Epitaph.Deactivated => TombstoneReason.DeactivatedAuthor
|
||||
case Epitaph.CommunityTweetHidden => TombstoneReason.CommunityTweetHidden
|
||||
case Epitaph.CommunityTweetCommunityIsSuspended =>
|
||||
TombstoneReason.CommunityTweetCommunityIsSuspended
|
||||
case Epitaph.DevelopmentOnly => TombstoneReason.DevelopmentOnly
|
||||
case Epitaph.AdultMedia => TombstoneReason.AdultMedia
|
||||
case Epitaph.ViolentMedia => TombstoneReason.ViolentMedia
|
||||
case Epitaph.OtherSensitiveMedia => TombstoneReason.OtherSensitiveMedia
|
||||
case Epitaph.DmcaWithheldMedia => TombstoneReason.DmcaWithheldMedia
|
||||
case Epitaph.LegalDemandsWithheldMedia => TombstoneReason.LegalDemandsWithheldMedia
|
||||
case Epitaph.LocalLawsWithheldMedia => TombstoneReason.LocalLawsWithheldMedia
|
||||
case Epitaph.ToxicReplyFiltered => TombstoneReason.ReplyFiltered
|
||||
case _ => TombstoneReason.Unspecified
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
scala_library(
|
||||
sources = ["*.scala"],
|
||||
compiler_option_sets = ["fatal_warnings"],
|
||||
platform = "java8",
|
||||
strict_deps = True,
|
||||
tags = ["bazel-compatible"],
|
||||
dependencies = [
|
||||
"3rdparty/jvm/com/twitter/src/java/com/twitter/logpipeline/client:logpipeline-event-publisher-thin",
|
||||
"decider/src/main/scala",
|
||||
"mediaservices/media-util/src/main/scala",
|
||||
"servo/decider/src/main/scala",
|
||||
"src/thrift/com/twitter/escherbird:media-annotation-structs-scala",
|
||||
"src/thrift/com/twitter/spam/rtf:safety-level-scala",
|
||||
"src/thrift/com/twitter/spam/rtf:tweet-rtf-event-scala",
|
||||
"src/thrift/com/twitter/tweetypie:tweet-scala",
|
||||
"stitch/stitch-core",
|
||||
"strato/src/main/scala/com/twitter/strato/client",
|
||||
"visibility/common/src/main/scala/com/twitter/visibility/common",
|
||||
"visibility/lib/src/main/resources/config",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/builder/media",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/builder/tweets",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/builder/users",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/configapi/configs",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/features",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/interfaces/common/blender",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/interfaces/common/tweets",
|
||||
"visibility/lib/src/main/thrift/com/twitter/visibility/logging:vf-logging-scala",
|
||||
],
|
||||
exports = [
|
||||
"visibility/common/src/main/scala/com/twitter/visibility/common",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility",
|
||||
],
|
||||
)
|
@ -0,0 +1,416 @@
|
||||
package com.twitter.visibility.interfaces.blender
|
||||
|
||||
import com.twitter.decider.Decider
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.mediaservices.media_util.GenericMediaKey
|
||||
import com.twitter.servo.util.Gate
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.strato.client.{Client => StratoClient}
|
||||
import com.twitter.tweetypie.thriftscala.Tweet
|
||||
import com.twitter.util.Stopwatch
|
||||
import com.twitter.visibility.VisibilityLibrary
|
||||
import com.twitter.visibility.builder.VerdictLogger
|
||||
import com.twitter.visibility.builder.VisibilityResult
|
||||
import com.twitter.visibility.builder.media.MediaFeatures
|
||||
import com.twitter.visibility.builder.media.StratoMediaLabelMaps
|
||||
import com.twitter.visibility.builder.tweets._
|
||||
import com.twitter.visibility.builder.users.AuthorFeatures
|
||||
import com.twitter.visibility.builder.users.RelationshipFeatures
|
||||
import com.twitter.visibility.builder.users.ViewerFeatures
|
||||
import com.twitter.visibility.common.MediaSafetyLabelMapSource
|
||||
import com.twitter.visibility.common.MisinformationPolicySource
|
||||
import com.twitter.visibility.common.SafetyLabelMapSource
|
||||
import com.twitter.visibility.common.TrustedFriendsSource
|
||||
import com.twitter.visibility.common.UserRelationshipSource
|
||||
import com.twitter.visibility.common.UserSource
|
||||
import com.twitter.visibility.rules.ComposableActions.ComposableActionsWithInterstitial
|
||||
import com.twitter.visibility.configapi.configs.VisibilityDeciderGates
|
||||
import com.twitter.visibility.features.FeatureMap
|
||||
import com.twitter.visibility.features.TweetIsInnerQuotedTweet
|
||||
import com.twitter.visibility.features.TweetIsRetweet
|
||||
import com.twitter.visibility.features.TweetIsSourceTweet
|
||||
import com.twitter.visibility.logging.thriftscala.VFLibType
|
||||
import com.twitter.visibility.models.ContentId
|
||||
import com.twitter.visibility.models.ContentId.BlenderTweetId
|
||||
import com.twitter.visibility.models.ContentId.TweetId
|
||||
import com.twitter.visibility.models.SafetyLevel
|
||||
import com.twitter.visibility.models.SafetyLevel.toThrift
|
||||
import com.twitter.visibility.rules.Action
|
||||
import com.twitter.visibility.rules.Allow
|
||||
import com.twitter.visibility.rules.Drop
|
||||
import com.twitter.visibility.rules.Interstitial
|
||||
import com.twitter.visibility.rules.TweetInterstitial
|
||||
|
||||
object TweetType extends Enumeration {
|
||||
type TweetType = Value
|
||||
val ORIGINAL, SOURCE, QUOTED = Value
|
||||
}
|
||||
import com.twitter.visibility.interfaces.blender.TweetType._
|
||||
|
||||
object BlenderVisibilityLibrary {
|
||||
def buildWithStratoClient(
|
||||
visibilityLibrary: VisibilityLibrary,
|
||||
decider: Decider,
|
||||
stratoClient: StratoClient,
|
||||
userSource: UserSource,
|
||||
userRelationshipSource: UserRelationshipSource
|
||||
): BlenderVisibilityLibrary = new BlenderVisibilityLibrary(
|
||||
visibilityLibrary,
|
||||
decider,
|
||||
stratoClient,
|
||||
userSource,
|
||||
userRelationshipSource,
|
||||
None
|
||||
)
|
||||
|
||||
def buildWithSafetyLabelMapSource(
|
||||
visibilityLibrary: VisibilityLibrary,
|
||||
decider: Decider,
|
||||
stratoClient: StratoClient,
|
||||
userSource: UserSource,
|
||||
userRelationshipSource: UserRelationshipSource,
|
||||
safetyLabelMapSource: SafetyLabelMapSource
|
||||
): BlenderVisibilityLibrary = new BlenderVisibilityLibrary(
|
||||
visibilityLibrary,
|
||||
decider,
|
||||
stratoClient,
|
||||
userSource,
|
||||
userRelationshipSource,
|
||||
Some(safetyLabelMapSource)
|
||||
)
|
||||
|
||||
def createVerdictLogger(
|
||||
enableVerdictLogger: Gate[Unit],
|
||||
decider: Decider,
|
||||
statsReceiver: StatsReceiver
|
||||
): VerdictLogger = {
|
||||
if (enableVerdictLogger()) {
|
||||
VerdictLogger(statsReceiver, decider)
|
||||
} else {
|
||||
VerdictLogger.Empty
|
||||
}
|
||||
}
|
||||
|
||||
def scribeVisibilityVerdict(
|
||||
result: CombinedVisibilityResult,
|
||||
enableVerdictScribing: Gate[Unit],
|
||||
verdictLogger: VerdictLogger,
|
||||
viewerId: Option[Long],
|
||||
safetyLevel: SafetyLevel
|
||||
): Unit = if (enableVerdictScribing()) {
|
||||
verdictLogger.scribeVerdict(
|
||||
visibilityResult = result.tweetVisibilityResult,
|
||||
viewerId = viewerId,
|
||||
safetyLevel = toThrift(safetyLevel),
|
||||
vfLibType = VFLibType.BlenderVisibilityLibrary)
|
||||
|
||||
result.quotedTweetVisibilityResult.map(quotedTweetVisibilityResult =>
|
||||
verdictLogger.scribeVerdict(
|
||||
visibilityResult = quotedTweetVisibilityResult,
|
||||
viewerId = viewerId,
|
||||
safetyLevel = toThrift(safetyLevel),
|
||||
vfLibType = VFLibType.BlenderVisibilityLibrary))
|
||||
}
|
||||
}
|
||||
|
||||
class BlenderVisibilityLibrary(
|
||||
visibilityLibrary: VisibilityLibrary,
|
||||
decider: Decider,
|
||||
stratoClient: StratoClient,
|
||||
userSource: UserSource,
|
||||
userRelationshipSource: UserRelationshipSource,
|
||||
safetyLabelMapSourceOption: Option[SafetyLabelMapSource]) {
|
||||
|
||||
val libraryStatsReceiver = visibilityLibrary.statsReceiver
|
||||
val stratoClientStatsReceiver = visibilityLibrary.statsReceiver.scope("strato")
|
||||
val vfEngineCounter = libraryStatsReceiver.counter("vf_engine_requests")
|
||||
val bvlRequestCounter = libraryStatsReceiver.counter("bvl_requests")
|
||||
val vfLatencyOverallStat = libraryStatsReceiver.stat("vf_latency_overall")
|
||||
val vfLatencyStitchBuildStat = libraryStatsReceiver.stat("vf_latency_stitch_build")
|
||||
val vfLatencyStitchRunStat = libraryStatsReceiver.stat("vf_latency_stitch_run")
|
||||
val visibilityDeciderGates = VisibilityDeciderGates(decider)
|
||||
val verdictLogger = BlenderVisibilityLibrary.createVerdictLogger(
|
||||
visibilityDeciderGates.enableVerdictLoggerBVL,
|
||||
decider,
|
||||
libraryStatsReceiver)
|
||||
|
||||
val tweetLabels = safetyLabelMapSourceOption match {
|
||||
case Some(safetyLabelMapSource) =>
|
||||
new StratoTweetLabelMaps(safetyLabelMapSource)
|
||||
case None =>
|
||||
new StratoTweetLabelMaps(
|
||||
SafetyLabelMapSource.fromStrato(stratoClient, stratoClientStatsReceiver))
|
||||
}
|
||||
|
||||
val mediaLabelMaps = new StratoMediaLabelMaps(
|
||||
MediaSafetyLabelMapSource.fromStrato(stratoClient, stratoClientStatsReceiver))
|
||||
|
||||
val tweetFeatures = new TweetFeatures(tweetLabels, libraryStatsReceiver)
|
||||
val blenderContextFeatures = new BlenderContextFeatures(libraryStatsReceiver)
|
||||
val authorFeatures = new AuthorFeatures(userSource, libraryStatsReceiver)
|
||||
val viewerFeatures = new ViewerFeatures(userSource, libraryStatsReceiver)
|
||||
val relationshipFeatures =
|
||||
new RelationshipFeatures(userRelationshipSource, libraryStatsReceiver)
|
||||
val fonsrRelationshipFeatures =
|
||||
new FosnrRelationshipFeatures(
|
||||
tweetLabels = tweetLabels,
|
||||
userRelationshipSource = userRelationshipSource,
|
||||
statsReceiver = libraryStatsReceiver)
|
||||
val misinfoPolicySource =
|
||||
MisinformationPolicySource.fromStrato(stratoClient, stratoClientStatsReceiver)
|
||||
val misinfoPolicyFeatures =
|
||||
new MisinformationPolicyFeatures(misinfoPolicySource, stratoClientStatsReceiver)
|
||||
val exclusiveTweetFeatures =
|
||||
new ExclusiveTweetFeatures(userRelationshipSource, libraryStatsReceiver)
|
||||
val mediaFeatures = new MediaFeatures(mediaLabelMaps, libraryStatsReceiver)
|
||||
val trustedFriendsTweetFeatures = new TrustedFriendsFeatures(
|
||||
trustedFriendsSource = TrustedFriendsSource.fromStrato(stratoClient, stratoClientStatsReceiver))
|
||||
val editTweetFeatures = new EditTweetFeatures(libraryStatsReceiver)
|
||||
|
||||
def getCombinedVisibilityResult(
|
||||
bvRequest: BlenderVisibilityRequest
|
||||
): Stitch[CombinedVisibilityResult] = {
|
||||
val elapsed = Stopwatch.start()
|
||||
bvlRequestCounter.incr()
|
||||
|
||||
val (
|
||||
requestTweetVisibilityResult,
|
||||
quotedTweetVisibilityResultOption,
|
||||
sourceTweetVisibilityResultOption
|
||||
) = getAllVisibilityResults(bvRequest: BlenderVisibilityRequest)
|
||||
|
||||
val response: Stitch[CombinedVisibilityResult] = {
|
||||
(
|
||||
requestTweetVisibilityResult,
|
||||
quotedTweetVisibilityResultOption,
|
||||
sourceTweetVisibilityResultOption) match {
|
||||
case (requestTweetVisResult, Some(quotedTweetVisResult), Some(sourceTweetVisResult)) => {
|
||||
Stitch
|
||||
.join(
|
||||
requestTweetVisResult,
|
||||
quotedTweetVisResult,
|
||||
sourceTweetVisResult
|
||||
).map {
|
||||
case (requestTweetVisResult, quotedTweetVisResult, sourceTweetVisResult) => {
|
||||
requestTweetVisResult.verdict match {
|
||||
case Allow =>
|
||||
CombinedVisibilityResult(sourceTweetVisResult, Some(quotedTweetVisResult))
|
||||
case _ =>
|
||||
CombinedVisibilityResult(requestTweetVisResult, Some(quotedTweetVisResult))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case (requestTweetVisResult, None, Some(sourceTweetVisResult)) => {
|
||||
Stitch
|
||||
.join(
|
||||
requestTweetVisResult,
|
||||
sourceTweetVisResult
|
||||
).map {
|
||||
case (requestTweetVisResult, sourceTweetVisResult) => {
|
||||
requestTweetVisResult.verdict match {
|
||||
case Allow =>
|
||||
CombinedVisibilityResult(sourceTweetVisResult, None)
|
||||
case _ =>
|
||||
CombinedVisibilityResult(requestTweetVisResult, None)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case (requestTweetVisResult, Some(quotedTweetVisResult), None) => {
|
||||
Stitch
|
||||
.join(
|
||||
requestTweetVisResult,
|
||||
quotedTweetVisResult
|
||||
).map {
|
||||
case (requestTweetVisResult, quotedTweetVisResult) => {
|
||||
CombinedVisibilityResult(requestTweetVisResult, Some(quotedTweetVisResult))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case (requestTweetVisResult, None, None) => {
|
||||
requestTweetVisResult.map {
|
||||
CombinedVisibilityResult(_, None)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val runStitchStartMs = elapsed().inMilliseconds
|
||||
val buildStitchStatMs = elapsed().inMilliseconds
|
||||
vfLatencyStitchBuildStat.add(buildStitchStatMs)
|
||||
|
||||
response
|
||||
.onSuccess(_ => {
|
||||
val overallMs = elapsed().inMilliseconds
|
||||
vfLatencyOverallStat.add(overallMs)
|
||||
val stitchRunMs = elapsed().inMilliseconds - runStitchStartMs
|
||||
vfLatencyStitchRunStat.add(stitchRunMs)
|
||||
})
|
||||
.onSuccess(
|
||||
BlenderVisibilityLibrary.scribeVisibilityVerdict(
|
||||
_,
|
||||
visibilityDeciderGates.enableVerdictScribingBVL,
|
||||
verdictLogger,
|
||||
bvRequest.viewerContext.userId,
|
||||
bvRequest.safetyLevel))
|
||||
}
|
||||
|
||||
def getContentId(viewerId: Option[Long], authorId: Long, tweet: Tweet): ContentId = {
|
||||
if (viewerId.contains(authorId))
|
||||
TweetId(tweet.id)
|
||||
else BlenderTweetId(tweet.id)
|
||||
}
|
||||
|
||||
def getAllVisibilityResults(bvRequest: BlenderVisibilityRequest): (
|
||||
Stitch[VisibilityResult],
|
||||
Option[Stitch[VisibilityResult]],
|
||||
Option[Stitch[VisibilityResult]]
|
||||
) = {
|
||||
val tweetContentId = getContentId(
|
||||
viewerId = bvRequest.viewerContext.userId,
|
||||
authorId = bvRequest.tweet.coreData.get.userId,
|
||||
tweet = bvRequest.tweet)
|
||||
|
||||
val tweetFeatureMap =
|
||||
buildFeatureMap(bvRequest, bvRequest.tweet, ORIGINAL)
|
||||
vfEngineCounter.incr()
|
||||
val requestTweetVisibilityResult = visibilityLibrary
|
||||
.runRuleEngine(
|
||||
tweetContentId,
|
||||
tweetFeatureMap,
|
||||
bvRequest.viewerContext,
|
||||
bvRequest.safetyLevel
|
||||
).map(handleComposableVisibilityResult)
|
||||
|
||||
val quotedTweetVisibilityResultOption = bvRequest.quotedTweet.map(quotedTweet => {
|
||||
val quotedTweetContentId = getContentId(
|
||||
viewerId = bvRequest.viewerContext.userId,
|
||||
authorId = quotedTweet.coreData.get.userId,
|
||||
tweet = quotedTweet)
|
||||
|
||||
val quotedInnerTweetFeatureMap =
|
||||
buildFeatureMap(bvRequest, quotedTweet, QUOTED)
|
||||
vfEngineCounter.incr()
|
||||
visibilityLibrary
|
||||
.runRuleEngine(
|
||||
quotedTweetContentId,
|
||||
quotedInnerTweetFeatureMap,
|
||||
bvRequest.viewerContext,
|
||||
bvRequest.safetyLevel
|
||||
)
|
||||
.map(handleComposableVisibilityResult)
|
||||
.map(handleInnerQuotedTweetVisibilityResult)
|
||||
})
|
||||
|
||||
val sourceTweetVisibilityResultOption = bvRequest.retweetSourceTweet.map(sourceTweet => {
|
||||
val sourceTweetContentId = getContentId(
|
||||
viewerId = bvRequest.viewerContext.userId,
|
||||
authorId = sourceTweet.coreData.get.userId,
|
||||
tweet = sourceTweet)
|
||||
|
||||
val sourceTweetFeatureMap =
|
||||
buildFeatureMap(bvRequest, sourceTweet, SOURCE)
|
||||
vfEngineCounter.incr()
|
||||
visibilityLibrary
|
||||
.runRuleEngine(
|
||||
sourceTweetContentId,
|
||||
sourceTweetFeatureMap,
|
||||
bvRequest.viewerContext,
|
||||
bvRequest.safetyLevel
|
||||
)
|
||||
.map(handleComposableVisibilityResult)
|
||||
})
|
||||
|
||||
(
|
||||
requestTweetVisibilityResult,
|
||||
quotedTweetVisibilityResultOption,
|
||||
sourceTweetVisibilityResultOption)
|
||||
}
|
||||
|
||||
def buildFeatureMap(
|
||||
bvRequest: BlenderVisibilityRequest,
|
||||
tweet: Tweet,
|
||||
tweetType: TweetType
|
||||
): FeatureMap = {
|
||||
val authorId = tweet.coreData.get.userId
|
||||
val viewerId = bvRequest.viewerContext.userId
|
||||
val isRetweet = if (tweetType.equals(ORIGINAL)) bvRequest.isRetweet else false
|
||||
val isSourceTweet = tweetType.equals(SOURCE)
|
||||
val isQuotedTweet = tweetType.equals(QUOTED)
|
||||
val tweetMediaKeys: Seq[GenericMediaKey] = tweet.media
|
||||
.getOrElse(Seq.empty)
|
||||
.flatMap(_.mediaKey.map(GenericMediaKey.apply))
|
||||
|
||||
visibilityLibrary.featureMapBuilder(
|
||||
Seq(
|
||||
viewerFeatures
|
||||
.forViewerBlenderContext(bvRequest.blenderVFRequestContext, bvRequest.viewerContext),
|
||||
relationshipFeatures.forAuthorId(authorId, viewerId),
|
||||
fonsrRelationshipFeatures
|
||||
.forTweetAndAuthorId(tweet = tweet, authorId = authorId, viewerId = viewerId),
|
||||
tweetFeatures.forTweet(tweet),
|
||||
mediaFeatures.forMediaKeys(tweetMediaKeys),
|
||||
authorFeatures.forAuthorId(authorId),
|
||||
blenderContextFeatures.forBlenderContext(bvRequest.blenderVFRequestContext),
|
||||
_.withConstantFeature(TweetIsRetweet, isRetweet),
|
||||
misinfoPolicyFeatures.forTweet(tweet, bvRequest.viewerContext),
|
||||
exclusiveTweetFeatures.forTweet(tweet, bvRequest.viewerContext),
|
||||
trustedFriendsTweetFeatures.forTweet(tweet, viewerId),
|
||||
editTweetFeatures.forTweet(tweet),
|
||||
_.withConstantFeature(TweetIsInnerQuotedTweet, isQuotedTweet),
|
||||
_.withConstantFeature(TweetIsSourceTweet, isSourceTweet),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def handleComposableVisibilityResult(result: VisibilityResult): VisibilityResult = {
|
||||
if (result.secondaryVerdicts.nonEmpty) {
|
||||
result.copy(verdict = composeActions(result.verdict, result.secondaryVerdicts))
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
private def composeActions(primary: Action, secondary: Seq[Action]): Action = {
|
||||
if (primary.isComposable && secondary.nonEmpty) {
|
||||
val actions = Seq[Action] { primary } ++ secondary
|
||||
val interstitialOpt = Action.getFirstInterstitial(actions: _*)
|
||||
val softInterventionOpt = Action.getFirstSoftIntervention(actions: _*)
|
||||
val limitedEngagementsOpt = Action.getFirstLimitedEngagements(actions: _*)
|
||||
val avoidOpt = Action.getFirstAvoid(actions: _*)
|
||||
|
||||
val numActions =
|
||||
Seq[Option[_]](interstitialOpt, softInterventionOpt, limitedEngagementsOpt, avoidOpt)
|
||||
.count(_.isDefined)
|
||||
if (numActions > 1) {
|
||||
TweetInterstitial(
|
||||
interstitialOpt,
|
||||
softInterventionOpt,
|
||||
limitedEngagementsOpt,
|
||||
None,
|
||||
avoidOpt
|
||||
)
|
||||
} else {
|
||||
primary
|
||||
}
|
||||
} else {
|
||||
primary
|
||||
}
|
||||
}
|
||||
|
||||
def handleInnerQuotedTweetVisibilityResult(
|
||||
result: VisibilityResult
|
||||
): VisibilityResult = {
|
||||
val newVerdict: Action =
|
||||
result.verdict match {
|
||||
case interstitial: Interstitial => Drop(interstitial.reason)
|
||||
case ComposableActionsWithInterstitial(tweetInterstitial) => Drop(tweetInterstitial.reason)
|
||||
case verdict => verdict
|
||||
}
|
||||
|
||||
result.copy(verdict = newVerdict)
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package com.twitter.visibility.interfaces.blender
|
||||
|
||||
import com.twitter.tweetypie.thriftscala.Tweet
|
||||
import com.twitter.visibility.models.SafetyLevel
|
||||
import com.twitter.visibility.models.ViewerContext
|
||||
import com.twitter.visibility.interfaces.common.blender.BlenderVFRequestContext
|
||||
|
||||
case class BlenderVisibilityRequest(
|
||||
tweet: Tweet,
|
||||
quotedTweet: Option[Tweet],
|
||||
retweetSourceTweet: Option[Tweet] = None,
|
||||
isRetweet: Boolean,
|
||||
safetyLevel: SafetyLevel,
|
||||
viewerContext: ViewerContext,
|
||||
blenderVFRequestContext: BlenderVFRequestContext) {
|
||||
|
||||
def getTweetID: Long = tweet.id
|
||||
|
||||
def hasQuotedTweet: Boolean = {
|
||||
quotedTweet.nonEmpty
|
||||
}
|
||||
def hasSourceTweet: Boolean = {
|
||||
retweetSourceTweet.nonEmpty
|
||||
}
|
||||
|
||||
def getQuotedTweetId: Long = {
|
||||
quotedTweet match {
|
||||
case Some(qTweet) =>
|
||||
qTweet.id
|
||||
case None =>
|
||||
-1
|
||||
}
|
||||
}
|
||||
def getSourceTweetId: Long = {
|
||||
retweetSourceTweet match {
|
||||
case Some(sourceTweet) =>
|
||||
sourceTweet.id
|
||||
case None =>
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.twitter.visibility.interfaces.blender
|
||||
|
||||
import com.twitter.visibility.builder.VisibilityResult
|
||||
|
||||
case class CombinedVisibilityResult(
|
||||
tweetVisibilityResult: VisibilityResult,
|
||||
quotedTweetVisibilityResult: Option[VisibilityResult])
|
@ -0,0 +1,17 @@
|
||||
scala_library(
|
||||
sources = ["*.scala"],
|
||||
compiler_option_sets = ["fatal_warnings"],
|
||||
platform = "java8",
|
||||
strict_deps = False,
|
||||
tags = ["bazel-compatible"],
|
||||
dependencies = [
|
||||
"appsec/sanitization-lib/src/main/scala",
|
||||
"src/thrift/com/twitter/expandodo:cards-scala",
|
||||
"stitch/stitch-core",
|
||||
"visibility/lib/src/main/resources/config",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/builder/tweets",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/builder/users",
|
||||
"visibility/lib/src/main/scala/com/twitter/visibility/features",
|
||||
],
|
||||
)
|
@ -0,0 +1,187 @@
|
||||
package com.twitter.visibility.interfaces.cards
|
||||
|
||||
import com.twitter.appsec.sanitization.URLSafety
|
||||
import com.twitter.decider.Decider
|
||||
import com.twitter.servo.util.Gate
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.tweetypie.{thriftscala => tweetypiethrift}
|
||||
import com.twitter.util.Stopwatch
|
||||
import com.twitter.visibility.VisibilityLibrary
|
||||
import com.twitter.visibility.builder.FeatureMapBuilder
|
||||
import com.twitter.visibility.builder.VisibilityResult
|
||||
import com.twitter.visibility.builder.tweets.CommunityTweetFeatures
|
||||
import com.twitter.visibility.builder.tweets.CommunityTweetFeaturesV2
|
||||
import com.twitter.visibility.builder.tweets.NilTweetLabelMaps
|
||||
import com.twitter.visibility.builder.tweets.TweetFeatures
|
||||
import com.twitter.visibility.builder.users.AuthorFeatures
|
||||
import com.twitter.visibility.builder.users.RelationshipFeatures
|
||||
import com.twitter.visibility.builder.users.ViewerFeatures
|
||||
import com.twitter.visibility.common.CommunitiesSource
|
||||
import com.twitter.visibility.common.UserId
|
||||
import com.twitter.visibility.common.UserRelationshipSource
|
||||
import com.twitter.visibility.common.UserSource
|
||||
import com.twitter.visibility.configapi.configs.VisibilityDeciderGates
|
||||
import com.twitter.visibility.features.CardIsPoll
|
||||
import com.twitter.visibility.features.CardUriHost
|
||||
import com.twitter.visibility.features.FeatureMap
|
||||
import com.twitter.visibility.models.ContentId.CardId
|
||||
import com.twitter.visibility.models.ViewerContext
|
||||
|
||||
object CardVisibilityLibrary {
|
||||
type Type = CardVisibilityRequest => Stitch[VisibilityResult]
|
||||
|
||||
private[this] def getAuthorFeatures(
|
||||
authorIdOpt: Option[Long],
|
||||
authorFeatures: AuthorFeatures
|
||||
): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
authorIdOpt match {
|
||||
case Some(authorId) => authorFeatures.forAuthorId(authorId)
|
||||
case _ => authorFeatures.forNoAuthor()
|
||||
}
|
||||
}
|
||||
|
||||
private[this] def getCardUriFeatures(
|
||||
cardUri: String,
|
||||
enableCardVisibilityLibraryCardUriParsing: Boolean,
|
||||
trackCardUriHost: Option[String] => Unit
|
||||
): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
if (enableCardVisibilityLibraryCardUriParsing) {
|
||||
val safeCardUriHost = URLSafety.getHostSafe(cardUri)
|
||||
trackCardUriHost(safeCardUriHost)
|
||||
|
||||
_.withConstantFeature(CardUriHost, safeCardUriHost)
|
||||
} else {
|
||||
identity
|
||||
}
|
||||
}
|
||||
|
||||
private[this] def getRelationshipFeatures(
|
||||
authorIdOpt: Option[Long],
|
||||
viewerIdOpt: Option[Long],
|
||||
relationshipFeatures: RelationshipFeatures
|
||||
): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
authorIdOpt match {
|
||||
case Some(authorId) => relationshipFeatures.forAuthorId(authorId, viewerIdOpt)
|
||||
case _ => relationshipFeatures.forNoAuthor()
|
||||
}
|
||||
}
|
||||
|
||||
private[this] def getTweetFeatures(
|
||||
tweetOpt: Option[tweetypiethrift.Tweet],
|
||||
tweetFeatures: TweetFeatures
|
||||
): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
tweetOpt match {
|
||||
case Some(tweet) => tweetFeatures.forTweet(tweet)
|
||||
case _ => identity
|
||||
}
|
||||
}
|
||||
|
||||
private[this] def getCommunityFeatures(
|
||||
tweetOpt: Option[tweetypiethrift.Tweet],
|
||||
viewerContext: ViewerContext,
|
||||
communityTweetFeatures: CommunityTweetFeatures
|
||||
): FeatureMapBuilder => FeatureMapBuilder = {
|
||||
tweetOpt match {
|
||||
case Some(tweet) => communityTweetFeatures.forTweet(tweet, viewerContext)
|
||||
case _ => identity
|
||||
}
|
||||
}
|
||||
|
||||
def apply(
|
||||
visibilityLibrary: VisibilityLibrary,
|
||||
userSource: UserSource = UserSource.empty,
|
||||
userRelationshipSource: UserRelationshipSource = UserRelationshipSource.empty,
|
||||
communitiesSource: CommunitiesSource = CommunitiesSource.empty,
|
||||
enableVfParityTest: Gate[Unit] = Gate.False,
|
||||
enableVfFeatureHydration: Gate[Unit] = Gate.False,
|
||||
decider: Decider
|
||||
): Type = {
|
||||
val libraryStatsReceiver = visibilityLibrary.statsReceiver
|
||||
val vfLatencyOverallStat = libraryStatsReceiver.stat("vf_latency_overall")
|
||||
val vfLatencyStitchBuildStat = libraryStatsReceiver.stat("vf_latency_stitch_build")
|
||||
val vfLatencyStitchRunStat = libraryStatsReceiver.stat("vf_latency_stitch_run")
|
||||
val cardUriStats = libraryStatsReceiver.scope("card_uri")
|
||||
val visibilityDeciderGates = VisibilityDeciderGates(decider)
|
||||
|
||||
val authorFeatures = new AuthorFeatures(userSource, libraryStatsReceiver)
|
||||
val viewerFeatures = new ViewerFeatures(userSource, libraryStatsReceiver)
|
||||
val tweetFeatures = new TweetFeatures(NilTweetLabelMaps, libraryStatsReceiver)
|
||||
val communityTweetFeatures = new CommunityTweetFeaturesV2(
|
||||
communitiesSource = communitiesSource,
|
||||
)
|
||||
val relationshipFeatures =
|
||||
new RelationshipFeatures(userRelationshipSource, libraryStatsReceiver)
|
||||
val parityTest = new CardVisibilityLibraryParityTest(libraryStatsReceiver)
|
||||
|
||||
{ r: CardVisibilityRequest =>
|
||||
val elapsed = Stopwatch.start()
|
||||
var runStitchStartMs = 0L
|
||||
|
||||
val viewerId: Option[UserId] = r.viewerContext.userId
|
||||
|
||||
val featureMap =
|
||||
visibilityLibrary
|
||||
.featureMapBuilder(
|
||||
Seq(
|
||||
viewerFeatures.forViewerId(viewerId),
|
||||
getAuthorFeatures(r.authorId, authorFeatures),
|
||||
getCardUriFeatures(
|
||||
cardUri = r.cardUri,
|
||||
enableCardVisibilityLibraryCardUriParsing =
|
||||
visibilityDeciderGates.enableCardVisibilityLibraryCardUriParsing(),
|
||||
trackCardUriHost = { safeCardUriHost: Option[String] =>
|
||||
if (safeCardUriHost.isEmpty) {
|
||||
cardUriStats.counter("empty").incr()
|
||||
}
|
||||
}
|
||||
),
|
||||
getCommunityFeatures(r.tweetOpt, r.viewerContext, communityTweetFeatures),
|
||||
getRelationshipFeatures(r.authorId, r.viewerContext.userId, relationshipFeatures),
|
||||
getTweetFeatures(r.tweetOpt, tweetFeatures),
|
||||
_.withConstantFeature(CardIsPoll, r.isPollCardType)
|
||||
)
|
||||
)
|
||||
|
||||
val response = visibilityLibrary
|
||||
.runRuleEngine(
|
||||
CardId(r.cardUri),
|
||||
featureMap,
|
||||
r.viewerContext,
|
||||
r.safetyLevel
|
||||
)
|
||||
.onSuccess(_ => {
|
||||
val overallStatMs = elapsed().inMilliseconds
|
||||
vfLatencyOverallStat.add(overallStatMs)
|
||||
val runStitchEndMs = elapsed().inMilliseconds
|
||||
vfLatencyStitchRunStat.add(runStitchEndMs - runStitchStartMs)
|
||||
})
|
||||
|
||||
runStitchStartMs = elapsed().inMilliseconds
|
||||
val buildStitchStatMs = elapsed().inMilliseconds
|
||||
vfLatencyStitchBuildStat.add(buildStitchStatMs)
|
||||
|
||||
lazy val hydratedFeatureResponse: Stitch[VisibilityResult] =
|
||||
FeatureMap.resolve(featureMap, libraryStatsReceiver).flatMap { resolvedFeatureMap =>
|
||||
visibilityLibrary.runRuleEngine(
|
||||
CardId(r.cardUri),
|
||||
resolvedFeatureMap,
|
||||
r.viewerContext,
|
||||
r.safetyLevel
|
||||
)
|
||||
}
|
||||
|
||||
val isVfParityTestEnabled = enableVfParityTest()
|
||||
val isVfFeatureHydrationEnabled = enableVfFeatureHydration()
|
||||
|
||||
if (!isVfParityTestEnabled && !isVfFeatureHydrationEnabled) {
|
||||
response
|
||||
} else if (isVfParityTestEnabled && !isVfFeatureHydrationEnabled) {
|
||||
response.applyEffect { resp =>
|
||||
Stitch.async(parityTest.runParityTest(hydratedFeatureResponse, resp))
|
||||
}
|
||||
} else {
|
||||
hydratedFeatureResponse
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package com.twitter.visibility.interfaces.cards
|
||||
|
||||
import com.twitter.finagle.stats.StatsReceiver
|
||||
import com.twitter.stitch.Stitch
|
||||
import com.twitter.visibility.builder.VisibilityResult
|
||||
|
||||
class CardVisibilityLibraryParityTest(statsReceiver: StatsReceiver) {
|
||||
private val parityTestScope = statsReceiver.scope("card_visibility_library_parity")
|
||||
private val requests = parityTestScope.counter("requests")
|
||||
private val equal = parityTestScope.counter("equal")
|
||||
private val incorrect = parityTestScope.counter("incorrect")
|
||||
private val failures = parityTestScope.counter("failures")
|
||||
|
||||
def runParityTest(
|
||||
preHydratedFeatureVisibilityResult: Stitch[VisibilityResult],
|
||||
resp: VisibilityResult
|
||||
): Stitch[Unit] = {
|
||||
requests.incr()
|
||||
|
||||
preHydratedFeatureVisibilityResult
|
||||
.flatMap { parityResponse =>
|
||||
if (parityResponse.verdict == resp.verdict) {
|
||||
equal.incr()
|
||||
} else {
|
||||
incorrect.incr()
|
||||
}
|
||||
|
||||
Stitch.Done
|
||||
}.rescue {
|
||||
case t: Throwable =>
|
||||
failures.incr()
|
||||
Stitch.Done
|
||||
}.unit
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package com.twitter.visibility.interfaces.cards
|
||||
|
||||
import com.twitter.tweetypie.{thriftscala => tweetypiethrift}
|
||||
import com.twitter.visibility.models.SafetyLevel
|
||||
import com.twitter.visibility.models.ViewerContext
|
||||
|
||||
case class CardVisibilityRequest(
|
||||
cardUri: String,
|
||||
authorId: Option[Long],
|
||||
tweetOpt: Option[tweetypiethrift.Tweet],
|
||||
isPollCardType: Boolean,
|
||||
safetyLevel: SafetyLevel,
|
||||
viewerContext: ViewerContext)
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user