insta/
client.rs

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/// A Client
13#[derive(Debug, Clone)]
14pub struct Client {
15    /// The inner http client.
16    ///
17    /// This probably shouldn't be used by you.
18    pub client: reqwest::Client,
19
20    /// The inner cookie store.
21    ///
22    /// This probably shouldn't be used by you.
23    pub cookie_store: Arc<CookieStoreMutex>,
24}
25
26impl Client {
27    /// Make a new [`Client`].
28    pub fn new() -> Self {
29        let cookie_store = Arc::new(CookieStoreMutex::new(Default::default()));
30        Self::with_cookie_store(cookie_store)
31    }
32
33    /// Make a new [`Client`] from a CookieStore.
34    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    /// Log in
59    pub async fn login(&self, username: &str, password: &str) -> Result<LoginResponse, Error> {
60        // TODO: Only run a get on the login page if we are missing a csrf token
61        // Get CSRF Cookie
62        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    /// Send a GET to a url and return the response.
94    ///
95    /// This returns an error if the instagram forces the user to log in.
96    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    /// Get a post by url.
107    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        // TODO: Run on threadpool?
114        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}