x_api_rs/
auth.rs

1use std::{path::PathBuf, sync::Arc};
2
3use super::{TwAPI, BEARER_TOKEN, GUEST_ACTIVE_URL, LOGIN_URL, VERIFY_CREDENTIALS_URL};
4use log::debug;
5use env_logger;
6use serde::Deserialize;
7use serde_json::{self, json};
8use eyre::{bail, Context, ContextCompat, Result};
9use reqwest_cookie_store;
10
11#[derive(Clone, Debug, thiserror::Error)]
12#[error("Suspicious Login")]
13pub struct SuspiciousLoginError(
14    pub String,
15    pub Flow, // error message, latest flow
16);
17
18#[derive(Deserialize, Debug, Clone)]
19pub struct User {
20    pub id: i64,
21    pub id_str: String,
22    pub name: String,
23    pub screen_name: String,
24}
25#[derive(Deserialize, Debug, Clone)]
26pub struct OpenAccount {
27    pub user: Option<User>,
28    pub next_link: Option<Link>,
29    pub attribution_event: Option<String>,
30}
31
32#[derive(Deserialize, Debug, Clone)]
33pub struct Subtask {
34    pub subtask_id: String,
35    pub open_account: Option<OpenAccount>,
36}
37
38#[derive(Deserialize, Debug, Clone)]
39pub struct ApiError {
40    pub code: i64,
41    pub message: String,
42}
43
44#[derive(Deserialize, Debug, Clone)]
45pub struct Flow {
46    pub errors: Option<Vec<ApiError>>,
47    pub flow_token: String,
48    pub status: String,
49    pub subtasks: Vec<Subtask>,
50    pub js_instrumentation: Option<Insrumentation>,
51}
52
53#[derive(Deserialize, Debug, Clone)]
54pub struct Insrumentation {
55    pub url: String,
56    pub timeout_ms: i64,
57    pub next_link: Link,
58}
59
60#[derive(Deserialize, Debug, Clone)]
61pub struct Link {
62    pub link_type: String,
63    pub link_id: String,
64}
65
66#[derive(Deserialize, Debug, Clone)]
67pub struct GuestToken {
68    pub guest_token: String,
69}
70
71#[derive(Deserialize, Debug, Clone)]
72pub struct VerifyCredentials {
73    pub errors: Option<Vec<ApiError>>,
74}
75
76impl TwAPI {
77    pub fn new(session_path: Option<PathBuf>) -> Result<TwAPI> {
78        let _ = env_logger::try_init();
79        let client_builder = reqwest::ClientBuilder::new();
80        let cookie_store: Arc<reqwest_cookie_store::CookieStoreMutex>;
81    
82        if let Some(session_path) = session_path.as_ref().filter(|path| path.exists()) {
83            let file = std::fs::File::open(session_path.to_str().unwrap())
84                .map(std::io::BufReader::new)
85                .unwrap();
86            debug!("Load json session from {session_path:?}");
87            let provider: reqwest_cookie_store::CookieStore = reqwest_cookie_store::CookieStore::load_json(file).unwrap();
88            
89            cookie_store = std::sync::Arc::new(reqwest_cookie_store::CookieStoreMutex::new(provider));
90        } else {
91            let provider: reqwest_cookie_store::CookieStore = reqwest_cookie_store::CookieStore::new(None);
92            
93            cookie_store = std::sync::Arc::new(reqwest_cookie_store::CookieStoreMutex::new(provider));
94        }
95    
96        let client = client_builder
97            .cookie_provider(std::sync::Arc::clone(&cookie_store))
98            .build()
99            .context("can't build cookie store")?;
100    
101        Ok(TwAPI {
102            client,
103            csrf_token: String::from(""),
104            guest_token: String::from(""),
105            cookie_store,
106            session_path,
107        })
108    }
109    
110    async fn get_flow(&mut self, body: serde_json::Value) -> Result<Flow> {
111        if self.guest_token.is_empty() {
112            self.get_guest_token().await?
113        }
114        let res = self
115            .client
116            .post(LOGIN_URL)
117            .header("Authorization", format!("Bearer {}", BEARER_TOKEN))
118            .header("Content-Type", "application/json")
119            .header("User-Agent", "TwitterAndroid/99")
120            .header("X-Guest-Token", self.guest_token.replace("\"", ""))
121            .header("X-Twitter-Auth-Type", "OAuth2Client")
122            .header("X-Twitter-Active-User", "yes")
123            .header("X-Twitter-Client-Language", "en")
124            .json(&body)
125            .send().await?;
126
127        let cookies = res.cookies();
128        for cookie in cookies {
129            if cookie.name().eq("ct0") {
130                self.csrf_token = cookie.value().to_string()
131            }
132        }
133        let text = res.text().await?;
134        debug!("text: {text}");
135        let result: Flow = serde_json::from_str(text.as_str())?;
136        return Ok(result);
137    }
138
139    pub async fn get_flow_token(
140        &mut self,
141        data: serde_json::Value,
142    ) -> Result<Option<Flow>> {
143        let res = self.get_flow(data).await;
144        match res {
145            Ok(info) => {
146                if info.subtasks.len() > 0 {
147                    let subtask_id = info.subtasks[0].subtask_id.as_str();
148                    match subtask_id {
149                        // "LoginEnterAlternateIdentifierSubtask"
150                        "LoginAcid" | "LoginTwoFactorAuthChallenge" | "DenyLoginSubtask" => {
151                            bail!("Auth error: {}", subtask_id);
152                        }
153                        _ => return Ok(Some(info)),
154                    }
155                }
156                return Ok(Some(info));
157            }
158            Err(e) => {
159                bail!("Request error: {}", e.to_string())
160            },
161        }
162    }
163
164    async fn get_guest_token(&mut self) -> Result<()> {
165        let token = format!("Bearer {}", BEARER_TOKEN);
166        let res = self
167            .client
168            .post(GUEST_ACTIVE_URL)
169            .header("Authorization", token)
170            .send().await?;
171        let op = res.json::<serde_json::Value>().await?;
172        let guest_token = op.get("guest_token").context("cant get guest_token")?;
173        self.guest_token = guest_token.to_string();
174        Ok(())
175    }
176
177
178
179    pub async fn before_password_steps(&mut self, username: String) -> Result<Flow> {
180        let data = json!(
181            {
182                "flow_name": "login",
183                "input_flow_data": {
184                    "flow_context" : {
185                        "debug_overrides": {},
186                        "start_location": {
187                            "location": "splash_screen"
188                        }
189                    }
190                }
191            }
192        );
193        let flow_token = self.get_flow_token(data).await?.context("cant get folow token")?.flow_token;
194
195        // flow instrumentation step
196        let data = json!(
197            {
198                "flow_token": flow_token,
199                "subtask_inputs" : [{
200                    "subtask_id": "LoginJsInstrumentationSubtask",
201                    "js_instrumentation":{
202                        "response": "{}",
203                        "link": "next_link"
204                    }
205                }],
206            }
207        );
208        let flow_token = self.get_flow_token(data).await?.context("cant get folow token")?.flow_token;
209
210        // flow username step
211        let data = json!(
212            {
213                "flow_token": flow_token,
214                "subtask_inputs" : [{
215                    "subtask_id": "LoginEnterUserIdentifierSSO",
216                    "settings_list": {
217                        "setting_responses" : [{
218                            "key":           "user_identifier",
219                            "response_data": {
220                                "text_data" :{
221                                    "result": username
222                                }
223                            }
224                        }],
225                        "link": "next_link"
226                    }
227                }]
228            }
229        );
230
231        let flow = self.get_flow_token(data).await?.context("flow is none")?;
232        // let token = flow.flow_token.to_owned();
233        let subtask_id = flow.subtasks[0].subtask_id.clone();
234
235        // asking for username because of suspicies log in
236        if subtask_id == "LoginEnterAlternateIdentifierSubtask" {
237            return Err(SuspiciousLoginError("".into(), flow).into());
238        }
239        Ok(flow)
240    }
241
242    pub async fn login(
243        &mut self,
244        username: &str,
245        password: &str,
246        confirmation: &str,
247        latest_flow: Option<Flow>,
248    ) -> Result<Option<Flow>> {
249        // flow start
250
251        let mut flow: Flow;
252        if latest_flow.is_some() {
253            debug!("taking latest flow");
254            flow = latest_flow.context("latest flow is none")?;
255            let subtask_id = flow.subtasks[0].subtask_id.clone();
256            let data = json!({
257                "flow_token": flow.flow_token,
258                "subtask_inputs": [{"subtask_id": subtask_id, "enter_text": {"text": username,"link":"next_link"}}]
259            });
260            // self.handle_suspicies(token.clone(), subtask_id.clone());
261            flow = self.get_flow_token(data).await.context("flow token is none")?.context("inner flow token is none")?;
262        } else {
263            flow = self.before_password_steps(username.into()).await?;
264        }
265        // flow password step
266        let data = json!(
267            {
268                "flow_token": flow.flow_token,
269                "subtask_inputs": [{
270                    "subtask_id":     "LoginEnterPassword",
271                    "enter_password": {
272                        "password": password,
273                        "link": "next_link"
274                    },
275                }]
276            }
277        );
278
279        let flow_token = self.get_flow_token(data).await?.context("flow token is none in password step")?.flow_token;
280
281        // flow duplication check
282        let data = json!(
283            {
284                "flow_token": flow_token,
285                "subtask_inputs": [{
286                    "subtask_id":              "AccountDuplicationCheck",
287                    "check_logged_in_account": {
288                        "link": "AccountDuplicationCheck_false"
289                    },
290                }]
291            }
292        );
293        let flow_token = self.get_flow_token(data).await;
294
295        match flow_token {
296            Err(e) => {
297                let mut confirmation_subtask = "";
298                for item in vec!["LoginAcid", "LoginTwoFactorAuthChallenge"] {
299                    if e.to_string().contains(item) {
300                        confirmation_subtask = item;
301                        break;
302                    }
303                }
304                if !confirmation_subtask.is_empty() {
305                    if confirmation.is_empty() {
306                        let msg = format!(
307                            "confirmation data required for {}",
308                            confirmation_subtask.to_owned()
309                        );
310                        bail!(msg)
311                    }
312                    let data = json!(
313                        {
314                            "flow_token": "",
315                            "subtask_inputs": {
316                                    "subtask_id": confirmation_subtask,
317                                    "enter_text": {
318                                        "text": confirmation,
319                                        "link": "next_link",
320                                    },
321                            },
322                        }
323                    );
324                    return self.get_flow_token(data).await;
325                }
326                Ok(None)
327            }
328            Ok(_) => return Ok(None),
329        }
330    }
331
332    pub async fn is_logged_in(&mut self) -> Result<bool> {
333        let req = self
334            .client
335            .get(VERIFY_CREDENTIALS_URL)
336            .header("Authorization", format!("Bearer {}", BEARER_TOKEN))
337            .header("X-CSRF-Token", self.csrf_token.to_owned())
338            .build()
339            .context("Cant build http request in logged in")?;
340        let res = self.client.execute(req).await.context("failed execute request")?;
341        let cookies = res.cookies();
342        for cookie in cookies {
343            if cookie.name().eq("ct0") {
344                self.csrf_token = cookie.value().to_string()
345            }
346        }
347        let text = res.text().await.context("res is not text")?;
348        let res: VerifyCredentials = serde_json::from_str(&text).context("res is not diseralizable")?;
349        Ok(res.errors.is_none())
350    }
351
352    pub fn save_session(&mut self) -> Result<()> {
353        if let Some(path) = &self.session_path {
354            let mut writer = std::fs::File::create(path)
355            .map(std::io::BufWriter::new)
356            .unwrap();
357            let store = self.cookie_store.lock().unwrap();
358            store.save_json(&mut writer).unwrap();
359            Ok(())
360        } else {
361            bail!("Session path must set when creating API")
362        }
363    }
364}