Skip to main content

threads_rs/api/
users.rs

1use std::collections::HashMap;
2
3use crate::client::Client;
4use crate::constants;
5use crate::error;
6use crate::types::{
7    PostsOptions, PostsResponse, PublicUser, RecentSearch, RepliesResponse, User, UserId,
8};
9use crate::validation;
10
11impl Client {
12    /// Get a user profile by ID.
13    pub async fn get_user(&self, user_id: &UserId) -> crate::Result<User> {
14        if !user_id.is_valid() {
15            return Err(error::new_validation_error(
16                0,
17                constants::ERR_EMPTY_USER_ID,
18                "",
19                "user_id",
20            ));
21        }
22
23        let token = self.access_token().await;
24        let mut params = HashMap::new();
25        params.insert("fields".into(), constants::USER_PROFILE_FIELDS.into());
26
27        let path = format!("/{}", user_id);
28        let resp = self.http_client.get(&path, params, &token).await?;
29        resp.json()
30    }
31
32    /// Get the authenticated user's profile.
33    pub async fn get_me(&self) -> crate::Result<User> {
34        let uid = self.user_id().await;
35        if uid.is_empty() {
36            return Err(error::new_authentication_error(
37                401,
38                constants::ERR_EMPTY_USER_ID,
39                "No user ID available from token",
40            ));
41        }
42        let user_id = UserId::from(uid);
43        self.get_user(&user_id).await
44    }
45
46    /// Look up a public user profile by username.
47    pub async fn lookup_public_profile(&self, username: &str) -> crate::Result<PublicUser> {
48        if username.is_empty() {
49            return Err(error::new_validation_error(
50                0,
51                "Username is required",
52                "",
53                "username",
54            ));
55        }
56
57        let token = self.access_token().await;
58        let mut params = HashMap::new();
59        params.insert("username".into(), username.to_owned());
60        params.insert("fields".into(), constants::PUBLIC_USER_FIELDS.into());
61
62        let resp = self
63            .http_client
64            .get("/profile_lookup", params, &token)
65            .await?;
66        resp.json()
67    }
68
69    /// Get a user profile with custom field selection.
70    pub async fn get_user_with_fields(
71        &self,
72        user_id: &UserId,
73        fields: &[&str],
74    ) -> crate::Result<User> {
75        if !user_id.is_valid() {
76            return Err(error::new_validation_error(
77                0,
78                constants::ERR_EMPTY_USER_ID,
79                "",
80                "user_id",
81            ));
82        }
83
84        let token = self.access_token().await;
85        let mut params = HashMap::new();
86
87        let fields_str = if fields.is_empty() {
88            constants::USER_PROFILE_FIELDS.to_owned()
89        } else {
90            fields.join(",")
91        };
92        params.insert("fields".into(), fields_str);
93
94        let path = format!("/{}", user_id);
95        let resp = self.http_client.get(&path, params, &token).await?;
96        resp.json()
97    }
98
99    /// Get posts from a public user profile by username.
100    pub async fn get_public_profile_posts(
101        &self,
102        username: &str,
103        opts: Option<&PostsOptions>,
104    ) -> crate::Result<PostsResponse> {
105        if username.is_empty() {
106            return Err(error::new_validation_error(
107                0,
108                "Username is required",
109                "",
110                "username",
111            ));
112        }
113
114        if let Some(opts) = opts {
115            validation::validate_posts_options(opts)?;
116        }
117
118        let token = self.access_token().await;
119        let mut params = HashMap::new();
120        params.insert("username".into(), username.to_owned());
121        params.insert("fields".into(), constants::POST_EXTENDED_FIELDS.into());
122
123        if let Some(opts) = opts {
124            if let Some(limit) = opts.limit {
125                params.insert("limit".into(), limit.to_string());
126            }
127            if let Some(ref before) = opts.before {
128                params.insert("before".into(), before.clone());
129            }
130            if let Some(ref after) = opts.after {
131                params.insert("after".into(), after.clone());
132            }
133            if let Some(since) = opts.since {
134                params.insert("since".into(), since.to_string());
135            }
136            if let Some(until) = opts.until {
137                params.insert("until".into(), until.to_string());
138            }
139        }
140
141        let resp = self
142            .http_client
143            .get("/profile_posts", params, &token)
144            .await?;
145        resp.json()
146    }
147
148    /// Get replies posted by a user.
149    pub async fn get_user_replies(
150        &self,
151        user_id: &UserId,
152        opts: Option<&PostsOptions>,
153    ) -> crate::Result<RepliesResponse> {
154        if !user_id.is_valid() {
155            return Err(error::new_validation_error(
156                0,
157                constants::ERR_EMPTY_USER_ID,
158                "",
159                "user_id",
160            ));
161        }
162
163        if let Some(opts) = opts {
164            validation::validate_posts_options(opts)?;
165        }
166
167        let token = self.access_token().await;
168        let mut params = HashMap::new();
169        params.insert("fields".into(), constants::REPLY_FIELDS.into());
170
171        if let Some(opts) = opts {
172            if let Some(limit) = opts.limit {
173                params.insert("limit".into(), limit.to_string());
174            }
175            if let Some(ref before) = opts.before {
176                params.insert("before".into(), before.clone());
177            }
178            if let Some(ref after) = opts.after {
179                params.insert("after".into(), after.clone());
180            }
181            if let Some(since) = opts.since {
182                params.insert("since".into(), since.to_string());
183            }
184            if let Some(until) = opts.until {
185                params.insert("until".into(), until.to_string());
186            }
187        }
188
189        let path = format!("/{}/replies", user_id);
190        let resp = self.http_client.get(&path, params, &token).await?;
191        resp.json()
192    }
193
194    // ---- Convenience methods using `me` ----
195
196    /// Get the authenticated user's posts.
197    pub async fn get_my_posts(&self, opts: Option<&PostsOptions>) -> crate::Result<PostsResponse> {
198        let uid = self.user_id().await;
199        if uid.is_empty() {
200            return Err(error::new_authentication_error(
201                401,
202                constants::ERR_EMPTY_USER_ID,
203                "No user ID available from token",
204            ));
205        }
206        let user_id = UserId::from(uid);
207        self.get_user_posts(&user_id, opts).await
208    }
209
210    /// Get the authenticated user's replies.
211    pub async fn get_my_replies(
212        &self,
213        opts: Option<&PostsOptions>,
214    ) -> crate::Result<RepliesResponse> {
215        let uid = self.user_id().await;
216        if uid.is_empty() {
217            return Err(error::new_authentication_error(
218                401,
219                constants::ERR_EMPTY_USER_ID,
220                "No user ID available from token",
221            ));
222        }
223        let user_id = UserId::from(uid);
224        self.get_user_replies(&user_id, opts).await
225    }
226
227    /// Get posts where the authenticated user is mentioned.
228    pub async fn get_my_mentions(
229        &self,
230        opts: Option<&PostsOptions>,
231    ) -> crate::Result<PostsResponse> {
232        let uid = self.user_id().await;
233        if uid.is_empty() {
234            return Err(error::new_authentication_error(
235                401,
236                constants::ERR_EMPTY_USER_ID,
237                "No user ID available from token",
238            ));
239        }
240        let user_id = UserId::from(uid);
241        self.get_user_mentions(&user_id, opts).await
242    }
243
244    /// Get the authenticated user's ghost posts.
245    pub async fn get_my_ghost_posts(
246        &self,
247        opts: Option<&PostsOptions>,
248    ) -> crate::Result<PostsResponse> {
249        let uid = self.user_id().await;
250        if uid.is_empty() {
251            return Err(error::new_authentication_error(
252                401,
253                constants::ERR_EMPTY_USER_ID,
254                "No user ID available from token",
255            ));
256        }
257        let user_id = UserId::from(uid);
258        self.get_user_ghost_posts(&user_id, opts).await
259    }
260
261    /// Get the authenticated user's recently searched keywords.
262    pub async fn get_recently_searched_keywords(&self) -> crate::Result<Vec<RecentSearch>> {
263        let uid = self.user_id().await;
264        if uid.is_empty() {
265            return Err(error::new_authentication_error(
266                401,
267                constants::ERR_EMPTY_USER_ID,
268                "No user ID available from token",
269            ));
270        }
271        let user_id = UserId::from(uid);
272        let user = self
273            .get_user_with_fields(&user_id, &["recently_searched_keywords"])
274            .await?;
275        Ok(user.recently_searched_keywords.unwrap_or_default())
276    }
277}