x_api_rs/
search.rs

1use crate::BEARER_TOKEN;
2use log::debug;
3use serde_json::json;
4use std::cmp;
5use eyre::{ContextCompat, Result};
6use super::{
7    types::{parse_legacy_tweet, Data, Tweet},
8    TwAPI,
9};
10
11const SEARCH_URL: &str = "https://twitter.com/i/api/graphql/nK1dw4oV3k4w5TdtcAdSww/SearchTimeline";
12
13impl TwAPI {
14    pub async fn search(
15        &self,
16        query: &str,
17        limit: u8,
18        cursor: &str,
19    ) -> Result<Data> {
20        let limit = cmp::min(50u8, limit);
21
22        let mut variables = json!(
23            {
24                "rawQuery":     query.to_string(),
25                "count":        limit,
26                "querySource":  "typed_query",
27                "product":      "Top"
28            }
29        );
30        let features = json!(
31            {
32                "rweb_lists_timeline_redesign_enabled":                                    true,
33                "responsive_web_graphql_exclude_directive_enabled":                        true,
34                "verified_phone_label_enabled":                                            false,
35                "creator_subscriptions_tweet_preview_api_enabled":                         true,
36                "responsive_web_graphql_timeline_navigation_enabled":                      true,
37                "responsive_web_graphql_skip_user_profile_image_extensions_enabled":       false,
38                "tweetypie_unmention_optimization_enabled":                                true,
39                "responsive_web_edit_tweet_api_enabled":                                   true,
40                "graphql_is_translatable_rweb_tweet_is_translatable_enabled":              true,
41                "view_counts_everywhere_api_enabled":                                      true,
42                "longform_notetweets_consumption_enabled":                                 true,
43                "responsive_web_twitter_article_tweet_consumption_enabled":                false,
44                "tweet_awards_web_tipping_enabled":                                        false,
45                "freedom_of_speech_not_reach_fetch_enabled":                               true,
46                "standardized_nudges_misinfo":                                             true,
47                "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": true,
48                "longform_notetweets_rich_text_read_enabled":                              true,
49                "longform_notetweets_inline_media_enabled":                                true,
50                "responsive_web_media_download_video_enabled":                             false,
51                "responsive_web_enhance_cards_enabled":                                    false,
52            }
53        );
54        let field_toggles = json!(
55            {
56                "withArticleRichContentState": false
57            }
58        );
59        if cursor.ne("") {
60            variables["cursor"] = cursor.to_string().into();
61        }
62        variables["product"] = "Latest".into();
63        let q = [
64            ("variables", variables.to_string()),
65            ("features", features.to_string()),
66            ("fieldToggles", field_toggles.to_string()),
67        ];
68        let req = self
69            .client
70            .get(SEARCH_URL)
71            .header("Authorization", format!("Bearer {}", BEARER_TOKEN))
72            .header("X-CSRF-Token", self.csrf_token.to_owned())
73            .query(&q)
74            .build()?;
75        let text = self
76            .client
77            .execute(req).await?
78            .text().await?;
79        let res: Data = serde_json::from_str(&text)?;
80        return Ok(res);
81    }
82
83    pub async fn search_tweets(
84        &self,
85        query: &str,
86        limit: u8,
87        cursor: &str,
88    ) -> Result<(Vec<Tweet>, String)> {
89        let search_result = self.search(query, limit, cursor).await;
90        let mut cursor = String::from("");
91        match search_result {
92            Ok(res) => {
93                let mut tweets: Vec<Tweet> = vec![];
94                let instructions = res
95                    .data
96                    .search_by_raw_query
97                    .search_timeline
98                    .timeline
99                    .instructions
100                    .context("can't parse tweets from timeline, instructions is none")?;
101                for item in instructions {
102                    if item.instruction_type.ne("TimelineAddEntries")
103                        && item.instruction_type.ne("TimelineReplaceEntry")
104                    {
105                        continue;
106                    }
107                    if item.entry.is_some() {
108                        let entry = item.entry.context("entry is none")?;
109                        let cursor_type = entry.content.cursor_type.unwrap_or("".to_string());
110                        if cursor_type.eq("Bottom") {
111                            if entry.content.value.is_some() {
112                                cursor = entry.content.value.context("content value is none")?;
113                            }
114                        }
115                    }
116                    for entry in item.entries {
117                        if entry.content.item_content.is_none() {
118                            continue;
119                        }
120                        let item = entry.content.item_content.context("item content is none")?;
121                        if item.tweet_display_type.eq("Tweet") {
122                            let core = item.tweet_results.result.core;
123                            if core.is_none() {
124                                continue;
125                            }
126                            let u = core.context("core is none")?.user_results.result.legacy.context("legacy is none")?;
127                            let t = item.tweet_results.result.legacy;
128                            if let Ok(tweet) = parse_legacy_tweet(&u, &t) {
129                                tweets.push(tweet)
130                            }
131                        }
132                    }
133                }
134                debug!("Found tweets: {tweets:?}");
135                Ok((tweets, cursor))
136            }
137            Err(e) => Err(e),
138        }
139    }
140}