Skip to main content

xurl/api/
shortcuts.rs

1//! API shortcut functions — high-level X API v2 operations.
2//!
3//! Each function maps to one of the 27 shortcut commands, building the
4//! appropriate endpoint target and request body. Paths are spec-shaped
5//! templates (e.g. `/2/users/{id}/likes`) so the auth-matrix validator
6//! can key on the same string the build-time codegen ingests.
7
8use std::collections::HashMap;
9
10use serde::Serialize;
11
12use super::request::{ApiClient, CallOptions, RequestTarget};
13use super::response::types::{
14    ApiResponse, BookmarkedResult, DeletedResult, DmEvent, FollowingResult, LikedResult,
15    MutingResult, RetweetedResult, Tweet, UsageData, User, deserialize_response,
16};
17use crate::error::Result;
18
19// ── Request body types ───────────────────────────────────────────────
20
21#[derive(Serialize)]
22struct PostBody {
23    text: String,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    reply: Option<PostReply>,
26    #[serde(rename = "quote_tweet_id", skip_serializing_if = "Option::is_none")]
27    quote: Option<String>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    media: Option<PostMedia>,
30}
31
32#[derive(Serialize)]
33struct PostReply {
34    #[serde(rename = "in_reply_to_tweet_id")]
35    in_reply_to_post_id: String,
36}
37
38#[derive(Serialize)]
39struct PostMedia {
40    media_ids: Vec<String>,
41}
42
43// ── Helpers ──────────────────────────────────────────────────────────
44
45/// Extracts a post ID from a full URL or returns the input as-is.
46#[must_use]
47pub fn resolve_post_id(input: &str) -> String {
48    let input = input.trim();
49
50    if (input.starts_with("http://") || input.starts_with("https://"))
51        && let Ok(parsed) = url::Url::parse(input)
52    {
53        let parts: Vec<&str> = parsed.path().trim_matches('/').split('/').collect();
54        for (i, p) in parts.iter().enumerate() {
55            if *p == "status" && i + 1 < parts.len() {
56                return parts[i + 1].to_string();
57            }
58        }
59    }
60    input.to_string()
61}
62
63/// Normalizes a username — strips a leading "@" if present.
64#[must_use]
65pub fn resolve_username(input: &str) -> String {
66    input.trim().trim_start_matches('@').to_string()
67}
68
69// ── Write-op validators (U7) ─────────────────────────────────────────
70//
71// Each validator returns `Ok(())` when the inputs would be accepted by the
72// API and `Err(reason)` with a kebab-case reason otherwise. Callers compose
73// these into the canonical dry-run envelope without issuing HTTP.
74
75/// X API post body length budget. The platform rejects > 280 chars.
76pub const POST_BODY_MAX_CHARS: usize = 280;
77
78/// X API media attachment cap per post.
79pub const POST_MEDIA_MAX: usize = 4;
80
81/// Validates a post / reply / quote body.
82///
83/// # Errors
84/// Returns the kebab-case reason: `empty-body`, `body-too-long`.
85pub fn validate_post_body(text: &str) -> std::result::Result<(), &'static str> {
86    if text.is_empty() {
87        return Err("empty-body");
88    }
89    if text.chars().count() > POST_BODY_MAX_CHARS {
90        return Err("body-too-long");
91    }
92    Ok(())
93}
94
95/// Validates a media-attachment list for a post.
96///
97/// # Errors
98/// Returns `too-many-attachments` when more than [`POST_MEDIA_MAX`] are passed.
99pub fn validate_media_attachments(ids: &[String]) -> std::result::Result<(), &'static str> {
100    if ids.len() > POST_MEDIA_MAX {
101        return Err("too-many-attachments");
102    }
103    Ok(())
104}
105
106/// Validates a DM body.
107///
108/// # Errors
109/// Returns `empty-body` for an empty string. (DM length is enforced server-side.)
110pub fn validate_dm_body(text: &str) -> std::result::Result<(), &'static str> {
111    if text.is_empty() {
112        return Err("empty-body");
113    }
114    Ok(())
115}
116
117/// Validates a username target (strips a leading `@` first).
118///
119/// # Errors
120/// Returns `empty-username` when the trimmed input is empty.
121pub fn validate_target_username(input: &str) -> std::result::Result<(), &'static str> {
122    if resolve_username(input).is_empty() {
123        return Err("empty-username");
124    }
125    Ok(())
126}
127
128/// Validates a post identifier (URL or ID).
129///
130/// # Errors
131/// Returns `empty-post-id` when the resolver yields the empty string.
132pub fn validate_post_id(input: &str) -> std::result::Result<(), &'static str> {
133    if resolve_post_id(input).is_empty() {
134        return Err("empty-post-id");
135    }
136    Ok(())
137}
138
139// ── Shortcut methods on ApiClient ───────────────────────────────────
140
141/// Returns a base query vec for a paginated shortcut.
142///
143/// When the caller threaded a cursor into [`CallOptions::pagination_token`],
144/// emits `[("pagination_token", token)]`; otherwise empty. The percent-
145/// encoding happens at URL render time in `build_url_for_target`.
146fn build_query(opts: &CallOptions) -> Vec<(String, String)> {
147    if opts.pagination_token.is_empty() {
148        Vec::new()
149    } else {
150        vec![(
151            "pagination_token".to_string(),
152            opts.pagination_token.clone(),
153        )]
154    }
155}
156
157impl ApiClient {
158    /// Creates a new post.
159    ///
160    /// # Errors
161    ///
162    /// Returns an error if the request fails or the API returns an error.
163    ///
164    /// # Example
165    ///
166    /// ```rust,no_run
167    /// use xurl::api::{ApiClient, CallOptions};
168    /// use xurl::auth::Auth;
169    /// use xurl::config::Config;
170    ///
171    /// let cfg = Config::new();
172    /// let auth = Auth::new(&cfg);
173    /// let mut client = ApiClient::new(&cfg, auth);
174    ///
175    /// let resp = client.create_post("hello from xurl", &[], &CallOptions::default())?;
176    /// println!("created post id={}", resp.data.id);
177    /// # Ok::<(), xurl::error::XurlError>(())
178    /// ```
179    pub fn create_post(
180        &mut self,
181        text: &str,
182        media_ids: &[String],
183        opts: &CallOptions,
184    ) -> Result<ApiResponse<Tweet>> {
185        let mut body = PostBody {
186            text: text.to_string(),
187            reply: None,
188            quote: None,
189            media: None,
190        };
191        if !media_ids.is_empty() {
192            body.media = Some(PostMedia {
193                media_ids: media_ids.to_vec(),
194            });
195        }
196
197        let data = serde_json::to_string(&body)?;
198        let mut req = opts.to_request_options();
199        req.method = "POST".to_string();
200        req.target = RequestTarget::Template {
201            path: "/2/tweets".to_string(),
202            path_params: HashMap::new(),
203            query: Vec::new(),
204        };
205        req.data = data;
206
207        deserialize_response(self.send_request(&req)?)
208    }
209
210    /// Replies to an existing post.
211    ///
212    /// # Errors
213    ///
214    /// Returns an error if the request fails or the API returns an error.
215    pub fn reply_to_post(
216        &mut self,
217        post_id: &str,
218        text: &str,
219        media_ids: &[String],
220        opts: &CallOptions,
221    ) -> Result<ApiResponse<Tweet>> {
222        let post_id = resolve_post_id(post_id);
223        let mut body = PostBody {
224            text: text.to_string(),
225            reply: Some(PostReply {
226                in_reply_to_post_id: post_id,
227            }),
228            quote: None,
229            media: None,
230        };
231        if !media_ids.is_empty() {
232            body.media = Some(PostMedia {
233                media_ids: media_ids.to_vec(),
234            });
235        }
236
237        let data = serde_json::to_string(&body)?;
238        let mut req = opts.to_request_options();
239        req.method = "POST".to_string();
240        req.target = RequestTarget::Template {
241            path: "/2/tweets".to_string(),
242            path_params: HashMap::new(),
243            query: Vec::new(),
244        };
245        req.data = data;
246
247        deserialize_response(self.send_request(&req)?)
248    }
249
250    /// Quotes an existing post.
251    ///
252    /// # Errors
253    ///
254    /// Returns an error if the request fails or the API returns an error.
255    pub fn quote_post(
256        &mut self,
257        post_id: &str,
258        text: &str,
259        opts: &CallOptions,
260    ) -> Result<ApiResponse<Tweet>> {
261        let post_id = resolve_post_id(post_id);
262        let body = PostBody {
263            text: text.to_string(),
264            reply: None,
265            quote: Some(post_id),
266            media: None,
267        };
268
269        let data = serde_json::to_string(&body)?;
270        let mut req = opts.to_request_options();
271        req.method = "POST".to_string();
272        req.target = RequestTarget::Template {
273            path: "/2/tweets".to_string(),
274            path_params: HashMap::new(),
275            query: Vec::new(),
276        };
277        req.data = data;
278
279        deserialize_response(self.send_request(&req)?)
280    }
281
282    /// Deletes a post.
283    ///
284    /// # Errors
285    ///
286    /// Returns an error if the request fails or the API returns an error.
287    pub fn delete_post(
288        &mut self,
289        post_id: &str,
290        opts: &CallOptions,
291    ) -> Result<ApiResponse<DeletedResult>> {
292        let post_id = resolve_post_id(post_id);
293        let mut req = opts.to_request_options();
294        req.method = "DELETE".to_string();
295        req.target = RequestTarget::Template {
296            path: "/2/tweets/{id}".to_string(),
297            path_params: HashMap::from([("id".to_string(), post_id)]),
298            query: Vec::new(),
299        };
300        req.data.clear();
301
302        deserialize_response(self.send_request(&req)?)
303    }
304
305    /// Reads a single post with expansions.
306    ///
307    /// # Errors
308    ///
309    /// Returns an error if the request fails or the API returns an error.
310    pub fn read_post(&mut self, post_id: &str, opts: &CallOptions) -> Result<ApiResponse<Tweet>> {
311        let post_id = resolve_post_id(post_id);
312        let mut req = opts.to_request_options();
313        req.method = "GET".to_string();
314        req.target = RequestTarget::Template {
315            path: "/2/tweets/{id}".to_string(),
316            path_params: HashMap::from([("id".to_string(), post_id)]),
317            query: vec![
318                (
319                    "tweet.fields".to_string(),
320                    "created_at,public_metrics,conversation_id,in_reply_to_user_id,referenced_tweets,entities,attachments".to_string(),
321                ),
322                (
323                    "expansions".to_string(),
324                    "author_id,referenced_tweets.id".to_string(),
325                ),
326                (
327                    "user.fields".to_string(),
328                    "username,name,verified".to_string(),
329                ),
330            ],
331        };
332        req.data.clear();
333
334        deserialize_response(self.send_request(&req)?)
335    }
336
337    /// Searches recent posts.
338    ///
339    /// # Errors
340    ///
341    /// Returns an error if the request fails or the API returns an error.
342    ///
343    /// # Example
344    ///
345    /// ```rust,no_run
346    /// use xurl::api::{ApiClient, CallOptions};
347    /// use xurl::auth::Auth;
348    /// use xurl::config::Config;
349    ///
350    /// let cfg = Config::new();
351    /// let auth = Auth::new(&cfg);
352    /// let mut client = ApiClient::new(&cfg, auth);
353    ///
354    /// let resp = client.search_posts("rustlang", 25, &CallOptions::default())?;
355    /// for tweet in &resp.data {
356    ///     println!("{}: {}", tweet.id, tweet.text);
357    /// }
358    /// # Ok::<(), xurl::error::XurlError>(())
359    /// ```
360    pub fn search_posts(
361        &mut self,
362        query: &str,
363        max_results: i32,
364        opts: &CallOptions,
365    ) -> Result<ApiResponse<Vec<Tweet>>> {
366        let max_results = max_results.clamp(10, 100);
367
368        let mut q = vec![
369            ("query".to_string(), query.to_string()),
370            ("max_results".to_string(), max_results.to_string()),
371            (
372                "tweet.fields".to_string(),
373                "created_at,public_metrics,conversation_id,entities".to_string(),
374            ),
375            ("expansions".to_string(), "author_id".to_string()),
376            (
377                "user.fields".to_string(),
378                "username,name,verified".to_string(),
379            ),
380        ];
381        q.extend(build_query(opts));
382
383        let mut req = opts.to_request_options();
384        req.method = "GET".to_string();
385        req.target = RequestTarget::Template {
386            path: "/2/tweets/search/recent".to_string(),
387            path_params: HashMap::new(),
388            query: q,
389        };
390        req.data.clear();
391
392        deserialize_response(self.send_request(&req)?)
393    }
394
395    /// Fetches the authenticated user's profile.
396    ///
397    /// # Errors
398    ///
399    /// Returns an error if the request fails or the API returns an error.
400    ///
401    /// # Example
402    ///
403    /// ```rust,no_run
404    /// use xurl::api::{ApiClient, CallOptions};
405    /// use xurl::auth::Auth;
406    /// use xurl::config::Config;
407    ///
408    /// let cfg = Config::new();
409    /// let auth = Auth::new(&cfg);
410    /// let mut client = ApiClient::new(&cfg, auth);
411    ///
412    /// let resp = client.get_me(&CallOptions::default())?;
413    /// println!("@{} ({})", resp.data.username, resp.data.id);
414    /// # Ok::<(), xurl::error::XurlError>(())
415    /// ```
416    pub fn get_me(&mut self, opts: &CallOptions) -> Result<ApiResponse<User>> {
417        let mut req = opts.to_request_options();
418        req.method = "GET".to_string();
419        req.target = RequestTarget::Template {
420            path: "/2/users/me".to_string(),
421            path_params: HashMap::new(),
422            query: vec![(
423                "user.fields".to_string(),
424                "created_at,description,public_metrics,verified,profile_image_url".to_string(),
425            )],
426        };
427        req.data.clear();
428
429        deserialize_response(self.send_request(&req)?)
430    }
431
432    /// Looks up a user by username.
433    ///
434    /// # Errors
435    ///
436    /// Returns an error if the request fails or the API returns an error.
437    pub fn lookup_user(&mut self, username: &str, opts: &CallOptions) -> Result<ApiResponse<User>> {
438        let username = resolve_username(username);
439        let mut req = opts.to_request_options();
440        req.method = "GET".to_string();
441        req.target = RequestTarget::Template {
442            path: "/2/users/by/username/{username}".to_string(),
443            path_params: HashMap::from([("username".to_string(), username)]),
444            query: vec![(
445                "user.fields".to_string(),
446                "created_at,description,public_metrics,verified,profile_image_url".to_string(),
447            )],
448        };
449        req.data.clear();
450
451        deserialize_response(self.send_request(&req)?)
452    }
453
454    /// Fetches the home timeline.
455    ///
456    /// # Errors
457    ///
458    /// Returns an error if the request fails or the API returns an error.
459    pub fn get_timeline(
460        &mut self,
461        user_id: &str,
462        max_results: i32,
463        opts: &CallOptions,
464    ) -> Result<ApiResponse<Vec<Tweet>>> {
465        let mut q = vec![
466            ("max_results".to_string(), max_results.to_string()),
467            (
468                "tweet.fields".to_string(),
469                "created_at,public_metrics,conversation_id,entities".to_string(),
470            ),
471            ("expansions".to_string(), "author_id".to_string()),
472            ("user.fields".to_string(), "username,name".to_string()),
473        ];
474        q.extend(build_query(opts));
475
476        let mut req = opts.to_request_options();
477        req.method = "GET".to_string();
478        req.target = RequestTarget::Template {
479            path: "/2/users/{id}/timelines/reverse_chronological".to_string(),
480            path_params: HashMap::from([("id".to_string(), user_id.to_string())]),
481            query: q,
482        };
483        req.data.clear();
484
485        deserialize_response(self.send_request(&req)?)
486    }
487
488    /// Fetches recent mentions.
489    ///
490    /// # Errors
491    ///
492    /// Returns an error if the request fails or the API returns an error.
493    pub fn get_mentions(
494        &mut self,
495        user_id: &str,
496        max_results: i32,
497        opts: &CallOptions,
498    ) -> Result<ApiResponse<Vec<Tweet>>> {
499        let mut q = vec![
500            ("max_results".to_string(), max_results.to_string()),
501            (
502                "tweet.fields".to_string(),
503                "created_at,public_metrics,conversation_id,entities".to_string(),
504            ),
505            ("expansions".to_string(), "author_id".to_string()),
506            ("user.fields".to_string(), "username,name".to_string()),
507        ];
508        q.extend(build_query(opts));
509
510        let mut req = opts.to_request_options();
511        req.method = "GET".to_string();
512        req.target = RequestTarget::Template {
513            path: "/2/users/{id}/mentions".to_string(),
514            path_params: HashMap::from([("id".to_string(), user_id.to_string())]),
515            query: q,
516        };
517        req.data.clear();
518
519        deserialize_response(self.send_request(&req)?)
520    }
521
522    /// Likes a post.
523    ///
524    /// # Errors
525    ///
526    /// Returns an error if the request fails or the API returns an error.
527    pub fn like_post(
528        &mut self,
529        user_id: &str,
530        post_id: &str,
531        opts: &CallOptions,
532    ) -> Result<ApiResponse<LikedResult>> {
533        let post_id = resolve_post_id(post_id);
534        let mut req = opts.to_request_options();
535        req.method = "POST".to_string();
536        req.target = RequestTarget::Template {
537            path: "/2/users/{id}/likes".to_string(),
538            path_params: HashMap::from([("id".to_string(), user_id.to_string())]),
539            query: Vec::new(),
540        };
541        req.data = format!(r#"{{"tweet_id":"{post_id}"}}"#);
542
543        deserialize_response(self.send_request(&req)?)
544    }
545
546    /// Unlikes a post.
547    ///
548    /// # Errors
549    ///
550    /// Returns an error if the request fails or the API returns an error.
551    pub fn unlike_post(
552        &mut self,
553        user_id: &str,
554        post_id: &str,
555        opts: &CallOptions,
556    ) -> Result<ApiResponse<LikedResult>> {
557        let post_id = resolve_post_id(post_id);
558        let mut req = opts.to_request_options();
559        req.method = "DELETE".to_string();
560        req.target = RequestTarget::Template {
561            path: "/2/users/{id}/likes/{tweet_id}".to_string(),
562            path_params: HashMap::from([
563                ("id".to_string(), user_id.to_string()),
564                ("tweet_id".to_string(), post_id),
565            ]),
566            query: Vec::new(),
567        };
568        req.data.clear();
569
570        deserialize_response(self.send_request(&req)?)
571    }
572
573    /// Reposts a post.
574    ///
575    /// # Errors
576    ///
577    /// Returns an error if the request fails or the API returns an error.
578    pub fn repost(
579        &mut self,
580        user_id: &str,
581        post_id: &str,
582        opts: &CallOptions,
583    ) -> Result<ApiResponse<RetweetedResult>> {
584        let post_id = resolve_post_id(post_id);
585        let mut req = opts.to_request_options();
586        req.method = "POST".to_string();
587        req.target = RequestTarget::Template {
588            path: "/2/users/{id}/retweets".to_string(),
589            path_params: HashMap::from([("id".to_string(), user_id.to_string())]),
590            query: Vec::new(),
591        };
592        req.data = format!(r#"{{"tweet_id":"{post_id}"}}"#);
593
594        deserialize_response(self.send_request(&req)?)
595    }
596
597    /// Removes a repost.
598    ///
599    /// # Errors
600    ///
601    /// Returns an error if the request fails or the API returns an error.
602    pub fn unrepost(
603        &mut self,
604        user_id: &str,
605        post_id: &str,
606        opts: &CallOptions,
607    ) -> Result<ApiResponse<RetweetedResult>> {
608        let post_id = resolve_post_id(post_id);
609        let mut req = opts.to_request_options();
610        req.method = "DELETE".to_string();
611        req.target = RequestTarget::Template {
612            path: "/2/users/{id}/retweets/{source_tweet_id}".to_string(),
613            path_params: HashMap::from([
614                ("id".to_string(), user_id.to_string()),
615                ("source_tweet_id".to_string(), post_id),
616            ]),
617            query: Vec::new(),
618        };
619        req.data.clear();
620
621        deserialize_response(self.send_request(&req)?)
622    }
623
624    /// Bookmarks a post.
625    ///
626    /// # Errors
627    ///
628    /// Returns an error if the request fails or the API returns an error.
629    pub fn bookmark(
630        &mut self,
631        user_id: &str,
632        post_id: &str,
633        opts: &CallOptions,
634    ) -> Result<ApiResponse<BookmarkedResult>> {
635        let post_id = resolve_post_id(post_id);
636        let mut req = opts.to_request_options();
637        req.method = "POST".to_string();
638        req.target = RequestTarget::Template {
639            path: "/2/users/{id}/bookmarks".to_string(),
640            path_params: HashMap::from([("id".to_string(), user_id.to_string())]),
641            query: Vec::new(),
642        };
643        req.data = format!(r#"{{"tweet_id":"{post_id}"}}"#);
644
645        deserialize_response(self.send_request(&req)?)
646    }
647
648    /// Removes a bookmark.
649    ///
650    /// # Errors
651    ///
652    /// Returns an error if the request fails or the API returns an error.
653    pub fn unbookmark(
654        &mut self,
655        user_id: &str,
656        post_id: &str,
657        opts: &CallOptions,
658    ) -> Result<ApiResponse<BookmarkedResult>> {
659        let post_id = resolve_post_id(post_id);
660        let mut req = opts.to_request_options();
661        req.method = "DELETE".to_string();
662        req.target = RequestTarget::Template {
663            path: "/2/users/{id}/bookmarks/{tweet_id}".to_string(),
664            path_params: HashMap::from([
665                ("id".to_string(), user_id.to_string()),
666                ("tweet_id".to_string(), post_id),
667            ]),
668            query: Vec::new(),
669        };
670        req.data.clear();
671
672        deserialize_response(self.send_request(&req)?)
673    }
674
675    /// Fetches bookmarks.
676    ///
677    /// # Errors
678    ///
679    /// Returns an error if the request fails or the API returns an error.
680    pub fn get_bookmarks(
681        &mut self,
682        user_id: &str,
683        max_results: i32,
684        opts: &CallOptions,
685    ) -> Result<ApiResponse<Vec<Tweet>>> {
686        let mut q = vec![
687            ("max_results".to_string(), max_results.to_string()),
688            (
689                "tweet.fields".to_string(),
690                "created_at,public_metrics,entities".to_string(),
691            ),
692            ("expansions".to_string(), "author_id".to_string()),
693            ("user.fields".to_string(), "username,name".to_string()),
694        ];
695        q.extend(build_query(opts));
696
697        let mut req = opts.to_request_options();
698        req.method = "GET".to_string();
699        req.target = RequestTarget::Template {
700            path: "/2/users/{id}/bookmarks".to_string(),
701            path_params: HashMap::from([("id".to_string(), user_id.to_string())]),
702            query: q,
703        };
704        req.data.clear();
705
706        deserialize_response(self.send_request(&req)?)
707    }
708
709    /// Follows a user.
710    ///
711    /// # Errors
712    ///
713    /// Returns an error if the request fails or the API returns an error.
714    pub fn follow_user(
715        &mut self,
716        source_user_id: &str,
717        target_user_id: &str,
718        opts: &CallOptions,
719    ) -> Result<ApiResponse<FollowingResult>> {
720        let mut req = opts.to_request_options();
721        req.method = "POST".to_string();
722        req.target = RequestTarget::Template {
723            path: "/2/users/{id}/following".to_string(),
724            path_params: HashMap::from([("id".to_string(), source_user_id.to_string())]),
725            query: Vec::new(),
726        };
727        req.data = format!(r#"{{"target_user_id":"{target_user_id}"}}"#);
728
729        deserialize_response(self.send_request(&req)?)
730    }
731
732    /// Unfollows a user.
733    ///
734    /// # Errors
735    ///
736    /// Returns an error if the request fails or the API returns an error.
737    pub fn unfollow_user(
738        &mut self,
739        source_user_id: &str,
740        target_user_id: &str,
741        opts: &CallOptions,
742    ) -> Result<ApiResponse<FollowingResult>> {
743        let mut req = opts.to_request_options();
744        req.method = "DELETE".to_string();
745        req.target = RequestTarget::Template {
746            path: "/2/users/{source_user_id}/following/{target_user_id}".to_string(),
747            path_params: HashMap::from([
748                ("source_user_id".to_string(), source_user_id.to_string()),
749                ("target_user_id".to_string(), target_user_id.to_string()),
750            ]),
751            query: Vec::new(),
752        };
753        req.data.clear();
754
755        deserialize_response(self.send_request(&req)?)
756    }
757
758    /// Fetches users that a given user follows.
759    ///
760    /// # Errors
761    ///
762    /// Returns an error if the request fails or the API returns an error.
763    pub fn get_following(
764        &mut self,
765        user_id: &str,
766        max_results: i32,
767        opts: &CallOptions,
768    ) -> Result<ApiResponse<Vec<User>>> {
769        let mut q = vec![
770            ("max_results".to_string(), max_results.to_string()),
771            (
772                "user.fields".to_string(),
773                "created_at,description,public_metrics,verified".to_string(),
774            ),
775        ];
776        q.extend(build_query(opts));
777
778        let mut req = opts.to_request_options();
779        req.method = "GET".to_string();
780        req.target = RequestTarget::Template {
781            path: "/2/users/{id}/following".to_string(),
782            path_params: HashMap::from([("id".to_string(), user_id.to_string())]),
783            query: q,
784        };
785        req.data.clear();
786
787        deserialize_response(self.send_request(&req)?)
788    }
789
790    /// Fetches followers of a given user.
791    ///
792    /// # Errors
793    ///
794    /// Returns an error if the request fails or the API returns an error.
795    pub fn get_followers(
796        &mut self,
797        user_id: &str,
798        max_results: i32,
799        opts: &CallOptions,
800    ) -> Result<ApiResponse<Vec<User>>> {
801        let mut q = vec![
802            ("max_results".to_string(), max_results.to_string()),
803            (
804                "user.fields".to_string(),
805                "created_at,description,public_metrics,verified".to_string(),
806            ),
807        ];
808        q.extend(build_query(opts));
809
810        let mut req = opts.to_request_options();
811        req.method = "GET".to_string();
812        req.target = RequestTarget::Template {
813            path: "/2/users/{id}/followers".to_string(),
814            path_params: HashMap::from([("id".to_string(), user_id.to_string())]),
815            query: q,
816        };
817        req.data.clear();
818
819        deserialize_response(self.send_request(&req)?)
820    }
821
822    /// Sends a direct message.
823    ///
824    /// # Errors
825    ///
826    /// Returns an error if the request fails or the API returns an error.
827    pub fn send_dm(
828        &mut self,
829        participant_id: &str,
830        text: &str,
831        opts: &CallOptions,
832    ) -> Result<ApiResponse<DmEvent>> {
833        let body = serde_json::json!({"text": text});
834        let mut req = opts.to_request_options();
835        req.method = "POST".to_string();
836        req.target = RequestTarget::Template {
837            path: "/2/dm_conversations/with/{participant_id}/messages".to_string(),
838            path_params: HashMap::from([(
839                "participant_id".to_string(),
840                participant_id.to_string(),
841            )]),
842            query: Vec::new(),
843        };
844        req.data = serde_json::to_string(&body)?;
845
846        deserialize_response(self.send_request(&req)?)
847    }
848
849    /// Fetches recent DM events.
850    ///
851    /// # Errors
852    ///
853    /// Returns an error if the request fails or the API returns an error.
854    pub fn get_dm_events(
855        &mut self,
856        max_results: i32,
857        opts: &CallOptions,
858    ) -> Result<ApiResponse<Vec<DmEvent>>> {
859        let mut q = vec![
860            ("max_results".to_string(), max_results.to_string()),
861            (
862                "dm_event.fields".to_string(),
863                "created_at,dm_conversation_id,sender_id,text".to_string(),
864            ),
865            ("expansions".to_string(), "sender_id".to_string()),
866            ("user.fields".to_string(), "username,name".to_string()),
867        ];
868        q.extend(build_query(opts));
869
870        let mut req = opts.to_request_options();
871        req.method = "GET".to_string();
872        req.target = RequestTarget::Template {
873            path: "/2/dm_events".to_string(),
874            path_params: HashMap::new(),
875            query: q,
876        };
877        req.data.clear();
878
879        deserialize_response(self.send_request(&req)?)
880    }
881
882    /// Fetches posts liked by a user.
883    ///
884    /// # Errors
885    ///
886    /// Returns an error if the request fails or the API returns an error.
887    pub fn get_liked_posts(
888        &mut self,
889        user_id: &str,
890        max_results: i32,
891        opts: &CallOptions,
892    ) -> Result<ApiResponse<Vec<Tweet>>> {
893        let mut q = vec![
894            ("max_results".to_string(), max_results.to_string()),
895            (
896                "tweet.fields".to_string(),
897                "created_at,public_metrics,entities".to_string(),
898            ),
899            ("expansions".to_string(), "author_id".to_string()),
900            ("user.fields".to_string(), "username,name".to_string()),
901        ];
902        q.extend(build_query(opts));
903
904        let mut req = opts.to_request_options();
905        req.method = "GET".to_string();
906        req.target = RequestTarget::Template {
907            path: "/2/users/{id}/liked_tweets".to_string(),
908            path_params: HashMap::from([("id".to_string(), user_id.to_string())]),
909            query: q,
910        };
911        req.data.clear();
912
913        deserialize_response(self.send_request(&req)?)
914    }
915
916    /// Mutes a user.
917    ///
918    /// # Errors
919    ///
920    /// Returns an error if the request fails or the API returns an error.
921    pub fn mute_user(
922        &mut self,
923        source_user_id: &str,
924        target_user_id: &str,
925        opts: &CallOptions,
926    ) -> Result<ApiResponse<MutingResult>> {
927        let mut req = opts.to_request_options();
928        req.method = "POST".to_string();
929        req.target = RequestTarget::Template {
930            path: "/2/users/{id}/muting".to_string(),
931            path_params: HashMap::from([("id".to_string(), source_user_id.to_string())]),
932            query: Vec::new(),
933        };
934        req.data = format!(r#"{{"target_user_id":"{target_user_id}"}}"#);
935
936        deserialize_response(self.send_request(&req)?)
937    }
938
939    /// Fetches API usage data (tweet caps, daily breakdowns).
940    ///
941    /// # Errors
942    ///
943    /// Returns an error if the request fails or the API returns an error.
944    pub fn get_usage(&mut self, opts: &CallOptions) -> Result<ApiResponse<UsageData>> {
945        let mut req = opts.to_request_options();
946        req.method = "GET".to_string();
947        req.target = RequestTarget::Template {
948            path: "/2/usage/tweets".to_string(),
949            path_params: HashMap::new(),
950            query: vec![(
951                "usage.fields".to_string(),
952                "daily_project_usage,daily_client_app_usage".to_string(),
953            )],
954        };
955        req.data.clear();
956
957        deserialize_response(self.send_request(&req)?)
958    }
959
960    /// Unmutes a user.
961    ///
962    /// # Errors
963    ///
964    /// Returns an error if the request fails or the API returns an error.
965    pub fn unmute_user(
966        &mut self,
967        source_user_id: &str,
968        target_user_id: &str,
969        opts: &CallOptions,
970    ) -> Result<ApiResponse<MutingResult>> {
971        let mut req = opts.to_request_options();
972        req.method = "DELETE".to_string();
973        req.target = RequestTarget::Template {
974            path: "/2/users/{source_user_id}/muting/{target_user_id}".to_string(),
975            path_params: HashMap::from([
976                ("source_user_id".to_string(), source_user_id.to_string()),
977                ("target_user_id".to_string(), target_user_id.to_string()),
978            ]),
979            query: Vec::new(),
980        };
981        req.data.clear();
982
983        deserialize_response(self.send_request(&req)?)
984    }
985}