insta_rs/
lib.rs

1//! Instagram data scraping library for Rust.
2//!
3//! # Example
4//!
5//! ```no_run
6//! use insta_rs::Instagram;
7//!
8//! #[tokio::main]
9//! async fn main() -> insta_rs::Result<()> {
10//!     let client = Instagram::builder().build()?;
11//!
12//!     let user = client.user("instagram").await?;
13//!     println!("{} has {} followers", user.username, user.follower_count);
14//!
15//!     Ok(())
16//! }
17//! ```
18//!
19//! # Authentication
20//!
21//! The client supports multiple authentication methods:
22//!
23//! ```no_run
24//! use insta_rs::{Instagram, SessionData};
25//!
26//! # async fn example() -> insta_rs::Result<()> {
27//! // anonymous access (limited)
28//! let client = Instagram::builder().build()?;
29//!
30//! // with credentials (auto-fetches csrf token)
31//! let client = Instagram::builder()
32//!     .id("username")
33//!     .password("password")
34//!     .build()?;
35//! client.login().await?;
36//!
37//! // with existing session
38//! let session = SessionData::new("session_id", "csrf_token");
39//! let client = Instagram::builder()
40//!     .session(session)
41//!     .build()?;
42//! # Ok(())
43//! # }
44//! ```
45//!
46//! # Fetching Data
47//!
48//! ```no_run
49//! use insta_rs::Instagram;
50//!
51//! # async fn example() -> insta_rs::Result<()> {
52//! let client = Instagram::builder().build()?;
53//!
54//! // fetch user profile
55//! let user = client.user("natgeo").await?;
56//!
57//! // fetch a specific post
58//! let post = client.media("ABC123xyz").await?;
59//!
60//! // fetch reel data
61//! let reel = client.reel("DEF456uvw").await?;
62//!
63//! // fetch user posts with pagination
64//! let mut cursor = None;
65//! loop {
66//!     let page = client.user_posts(&user.id, 12, cursor.as_deref()).await?;
67//!     for post in &page.items {
68//!         println!("{}: {} likes", post.shortcode, post.like_count);
69//!     }
70//!     if !page.has_next {
71//!         break;
72//!     }
73//!     cursor = page.cursor;
74//! }
75//! # Ok(())
76//! # }
77//! ```
78//!
79//! # Comments and Likes
80//!
81//! ```no_run
82//! use insta_rs::Instagram;
83//!
84//! # async fn example() -> insta_rs::Result<()> {
85//! let client = Instagram::builder().build()?;
86//!
87//! // fetch comments on a post
88//! let comments = client.comments("media_id", 50, None).await?;
89//! for comment in &comments.items {
90//!     println!("{}: {}", comment.owner.username, comment.text);
91//! }
92//!
93//! // fetch users who liked a post
94//! let likers = client.likers("shortcode", 50, None).await?;
95//! for liker in &likers.items {
96//!     println!("{}", liker.username);
97//! }
98//! # Ok(())
99//! # }
100//! ```
101//!
102//! # Search
103//!
104//! ```no_run
105//! use insta_rs::Instagram;
106//!
107//! # async fn example() -> insta_rs::Result<()> {
108//! let client = Instagram::builder().build()?;
109//!
110//! let results = client.search("rust programming").await?;
111//!
112//! for user in &results.users {
113//!     println!("user: @{}", user.username);
114//! }
115//! for tag in &results.hashtags {
116//!     println!("tag: #{} ({} posts)", tag.name, tag.media_count);
117//! }
118//! # Ok(())
119//! # }
120//! ```
121
122mod auth;
123mod client;
124mod endpoints;
125mod error;
126mod types;
127
128pub use auth::{Auth, SessionData};
129pub use client::{CredentialsBuilder, Instagram, InstagramBuilder};
130pub use endpoints::QUERY_HASH;
131pub use error::{Error, Result};
132pub use types::*;
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn build_anonymous_client() {
140        let client = Instagram::builder().build();
141        assert!(client.is_ok());
142    }
143
144    #[test]
145    fn build_with_credentials() {
146        let client = Instagram::builder()
147            .id("test_user")
148            .password("test_pass")
149            .build();
150        assert!(client.is_ok());
151    }
152
153    #[test]
154    fn build_with_session() {
155        let session = SessionData::new("sess123", "csrf456");
156        let client = Instagram::builder().session(session).build();
157        assert!(client.is_ok());
158    }
159
160    #[test]
161    fn session_with_user_id() {
162        let session = SessionData::new("sess123", "csrf456").with_user_id("user789");
163        assert_eq!(session.user_id, Some("user789".into()));
164    }
165
166    #[test]
167    fn custom_user_agent() {
168        let client = Instagram::builder().user_agent("CustomBot/1.0").build();
169        assert!(client.is_ok());
170    }
171
172    #[test]
173    fn pagination_default() {
174        let page: Pagination<Media> = Pagination::default();
175        assert!(page.items.is_empty());
176        assert!(!page.has_next);
177        assert!(page.cursor.is_none());
178    }
179
180    #[test]
181    fn endpoint_urls() {
182        let url = endpoints::profile_url("testuser");
183        assert!(url.contains("testuser"));
184
185        let url = endpoints::user_info_url("12345");
186        assert!(url.contains("12345"));
187
188        let url = endpoints::media_info_url("ABC");
189        assert!(url.contains("ABC"));
190    }
191
192    #[test]
193    fn action_urls() {
194        let url = endpoints::like_url("123");
195        assert!(url.contains("123") && url.contains("like"));
196
197        let url = endpoints::follow_url("456");
198        assert!(url.contains("456") && url.contains("follow"));
199
200        let url = endpoints::comment_url("789");
201        assert!(url.contains("789") && url.contains("comment"));
202    }
203
204    #[test]
205    fn auth_variants() {
206        let auth = Auth::None;
207        assert!(matches!(auth, Auth::None));
208
209        let auth = Auth::Credentials {
210            id: "u".into(),
211            password: "p".into(),
212        };
213        assert!(matches!(auth, Auth::Credentials { .. }));
214
215        let session = SessionData::new("s", "c");
216        let auth = Auth::Session(session);
217        assert!(matches!(auth, Auth::Session(_)));
218    }
219
220    #[test]
221    fn media_type_serialization() {
222        let reel = MediaType::Reel;
223        let json = serde_json::to_string(&reel).unwrap();
224        assert_eq!(json, "\"reel\"");
225
226        let carousel = MediaType::Carousel;
227        let json = serde_json::to_string(&carousel).unwrap();
228        assert_eq!(json, "\"carousel\"");
229    }
230}
231
232#[cfg(test)]
233mod api_tests {
234    use super::*;
235
236    fn client() -> Instagram {
237        Instagram::builder().build().unwrap()
238    }
239
240    async fn auth_client() -> Option<Instagram> {
241        let id = "id";
242        let password = "password";
243
244        let _ig = Instagram::builder().id(id).password(password).build();
245        match &_ig {
246            Ok(_) => println!("Ok"),
247            Err(err) => println!("{:?}", err),
248        }
249        let ig = _ig.ok()?;
250
251        let res = ig.login().await;
252        println!("{:?}", res);
253        res.ok()?;
254        println!("gurt: yo");
255        Some(ig)
256    }
257
258    #[tokio::test]
259    #[ignore]
260    async fn fetch_user_profile() {
261        let ig = client();
262        let user = ig.user("instagram").await.unwrap();
263        assert_eq!(user.username, "instagram");
264        assert!(user.follower_count > 0);
265    }
266
267    #[tokio::test]
268    #[ignore]
269    async fn fetch_media_post() {
270        let ig = client();
271        let media = ig.media("CvIBvWQPxKB").await.unwrap();
272        assert!(!media.id.is_empty());
273        assert!(!media.shortcode.is_empty());
274    }
275
276    #[tokio::test]
277    #[ignore]
278    async fn fetch_reel() {
279        let Some(ig) = auth_client().await else {
280            println!("Fail");
281            return;
282        };
283        let reel = ig.reel("DNaaHuTRxKF").await.unwrap();
284        assert!(!reel.id.is_empty());
285        assert!(!reel.video_url.is_empty());
286        assert!(reel.duration > 0.0);
287        println!("{:?}", reel);
288    }
289
290    #[tokio::test]
291    #[ignore]
292    async fn search_users() {
293        let ig = client();
294        let results = ig.search("instagram").await.unwrap();
295        assert!(!results.users.is_empty());
296    }
297
298    #[tokio::test]
299    #[ignore]
300    async fn fetch_user_posts() {
301        let Some(ig) = auth_client().await else {
302            return;
303        };
304        let user = ig.user("instagram").await.unwrap();
305        let posts = ig.user_posts(&user.id, 12, None).await.unwrap();
306        assert!(!posts.items.is_empty());
307    }
308
309    #[tokio::test]
310    #[ignore]
311    async fn fetch_comments() {
312        let Some(ig) = auth_client().await else {
313            return;
314        };
315        let shortcode = "DSdV9bYkg0G";
316        let comments = ig.comments(shortcode, 20, None).await.unwrap();
317        println!("{:?}", comments);
318        assert!(!comments.items.is_empty());
319    }
320
321    #[tokio::test]
322    #[ignore]
323    async fn fetch_likers() {
324        let Some(ig) = auth_client().await else {
325            return;
326        };
327        let likers = ig.likers("CvIBvWQPxKB", 20, None).await.unwrap();
328        assert!(!likers.items.is_empty());
329    }
330
331    #[tokio::test]
332    #[ignore]
333    async fn fetch_followers() {
334        let Some(ig) = auth_client().await else {
335            return;
336        };
337        let user = ig.user("instagram").await.unwrap();
338        let followers = ig.followers(&user.id, 20, None).await.unwrap();
339        println!("{:?}", followers);
340        assert!(!followers.items.is_empty());
341    }
342
343    #[tokio::test]
344    #[ignore]
345    async fn fetch_following() {
346        let Some(ig) = auth_client().await else {
347            return;
348        };
349        let user = ig.user("instagram").await.unwrap();
350        let following = ig.following(&user.id, 20, None).await.unwrap();
351        assert!(!following.items.is_empty());
352    }
353
354    #[tokio::test]
355    #[ignore]
356    async fn fetch_highlights() {
357        let Some(ig) = auth_client().await else {
358            return;
359        };
360        let user = ig.user("instagram").await.unwrap();
361        let highlights = ig.highlights(&user.id).await.unwrap();
362        assert!(!highlights.is_empty());
363    }
364
365    #[tokio::test]
366    #[ignore]
367    async fn fetch_hashtag_media() {
368        let Some(ig) = auth_client().await else {
369            return;
370        };
371        let media = ig.hashtag_media("rust", 12, None).await.unwrap();
372        println!("{:?}", media);
373        assert!(!media.items.is_empty());
374    }
375
376    #[tokio::test]
377    #[ignore]
378    async fn pagination_flow() {
379        let Some(ig) = auth_client().await else {
380            return;
381        };
382        let user = ig.user("instagram").await.unwrap();
383
384        let first = ig.user_posts(&user.id, 5, None).await.unwrap();
385        assert!(first.has_next);
386
387        let second = ig
388            .user_posts(&user.id, 5, first.cursor.as_deref())
389            .await
390            .unwrap();
391        assert!(!second.items.is_empty());
392    }
393
394    #[tokio::test]
395    #[ignore]
396    async fn login_flow() {
397        let Some(ig) = auth_client().await else {
398            println!("skipping login test / INSTA_ID and INSTA_PASSWORD not set");
399            return;
400        };
401        let user = ig.user("instagram").await.unwrap();
402        println!("{:?}", user);
403        assert!(!user.id.is_empty());
404    }
405}