1use crate::error::{XError, XResult};
4use crate::types::{SearchTweetsParams, Tweet, User, XResponse};
5use reqwest::Client;
6
7#[derive(Debug, Clone)]
9pub struct XClient {
10 client: Client,
11 bearer_token: String,
12 base_url: String,
13}
14
15impl XClient {
16 pub fn new(bearer_token: String) -> Self {
18 Self {
19 client: Client::new(),
20 bearer_token,
21 base_url: "https://api.twitter.com/2".to_string(),
22 }
23 }
24
25 pub fn from_env() -> XResult<Self> {
27 let bearer_token = std::env::var("X_BEARER_TOKEN")
28 .map_err(|_| XError::Config("X_BEARER_TOKEN not found".to_string()))?;
29 Ok(Self::new(bearer_token))
30 }
31
32 pub async fn get_user_by_username(&self, username: &str) -> XResult<Option<User>> {
34 let url = format!("{}/users/by/username/{}", self.base_url, username);
35
36 let response = self.client
37 .get(&url)
38 .bearer_auth(&self.bearer_token)
39 .query(&[("user.fields", "id,name,username,description,public_metrics,profile_image_url,verified,created_at")])
40 .send()
41 .await?;
42
43 let status = response.status();
44 if !status.is_success() {
45 let error_text = response.text().await.unwrap_or_default();
46 return Err(XError::Api {
47 status: status.as_u16(),
48 message: error_text,
49 });
50 }
51
52 let api_response: XResponse<User> = response.json().await?;
53
54 if let Some(errors) = api_response.errors {
55 if !errors.is_empty() {
56 return Err(XError::Api {
57 status: 400,
58 message: format!("API errors: {:?}", errors),
59 });
60 }
61 }
62
63 Ok(api_response.data)
64 }
65
66 pub async fn get_user_by_id(&self, user_id: &str) -> XResult<Option<User>> {
68 let url = format!("{}/users/{}", self.base_url, user_id);
69
70 let response = self.client
71 .get(&url)
72 .bearer_auth(&self.bearer_token)
73 .query(&[("user.fields", "id,name,username,description,public_metrics,profile_image_url,verified,created_at")])
74 .send()
75 .await?;
76
77 let status = response.status();
78 if !status.is_success() {
79 let error_text = response.text().await.unwrap_or_default();
80 return Err(XError::Api {
81 status: status.as_u16(),
82 message: error_text,
83 });
84 }
85
86 let api_response: XResponse<User> = response.json().await?;
87
88 if let Some(errors) = api_response.errors {
89 if !errors.is_empty() {
90 return Err(XError::Api {
91 status: 400,
92 message: format!("API errors: {:?}", errors),
93 });
94 }
95 }
96
97 Ok(api_response.data)
98 }
99
100 pub async fn search_tweets(&self, params: SearchTweetsParams) -> XResult<Vec<Tweet>> {
102 let url = format!("{}/tweets/search/recent", self.base_url);
103
104 let mut query_params = vec![("query", params.query)];
105
106 if let Some(max_results) = params.max_results {
107 query_params.push(("max_results", max_results.to_string()));
108 }
109
110 if let Some(tweet_fields) = params.tweet_fields {
111 query_params.push(("tweet.fields", tweet_fields.join(",")));
112 }
113
114 if let Some(user_fields) = params.user_fields {
115 query_params.push(("user.fields", user_fields.join(",")));
116 }
117
118 if let Some(expansions) = params.expansions {
119 query_params.push(("expansions", expansions.join(",")));
120 }
121
122 let response = self.client
123 .get(&url)
124 .bearer_auth(&self.bearer_token)
125 .query(&query_params)
126 .send()
127 .await?;
128
129 let status = response.status();
130 if !status.is_success() {
131 let error_text = response.text().await.unwrap_or_default();
132 return Err(XError::Api {
133 status: status.as_u16(),
134 message: error_text,
135 });
136 }
137
138 let api_response: XResponse<Vec<Tweet>> = response.json().await?;
139
140 if let Some(errors) = api_response.errors {
141 if !errors.is_empty() {
142 return Err(XError::Api {
143 status: 400,
144 message: format!("API errors: {:?}", errors),
145 });
146 }
147 }
148
149 Ok(api_response.data.unwrap_or_default())
150 }
151
152 pub async fn get_tweet(&self, tweet_id: &str) -> XResult<Option<Tweet>> {
154 let url = format!("{}/tweets/{}", self.base_url, tweet_id);
155
156 let response = self.client
157 .get(&url)
158 .bearer_auth(&self.bearer_token)
159 .query(&[
160 ("tweet.fields", "id,text,author_id,created_at,public_metrics,context_annotations,referenced_tweets"),
161 ("expansions", "author_id"),
162 ])
163 .send()
164 .await?;
165
166 let status = response.status();
167 if !status.is_success() {
168 let error_text = response.text().await.unwrap_or_default();
169 return Err(XError::Api {
170 status: status.as_u16(),
171 message: error_text,
172 });
173 }
174
175 let api_response: XResponse<Tweet> = response.json().await?;
176
177 if let Some(errors) = api_response.errors {
178 if !errors.is_empty() {
179 return Err(XError::Api {
180 status: 400,
181 message: format!("API errors: {:?}", errors),
182 });
183 }
184 }
185
186 Ok(api_response.data)
187 }
188
189 pub async fn get_user_tweets(&self, user_id: &str, max_results: Option<u32>) -> XResult<Vec<Tweet>> {
191 let url = format!("{}/users/{}/tweets", self.base_url, user_id);
192
193 let mut query_params = vec![
194 ("tweet.fields", "id,text,author_id,created_at,public_metrics".to_string()),
195 ];
196
197 if let Some(max) = max_results {
198 query_params.push(("max_results", max.to_string()));
199 }
200
201 let response = self.client
202 .get(&url)
203 .bearer_auth(&self.bearer_token)
204 .query(&query_params)
205 .send()
206 .await?;
207
208 let status = response.status();
209 if !status.is_success() {
210 let error_text = response.text().await.unwrap_or_default();
211 return Err(XError::Api {
212 status: status.as_u16(),
213 message: error_text,
214 });
215 }
216
217 let api_response: XResponse<Vec<Tweet>> = response.json().await?;
218
219 if let Some(errors) = api_response.errors {
220 if !errors.is_empty() {
221 return Err(XError::Api {
222 status: 400,
223 message: format!("API errors: {:?}", errors),
224 });
225 }
226 }
227
228 Ok(api_response.data.unwrap_or_default())
229 }
230}