twitter_internal_api/api/
client.rs1use 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(¶ms)
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}