x_mcp_server/
client.rs

1//! X API client implementation using Bearer Token
2
3use crate::error::{XError, XResult};
4use crate::types::{SearchTweetsParams, Tweet, User, XResponse};
5use reqwest::Client;
6
7/// X API client
8#[derive(Debug, Clone)]
9pub struct XClient {
10    client: Client,
11    bearer_token: String,
12    base_url: String,
13}
14
15impl XClient {
16    /// Create a new X API client
17    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    /// Create client from environment variables
26    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    /// Get user information by username
33    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    /// Get user information by user ID
67    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    /// Search for tweets
101    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    /// Get a tweet by ID
153    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    /// Get user's recent tweets
190    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}