insta/
lib.rs

1#![allow(clippy::uninlined_format_args)]
2
3mod client;
4mod types;
5
6pub use self::{
7    client::Client,
8    types::{
9        AdditionalDataLoaded,
10        LoginResponse,
11        MediaType,
12        PostPage,
13    },
14};
15pub use cookie_store::CookieStore;
16pub use reqwest_cookie_store::CookieStoreMutex;
17
18const USER_AGENT_STR: &str = "Instagram 123.0.0.21.114 (iPhone; CPU iPhone OS 11_4 like Mac OS X; en_US; en-US; scale=2.00; 750x1334) AppleWebKit/605.1.15";
19
20/// Error type
21#[derive(Debug, thiserror::Error)]
22pub enum Error {
23    /// Reqwest Error
24    #[error(transparent)]
25    Reqwest(#[from] reqwest::Error),
26
27    /// Instagram is forcing a log-in
28    #[error("login required")]
29    LoginRequired,
30
31    /// Missing a csrf token
32    #[error("missing csrf token")]
33    MissingCsrfToken,
34
35    /// Missing additionalDataLoaded
36    #[error("missing `additionalDataLoaded` field")]
37    MissingAdditionalDataLoaded,
38
39    /// Json
40    #[error(transparent)]
41    Json(#[from] serde_json::Error),
42}
43
44#[cfg(test)]
45mod test {
46    use super::*;
47    use std::sync::Arc;
48    use tokio::sync::OnceCell;
49
50    #[derive(Debug, serde::Deserialize)]
51    struct TestConfig {
52        username: String,
53        password: String,
54    }
55
56    impl TestConfig {
57        fn new() -> Self {
58            let data = std::fs::read_to_string("test-config.json")
59                .expect("failed to load `test-config.json`");
60            serde_json::from_str(&data).expect("failed to parse `test-config.json`")
61        }
62    }
63
64    async fn get_client() -> &'static Client {
65        static CLIENT: OnceCell<Client> = OnceCell::const_new();
66
67        CLIENT
68            .get_or_init(|| async move {
69                tokio::task::spawn_blocking(|| {
70                    use std::{
71                        fs::File,
72                        io::BufReader,
73                    };
74                    use tokio::runtime::Handle;
75
76                    let session_file_path = "session.json";
77
78                    match File::open(session_file_path).map(BufReader::new) {
79                        Ok(file) => {
80                            let cookie_store = cookie_store::serde::json::load(file)
81                                .expect("failed to load session file");
82                            Client::with_cookie_store(Arc::new(CookieStoreMutex::new(cookie_store)))
83                        }
84                        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
85                            let test_config = TestConfig::new();
86
87                            let client = Client::new();
88                            Handle::current().block_on(async {
89                                let login_response = client
90                                    .login(&test_config.username, &test_config.password)
91                                    .await
92                                    .expect("failed to log in");
93
94                                assert!(login_response.authenticated);
95                            });
96
97                            let mut session_file = File::create(session_file_path)
98                                .expect("failed to open session file");
99
100                            cookie_store::serde::json::save(
101                                &client.cookie_store.lock().expect("cookie jar poisoned"),
102                                &mut session_file,
103                            )
104                            .expect("failed to save to session file");
105
106                            client
107                        }
108                        Err(e) => {
109                            panic!("failed to open session file: {}", e);
110                        }
111                    }
112                })
113                .await
114                .expect("task failed to join")
115            })
116            .await
117    }
118
119    /// Fails on CI since other people hit the rate limit.
120    #[ignore]
121    #[tokio::test]
122    async fn get_post() {
123        let client = get_client().await;
124
125        let post_page = client
126            .get_post("https://www.instagram.com/p/CIlZpXKFfNt/")
127            .await
128            .expect("failed to get post page");
129
130        let video_version = post_page
131            .items
132            .first()
133            .expect("missing post item")
134            .get_best_video_version()
135            .expect("failed to get the best video version");
136
137        dbg!(video_version);
138    }
139}