agent_twitter_client/auth/
user_auth.rs

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}