reverse_engineered_twitter_api/
relation.rs

1use std::error::Error;
2use async_trait::async_trait;
3use serde_json::json;
4
5use crate::{
6    types_resp::{followers_types::FollowersResp, following_types::FollowingResp},
7    ReAPI, BEARER_TOKEN,
8};
9
10#[async_trait]
11pub trait Relation {
12    async fn get_followers(&self, uid: &String) -> Result<FollowersResp, Box<dyn Error>>;
13    async fn get_following(
14        &self,
15        uid: &String,
16        cursor: Option<String>,
17    ) -> Result<FollowingResp, Box<dyn Error>>;
18    async fn check_following(
19        &self,
20        uid: &String,
21        target_uid: &String,
22    ) -> Result<bool, Box<dyn Error>>;
23}
24
25#[async_trait]
26impl Relation for ReAPI {
27    async fn check_following(
28        &self,
29        uid: &String,
30        target_uid: &String,
31    ) -> Result<bool, Box<dyn Error>> {
32        // check whether target_uid is in following list
33        let mut is_following = false;
34        let mut cursor: Option<String> = None;
35        let mut is_continue = true;
36        while is_continue {
37            // if cursor is not empty, then use cursor
38            let res: FollowingResp = self.get_following(uid, cursor.clone()).await?;
39            res.data
40                .user
41                .result
42                .timeline
43                .timeline
44                .instructions
45                .iter()
46                .for_each(|instruction| {
47                    if let Some(entries) = &instruction.entries {
48                        entries.iter().for_each(|entry| {
49                            // if target_uid is == user-xxxxxxx
50                            let target_uid_str = format!("user-{}", target_uid);
51                            if entry.entry_id == target_uid_str {
52                                is_following = true;
53                                is_continue = false;
54                            }
55                            // cursor Type exists
56                            let cursor_type =
57                                &entry.content.cursor_type.clone().unwrap_or_default();
58                            let value = &entry.content.value.clone().unwrap_or_default();
59                            // check whether cursor_type is "Bottom" and the content value is not strat with 0
60                            if cursor_type == "Bottom" && !value.starts_with("0") {
61                                cursor = Some(value.to_string());
62                                is_continue = true;
63                            } else {
64                                is_continue = false;
65                                is_following = false;
66                            }
67                        })
68                    }
69                });
70            // sleep 0.5s
71            tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
72        }
73        Ok(is_following)
74    }
75
76    async fn get_followers(&self, uid: &String) -> Result<FollowersResp, Box<dyn Error>> {
77        let variables = json!(
78            {"userId":uid.as_str(),"count":20,"includePromotedContent":false}
79        );
80        let features = json!(
81            {"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}
82        );
83        // variables["product"] = "Latest".into();
84        let q = [
85            ("variables", variables.to_string()),
86            ("features", features.to_string()),
87        ];
88        let req = self
89            .client
90            .get("https://twitter.com/i/api/graphql/9LlZicVr2IBf4u2qW5n4-A/Followers")
91            .header("Authorization", format!("Bearer {}", BEARER_TOKEN))
92            .header("X-CSRF-Token", self.csrf_token.to_owned())
93            .query(&q)
94            .build()
95            .unwrap();
96        let text = self
97            .client
98            .execute(req)
99            .await
100            .unwrap()
101            .text()
102            .await
103            .unwrap();
104        let res: FollowersResp = serde_json::from_str(&text).unwrap();
105        return Ok(res);
106    }
107
108    async fn get_following(
109        &self,
110        uid: &String,
111        cursor: Option<String>,
112    ) -> Result<FollowingResp, Box<dyn Error>> {
113        let mut variables = json!(
114            {"userId":uid.as_str(),"count":20,"includePromotedContent":false}
115        );
116        let features = json!(
117            {"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}
118        );
119        variables["product"] = "Latest".into();
120        // check cursor
121        if let Some(c) = cursor {
122            variables["cursor"] = c.as_str().into();
123        }
124        let q = [
125            ("variables", variables.to_string()),
126            ("features", features.to_string()),
127        ];
128        let req = self
129            .client
130            .get("https://twitter.com/i/api/graphql/8cyc0OKedV_XD62fBjzxUw/Following")
131            .header("Authorization", format!("Bearer {}", BEARER_TOKEN))
132            .header("X-CSRF-Token", self.csrf_token.to_owned())
133            .query(&q)
134            .build()
135            .unwrap();
136        let text = self
137            .client
138            .execute(req)
139            .await
140            .unwrap()
141            .text()
142            .await
143            .unwrap();
144        let res = serde_json::from_str(&text);
145        if let Err(e) = res {
146            return Err(Box::new(e));
147        }
148        return Ok(res.unwrap());
149    }
150}
151
152#[cfg(test)]
153mod test_telation {
154    use crate::{relation::Relation, ReAPI};
155
156    async fn login(api: &mut ReAPI) -> Result<String, String> {
157        dotenv::dotenv().ok();
158        let name = std::env::var("TWITTER_USER_NAME").unwrap();
159        let pwd = std::env::var("TWITTER_USER_PASSWORD").unwrap();
160        api.login(&name, &pwd, "").await
161    }
162
163    #[tokio::test]
164    async fn test_get_followers() {
165        let uid = "1439140186378567683".to_string();
166        let mut api = ReAPI::new();
167        let _loggined = login(&mut api).await;
168        let result = api.get_followers(&uid).await;
169        println!("result {:?}", result);
170    }
171
172    #[tokio::test]
173    async fn test_get_following() {
174        let uid = "1439140186378567683".to_string();
175        let mut api = ReAPI::new();
176        let _loggined = login(&mut api).await;
177        let result = api.get_following(&uid, None).await;
178        println!("result {:?}", result);
179    }
180
181    #[tokio::test]
182    async fn test_user_follow_target_user() {
183        let uid = "1439140186378567683".to_string();
184        let target_uid = "1456507428208398336".to_string();
185        let mut api = ReAPI::new();
186        let _loggined = login(&mut api).await;
187        let result = api.check_following(&uid, &target_uid).await;
188        println!("user is following {:?}", result);
189    }
190}