Skip to main content

twitter_internal_api/api/
client.rs

1use crate::api::params::search_time_line::{
2    Product, SearchTimeLineResponse, SearchTimeLineResponseType, SearchTimelineParams,
3};
4use crate::utils::headers::extract_headers;
5use reqwest::redirect::Policy;
6use serde_json::Value;
7
8#[derive(Debug, Clone)]
9pub struct UserAgent(pub String);
10
11impl UserAgent {
12    pub fn new(s: &str) -> Self {
13        Self(s.to_string())
14    }
15}
16
17impl Default for UserAgent {
18    fn default() -> Self {
19        Self::new(
20            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
21        )
22    }
23}
24
25#[derive(Debug, Clone)]
26pub struct ApiClient {
27    pub reqwest_client: reqwest::Client,
28    pub csrf: String,
29    pub bearer_token: String,
30    pub cookie: String,
31    pub user_agent: UserAgent,
32    pub transaction_id: String,
33    pub search_timeline_endpoint: String,
34}
35
36impl ApiClient {
37    pub fn new(
38        search_timeline_endpoint: &str,
39        csrf: &str,
40        bearer_token: &str,
41        cookie: &str,
42        transaction_id: &str,
43    ) -> anyhow::Result<Self> {
44        let user_agent = UserAgent::default();
45        let reqwest_client = reqwest::Client::builder()
46            .user_agent(user_agent.0.clone())
47            .redirect(Policy::limited(10))
48            .build()?;
49        Ok(Self {
50            transaction_id: transaction_id.to_string(),
51            cookie: cookie.to_string(),
52            user_agent,
53            reqwest_client,
54            csrf: csrf.to_string(),
55            bearer_token: bearer_token.to_string(),
56            search_timeline_endpoint: search_timeline_endpoint.to_string(),
57        })
58    }
59
60    pub fn update_search_timeline_endpoint(&mut self, search_timeline_endpoint: &str) {
61        self.search_timeline_endpoint = search_timeline_endpoint.to_string();
62    }
63
64    pub async fn search_timeline(
65        &self,
66        count: u32,
67        raw_query: &str,
68        cursor: Option<String>,
69        search_mode: &Product,
70    ) -> anyhow::Result<SearchTimeLineResponse> {
71        let mut search_time_line_params = SearchTimelineParams::default();
72        search_time_line_params.update_count(count);
73        search_time_line_params.update_raw_query(raw_query);
74        search_time_line_params.update_cursor(cursor.clone());
75        search_time_line_params.update_product(search_mode.clone());
76        let params = search_time_line_params.params()?;
77        let url = format!(
78            "https://x.com/i/api/graphql/{}/SearchTimeline",
79            self.search_timeline_endpoint
80        );
81        tracing::info!("url = {url}");
82        let request = self
83            .reqwest_client
84            .get(url)
85            .query(&params)
86            .bearer_auth(&self.bearer_token)
87            .header("x-csrf-token", &self.csrf)
88            .header("x-twitter-active-user", "yes")
89            .header("x-twitter-auth-type", "OAuth2Session")
90            .header("x-twitter-client-language", "en")
91            .header("x-client-transaction-id", &self.transaction_id)
92            .header("Cookie", &self.cookie)
93            .header("accept", "*/*")
94            .build()?;
95        let response = self.reqwest_client.execute(request).await?;
96        let headers = response.headers().clone();
97        let rate_limit_headers = extract_headers(headers);
98        let code = response.status();
99        tracing::info!("code = {}", code);
100        let response = response.text().await?;
101        let response = response.trim();
102        if response.contains("Rate limit exceeded") {
103            Ok(SearchTimeLineResponse {
104                response_type: SearchTimeLineResponseType::RateLimit,
105                response_content: None,
106                response_headers: rate_limit_headers,
107            })
108        } else if response.contains("Could not authenticate you") {
109            Ok(SearchTimeLineResponse {
110                response_type: SearchTimeLineResponseType::AuthError,
111                response_content: None,
112                response_headers: rate_limit_headers,
113            })
114        } else {
115            let value: Value = serde_json::from_str(response)?;
116            Ok(SearchTimeLineResponse {
117                response_type: SearchTimeLineResponseType::Success,
118                response_content: Some(value),
119                response_headers: rate_limit_headers,
120            })
121        }
122    }
123}