linkedin_api/
client.rs

1use std::fs::{File, OpenOptions};
2use std::io::{BufReader, BufWriter};
3use std::path::Path;
4use std::sync::Arc;
5
6use reqwest::cookie::{CookieStore, Jar};
7use reqwest::{header, Client as ReqwestClient, Response, Url};
8use serde_json::Value;
9
10use crate::error::LinkedinError;
11use crate::utils::evade;
12use crate::Identity;
13
14const API_BASE_URL: &str = "https://www.linkedin.com/voyager/api";
15const AUTH_BASE_URL: &str = "https://www.linkedin.com";
16const COOKIE_FILE_PATH: &str = ".cookies.json";
17
18#[derive(Clone)]
19pub struct Client {
20    pub(crate) client: ReqwestClient,
21    cookie_jar: Arc<Jar>,
22}
23
24impl Client {
25    pub fn new() -> Result<Self, LinkedinError> {
26        let jar = Arc::new(Jar::default());
27
28        let mut headers = header::HeaderMap::new();
29        headers.insert(
30            "user-agent",
31            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36"
32                .parse()?,
33        );
34        headers.insert(
35            "accept-language",
36            "en-AU,en-GB;q=0.9,en-US;q=0.8,en;q=0.7".parse()?,
37        );
38        headers.insert("x-li-lang", "en_US".parse()?);
39        headers.insert("x-restli-protocol-version", "2.0.0".parse()?);
40
41        let client = ReqwestClient::builder()
42            .cookie_provider(jar.clone())
43            .default_headers(headers)
44            .build()?;
45
46        Ok(Self {
47            client,
48            cookie_jar: jar,
49        })
50    }
51
52    pub async fn authenticate(
53        &self,
54        identity: &Identity,
55        refresh: bool,
56    ) -> Result<(), LinkedinError> {
57        let url = Url::parse("https://www.linkedin.com")?;
58        if !refresh
59            && self.load_cookies().is_ok() {
60                return Ok(());
61            }
62
63        // Request session cookies
64        self.request_session_cookies().await?;
65
66        self.cookie_jar.add_cookie_str(
67            &format!(
68                "li_at={}; Domain=.linkedin.com; Path=/; Secure; HttpOnly",
69                identity.authentication_token
70            ),
71            &url,
72        );
73        self.cookie_jar.add_cookie_str(
74            &format!(
75                "JSESSIONID={}; Domain=.linkedin.com; Path=/; Secure; HttpOnly",
76                identity.session_cookie
77            ),
78            &url,
79        );
80
81        self.save_cookies()?;
82
83        Ok(())
84    }
85
86    async fn request_session_cookies(&self) -> Result<(), LinkedinError> {
87        let mut headers = header::HeaderMap::new();
88        headers.insert(
89            "X-Li-User-Agent",
90            "LIAuthLibrary:3.2.4 com.linkedin.LinkedIn:8.8.1 iPhone:8.3".parse()?,
91        );
92        headers.insert(
93            "User-Agent",
94            "LinkedIn/8.8.1 CFNetwork/711.3.18 Darwin/14.0.0".parse()?,
95        );
96        headers.insert("X-User-Language", "en".parse()?);
97        headers.insert("X-User-Locale", "en_US".parse()?);
98        headers.insert("Accept-Language", "en-us".parse()?);
99
100        let _res = self
101            .client
102            .get(format!("{AUTH_BASE_URL}/uas/authenticate"))
103            .headers(headers)
104            .send()
105            .await?;
106
107        Ok(())
108    }
109
110    fn get_jsession_id(&self) -> String {
111        let url = Url::parse(AUTH_BASE_URL).unwrap();
112        if let Some(cookies) = self.cookie_jar.cookies(&url) {
113            for cookie in cookies.to_str().unwrap_or("").split(';') {
114                let cookie = cookie.trim();
115                if cookie.starts_with("JSESSIONID=") {
116                    return cookie
117                        .replace("JSESSIONID=", "")
118                        .trim_matches('"')
119                        .to_string();
120                }
121            }
122        }
123        String::new()
124    }
125
126    fn load_cookies(&self) -> Result<(), LinkedinError> {
127        let path = Path::new(COOKIE_FILE_PATH);
128        if !path.exists() {
129            return Err(LinkedinError::Io(std::io::Error::new(
130                std::io::ErrorKind::NotFound,
131                "Cookie file not found",
132            )));
133        }
134        let file = File::open(path)?;
135        let reader = BufReader::new(file);
136        let cookies: Vec<String> = serde_json::from_reader(reader)?;
137
138        let url = Url::parse(AUTH_BASE_URL)?;
139        for cookie in cookies {
140            self.cookie_jar.add_cookie_str(&cookie, &url);
141        }
142
143        Ok(())
144    }
145
146    fn save_cookies(&self) -> Result<(), LinkedinError> {
147        let url = Url::parse(AUTH_BASE_URL)?;
148        let cookies: Vec<String> = if let Some(cookie_header) = self.cookie_jar.cookies(&url) {
149            cookie_header
150                .to_str()?
151                .split(';')
152                .map(|s| s.trim().to_string())
153                .collect()
154        } else {
155            vec![]
156        };
157
158        let file = OpenOptions::new()
159            .write(true)
160            .create(true)
161            .truncate(true)
162            .open(COOKIE_FILE_PATH)?;
163        let writer = BufWriter::new(file);
164        serde_json::to_writer(writer, &cookies)?;
165        Ok(())
166    }
167
168    pub async fn get(&self, uri: &str) -> Result<Response, LinkedinError> {
169        evade().await;
170        let url = format!("{API_BASE_URL}{uri}");
171
172        let mut headers = header::HeaderMap::new();
173        headers.insert("csrf-token", self.get_jsession_id().parse()?);
174
175        let res = self.client.get(&url).headers(headers).send().await?;
176        Ok(res)
177    }
178
179    pub async fn post(&self, uri: &str, data: &Value) -> Result<Response, LinkedinError> {
180        evade().await;
181        let url = format!("{API_BASE_URL}{uri}");
182
183        let mut headers = header::HeaderMap::new();
184        headers.insert("csrf-token", self.get_jsession_id().parse()?);
185        headers.insert("content-type", "application/json".parse()?);
186
187        let res = self
188            .client
189            .post(&url)
190            .headers(headers)
191            .json(data)
192            .send()
193            .await?;
194        Ok(res)
195    }
196}