agent_twitter_client/
search.rs

1use crate::api::requests::request_api;
2use crate::error::Result;
3use crate::timeline::search::{
4    parse_search_timeline_tweets, parse_search_timeline_users, SearchTimeline,
5};
6use crate::timeline::v1::{QueryProfilesResponse, QueryTweetsResponse};
7use reqwest::Method;
8use serde_json::json;
9use crate::api::client::TwitterClient;
10#[derive(Debug, Clone, Copy)]
11pub enum SearchMode {
12    Top,
13    Latest,
14    Photos,
15    Videos,
16    Users,
17}
18
19pub async fn fetch_search_tweets(
20    client: &TwitterClient,
21    query: &str,
22    max_tweets: i32,
23    search_mode: SearchMode,
24    cursor: Option<String>,
25) -> Result<QueryTweetsResponse> {
26    let timeline = get_search_timeline(client, query, max_tweets, search_mode, cursor).await?;
27
28    Ok(parse_search_timeline_tweets(&timeline))
29}
30
31pub async fn search_profiles(
32    client: &TwitterClient,
33    query: &str,
34    max_profiles: i32,
35    cursor: Option<String>,
36) -> Result<QueryProfilesResponse> {
37    let timeline =
38        get_search_timeline(client, query, max_profiles, SearchMode::Users, cursor).await?;
39
40    Ok(parse_search_timeline_users(&timeline))
41}
42
43async fn get_search_timeline(
44    client: &TwitterClient, 
45    query: &str,
46    max_items: i32,
47    search_mode: SearchMode,
48    _cursor: Option<String>,
49) -> Result<SearchTimeline> {
50
51    let max_items = if max_items > 50 { 50 } else { max_items };
52
53    let mut variables = json!({
54        "rawQuery": query,
55        "count": max_items,
56        "querySource": "typed_query",
57        "product": "Top"
58    });
59
60    // Set product based on search mode
61    match search_mode {
62        SearchMode::Latest => {
63            variables["product"] = json!("Latest");
64        }
65        SearchMode::Photos => {
66            variables["product"] = json!("Photos");
67        }
68        SearchMode::Videos => {
69            variables["product"] = json!("Videos");
70        }
71        SearchMode::Users => {
72            variables["product"] = json!("People");
73        }
74        _ => {}
75    }
76
77    let features = json!({
78        "longform_notetweets_inline_media_enabled": true,
79        "responsive_web_enhance_cards_enabled": false,
80        "responsive_web_media_download_video_enabled": false,
81        "responsive_web_twitter_article_tweet_consumption_enabled": false,
82        "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": true,
83        "interactive_text_enabled": false,
84        "responsive_web_text_conversations_enabled": false,
85        "vibe_api_enabled": false,
86        "rweb_lists_timeline_redesign_enabled": true,
87        "responsive_web_graphql_exclude_directive_enabled": true,
88        "verified_phone_label_enabled": false,
89        "creator_subscriptions_tweet_preview_api_enabled": true,
90        "responsive_web_graphql_timeline_navigation_enabled": true,
91        "responsive_web_graphql_skip_user_profile_image_extensions_enabled": false,
92        "tweetypie_unmention_optimization_enabled": true,
93        "responsive_web_edit_tweet_api_enabled": true,
94        "graphql_is_translatable_rweb_tweet_is_translatable_enabled": true,
95        "view_counts_everywhere_api_enabled": true,
96        "longform_notetweets_consumption_enabled": true,
97        "tweet_awards_web_tipping_enabled": false,
98        "freedom_of_speech_not_reach_fetch_enabled": true,
99        "standardized_nudges_misinfo": true,
100        "longform_notetweets_rich_text_read_enabled": true,
101        "responsive_web_enhance_cards_enabled": false,
102        "subscriptions_verification_info_enabled": true,
103        "subscriptions_verification_info_reason_enabled": true,
104        "subscriptions_verification_info_verified_since_enabled": true,
105        "super_follow_badge_privacy_enabled": false,
106        "super_follow_exclusive_tweet_notifications_enabled": false,
107        "super_follow_tweet_api_enabled": false,
108        "super_follow_user_api_enabled": false,
109        "android_graphql_skip_api_media_color_palette": false,
110        "creator_subscriptions_subscription_count_enabled": false,
111        "blue_business_profile_image_shape_enabled": false,
112        "unified_cards_ad_metadata_container_dynamic_card_content_query_enabled": false
113    });
114
115    let field_toggles = json!({
116        "withArticleRichContentState": false
117    });
118
119    let params = [("variables", serde_json::to_string(&variables)?),
120        ("features", serde_json::to_string(&features)?),
121        ("fieldToggles", serde_json::to_string(&field_toggles)?)];
122
123    let query_string = params
124        .iter()
125        .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
126        .collect::<Vec<_>>()
127        .join("&");
128
129    let mut headers = reqwest::header::HeaderMap::new();
130    client.auth.install_headers(&mut headers).await?;
131
132    let url = format!(
133        "https://api.twitter.com/graphql/gkjsKepM6gl_HmFWoWKfgg/SearchTimeline?{}",
134        query_string
135    );
136
137    let (response, _) = request_api::<SearchTimeline>(&client.client, &url, headers, Method::GET, None).await?;
138
139    Ok(response)
140}