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#[derive(Debug, thiserror::Error)]
22pub enum Error {
23 #[error(transparent)]
25 Reqwest(#[from] reqwest::Error),
26
27 #[error("login required")]
29 LoginRequired,
30
31 #[error("missing csrf token")]
33 MissingCsrfToken,
34
35 #[error("missing `additionalDataLoaded` field")]
37 MissingAdditionalDataLoaded,
38
39 #[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 #[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}