Skip to main content

x_twitter_sdk/
client.rs

1use crate::auth::{Credentials, OAuth};
2use crate::endpoints::Tweets;
3use crate::types::{XError, XResult};
4use reqwest::{Client as HttpClient, Request};
5use std::time::Duration;
6
7/// X (Twitter) API client
8#[derive(Clone)]
9pub struct XClient {
10    http_client: HttpClient,
11    oauth: OAuth,
12    base_url: String,
13}
14
15impl XClient {
16    /// Default API base URL
17    pub const DEFAULT_BASE_URL: &'static str = "https://api.twitter.com";
18
19    /// Default request timeout in seconds
20    pub const DEFAULT_TIMEOUT_SECS: u64 = 30;
21
22    /// Create a new X client with credentials
23    ///
24    /// # Arguments
25    /// * `credentials` - OAuth 1.0a credentials
26    ///
27    /// # Example
28    /// ```no_run
29    /// use x_twitter_sdk::{XClient, Credentials};
30    ///
31    /// let credentials = Credentials::new(
32    ///     "app_key",
33    ///     "app_secret",
34    ///     "access_token",
35    ///     "access_secret"
36    /// );
37    ///
38    /// let client = XClient::new(credentials).unwrap();
39    /// ```
40    pub fn new(credentials: Credentials) -> XResult<Self> {
41        Self::builder(credentials).build()
42    }
43
44    /// Create a new client builder
45    ///
46    /// # Arguments
47    /// * `credentials` - OAuth 1.0a credentials
48    pub fn builder(credentials: Credentials) -> XClientBuilder {
49        XClientBuilder::new(credentials)
50    }
51
52    /// Get tweets endpoint API
53    pub fn tweets(&self) -> Tweets<'_> {
54        Tweets::new(self)
55    }
56
57    /// Execute a request with OAuth signing
58    pub(crate) async fn execute(&self, mut request: Request) -> XResult<reqwest::Response> {
59        // Add User-Agent header
60        request.headers_mut().insert(
61            "User-Agent",
62            "x-sdk-rust/0.1.0"
63                .parse()
64                .map_err(|e| XError::Unknown(format!("Invalid User-Agent: {}", e)))?,
65        );
66
67        // Sign the request with OAuth
68        self.oauth.sign_request(&mut request)?;
69
70        // Execute the request
71        let response = self.http_client.execute(request).await?;
72
73        // Check for HTTP errors
74        let status = response.status();
75        if !status.is_success() {
76            return Err(self.handle_error_response(response).await);
77        }
78
79        Ok(response)
80    }
81
82    /// Handle error responses from the API
83    async fn handle_error_response(&self, response: reqwest::Response) -> XError {
84        let status = response.status();
85        let status_code = status.as_u16();
86
87        // Try to get retry-after header for rate limiting
88        let retry_after = response
89            .headers()
90            .get("x-rate-limit-reset")
91            .and_then(|v| v.to_str().ok())
92            .and_then(|v| v.parse::<u64>().ok());
93
94        // Try to get the error message from response body
95        let error_message = response
96            .text()
97            .await
98            .unwrap_or_else(|_| "Unknown error".to_string());
99
100        match status_code {
101            401 | 403 => XError::AuthenticationFailed(error_message),
102            400 | 422 => XError::InvalidRequest(error_message),
103            429 => XError::RateLimitExceeded {
104                retry_after,
105                message: error_message,
106            },
107            _ => XError::ApiError {
108                code: status_code,
109                message: error_message,
110            },
111        }
112    }
113
114    /// Get the base URL for API requests
115    pub(crate) fn base_url(&self) -> &str {
116        &self.base_url
117    }
118
119    /// Get a reference to the HTTP client
120    pub(crate) fn http_client(&self) -> &HttpClient {
121        &self.http_client
122    }
123}
124
125/// Builder for XClient
126pub struct XClientBuilder {
127    credentials: Credentials,
128    base_url: Option<String>,
129    timeout: Option<Duration>,
130    http_client: Option<HttpClient>,
131}
132
133impl XClientBuilder {
134    /// Create a new builder
135    fn new(credentials: Credentials) -> Self {
136        Self {
137            credentials,
138            base_url: None,
139            timeout: None,
140            http_client: None,
141        }
142    }
143
144    /// Set custom base URL (for testing or custom endpoints)
145    pub fn base_url(mut self, url: impl Into<String>) -> Self {
146        self.base_url = Some(url.into());
147        self
148    }
149
150    /// Set request timeout
151    pub fn timeout(mut self, timeout: Duration) -> Self {
152        self.timeout = Some(timeout);
153        self
154    }
155
156    /// Set custom HTTP client
157    pub fn http_client(mut self, client: HttpClient) -> Self {
158        self.http_client = Some(client);
159        self
160    }
161
162    /// Build the XClient
163    pub fn build(self) -> XResult<XClient> {
164        let oauth = OAuth::new(self.credentials)?;
165
166        let http_client = self.http_client.unwrap_or_else(|| {
167            HttpClient::builder()
168                .timeout(
169                    self.timeout
170                        .unwrap_or_else(|| Duration::from_secs(XClient::DEFAULT_TIMEOUT_SECS)),
171                )
172                .build()
173                .expect("Failed to build HTTP client")
174        });
175
176        Ok(XClient {
177            http_client,
178            oauth,
179            base_url: self
180                .base_url
181                .unwrap_or_else(|| XClient::DEFAULT_BASE_URL.to_string()),
182        })
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    fn test_credentials() -> Credentials {
191        Credentials::new("test_key", "test_secret", "test_token", "test_token_secret")
192    }
193
194    #[test]
195    fn test_client_new() {
196        let credentials = test_credentials();
197        let client = XClient::new(credentials);
198        assert!(client.is_ok());
199    }
200
201    #[test]
202    fn test_client_builder() {
203        let credentials = test_credentials();
204        let client = XClient::builder(credentials)
205            .base_url("https://test.example.com")
206            .timeout(Duration::from_secs(10))
207            .build();
208
209        assert!(client.is_ok());
210        let client = client.unwrap();
211        assert_eq!(client.base_url(), "https://test.example.com");
212    }
213
214    #[test]
215    fn test_client_with_invalid_credentials() {
216        let credentials = Credentials::new("", "", "", "");
217        let client = XClient::new(credentials);
218        assert!(client.is_err());
219    }
220}