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, );
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 "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 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 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 subtask_id = flow.subtasks[0].subtask_id.clone();
234
235 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 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 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 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 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}