xplore/
lib.rs

1//! Xplore - A X API client for Rust
2//!
3//! This crate provides a convenient way to interact with X's undocumented API.
4
5mod api;
6mod api_utils;
7mod auth;
8mod endpoints;
9pub mod profile;
10mod rate_limit;
11pub mod relationship;
12pub mod search;
13mod timeline_v1;
14mod timeline_v2;
15mod trend;
16pub mod tweets;
17
18use {
19    crate::{
20        auth::UserAuth,
21        profile::{get_profile, get_user_id, Profile},
22        rate_limit::RateLimitStrategy,
23        search::SearchMode,
24        timeline_v1::{QueryProfilesResponse, QueryTweetsResponse},
25        timeline_v2::QueryTweetsResponse as V2QueryTweetsResponse,
26        trend::get_trend,
27        tweets::{
28            create_long_tweet, fetch_list_tweets, fetch_tweets_and_replies, fetch_tweets_and_replies_by_user_id,
29            get_user_tweets, like_tweet, post_tweet, read_tweet, retweet, send_quote_tweet, Tweet,
30            TweetRetweetResponse,
31        },
32    },
33    chrono::Duration,
34    serde::Deserialize,
35    serde_json::Value,
36    thiserror::Error,
37};
38
39pub type Result<T> = std::result::Result<T, XploreError>;
40
41#[derive(Debug, Error, Deserialize)]
42pub enum XploreError {
43    #[error("API error: {0}")]
44    Api(String),
45
46    #[error("Authentication error: {0}")]
47    Auth(String),
48
49    #[error("Network error: {0}")]
50    #[serde(skip)]
51    Network(#[from] reqwest::Error),
52
53    #[error("Rate limit exceeded")]
54    RateLimit,
55
56    #[error("Invalid response format: {0}")]
57    InvalidResponse(String),
58
59    #[error("Missing environment variable: {0}")]
60    EnvVar(String),
61
62    #[error("Cookie error: {0}")]
63    Cookie(String),
64
65    #[error("JSON error: {0}")]
66    #[serde(skip)]
67    Json(#[from] serde_json::Error),
68
69    #[error("IO error: {0}")]
70    #[serde(skip)]
71    Io(#[from] std::io::Error),
72}
73
74/// Configuration options for the Xplore scraper.
75pub struct XploreOptions {
76    /// The rate limiting strategy to use when the scraper hits API limits.
77    pub rate_limit_strategy: Box<dyn RateLimitStrategy>,
78
79    /// Timeout for individual requests.
80    ///
81    /// Default: 30 seconds
82    pub request_timeout: Duration,
83
84    /// Maximum number of retries for failed requests.
85    ///
86    /// Default: 3
87    pub max_retries: u32,
88
89    /// Whether to automatically follow redirects.
90    ///
91    /// Default: true
92    pub follow_redirects: bool,
93}
94
95pub struct Xplore {
96    auth: UserAuth,
97}
98
99impl Xplore {
100    pub async fn new(_options: Option<XploreOptions>) -> Result<Self> {
101        let auth = UserAuth::new().await?;
102        Ok(Self { auth })
103    }
104}
105
106///! Login's API collection
107impl Xplore {
108    ///! Login Method
109    ///
110    /// Authenticates a user with the provided credentials.
111    ///
112    /// # Arguments
113    /// * `username` - The username of the user attempting to log in.
114    /// * `password` - The password of the user attempting to log in.
115    /// * `email` - Optional email address for additional authentication (if required by the service).
116    /// * `two_factor_secret` - Optional two-factor authentication secret (if 2FA is enabled).
117    ///
118    /// # Returns
119    /// * `Result<bool>` - Returns `Ok(true)` if login was successful, or an error if authentication failed.
120    ///
121    /// # Errors
122    /// Returns an error if:
123    /// - Invalid credentials were provided
124    /// - Two-factor authentication failed
125    /// - Network error occurred during authentication
126    /// - Server rejected the login request
127    pub async fn login(
128        &mut self,
129        username: &str,
130        password: &str,
131        email: Option<&str>,
132        two_factor_secret: Option<&str>,
133    ) -> Result<bool> {
134        let _ = self.auth.login(username, password, email, two_factor_secret).await;
135
136        Ok(true)
137    }
138
139    ///! Logout Method
140    ///
141    /// Terminates the current user session.
142    ///
143    /// # Returns
144    /// * `Result<bool>` - Returns `Ok(true)` if logout was successful, or an error if logout failed.
145    ///
146    /// # Errors
147    /// Returns an error if:
148    /// - No active session was found
149    /// - Network error occurred during logout
150    /// - Server rejected the logout request
151    pub async fn logout(&mut self) -> Result<bool> {
152        self.auth.logout().await;
153
154        Ok(true)
155    }
156
157    ///! Set Cookie Method
158    ///
159    /// Sets the authentication cookie from a raw cookie string.
160    ///
161    /// # Arguments
162    /// * `cookie` - The raw cookie string containing authentication information.
163    ///
164    /// # Returns
165    /// This method does not return a value, but may fail silently if the cookie is invalid.
166    ///
167    /// # Errors
168    /// Errors may occur internally during cookie parsing and validation, but are currently ignored.
169    pub async fn set_cookie(&mut self, cookie: &str) {
170        let _ = self.auth.set_from_cookie_string(cookie).await;
171    }
172
173    ///! Get Cookie Method
174    ///
175    /// Retrieves the current authentication cookie as a string.
176    ///
177    /// # Returns
178    /// * `Result<String>` - Returns `Ok(String)` containing the cookie if available, or an error if the cookie could not be retrieved.
179    ///
180    /// # Errors
181    /// Returns an error if:
182    /// - No active session exists
183    /// - Cookie could not be serialized to string
184    /// - Network error occurred during cookie retrieval
185    pub async fn get_cookie(&mut self) -> Result<String> {
186        self.auth.get_cookie_string().await
187    }
188}
189
190///! Profile's API collection
191impl Xplore {
192    ///! Fetches the profile of a user by their screen name.
193    /// # Arguments
194    /// * `screen_name` - The screen name of the user whose profile is to be fetched.
195    /// # Returns
196    /// * `Result<Profile>` - A result containing the user's profile if successful, or an error if not.
197    /// # Errors
198    /// Returns an error if the profile cannot be fetched, such as if the user does not exist or if there is a network issue.
199    pub async fn get_profile(&mut self, screen_name: &str) -> Result<Profile> {
200        get_profile(&mut self.auth, screen_name).await
201    }
202
203    ///! Fetches the user ID of a user by their screen name.
204    /// # Arguments
205    /// * `screen_name` - The screen name of the user whose ID is to be fetched.
206    /// # Returns
207    /// * `Result<String>` - A result containing the user's ID if successful, or an error if not.
208    /// # Errors
209    /// Returns an error if the user ID cannot be fetched, such as if the user does not exist or if there is a network issue.
210    pub async fn get_user_id(&mut self, screen_name: &str) -> Result<String> {
211        get_user_id(&mut self.auth, screen_name).await
212    }
213}
214
215///! Search's API collection
216impl Xplore {
217    ///! Searches for tweets based on a query string.
218    /// # Arguments
219    /// * `query` - The search query string to find tweets.
220    /// * `max_tweets` - The maximum number of tweets to return.
221    /// * `search_mode` - The mode of search to be used (e.g., recent, popular).
222    /// * `cursor` - An optional cursor for pagination.
223    /// # Returns
224    /// * `Result<QueryTweetsResponse>` - A result containing the response with tweets if successful, or an error if not.
225    /// # Errors
226    /// Returns an error if the search fails, such as if the query is invalid or if there is a network issue.
227    pub async fn search_tweets(
228        &mut self,
229        query: &str,
230        max_tweets: i32,
231        search_mode: SearchMode,
232        cursor: Option<String>,
233    ) -> Result<QueryTweetsResponse> {
234        search::search_tweets(&mut self.auth, query, max_tweets, search_mode, cursor).await
235    }
236
237    ///! Searches for user profiles based on a query string.
238    /// # Arguments
239    /// * `query` - The search query string to find user profiles.
240    /// * `max_profiles` - The maximum number of profiles to return.
241    /// * `cursor` - An optional cursor for pagination.
242    /// # Returns
243    /// * `Result<QueryProfilesResponse>` - A result containing the response with user profiles if successful, or an error if not.
244    /// # Errors
245    /// Returns an error if the search fails, such as if the query is invalid or if there is a network issue.
246    pub async fn search_profiles(
247        &mut self,
248        query: &str,
249        max_profiles: i32,
250        cursor: Option<String>,
251    ) -> Result<QueryProfilesResponse> {
252        search::search_profiles(&mut self.auth, query, max_profiles, cursor).await
253    }
254}
255
256///! Relationship's API collection
257impl Xplore {
258    ///! Fetches the home timeline with a specified count and a list of seen tweet IDs.
259    /// # Arguments
260    /// * `count` - The number of tweets to return.
261    /// * `seen_tweet_ids` - A vector of tweet IDs that have already been seen.
262    /// # Returns
263    /// * `Result<Vec<Value>>` - A result containing a vector of tweets if successful, or an error if not.
264    /// # Errors
265    /// Returns an error if the home timeline cannot be fetched, such as if there is a network issue or if the user is not authenticated.
266    pub async fn get_home_timeline(&mut self, count: i32, seen_tweet_ids: Vec<String>) -> Result<Vec<Value>> {
267        relationship::get_home_timeline(self, count, seen_tweet_ids).await
268    }
269
270    ///! Fetches the relationship status between the authenticated user and another user.
271    /// # Arguments
272    /// * `user_id` - The ID of the user whose relationship status is to be fetched.
273    /// # Returns
274    /// * `Result<Profile>` - A result containing the profile of the user if successful, or an error if not.
275    /// # Errors
276    /// Returns an error if the relationship status cannot be fetched, such as if the user does not exist or if there is a network issue.
277    pub async fn get_following(
278        &mut self,
279        user_id: &str,
280        count: i32,
281        cursor: Option<String>,
282    ) -> Result<(Vec<Profile>, Option<String>)> {
283        relationship::get_following(self, user_id, count, cursor).await
284    }
285
286    ///! Fetches the followers of a user.
287    /// # Arguments
288    /// * `user_id` - The ID of the user whose followers are to be fetched
289    /// * `count` - The maximum number of followers to return.
290    /// * `cursor` - An optional cursor for pagination.
291    /// # Returns
292    /// * `Result<(Vec<Profile>, Option<String>)>` - A result containing a tuple with a vector of profiles and an optional cursor for pagination if successful, or an error if not
293    /// # Errors
294    /// Returns an error if the followers cannot be fetched, such as if the user does not exist or if there is a network issue.
295    pub async fn get_followers(
296        &mut self,
297        user_id: &str,
298        count: i32,
299        cursor: Option<String>,
300    ) -> Result<(Vec<Profile>, Option<String>)> {
301        relationship::get_followers(self, user_id, count, cursor).await
302    }
303
304    ///! Follows a user by their username.
305    /// # Arguments
306    /// * `username` - The username of the user to follow.
307    /// # Returns
308    /// * `Result<()>` - A result indicating success or failure.
309    /// # Errors    
310    /// Returns an error if the follow action fails, such as if the user does not exist or if there is a network issue.
311    pub async fn follow(&mut self, username: &str) -> Result<()> {
312        relationship::follow(self, username).await
313    }
314
315    ///! Unfollows a user by their username.
316    /// # Arguments
317    /// * `username` - The username of the user to unfollow.
318    /// # Returns
319    /// * `Result<()>` - A result indicating success or failure.
320    /// # Errors
321    /// Returns an error if the unfollow action fails, such as if the user does not
322    pub async fn unfollow(&mut self, username: &str) -> Result<()> {
323        relationship::unfollow(self, username).await
324    }
325}
326
327///! Tweet's API collection
328impl Xplore {
329    ///! Posts a tweet with optional media attachments.
330    /// # Arguments
331    /// * `text` - The text content of the tweet.
332    /// * `reply_to` - An optional tweet ID to reply to.
333    /// * `media_data` - An optional vector of tuples containing media data and their corresponding media types.
334    /// # Returns
335    /// * `Result<Value>` - A result containing the response from the tweet posting if successful, or an error if not.
336    /// # Errors
337    /// Returns an error if the tweet cannot be posted, such as if the text is too long, if the media data is invalid, or if there is a network issue.
338    pub async fn post_tweet(
339        &mut self,
340        text: &str,
341        reply_to: Option<&str>,
342        media_data: Option<Vec<(Vec<u8>, String)>>,
343    ) -> Result<Value> {
344        post_tweet(self, text, reply_to, media_data).await
345    }
346
347    ///! reads a tweet by its ID.
348    /// # Arguments
349    /// * `tweet_id` - The ID of the tweet to be read.
350    /// # Returns
351    /// * `Result<Tweet>` - A result containing the tweet if successful, or an error if not.
352    /// # Errors
353    /// Returns an error if the tweet cannot be read, such as if the tweet does not exist or if there is a network issue.
354    pub async fn read_tweet(&mut self, tweet_id: &str) -> Result<Tweet> {
355        read_tweet(self, tweet_id).await
356    }
357
358    ///! Retweets a tweet by its ID.
359    /// # Arguments
360    /// * `tweet_id` - The ID of the tweet to be retweeted.
361    /// # Returns
362    /// * `Result<TweetRetweetResponse>` - A result containing the retweet response if successful, or an error if not.
363    /// # Errors
364    /// Returns an error if the retweet action fails, such as if the tweet does not exist, if the user has already retweeted it, or if there is a network issue.
365    pub async fn retweet(&mut self, tweet_id: &str) -> Result<TweetRetweetResponse> {
366        retweet(self, tweet_id).await
367    }
368
369    ///! Likes a tweet by its ID.
370    /// # Arguments
371    /// * `tweet_id` - The ID of the tweet to be liked.
372    /// # Returns
373    /// * `Result<Value>` - A result containing the response from the like action if successful, or an error if not.
374    /// # Errors
375    /// Returns an error if the like action fails, such as if the tweet does not exist, if the user has already liked it, or if there is a network issue.
376    pub async fn like_tweet(&mut self, tweet_id: &str) -> Result<Value> {
377        like_tweet(self, tweet_id).await
378    }
379
380    ///! Gets a user's tweets.
381    /// # Arguments
382    /// * `user_id` - The ID of the user whose tweets are to be fetched.
383    /// * `limit` - The maximum number of tweets to return.
384    /// # Returns
385    /// * `Result<Vec<Tweet>>` - A result containing a vector of tweets if successful, or an error if not.
386    /// # Errors
387    /// Returns an error if the tweets cannot be fetched, such as if the user does not exist or if there is a network issue.
388    pub async fn get_user_tweets(&mut self, user_id: &str, limit: usize) -> Result<Vec<Tweet>> {
389        get_user_tweets(self, user_id, limit).await
390    }
391
392    ///! Sends a quote tweet with optional media attachments.
393    /// # Arguments
394    /// * `text` - The text content of the quote tweet.
395    /// * `quoted_tweet_id` - The ID of the tweet being quoted.
396    /// * `media_data` - An optional vector of tuples containing media data and their corresponding media types.
397    /// # Returns
398    /// * `Result<Value>` - A result containing the response from the quote tweet action if successful, or an error if not.
399    /// # Errors
400    /// Returns an error if the quote tweet cannot be sent, such as if the text is too long, if the quoted tweet does not exist, if the media data is invalid, or if there is a network issue.
401    pub async fn send_quote_tweet(
402        &mut self,
403        text: &str,
404        quoted_tweet_id: &str,
405        media_data: Option<Vec<(Vec<u8>, String)>>,
406    ) -> Result<Value> {
407        send_quote_tweet(self, text, quoted_tweet_id, media_data).await
408    }
409
410    ///! Fetches tweets and replies from a user's timeline.
411    /// # Arguments
412    /// * `username` - The screen name of the user whose tweets and replies are to be fetched.
413    /// * `max_tweets` - The maximum number of tweets to return.
414    /// * `cursor` - An optional cursor for pagination.
415    /// # Returns
416    /// * `Result<V2QueryTweetsResponse>` - A result containing the response with tweets and replies if successful, or an error if not.
417    /// # Errors
418    /// Returns an error if the tweets and replies cannot be fetched, such as if the user does not exist or if there is a network issue.
419    pub async fn fetch_tweets_and_replies(
420        &mut self,
421        username: &str,
422        max_tweets: i32,
423        cursor: Option<&str>,
424    ) -> Result<V2QueryTweetsResponse> {
425        fetch_tweets_and_replies(self, username, max_tweets, cursor).await
426    }
427
428    ///! Fetches tweets and replies from a user's timeline by their user ID.
429    /// # Arguments
430    /// * `user_id` - The ID of the user whose tweets and replies are to be fetched.
431    /// * `max_tweets` - The maximum number of tweets to return.
432    /// * `cursor` - An optional cursor for pagination.
433    /// # Returns
434    /// * `Result<V2QueryTweetsResponse>` - A result containing the response with tweets and replies if successful, or an error if not.
435    /// # Errors
436    /// Returns an error if the tweets and replies cannot be fetched, such as if the user does not exist or if there is a network issue.
437    pub async fn fetch_tweets_and_replies_by_user_id(
438        &mut self,
439        user_id: &str,
440        max_tweets: i32,
441        cursor: Option<&str>,
442    ) -> Result<V2QueryTweetsResponse> {
443        fetch_tweets_and_replies_by_user_id(self, user_id, max_tweets, cursor).await
444    }
445
446    ///! Fetches tweets from a list by its ID.
447    /// # Arguments
448    /// * `list_id` - The ID of the list whose tweets are to be fetched.
449    /// * `max_tweets` - The maximum number of tweets to return.
450    /// * `cursor` - An optional cursor for pagination.
451    /// # Returns
452    /// * `Result<Value>` - A result containing the response with tweets if successful, or an error if not.
453    /// # Errors
454    /// Returns an error if the tweets cannot be fetched, such as if the list does not exist or if there is a network issue.
455    pub async fn fetch_list_tweets(&mut self, list_id: &str, max_tweets: i32, cursor: Option<&str>) -> Result<Value> {
456        fetch_list_tweets(self, list_id, max_tweets, cursor).await
457    }
458
459    ///! Creates a long tweet with optional media attachments.
460    /// # Arguments
461    /// * `text` - The text content of the long tweet.
462    /// * `reply_to` - An optional tweet ID to reply to.
463    /// * `media_ids` - An optional vector of media IDs to attach to the long tweet.
464    /// # Returns
465    /// * `Result<Value>` - A result containing the response from the long tweet creation if successful, or an error if not.
466    /// # Errors
467    /// Returns an error if the long tweet cannot be created, such as if the text is too long, if the media IDs are invalid, or if there is a network issue.
468    pub async fn create_long_tweet(
469        &mut self,
470        text: &str,
471        reply_to: Option<&str>,
472        media_ids: Option<Vec<String>>,
473    ) -> Result<Value> {
474        create_long_tweet(self, text, reply_to, media_ids).await
475    }
476}
477
478///! Trend's API collection
479impl Xplore {
480    ///! Fetches the current trending topics.
481    ///
482    /// Retrieves a list of current trending topics from the platform.
483    ///
484    /// # Arguments
485    /// * `self` - The mutable reference to the Xplore instance (implicitly passed).
486    ///
487    /// # Returns
488    /// * `Result<Vec<String>>` - A result containing a vector of trend names if successful,
489    ///   or an error if the trends cannot be fetched.
490    ///
491    /// # Errors
492    /// Returns an error if:
493    /// - The request to fetch trends fails (network or API error)
494    /// - The response cannot be parsed into a list of strings
495    /// - There is an authentication issue preventing access to trends
496    pub async fn get_trend(&mut self) -> Result<Vec<String>> {
497        get_trend(&mut self.auth).await
498    }
499}