1use crate::{
2 types::AdditionalDataLoaded,
3 Error,
4 LoginResponse,
5 USER_AGENT_STR,
6};
7use once_cell::sync::Lazy;
8use regex::Regex;
9use reqwest_cookie_store::CookieStoreMutex;
10use std::sync::Arc;
11
12#[derive(Debug, Clone)]
14pub struct Client {
15 pub client: reqwest::Client,
19
20 pub cookie_store: Arc<CookieStoreMutex>,
24}
25
26impl Client {
27 pub fn new() -> Self {
29 let cookie_store = Arc::new(CookieStoreMutex::new(Default::default()));
30 Self::with_cookie_store(cookie_store)
31 }
32
33 pub fn with_cookie_store(cookie_store: Arc<CookieStoreMutex>) -> Self {
35 let mut headers = reqwest::header::HeaderMap::new();
36 headers.insert(
37 reqwest::header::ACCEPT_LANGUAGE,
38 reqwest::header::HeaderValue::from_static("en-US,en;q=0.9"),
39 );
40 headers.insert(
41 reqwest::header::REFERER,
42 reqwest::header::HeaderValue::from_static("https://www.instagram.com/"),
43 );
44
45 let client = reqwest::Client::builder()
46 .user_agent(USER_AGENT_STR)
47 .default_headers(headers)
48 .cookie_provider(cookie_store.clone())
49 .build()
50 .expect("failed to build insta client");
51
52 Client {
53 client,
54 cookie_store,
55 }
56 }
57
58 pub async fn login(&self, username: &str, password: &str) -> Result<LoginResponse, Error> {
60 self.client
63 .get("https://www.instagram.com/accounts/login")
64 .send()
65 .await?
66 .error_for_status()?
67 .text()
68 .await?;
69
70 let csrf_token = {
71 let cookie_store = self.cookie_store.lock().expect("cookie store poisoned");
72 cookie_store
73 .get("instagram.com", "/", "csrftoken")
74 .ok_or(Error::MissingCsrfToken)?
75 .value()
76 .to_string()
77 };
78
79 let response = self
80 .client
81 .post("https://www.instagram.com/accounts/login/ajax/")
82 .header("X-CSRFToken", csrf_token)
83 .form(&[("username", username), ("password", password)])
84 .send()
85 .await?
86 .error_for_status()?
87 .json()
88 .await?;
89
90 Ok(response)
91 }
92
93 async fn get_response(&self, url: &str) -> Result<reqwest::Response, Error> {
97 let response = self.client.get(url).send().await?.error_for_status()?;
98
99 if response.url().path() == "/accounts/login/" {
100 return Err(Error::LoginRequired);
101 }
102
103 Ok(response)
104 }
105
106 pub async fn get_post(&self, url: &str) -> Result<AdditionalDataLoaded, Error> {
108 static ADDITIONAL_DATA_LOADED_REGEX: Lazy<Regex> = Lazy::new(|| {
109 Regex::new("window\\.__additionalDataLoaded\\('.*',(.*)\\);")
110 .expect("failed to compile `ADDITIONAL_DATA_LOADED_REGEX`")
111 });
112
113 let text = self.get_response(url).await?.text().await?;
115 let captures = ADDITIONAL_DATA_LOADED_REGEX.captures(&text);
116
117 Ok(serde_json::from_str(
118 captures
119 .ok_or(Error::MissingAdditionalDataLoaded)?
120 .get(1)
121 .ok_or(Error::MissingAdditionalDataLoaded)?
122 .as_str(),
123 )?)
124 }
125}
126
127impl Default for Client {
128 fn default() -> Self {
129 Self::new()
130 }
131}