reverse-engineered-twitter-api 0.1.4

Reverse Engineered Twitter API
Documentation
use std::error::Error;
use async_trait::async_trait;
use serde_json::json;

use crate::{
    types_resp::{followers_types::FollowersResp, following_types::FollowingResp},
    ReAPI, BEARER_TOKEN,
};

#[async_trait]
pub trait Relation {
    async fn get_followers(&self, uid: &String) -> Result<FollowersResp, Box<dyn Error>>;
    async fn get_following(
        &self,
        uid: &String,
        cursor: Option<String>,
    ) -> Result<FollowingResp, Box<dyn Error>>;
    async fn check_following(
        &self,
        uid: &String,
        target_uid: &String,
    ) -> Result<bool, Box<dyn Error>>;
}

#[async_trait]
impl Relation for ReAPI {
    async fn check_following(
        &self,
        uid: &String,
        target_uid: &String,
    ) -> Result<bool, Box<dyn Error>> {
        // check whether target_uid is in following list
        let mut is_following = false;
        let mut cursor: Option<String> = None;
        let mut is_continue = true;
        while is_continue {
            // if cursor is not empty, then use cursor
            let res: FollowingResp = self.get_following(uid, cursor.clone()).await?;
            res.data
                .user
                .result
                .timeline
                .timeline
                .instructions
                .iter()
                .for_each(|instruction| {
                    if let Some(entries) = &instruction.entries {
                        entries.iter().for_each(|entry| {
                            // if target_uid is == user-xxxxxxx
                            let target_uid_str = format!("user-{}", target_uid);
                            if entry.entry_id == target_uid_str {
                                is_following = true;
                                is_continue = false;
                            }
                            // cursor Type exists
                            let cursor_type =
                                &entry.content.cursor_type.clone().unwrap_or_default();
                            let value = &entry.content.value.clone().unwrap_or_default();
                            // check whether cursor_type is "Bottom" and the content value is not strat with 0
                            if cursor_type == "Bottom" && !value.starts_with("0") {
                                cursor = Some(value.to_string());
                                is_continue = true;
                            } else {
                                is_continue = false;
                                is_following = false;
                            }
                        })
                    }
                });
            // sleep 0.5s
            tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
        }
        Ok(is_following)
    }

    async fn get_followers(&self, uid: &String) -> Result<FollowersResp, Box<dyn Error>> {
        let variables = json!(
            {"userId":uid.as_str(),"count":20,"includePromotedContent":false}
        );
        let features = json!(
            {"responsive_web_graphql_exclude_directive_enabled":true,"verified_phone_label_enabled":false,"responsive_web_home_pinned_timelines_enabled":true,"creator_subscriptions_tweet_preview_api_enabled":true,"responsive_web_graphql_timeline_navigation_enabled":true,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"c9s_tweet_anatomy_moderator_badge_enabled":true,"tweetypie_unmention_optimization_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"view_counts_everywhere_api_enabled":true,"longform_notetweets_consumption_enabled":true,"responsive_web_twitter_article_tweet_consumption_enabled":false,"tweet_awards_web_tipping_enabled":false,"freedom_of_speech_not_reach_fetch_enabled":true,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":true,"longform_notetweets_rich_text_read_enabled":true,"longform_notetweets_inline_media_enabled":true,"responsive_web_media_download_video_enabled":false,"responsive_web_enhance_cards_enabled":false}
        );
        // variables["product"] = "Latest".into();
        let q = [
            ("variables", variables.to_string()),
            ("features", features.to_string()),
        ];
        let req = self
            .client
            .get("https://twitter.com/i/api/graphql/9LlZicVr2IBf4u2qW5n4-A/Followers")
            .header("Authorization", format!("Bearer {}", BEARER_TOKEN))
            .header("X-CSRF-Token", self.csrf_token.to_owned())
            .query(&q)
            .build()
            .unwrap();
        let text = self
            .client
            .execute(req)
            .await
            .unwrap()
            .text()
            .await
            .unwrap();
        let res: FollowersResp = serde_json::from_str(&text).unwrap();
        return Ok(res);
    }

    async fn get_following(
        &self,
        uid: &String,
        cursor: Option<String>,
    ) -> Result<FollowingResp, Box<dyn Error>> {
        let mut variables = json!(
            {"userId":uid.as_str(),"count":20,"includePromotedContent":false}
        );
        let features = json!(
            {"responsive_web_graphql_exclude_directive_enabled":true,"verified_phone_label_enabled":false,"responsive_web_home_pinned_timelines_enabled":true,"creator_subscriptions_tweet_preview_api_enabled":true,"responsive_web_graphql_timeline_navigation_enabled":true,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"c9s_tweet_anatomy_moderator_badge_enabled":true,"tweetypie_unmention_optimization_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"view_counts_everywhere_api_enabled":true,"longform_notetweets_consumption_enabled":true,"responsive_web_twitter_article_tweet_consumption_enabled":false,"tweet_awards_web_tipping_enabled":false,"freedom_of_speech_not_reach_fetch_enabled":true,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":true,"longform_notetweets_rich_text_read_enabled":true,"longform_notetweets_inline_media_enabled":true,"responsive_web_media_download_video_enabled":false,"responsive_web_enhance_cards_enabled":false}
        );
        variables["product"] = "Latest".into();
        // check cursor
        if let Some(c) = cursor {
            variables["cursor"] = c.as_str().into();
        }
        let q = [
            ("variables", variables.to_string()),
            ("features", features.to_string()),
        ];
        let req = self
            .client
            .get("https://twitter.com/i/api/graphql/8cyc0OKedV_XD62fBjzxUw/Following")
            .header("Authorization", format!("Bearer {}", BEARER_TOKEN))
            .header("X-CSRF-Token", self.csrf_token.to_owned())
            .query(&q)
            .build()
            .unwrap();
        let text = self
            .client
            .execute(req)
            .await
            .unwrap()
            .text()
            .await
            .unwrap();
        let res = serde_json::from_str(&text);
        if let Err(e) = res {
            return Err(Box::new(e));
        }
        return Ok(res.unwrap());
    }
}

#[cfg(test)]
mod test_telation {
    use crate::{relation::Relation, ReAPI};

    async fn login(api: &mut ReAPI) -> Result<String, String> {
        dotenv::dotenv().ok();
        let name = std::env::var("TWITTER_USER_NAME").unwrap();
        let pwd = std::env::var("TWITTER_USER_PASSWORD").unwrap();
        api.login(&name, &pwd, "").await
    }

    #[tokio::test]
    async fn test_get_followers() {
        let uid = "1439140186378567683".to_string();
        let mut api = ReAPI::new();
        let _loggined = login(&mut api).await;
        let result = api.get_followers(&uid).await;
        println!("result {:?}", result);
    }

    #[tokio::test]
    async fn test_get_following() {
        let uid = "1439140186378567683".to_string();
        let mut api = ReAPI::new();
        let _loggined = login(&mut api).await;
        let result = api.get_following(&uid, None).await;
        println!("result {:?}", result);
    }

    #[tokio::test]
    async fn test_user_follow_target_user() {
        let uid = "1439140186378567683".to_string();
        let target_uid = "1456507428208398336".to_string();
        let mut api = ReAPI::new();
        let _loggined = login(&mut api).await;
        let result = api.check_following(&uid, &target_uid).await;
        println!("user is following {:?}", result);
    }
}