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}