1use super::ToolkitError;
7use crate::x_api::types::{MentionResponse, SearchResponse, Tweet, User, UsersResponse};
8use crate::x_api::XApiClient;
9
10pub async fn get_tweet(client: &dyn XApiClient, tweet_id: &str) -> Result<Tweet, ToolkitError> {
12 super::validate_id(tweet_id, "tweet_id")?;
13 Ok(client.get_tweet(tweet_id).await?)
14}
15
16pub async fn get_user_by_username(
18 client: &dyn XApiClient,
19 username: &str,
20) -> Result<User, ToolkitError> {
21 super::validate_id(username, "username")?;
22 Ok(client.get_user_by_username(username).await?)
23}
24
25pub async fn get_user_by_id(client: &dyn XApiClient, user_id: &str) -> Result<User, ToolkitError> {
27 super::validate_id(user_id, "user_id")?;
28 Ok(client.get_user_by_id(user_id).await?)
29}
30
31pub async fn get_me(client: &dyn XApiClient) -> Result<User, ToolkitError> {
33 Ok(client.get_me().await?)
34}
35
36pub async fn search_tweets(
38 client: &dyn XApiClient,
39 query: &str,
40 max_results: u32,
41 since_id: Option<&str>,
42 pagination_token: Option<&str>,
43) -> Result<SearchResponse, ToolkitError> {
44 if query.is_empty() {
45 return Err(ToolkitError::InvalidInput {
46 message: "query must not be empty".into(),
47 });
48 }
49 Ok(client
50 .search_tweets(query, max_results, since_id, pagination_token)
51 .await?)
52}
53
54pub async fn get_mentions(
56 client: &dyn XApiClient,
57 user_id: &str,
58 since_id: Option<&str>,
59 pagination_token: Option<&str>,
60) -> Result<MentionResponse, ToolkitError> {
61 super::validate_id(user_id, "user_id")?;
62 Ok(client
63 .get_mentions(user_id, since_id, pagination_token)
64 .await?)
65}
66
67pub async fn get_user_tweets(
69 client: &dyn XApiClient,
70 user_id: &str,
71 max_results: u32,
72 pagination_token: Option<&str>,
73) -> Result<SearchResponse, ToolkitError> {
74 super::validate_id(user_id, "user_id")?;
75 Ok(client
76 .get_user_tweets(user_id, max_results, pagination_token)
77 .await?)
78}
79
80pub async fn get_home_timeline(
82 client: &dyn XApiClient,
83 user_id: &str,
84 max_results: u32,
85 pagination_token: Option<&str>,
86) -> Result<SearchResponse, ToolkitError> {
87 super::validate_id(user_id, "user_id")?;
88 Ok(client
89 .get_home_timeline(user_id, max_results, pagination_token)
90 .await?)
91}
92
93pub async fn get_followers(
95 client: &dyn XApiClient,
96 user_id: &str,
97 max_results: u32,
98 pagination_token: Option<&str>,
99) -> Result<UsersResponse, ToolkitError> {
100 super::validate_id(user_id, "user_id")?;
101 Ok(client
102 .get_followers(user_id, max_results, pagination_token)
103 .await?)
104}
105
106pub async fn get_following(
108 client: &dyn XApiClient,
109 user_id: &str,
110 max_results: u32,
111 pagination_token: Option<&str>,
112) -> Result<UsersResponse, ToolkitError> {
113 super::validate_id(user_id, "user_id")?;
114 Ok(client
115 .get_following(user_id, max_results, pagination_token)
116 .await?)
117}
118
119pub async fn get_liked_tweets(
121 client: &dyn XApiClient,
122 user_id: &str,
123 max_results: u32,
124 pagination_token: Option<&str>,
125) -> Result<SearchResponse, ToolkitError> {
126 super::validate_id(user_id, "user_id")?;
127 Ok(client
128 .get_liked_tweets(user_id, max_results, pagination_token)
129 .await?)
130}
131
132pub async fn get_bookmarks(
134 client: &dyn XApiClient,
135 user_id: &str,
136 max_results: u32,
137 pagination_token: Option<&str>,
138) -> Result<SearchResponse, ToolkitError> {
139 super::validate_id(user_id, "user_id")?;
140 Ok(client
141 .get_bookmarks(user_id, max_results, pagination_token)
142 .await?)
143}
144
145pub async fn get_users_by_ids(
147 client: &dyn XApiClient,
148 user_ids: &[&str],
149) -> Result<UsersResponse, ToolkitError> {
150 if user_ids.is_empty() || user_ids.len() > 100 {
151 return Err(ToolkitError::InvalidInput {
152 message: format!("user_ids must contain 1-100 IDs, got {}", user_ids.len()),
153 });
154 }
155 Ok(client.get_users_by_ids(user_ids).await?)
156}
157
158pub async fn get_tweet_liking_users(
160 client: &dyn XApiClient,
161 tweet_id: &str,
162 max_results: u32,
163 pagination_token: Option<&str>,
164) -> Result<UsersResponse, ToolkitError> {
165 super::validate_id(tweet_id, "tweet_id")?;
166 Ok(client
167 .get_tweet_liking_users(tweet_id, max_results, pagination_token)
168 .await?)
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use crate::error::XApiError;
175 use crate::x_api::types::*;
176
177 struct MockClient;
178
179 #[async_trait::async_trait]
180 impl XApiClient for MockClient {
181 async fn search_tweets(
182 &self,
183 _: &str,
184 _: u32,
185 _: Option<&str>,
186 _: Option<&str>,
187 ) -> Result<SearchResponse, XApiError> {
188 Ok(empty_search())
189 }
190 async fn get_mentions(
191 &self,
192 _: &str,
193 _: Option<&str>,
194 _: Option<&str>,
195 ) -> Result<MentionResponse, XApiError> {
196 Ok(empty_search())
197 }
198 async fn post_tweet(&self, _: &str) -> Result<PostedTweet, XApiError> {
199 Ok(PostedTweet {
200 id: "t1".into(),
201 text: "t".into(),
202 })
203 }
204 async fn reply_to_tweet(&self, _: &str, _: &str) -> Result<PostedTweet, XApiError> {
205 Ok(PostedTweet {
206 id: "t2".into(),
207 text: "t".into(),
208 })
209 }
210 async fn get_tweet(&self, id: &str) -> Result<Tweet, XApiError> {
211 Ok(Tweet {
212 id: id.to_string(),
213 text: "hello".into(),
214 author_id: "a1".into(),
215 created_at: String::new(),
216 public_metrics: PublicMetrics::default(),
217 conversation_id: None,
218 })
219 }
220 async fn get_me(&self) -> Result<User, XApiError> {
221 Ok(test_user("me"))
222 }
223 async fn get_user_tweets(
224 &self,
225 _: &str,
226 _: u32,
227 _: Option<&str>,
228 ) -> Result<SearchResponse, XApiError> {
229 Ok(empty_search())
230 }
231 async fn get_user_by_username(&self, u: &str) -> Result<User, XApiError> {
232 Ok(test_user(u))
233 }
234 }
235
236 fn test_user(id: &str) -> User {
237 User {
238 id: id.into(),
239 username: id.into(),
240 name: "Test".into(),
241 profile_image_url: None,
242 public_metrics: UserMetrics::default(),
243 }
244 }
245
246 fn empty_search() -> SearchResponse {
247 SearchResponse {
248 data: vec![],
249 includes: None,
250 meta: SearchMeta {
251 newest_id: None,
252 oldest_id: None,
253 result_count: 0,
254 next_token: None,
255 },
256 }
257 }
258
259 #[tokio::test]
260 async fn get_tweet_success() {
261 let t = get_tweet(&MockClient, "123").await.unwrap();
262 assert_eq!(t.id, "123");
263 }
264
265 #[tokio::test]
266 async fn get_tweet_empty_id() {
267 let e = get_tweet(&MockClient, "").await.unwrap_err();
268 assert!(matches!(e, ToolkitError::InvalidInput { .. }));
269 }
270
271 #[tokio::test]
272 async fn get_user_by_username_success() {
273 let u = get_user_by_username(&MockClient, "alice").await.unwrap();
274 assert_eq!(u.username, "alice");
275 }
276
277 #[tokio::test]
278 async fn get_user_by_username_empty() {
279 let e = get_user_by_username(&MockClient, "").await.unwrap_err();
280 assert!(matches!(e, ToolkitError::InvalidInput { .. }));
281 }
282
283 #[tokio::test]
284 async fn search_tweets_success() {
285 let r = search_tweets(&MockClient, "rust", 10, None, None)
286 .await
287 .unwrap();
288 assert_eq!(r.meta.result_count, 0);
289 }
290
291 #[tokio::test]
292 async fn search_tweets_empty_query() {
293 let e = search_tweets(&MockClient, "", 10, None, None)
294 .await
295 .unwrap_err();
296 assert!(matches!(e, ToolkitError::InvalidInput { .. }));
297 }
298
299 #[tokio::test]
300 async fn get_me_success() {
301 let u = get_me(&MockClient).await.unwrap();
302 assert_eq!(u.id, "me");
303 }
304
305 #[tokio::test]
306 async fn get_users_by_ids_empty() {
307 let e = get_users_by_ids(&MockClient, &[]).await.unwrap_err();
308 assert!(matches!(e, ToolkitError::InvalidInput { .. }));
309 }
310
311 #[tokio::test]
312 async fn get_users_by_ids_over_100() {
313 let ids: Vec<&str> = (0..101).map(|_| "x").collect();
314 let e = get_users_by_ids(&MockClient, &ids).await.unwrap_err();
315 assert!(matches!(e, ToolkitError::InvalidInput { .. }));
316 }
317
318 #[tokio::test]
319 async fn x_api_error_maps_to_toolkit_error() {
320 struct FailClient;
321 #[async_trait::async_trait]
322 impl XApiClient for FailClient {
323 async fn search_tweets(
324 &self,
325 _: &str,
326 _: u32,
327 _: Option<&str>,
328 _: Option<&str>,
329 ) -> Result<SearchResponse, XApiError> {
330 Err(XApiError::RateLimited {
331 retry_after: Some(30),
332 })
333 }
334 async fn get_mentions(
335 &self,
336 _: &str,
337 _: Option<&str>,
338 _: Option<&str>,
339 ) -> Result<MentionResponse, XApiError> {
340 Err(XApiError::AuthExpired)
341 }
342 async fn post_tweet(&self, _: &str) -> Result<PostedTweet, XApiError> {
343 Err(XApiError::AuthExpired)
344 }
345 async fn reply_to_tweet(&self, _: &str, _: &str) -> Result<PostedTweet, XApiError> {
346 Err(XApiError::AuthExpired)
347 }
348 async fn get_tweet(&self, _: &str) -> Result<Tweet, XApiError> {
349 Err(XApiError::ApiError {
350 status: 404,
351 message: "Not found".into(),
352 })
353 }
354 async fn get_me(&self) -> Result<User, XApiError> {
355 Err(XApiError::AuthExpired)
356 }
357 async fn get_user_tweets(
358 &self,
359 _: &str,
360 _: u32,
361 _: Option<&str>,
362 ) -> Result<SearchResponse, XApiError> {
363 Err(XApiError::AuthExpired)
364 }
365 async fn get_user_by_username(&self, _: &str) -> Result<User, XApiError> {
366 Err(XApiError::AuthExpired)
367 }
368 }
369
370 let e = get_tweet(&FailClient, "123").await.unwrap_err();
371 assert!(matches!(
372 e,
373 ToolkitError::XApi(XApiError::ApiError { status: 404, .. })
374 ));
375
376 let e = search_tweets(&FailClient, "q", 10, None, None)
377 .await
378 .unwrap_err();
379 assert!(matches!(
380 e,
381 ToolkitError::XApi(XApiError::RateLimited { .. })
382 ));
383 }
384}