1use crate::error::XApiError;
7use crate::x_api::types::{
8 ActionResultResponse, BookmarkTweetRequest, DeleteTweetResponse, FollowUserRequest,
9 LikeTweetRequest, MediaId, MediaPayload, MediaType, MentionResponse, PostTweetRequest,
10 PostTweetResponse, PostedTweet, RawApiResponse, ReplyTo, RetweetRequest, SearchResponse,
11 SingleTweetResponse, Tweet, User, UserResponse, UsersResponse,
12};
13use crate::x_api::XApiClient;
14
15use super::{XApiHttpClient, EXPANSIONS, TWEET_FIELDS, USER_FIELDS};
16
17#[async_trait::async_trait]
18impl XApiClient for XApiHttpClient {
19 async fn search_tweets(
20 &self,
21 query: &str,
22 max_results: u32,
23 since_id: Option<&str>,
24 pagination_token: Option<&str>,
25 ) -> Result<SearchResponse, XApiError> {
26 tracing::debug!(query = %query, max_results = max_results, "Search tweets");
27 let max_str = max_results.to_string();
28 let mut params = vec![
29 ("query", query),
30 ("max_results", &max_str),
31 ("tweet.fields", TWEET_FIELDS),
32 ("expansions", EXPANSIONS),
33 ("user.fields", USER_FIELDS),
34 ];
35
36 let since_id_owned;
37 if let Some(sid) = since_id {
38 since_id_owned = sid.to_string();
39 params.push(("since_id", &since_id_owned));
40 }
41
42 let pagination_token_owned;
43 if let Some(pt) = pagination_token {
44 pagination_token_owned = pt.to_string();
45 params.push(("pagination_token", &pagination_token_owned));
46 }
47
48 let response = self.get("/tweets/search/recent", ¶ms).await?;
49 let resp: SearchResponse = response
50 .json()
51 .await
52 .map_err(|e| XApiError::Network { source: e })?;
53 tracing::debug!(
54 query = %query,
55 results = resp.data.len(),
56 "Search tweets completed",
57 );
58 Ok(resp)
59 }
60
61 async fn get_mentions(
62 &self,
63 user_id: &str,
64 since_id: Option<&str>,
65 pagination_token: Option<&str>,
66 ) -> Result<MentionResponse, XApiError> {
67 let path = format!("/users/{user_id}/mentions");
68 let mut params = vec![
69 ("tweet.fields", TWEET_FIELDS),
70 ("expansions", EXPANSIONS),
71 ("user.fields", USER_FIELDS),
72 ];
73
74 let since_id_owned;
75 if let Some(sid) = since_id {
76 since_id_owned = sid.to_string();
77 params.push(("since_id", &since_id_owned));
78 }
79
80 let pagination_token_owned;
81 if let Some(pt) = pagination_token {
82 pagination_token_owned = pt.to_string();
83 params.push(("pagination_token", &pagination_token_owned));
84 }
85
86 let response = self.get(&path, ¶ms).await?;
87 response
88 .json::<MentionResponse>()
89 .await
90 .map_err(|e| XApiError::Network { source: e })
91 }
92
93 async fn post_tweet(&self, text: &str) -> Result<PostedTweet, XApiError> {
94 tracing::debug!(chars = text.len(), "Posting tweet");
95 let body = PostTweetRequest {
96 text: text.to_string(),
97 reply: None,
98 media: None,
99 quote_tweet_id: None,
100 };
101
102 let response = self.post_json("/tweets", &body).await?;
103 let resp: PostTweetResponse = response
104 .json()
105 .await
106 .map_err(|e| XApiError::Network { source: e })?;
107 Ok(resp.data)
108 }
109
110 async fn reply_to_tweet(
111 &self,
112 text: &str,
113 in_reply_to_id: &str,
114 ) -> Result<PostedTweet, XApiError> {
115 tracing::debug!(in_reply_to = %in_reply_to_id, chars = text.len(), "Posting reply");
116 let body = PostTweetRequest {
117 text: text.to_string(),
118 reply: Some(ReplyTo {
119 in_reply_to_tweet_id: in_reply_to_id.to_string(),
120 }),
121 media: None,
122 quote_tweet_id: None,
123 };
124
125 let response = self.post_json("/tweets", &body).await?;
126 let resp: PostTweetResponse = response
127 .json()
128 .await
129 .map_err(|e| XApiError::Network { source: e })?;
130 Ok(resp.data)
131 }
132
133 async fn upload_media(&self, data: &[u8], media_type: MediaType) -> Result<MediaId, XApiError> {
134 super::super::media::upload_media(
135 &self.client,
136 &self.upload_base_url,
137 &self.access_token.read().await,
138 data,
139 media_type,
140 )
141 .await
142 }
143
144 async fn post_tweet_with_media(
145 &self,
146 text: &str,
147 media_ids: &[String],
148 ) -> Result<PostedTweet, XApiError> {
149 tracing::debug!(
150 chars = text.len(),
151 media_count = media_ids.len(),
152 "Posting tweet with media"
153 );
154 let body = PostTweetRequest {
155 text: text.to_string(),
156 reply: None,
157 media: Some(MediaPayload {
158 media_ids: media_ids.to_vec(),
159 }),
160 quote_tweet_id: None,
161 };
162
163 let response = self.post_json("/tweets", &body).await?;
164 let resp: PostTweetResponse = response
165 .json()
166 .await
167 .map_err(|e| XApiError::Network { source: e })?;
168 Ok(resp.data)
169 }
170
171 async fn reply_to_tweet_with_media(
172 &self,
173 text: &str,
174 in_reply_to_id: &str,
175 media_ids: &[String],
176 ) -> Result<PostedTweet, XApiError> {
177 tracing::debug!(in_reply_to = %in_reply_to_id, chars = text.len(), media_count = media_ids.len(), "Posting reply with media");
178 let body = PostTweetRequest {
179 text: text.to_string(),
180 reply: Some(ReplyTo {
181 in_reply_to_tweet_id: in_reply_to_id.to_string(),
182 }),
183 media: Some(MediaPayload {
184 media_ids: media_ids.to_vec(),
185 }),
186 quote_tweet_id: None,
187 };
188
189 let response = self.post_json("/tweets", &body).await?;
190 let resp: PostTweetResponse = response
191 .json()
192 .await
193 .map_err(|e| XApiError::Network { source: e })?;
194 Ok(resp.data)
195 }
196
197 async fn get_tweet(&self, tweet_id: &str) -> Result<Tweet, XApiError> {
198 let path = format!("/tweets/{tweet_id}");
199 let params = [
200 ("tweet.fields", TWEET_FIELDS),
201 ("expansions", EXPANSIONS),
202 ("user.fields", USER_FIELDS),
203 ];
204
205 let response = self.get(&path, ¶ms).await?;
206 let resp: SingleTweetResponse = response
207 .json()
208 .await
209 .map_err(|e| XApiError::Network { source: e })?;
210 Ok(resp.data)
211 }
212
213 async fn get_me(&self) -> Result<User, XApiError> {
214 let params = [("user.fields", USER_FIELDS)];
215
216 let response = self.get("/users/me", ¶ms).await?;
217 let resp: UserResponse = response
218 .json()
219 .await
220 .map_err(|e| XApiError::Network { source: e })?;
221 Ok(resp.data)
222 }
223
224 async fn get_user_tweets(
225 &self,
226 user_id: &str,
227 max_results: u32,
228 pagination_token: Option<&str>,
229 ) -> Result<SearchResponse, XApiError> {
230 let path = format!("/users/{user_id}/tweets");
231 let max_str = max_results.to_string();
232 let mut params = vec![
233 ("max_results", max_str.as_str()),
234 ("tweet.fields", TWEET_FIELDS),
235 ("expansions", EXPANSIONS),
236 ("user.fields", USER_FIELDS),
237 ];
238
239 let pagination_token_owned;
240 if let Some(pt) = pagination_token {
241 pagination_token_owned = pt.to_string();
242 params.push(("pagination_token", &pagination_token_owned));
243 }
244
245 let response = self.get(&path, ¶ms).await?;
246 response
247 .json::<SearchResponse>()
248 .await
249 .map_err(|e| XApiError::Network { source: e })
250 }
251
252 async fn get_user_by_username(&self, username: &str) -> Result<User, XApiError> {
253 let path = format!("/users/by/username/{username}");
254 let params = [("user.fields", USER_FIELDS)];
255
256 let response = self.get(&path, ¶ms).await?;
257 let resp: UserResponse = response
258 .json()
259 .await
260 .map_err(|e| XApiError::Network { source: e })?;
261 Ok(resp.data)
262 }
263
264 async fn quote_tweet(
265 &self,
266 text: &str,
267 quoted_tweet_id: &str,
268 ) -> Result<PostedTweet, XApiError> {
269 tracing::debug!(chars = text.len(), quoted = %quoted_tweet_id, "Posting quote tweet");
270 let body = PostTweetRequest {
271 text: text.to_string(),
272 reply: None,
273 media: None,
274 quote_tweet_id: Some(quoted_tweet_id.to_string()),
275 };
276
277 let response = self.post_json("/tweets", &body).await?;
278 let resp: PostTweetResponse = response
279 .json()
280 .await
281 .map_err(|e| XApiError::Network { source: e })?;
282 Ok(resp.data)
283 }
284
285 async fn like_tweet(&self, user_id: &str, tweet_id: &str) -> Result<bool, XApiError> {
286 tracing::debug!(user_id = %user_id, tweet_id = %tweet_id, "Liking tweet");
287 let path = format!("/users/{user_id}/likes");
288 let body = LikeTweetRequest {
289 tweet_id: tweet_id.to_string(),
290 };
291
292 let response = self.post_json(&path, &body).await?;
293 let resp: ActionResultResponse = response
294 .json()
295 .await
296 .map_err(|e| XApiError::Network { source: e })?;
297 Ok(resp.data.result)
298 }
299
300 async fn follow_user(&self, user_id: &str, target_user_id: &str) -> Result<bool, XApiError> {
301 tracing::debug!(user_id = %user_id, target = %target_user_id, "Following user");
302 let path = format!("/users/{user_id}/following");
303 let body = FollowUserRequest {
304 target_user_id: target_user_id.to_string(),
305 };
306
307 let response = self.post_json(&path, &body).await?;
308 let resp: ActionResultResponse = response
309 .json()
310 .await
311 .map_err(|e| XApiError::Network { source: e })?;
312 Ok(resp.data.result)
313 }
314
315 async fn unfollow_user(&self, user_id: &str, target_user_id: &str) -> Result<bool, XApiError> {
316 tracing::debug!(user_id = %user_id, target = %target_user_id, "Unfollowing user");
317 let path = format!("/users/{user_id}/following/{target_user_id}");
318
319 let response = self.delete(&path).await?;
320 let resp: ActionResultResponse = response
321 .json()
322 .await
323 .map_err(|e| XApiError::Network { source: e })?;
324 Ok(resp.data.result)
325 }
326
327 async fn retweet(&self, user_id: &str, tweet_id: &str) -> Result<bool, XApiError> {
328 tracing::debug!(user_id = %user_id, tweet_id = %tweet_id, "Retweeting");
329 let path = format!("/users/{user_id}/retweets");
330 let body = RetweetRequest {
331 tweet_id: tweet_id.to_string(),
332 };
333
334 let response = self.post_json(&path, &body).await?;
335 let resp: ActionResultResponse = response
336 .json()
337 .await
338 .map_err(|e| XApiError::Network { source: e })?;
339 Ok(resp.data.result)
340 }
341
342 async fn unretweet(&self, user_id: &str, tweet_id: &str) -> Result<bool, XApiError> {
343 tracing::debug!(user_id = %user_id, tweet_id = %tweet_id, "Unretweeting");
344 let path = format!("/users/{user_id}/retweets/{tweet_id}");
345
346 let response = self.delete(&path).await?;
347 let resp: ActionResultResponse = response
348 .json()
349 .await
350 .map_err(|e| XApiError::Network { source: e })?;
351 Ok(resp.data.result)
352 }
353
354 async fn delete_tweet(&self, tweet_id: &str) -> Result<bool, XApiError> {
355 tracing::debug!(tweet_id = %tweet_id, "Deleting tweet");
356 let path = format!("/tweets/{tweet_id}");
357
358 let response = self.delete(&path).await?;
359 let resp: DeleteTweetResponse = response
360 .json()
361 .await
362 .map_err(|e| XApiError::Network { source: e })?;
363 Ok(resp.data.deleted)
364 }
365
366 async fn get_home_timeline(
367 &self,
368 user_id: &str,
369 max_results: u32,
370 pagination_token: Option<&str>,
371 ) -> Result<SearchResponse, XApiError> {
372 tracing::debug!(user_id = %user_id, max_results = max_results, "Getting home timeline");
373 let path = format!("/users/{user_id}/timelines/reverse_chronological");
374 let max_str = max_results.to_string();
375 let mut params = vec![
376 ("max_results", max_str.as_str()),
377 ("tweet.fields", TWEET_FIELDS),
378 ("expansions", EXPANSIONS),
379 ("user.fields", USER_FIELDS),
380 ];
381
382 let pagination_token_owned;
383 if let Some(pt) = pagination_token {
384 pagination_token_owned = pt.to_string();
385 params.push(("pagination_token", &pagination_token_owned));
386 }
387
388 let response = self.get(&path, ¶ms).await?;
389 response
390 .json::<SearchResponse>()
391 .await
392 .map_err(|e| XApiError::Network { source: e })
393 }
394
395 async fn unlike_tweet(&self, user_id: &str, tweet_id: &str) -> Result<bool, XApiError> {
396 tracing::debug!(user_id = %user_id, tweet_id = %tweet_id, "Unliking tweet");
397 let path = format!("/users/{user_id}/likes/{tweet_id}");
398
399 let response = self.delete(&path).await?;
400 let resp: ActionResultResponse = response
401 .json()
402 .await
403 .map_err(|e| XApiError::Network { source: e })?;
404 Ok(resp.data.result)
405 }
406
407 async fn get_followers(
408 &self,
409 user_id: &str,
410 max_results: u32,
411 pagination_token: Option<&str>,
412 ) -> Result<UsersResponse, XApiError> {
413 tracing::debug!(user_id = %user_id, max_results = max_results, "Getting followers");
414 let path = format!("/users/{user_id}/followers");
415 let max_str = max_results.to_string();
416 let mut params = vec![
417 ("max_results", max_str.as_str()),
418 ("user.fields", USER_FIELDS),
419 ];
420
421 let pagination_token_owned;
422 if let Some(pt) = pagination_token {
423 pagination_token_owned = pt.to_string();
424 params.push(("pagination_token", &pagination_token_owned));
425 }
426
427 let response = self.get(&path, ¶ms).await?;
428 response
429 .json::<UsersResponse>()
430 .await
431 .map_err(|e| XApiError::Network { source: e })
432 }
433
434 async fn get_following(
435 &self,
436 user_id: &str,
437 max_results: u32,
438 pagination_token: Option<&str>,
439 ) -> Result<UsersResponse, XApiError> {
440 tracing::debug!(user_id = %user_id, max_results = max_results, "Getting following");
441 let path = format!("/users/{user_id}/following");
442 let max_str = max_results.to_string();
443 let mut params = vec![
444 ("max_results", max_str.as_str()),
445 ("user.fields", USER_FIELDS),
446 ];
447
448 let pagination_token_owned;
449 if let Some(pt) = pagination_token {
450 pagination_token_owned = pt.to_string();
451 params.push(("pagination_token", &pagination_token_owned));
452 }
453
454 let response = self.get(&path, ¶ms).await?;
455 response
456 .json::<UsersResponse>()
457 .await
458 .map_err(|e| XApiError::Network { source: e })
459 }
460
461 async fn get_user_by_id(&self, user_id: &str) -> Result<User, XApiError> {
462 tracing::debug!(user_id = %user_id, "Getting user by ID");
463 let path = format!("/users/{user_id}");
464 let params = [("user.fields", USER_FIELDS)];
465
466 let response = self.get(&path, ¶ms).await?;
467 let resp: UserResponse = response
468 .json()
469 .await
470 .map_err(|e| XApiError::Network { source: e })?;
471 Ok(resp.data)
472 }
473
474 async fn get_liked_tweets(
475 &self,
476 user_id: &str,
477 max_results: u32,
478 pagination_token: Option<&str>,
479 ) -> Result<SearchResponse, XApiError> {
480 tracing::debug!(user_id = %user_id, max_results = max_results, "Getting liked tweets");
481 let path = format!("/users/{user_id}/liked_tweets");
482 let max_str = max_results.to_string();
483 let mut params = vec![
484 ("max_results", max_str.as_str()),
485 ("tweet.fields", TWEET_FIELDS),
486 ("expansions", EXPANSIONS),
487 ("user.fields", USER_FIELDS),
488 ];
489
490 let pagination_token_owned;
491 if let Some(pt) = pagination_token {
492 pagination_token_owned = pt.to_string();
493 params.push(("pagination_token", &pagination_token_owned));
494 }
495
496 let response = self.get(&path, ¶ms).await?;
497 response
498 .json::<SearchResponse>()
499 .await
500 .map_err(|e| XApiError::Network { source: e })
501 }
502
503 async fn get_bookmarks(
504 &self,
505 user_id: &str,
506 max_results: u32,
507 pagination_token: Option<&str>,
508 ) -> Result<SearchResponse, XApiError> {
509 tracing::debug!(user_id = %user_id, max_results = max_results, "Getting bookmarks");
510 let path = format!("/users/{user_id}/bookmarks");
511 let max_str = max_results.to_string();
512 let mut params = vec![
513 ("max_results", max_str.as_str()),
514 ("tweet.fields", TWEET_FIELDS),
515 ("expansions", EXPANSIONS),
516 ("user.fields", USER_FIELDS),
517 ];
518
519 let pagination_token_owned;
520 if let Some(pt) = pagination_token {
521 pagination_token_owned = pt.to_string();
522 params.push(("pagination_token", &pagination_token_owned));
523 }
524
525 let response = self.get(&path, ¶ms).await?;
526 response
527 .json::<SearchResponse>()
528 .await
529 .map_err(|e| XApiError::Network { source: e })
530 }
531
532 async fn bookmark_tweet(&self, user_id: &str, tweet_id: &str) -> Result<bool, XApiError> {
533 tracing::debug!(user_id = %user_id, tweet_id = %tweet_id, "Bookmarking tweet");
534 let path = format!("/users/{user_id}/bookmarks");
535 let body = BookmarkTweetRequest {
536 tweet_id: tweet_id.to_string(),
537 };
538
539 let response = self.post_json(&path, &body).await?;
540 let resp: ActionResultResponse = response
541 .json()
542 .await
543 .map_err(|e| XApiError::Network { source: e })?;
544 Ok(resp.data.result)
545 }
546
547 async fn unbookmark_tweet(&self, user_id: &str, tweet_id: &str) -> Result<bool, XApiError> {
548 tracing::debug!(user_id = %user_id, tweet_id = %tweet_id, "Unbookmarking tweet");
549 let path = format!("/users/{user_id}/bookmarks/{tweet_id}");
550
551 let response = self.delete(&path).await?;
552 let resp: ActionResultResponse = response
553 .json()
554 .await
555 .map_err(|e| XApiError::Network { source: e })?;
556 Ok(resp.data.result)
557 }
558
559 async fn get_users_by_ids(&self, user_ids: &[&str]) -> Result<UsersResponse, XApiError> {
560 tracing::debug!(count = user_ids.len(), "Getting users by IDs");
561 let ids = user_ids.join(",");
562 let params = [("ids", ids.as_str()), ("user.fields", USER_FIELDS)];
563
564 let response = self.get("/users", ¶ms).await?;
565 response
566 .json::<UsersResponse>()
567 .await
568 .map_err(|e| XApiError::Network { source: e })
569 }
570
571 async fn get_tweet_liking_users(
572 &self,
573 tweet_id: &str,
574 max_results: u32,
575 pagination_token: Option<&str>,
576 ) -> Result<UsersResponse, XApiError> {
577 tracing::debug!(tweet_id = %tweet_id, max_results = max_results, "Getting liking users");
578 let path = format!("/tweets/{tweet_id}/liking_users");
579 let max_str = max_results.to_string();
580 let mut params = vec![
581 ("max_results", max_str.as_str()),
582 ("user.fields", USER_FIELDS),
583 ];
584
585 let pagination_token_owned;
586 if let Some(pt) = pagination_token {
587 pagination_token_owned = pt.to_string();
588 params.push(("pagination_token", &pagination_token_owned));
589 }
590
591 let response = self.get(&path, ¶ms).await?;
592 response
593 .json::<UsersResponse>()
594 .await
595 .map_err(|e| XApiError::Network { source: e })
596 }
597
598 async fn raw_request(
599 &self,
600 method: &str,
601 url: &str,
602 query: Option<&[(String, String)]>,
603 body: Option<&str>,
604 headers: Option<&[(String, String)]>,
605 ) -> Result<RawApiResponse, XApiError> {
606 let token = self.access_token.read().await;
607 let req_method = match method.to_ascii_uppercase().as_str() {
608 "GET" => reqwest::Method::GET,
609 "POST" => reqwest::Method::POST,
610 "PUT" => reqwest::Method::PUT,
611 "DELETE" => reqwest::Method::DELETE,
612 other => {
613 return Err(XApiError::ApiError {
614 status: 0,
615 message: format!("unsupported HTTP method: {other}"),
616 })
617 }
618 };
619
620 let mut builder = self.client.request(req_method, url).bearer_auth(&*token);
621
622 if let Some(pairs) = query {
623 builder = builder.query(pairs);
624 }
625 if let Some(json_body) = body {
626 builder = builder
627 .header("Content-Type", "application/json")
628 .body(json_body.to_string());
629 }
630 if let Some(extra_headers) = headers {
631 for (k, v) in extra_headers {
632 builder = builder.header(k.as_str(), v.as_str());
633 }
634 }
635
636 let response = builder
637 .send()
638 .await
639 .map_err(|e| XApiError::Network { source: e })?;
640
641 let status = response.status().as_u16();
642 let rate_limit = Self::parse_rate_limit_headers(response.headers());
643
644 let mut resp_headers = std::collections::HashMap::new();
646 for key in [
647 "content-type",
648 "x-rate-limit-remaining",
649 "x-rate-limit-reset",
650 "x-rate-limit-limit",
651 ] {
652 if let Some(val) = response.headers().get(key) {
653 if let Ok(s) = val.to_str() {
654 resp_headers.insert(key.to_string(), s.to_string());
655 }
656 }
657 }
658
659 if let Ok(parsed) = reqwest::Url::parse(url) {
661 self.record_usage(parsed.path(), method, status);
662 }
663
664 let response_body = response
665 .text()
666 .await
667 .map_err(|e| XApiError::Network { source: e })?;
668
669 Ok(RawApiResponse {
670 status,
671 headers: resp_headers,
672 body: response_body,
673 rate_limit: Some(rate_limit),
674 })
675 }
676}