1use crate::api::requests::request_api;
2use crate::error::{Result, TwitterError};
3use async_trait::async_trait;
4use chrono::{DateTime, Utc};
5use cookie::CookieJar;
6use reqwest::header::{HeaderMap, HeaderValue};
7use serde::{Deserialize, Serialize};
8use serde_json::json;
9use std::any::Any;
10use std::fs::{File, OpenOptions};
11use std::io::{Read, Write};
12use std::path::Path;
13use std::sync::Arc;
14use tokio::sync::Mutex;
15use totp_rs::{Algorithm, TOTP};
16use tracing;
17use reqwest::Client;
18
19#[derive(Debug)]
20enum SubtaskType {
21 LoginJsInstrumentation,
22 LoginEnterUserIdentifier,
23 LoginEnterPassword,
24 LoginAcid,
25 AccountDuplicationCheck,
26 LoginTwoFactorAuthChallenge,
27 LoginEnterAlternateIdentifier,
28 LoginSuccess,
29 DenyLogin,
30 Unknown(String),
31}
32
33impl From<&str> for SubtaskType {
34 fn from(s: &str) -> Self {
35 match s {
36 "LoginJsInstrumentationSubtask" => Self::LoginJsInstrumentation,
37 "LoginEnterUserIdentifierSSO" => Self::LoginEnterUserIdentifier,
38 "LoginEnterPassword" => Self::LoginEnterPassword,
39 "LoginAcid" => Self::LoginAcid,
40 "AccountDuplicationCheck" => Self::AccountDuplicationCheck,
41 "LoginTwoFactorAuthChallenge" => Self::LoginTwoFactorAuthChallenge,
42 "LoginEnterAlternateIdentifierSubtask" => Self::LoginEnterAlternateIdentifier,
43 "LoginSuccessSubtask" => Self::LoginSuccess,
44 "DenyLoginSubtask" => Self::DenyLogin,
45 other => Self::Unknown(other.to_string()),
46 }
47 }
48}
49
50#[async_trait]
51pub trait TwitterAuth: Send + Sync + Any {
52 async fn install_headers(&self, headers: &mut HeaderMap) -> Result<()>;
53 async fn get_cookies(&self) -> Result<Vec<cookie::Cookie<'_>>>;
54 fn delete_token(&mut self);
55 fn as_any(&self) -> &dyn Any;
56}
57
58#[derive(Debug, Serialize)]
59struct FlowInitRequest {
60 flow_name: String,
61 input_flow_data: serde_json::Value,
62}
63
64#[derive(Debug, Serialize)]
65struct FlowTaskRequest {
66 flow_token: String,
67 subtask_inputs: Vec<serde_json::Value>,
68}
69
70#[derive(Debug, Deserialize)]
71struct FlowResponse {
72 flow_token: String,
73 subtasks: Option<Vec<Subtask>>,
74}
75
76#[derive(Debug, Deserialize)]
77struct Subtask {
78 subtask_id: String,
79}
80
81#[derive(Clone)]
82pub struct TwitterUserAuth {
83 bearer_token: String,
84 guest_token: Option<String>,
85 cookie_jar: Arc<Mutex<CookieJar>>,
86 created_at: Option<DateTime<Utc>>,
87}
88
89impl TwitterUserAuth {
90 pub async fn new(bearer_token: String) -> Result<Self> {
91 Ok(Self {
92 bearer_token,
93 guest_token: None,
94 cookie_jar: Arc::new(Mutex::new(CookieJar::new())),
95 created_at: None,
96 })
97 }
98
99 async fn init_login(&mut self, client: &Client) -> Result<FlowResponse> {
100 self.update_guest_token(client).await?;
101
102 let init_request = FlowInitRequest {
103 flow_name: "login".to_string(),
104 input_flow_data: json!({
105 "flow_context": {
106 "debug_overrides": {},
107 "start_location": {
108 "location": "splash_screen"
109 }
110 }
111 }),
112 };
113
114 let mut headers = HeaderMap::new();
115 self.install_headers(&mut headers).await?;
116
117 let (response, _) = request_api(
118 client,
119 "https://api.twitter.com/1.1/onboarding/task.json",
120 headers,
121 reqwest::Method::POST,
122 Some(json!(init_request)),
123 )
124 .await?;
125
126 Ok(response)
127 }
128
129 async fn execute_flow_task(&self, client: &Client, request: FlowTaskRequest) -> Result<FlowResponse> {
130 let mut headers = HeaderMap::new();
131 self.install_headers(&mut headers).await?;
132
133 let (flow_response, raw_response) = request_api::<FlowResponse>(
134 client,
135 "https://api.twitter.com/1.1/onboarding/task.json",
136 headers,
137 reqwest::Method::POST,
138 Some(json!(request)),
139 )
140 .await?;
141
142 let mut cookie_jar = self.cookie_jar.lock().await;
143 for cookie_header in raw_response.get_all("set-cookie") {
144 if let Ok(cookie_str) = cookie_header.to_str() {
145 if let Ok(cookie) = cookie::Cookie::parse(cookie_str) {
146 cookie_jar.add(cookie.into_owned());
147 }
148 }
149 }
150
151 if let Some(subtasks) = &flow_response.subtasks {
152 if subtasks.iter().any(|s| s.subtask_id == "DenyLoginSubtask") {
153 return Err(TwitterError::Auth("Login denied".into()));
154 }
155 }
156
157 Ok(flow_response)
158 }
159
160 pub async fn login(
161 &mut self,
162 client: &Client,
163 username: &str,
164 password: &str,
165 email: Option<&str>,
166 two_factor_secret: Option<&str>,
167 ) -> Result<()> {
168 let mut flow_response = self.init_login(client).await?;
169 let mut flow_token = flow_response.flow_token;
170
171 while let Some(subtasks) = &flow_response.subtasks {
172 if let Some(subtask) = subtasks.first() {
173 flow_response = match SubtaskType::from(subtask.subtask_id.as_str()) {
174 SubtaskType::LoginJsInstrumentation => {
175 self.handle_js_instrumentation_subtask(client, flow_token).await?
176 }
177 SubtaskType::LoginEnterUserIdentifier => {
178 self.handle_username_input(client, flow_token, username).await?
179 }
180 SubtaskType::LoginEnterPassword => {
181 self.handle_password_input(client, flow_token, password).await?
182 }
183 SubtaskType::LoginAcid => {
184 if let Some(email_str) = email {
185 self.handle_email_verification(client, flow_token, email_str)
186 .await?
187 } else {
188 return Err(TwitterError::Auth(
189 "Email required for verification".into(),
190 ));
191 }
192 }
193 SubtaskType::AccountDuplicationCheck => {
194 self.handle_account_duplication_check(client, flow_token).await?
195 }
196 SubtaskType::LoginTwoFactorAuthChallenge => {
197 if let Some(secret) = two_factor_secret {
198 self.handle_two_factor_auth(client, flow_token, secret).await?
199 } else {
200 return Err(TwitterError::Auth(
201 "Two factor authentication required".into(),
202 ));
203 }
204 }
205 SubtaskType::LoginEnterAlternateIdentifier => {
206 if let Some(email_str) = email {
207 self.handle_alternate_identifier(client, flow_token, email_str)
208 .await?
209 } else {
210 return Err(TwitterError::Auth(
211 "Email required for alternate identifier".into(),
212 ));
213 }
214 }
215 SubtaskType::LoginSuccess => self.handle_success_subtask(client, flow_token).await?,
216 SubtaskType::DenyLogin => {
217 return Err(TwitterError::Auth("Login denied".into()));
218 }
219 SubtaskType::Unknown(id) => {
220 return Err(TwitterError::Auth(format!(
221 "Unhandled subtask: {}",
222 id
223 )));
224 }
225 };
226 flow_token = flow_response.flow_token;
227 } else {
228 break;
229 }
230 }
231
232 Ok(())
233 }
234
235 async fn handle_js_instrumentation_subtask(&self, client: &Client, flow_token: String) -> Result<FlowResponse> {
236 let request = FlowTaskRequest {
237 flow_token,
238 subtask_inputs: vec![json!({
239 "subtask_id": "LoginJsInstrumentationSubtask",
240 "js_instrumentation": {
241 "response": "{}",
242 "link": "next_link"
243 }
244 })],
245 };
246 self.execute_flow_task(client, request).await
247 }
248
249 async fn handle_username_input(
250 &self,
251 client: &Client,
252 flow_token: String,
253 username: &str,
254 ) -> Result<FlowResponse> {
255 let request = FlowTaskRequest {
256 flow_token,
257 subtask_inputs: vec![json!({
258 "subtask_id": "LoginEnterUserIdentifierSSO",
259 "settings_list": {
260 "setting_responses": [
261 {
262 "key": "user_identifier",
263 "response_data": {
264 "text_data": {
265 "result": username
266 }
267 }
268 }
269 ],
270 "link": "next_link"
271 }
272 })],
273 };
274 self.execute_flow_task(client, request).await
275 }
276
277 async fn handle_password_input(
278 &self,
279 client: &Client,
280 flow_token: String,
281 password: &str,
282 ) -> Result<FlowResponse> {
283 let request = FlowTaskRequest {
284 flow_token,
285 subtask_inputs: vec![json!({
286 "subtask_id": "LoginEnterPassword",
287 "enter_password": {
288 "password": password,
289 "link": "next_link"
290 }
291 })],
292 };
293 self.execute_flow_task(client, request).await
294 }
295
296 async fn handle_email_verification(
297 &self,
298 client: &Client,
299 flow_token: String,
300 email: &str,
301 ) -> Result<FlowResponse> {
302 let request = FlowTaskRequest {
303 flow_token,
304 subtask_inputs: vec![json!({
305 "subtask_id": "LoginAcid",
306 "enter_text": {
307 "text": email,
308 "link": "next_link"
309 }
310 })],
311 };
312 self.execute_flow_task(client, request).await
313 }
314
315 async fn handle_account_duplication_check(&self, client: &Client, flow_token: String) -> Result<FlowResponse> {
316 let request = FlowTaskRequest {
317 flow_token,
318 subtask_inputs: vec![json!({
319 "subtask_id": "AccountDuplicationCheck",
320 "check_logged_in_account": {
321 "link": "AccountDuplicationCheck_false"
322 }
323 })],
324 };
325 self.execute_flow_task(client, request).await
326 }
327
328 async fn handle_two_factor_auth(
329 &self,
330 client: &Client,
331 flow_token: String,
332 secret: &str,
333 ) -> Result<FlowResponse> {
334 let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, secret.as_bytes().to_vec())
335 .map_err(|e| TwitterError::Auth(format!("Failed to create TOTP: {}", e)))?;
336
337 let code = totp
338 .generate_current()
339 .map_err(|e| TwitterError::Auth(format!("Failed to generate TOTP code: {}", e)))?;
340
341 let request = FlowTaskRequest {
342 flow_token,
343 subtask_inputs: vec![json!({
344 "subtask_id": "LoginTwoFactorAuthChallenge",
345 "enter_text": {
346 "text": code,
347 "link": "next_link"
348 }
349 })],
350 };
351 self.execute_flow_task(client, request).await
352 }
353
354 async fn handle_alternate_identifier(
355 &self,
356 client: &Client,
357 flow_token: String,
358 email: &str,
359 ) -> Result<FlowResponse> {
360 let request = FlowTaskRequest {
361 flow_token,
362 subtask_inputs: vec![json!({
363 "subtask_id": "LoginEnterAlternateIdentifierSubtask",
364 "enter_text": {
365 "text": email,
366 "link": "next_link"
367 }
368 })],
369 };
370 self.execute_flow_task(client, request).await
371 }
372
373 async fn handle_success_subtask(&self, client: &Client, flow_token: String) -> Result<FlowResponse> {
374 let request = FlowTaskRequest {
375 flow_token,
376 subtask_inputs: vec![],
377 };
378 self.execute_flow_task(client, request).await
379 }
380
381 async fn update_guest_token(&mut self, client: &Client) -> Result<()> {
382 let url = "https://api.twitter.com/1.1/guest/activate.json";
383
384 let mut headers = HeaderMap::new();
385 headers.insert(
386 "Authorization",
387 HeaderValue::from_str(&format!("Bearer {}", self.bearer_token))
388 .map_err(|e| TwitterError::Auth(e.to_string()))?,
389 );
390
391 let (response, _) =
392 request_api::<serde_json::Value>(client, url, headers, reqwest::Method::POST, None).await?;
393
394 let guest_token = response
395 .get("guest_token")
396 .and_then(|token| token.as_str())
397 .ok_or_else(|| TwitterError::Auth("Failed to get guest token".into()))?;
398
399 self.guest_token = Some(guest_token.to_string());
400 self.created_at = Some(Utc::now());
401
402 Ok(())
403 }
404
405 pub async fn update_cookies(&self, response: &reqwest::Response) -> Result<()> {
406 tracing::trace!("Updating cookies - attempting to lock");
407 let mut cookie_jar = self.cookie_jar.lock().await;
408
409 for cookie_header in response.headers().get_all("set-cookie") {
410 if let Ok(cookie_str) = cookie_header.to_str() {
411 if let Ok(cookie) = cookie::Cookie::parse(cookie_str) {
412 tracing::trace!(?cookie, "Adding cookie");
413 cookie_jar.add(cookie.into_owned());
414 }
415 }
416 }
417
418 Ok(())
419 }
420
421 pub async fn save_cookies_to_file(&self, file_path: &str) -> Result<()> {
422 tracing::trace!("Saving cookies - attempting to lock");
423 let cookie_jar = self.cookie_jar.lock().await;
424 let cookies: Vec<_> = cookie_jar.iter().collect();
425
426 let cookie_data: Vec<(String, String)> = cookies
427 .iter()
428 .map(|cookie| (cookie.name().to_string(), cookie.value().to_string()))
429 .collect();
430
431 let json = serde_json::to_string_pretty(&cookie_data)
432 .map_err(|e| TwitterError::Cookie(format!("Failed to serialize cookies: {}", e)))?;
433
434 let mut file = OpenOptions::new()
435 .write(true)
436 .create(true)
437 .truncate(true)
438 .open(file_path)
439 .map_err(|e| TwitterError::Cookie(format!("Failed to open cookie file: {}", e)))?;
440
441 file.write_all(json.as_bytes())
442 .map_err(|e| TwitterError::Cookie(format!("Failed to write cookies: {}", e)))?;
443
444 Ok(())
445 }
446
447 pub async fn load_cookies_from_file(&mut self, file_path: &str) -> Result<()> {
448 tracing::trace!("Loading cookies - attempting to lock");
449
450 if !Path::new(file_path).exists() {
451 return Err(TwitterError::Cookie("Cookie file does not exist".into()));
452 }
453 let mut file = File::open(file_path)
454 .map_err(|e| TwitterError::Cookie(format!("Failed to open cookie file: {}", e)))?;
455
456 let mut contents = String::new();
457 file.read_to_string(&mut contents)
458 .map_err(|e| TwitterError::Cookie(format!("Failed to read cookie file: {}", e)))?;
459 let cookie_data: Vec<(String, String)> = serde_json::from_str(&contents)
460 .map_err(|e| TwitterError::Cookie(format!("Failed to parse cookie file: {}", e)))?;
461
462 tracing::trace!(?cookie_data, "Loaded cookie data");
463
464 let mut cookie_jar = self.cookie_jar.lock().await;
465
466 *cookie_jar = CookieJar::new();
467
468 for (name, value) in cookie_data {
469 let cookie = cookie::Cookie::build(name, value)
470 .path("/")
471 .domain("twitter.com")
472 .secure(true)
473 .http_only(true)
474 .finish();
475 cookie_jar.add(cookie.into_owned());
476 }
477 let mut headers = HeaderMap::new();
478 self.install_headers(&mut headers).await?;
479 Ok(())
480 }
481
482 pub async fn get_cookie_string(&self) -> Result<String> {
483 let cookie_jar = self.cookie_jar.lock().await;
484 let cookies: Vec<_> = cookie_jar.iter().collect();
485
486 let cookie_string = cookies
487 .iter()
488 .map(|c| format!("{}={}", c.name(), c.value()))
489 .collect::<Vec<_>>()
490 .join("; ");
491
492 Ok(cookie_string)
493 }
494
495 pub async fn set_cookies(&mut self, json_str: &str) -> Result<()> {
496 let cookie_data: Vec<(String, String)> = serde_json::from_str(json_str)
497 .map_err(|e| TwitterError::Cookie(format!("Failed to parse cookie JSON: {}", e)))?;
498
499 let mut cookie_jar = self.cookie_jar.lock().await;
500
501 *cookie_jar = CookieJar::new();
502
503 for (name, value) in cookie_data {
504 let cookie = cookie::Cookie::build(name, value)
505 .path("/")
506 .domain("twitter.com")
507 .secure(true)
508 .http_only(true)
509 .finish();
510 cookie_jar.add(cookie.into_owned());
511 }
512
513 let mut headers = HeaderMap::new();
514 self.install_headers(&mut headers).await?;
515 Ok(())
516 }
517
518 pub async fn set_from_cookie_string(&mut self, cookie_string: &str) -> Result<()> {
519 let mut cookie_jar = self.cookie_jar.lock().await;
520 *cookie_jar = CookieJar::new();
521 for cookie_str in cookie_string.split(';') {
522 let cookie_str = cookie_str.trim();
523 if let Ok(cookie) = cookie::Cookie::parse(cookie_str) {
524 let cookie =
525 cookie::Cookie::build(cookie.name().to_string(), cookie.value().to_string())
526 .path("/")
527 .domain("twitter.com")
528 .secure(true)
529 .http_only(true)
530 .finish();
531 cookie_jar.add(cookie.into_owned());
532 }
533 }
534 let has_essential_cookies = cookie_jar.iter().any(|c| c.name() == "ct0")
535 && cookie_jar.iter().any(|c| c.name() == "auth_token");
536
537 if !has_essential_cookies {
538 return Err(TwitterError::Cookie(
539 "Missing essential cookies (ct0 or auth_token)".into(),
540 ));
541 }
542 Ok(())
543 }
544
545 pub async fn is_logged_in(&self, client: &Client) -> Result<bool> {
546 let mut headers = HeaderMap::new();
547 self.install_headers(&mut headers).await?;
548
549 let (response, _) = request_api::<serde_json::Value>(
550 client,
551 "https://api.twitter.com/1.1/account/verify_credentials.json",
552 headers,
553 reqwest::Method::GET,
554 None,
555 )
556 .await?;
557
558 if let Some(errors) = response.get("errors") {
559 if let Some(errors_array) = errors.as_array() {
560 if !errors_array.is_empty() {
561 let error_msg = errors_array
562 .first()
563 .and_then(|e| e.get("message"))
564 .and_then(|m| m.as_str())
565 .unwrap_or("Unknown error");
566 return Err(TwitterError::Auth(error_msg.to_string()));
567 }
568 }
569 }
570 Ok(true)
571 }
572}
573
574#[async_trait]
575impl TwitterAuth for TwitterUserAuth {
576 async fn install_headers(&self, headers: &mut HeaderMap) -> Result<()> {
577 let cookie_jar = self.cookie_jar.lock().await;
578 let cookies: Vec<_> = cookie_jar.iter().collect();
579 if !cookies.is_empty() {
580 let cookie_header = cookies
581 .iter()
582 .map(|c| format!("{}={}", c.name(), c.value()))
583 .collect::<Vec<_>>()
584 .join("; ");
585
586 headers.insert(
587 "Cookie",
588 HeaderValue::from_str(&cookie_header)
589 .map_err(|e| TwitterError::Auth(e.to_string()))?,
590 );
591
592 if let Some(csrf_cookie) = cookies.iter().find(|c| c.name() == "ct0") {
593 headers.insert(
594 "x-csrf-token",
595 HeaderValue::from_str(csrf_cookie.value())
596 .map_err(|e| TwitterError::Auth(e.to_string()))?,
597 );
598 }
599 }
600 headers.insert(
601 "Authorization",
602 HeaderValue::from_str(&format!("Bearer {}", self.bearer_token))
603 .map_err(|e| TwitterError::Auth(e.to_string()))?,
604 );
605 if let Some(token) = &self.guest_token {
606 headers.insert(
607 "x-guest-token",
608 HeaderValue::from_str(token).map_err(|e| TwitterError::Auth(e.to_string()))?,
609 );
610 }
611 headers.insert("x-twitter-active-user", HeaderValue::from_static("yes"));
612 headers.insert("x-twitter-client-language", HeaderValue::from_static("en"));
613 headers.insert(
614 "x-twitter-auth-type",
615 HeaderValue::from_static("OAuth2Client"),
616 );
617
618 Ok(())
619 }
620
621 async fn get_cookies(&self) -> Result<Vec<cookie::Cookie<'_>>> {
622 let jar = self.cookie_jar.lock().await;
623 Ok(jar.iter().map(|c| c.to_owned()).collect())
624 }
625
626 fn delete_token(&mut self) {
627 self.guest_token = None;
628 self.created_at = None;
629 }
630
631 fn as_any(&self) -> &dyn Any {
632 self
633 }
634}