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 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}