reverse_engineered_twitter_api/
relation.rs1use 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 let mut is_following = false;
34 let mut cursor: Option<String> = None;
35 let mut is_continue = true;
36 while is_continue {
37 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 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 let cursor_type =
57 &entry.content.cursor_type.clone().unwrap_or_default();
58 let value = &entry.content.value.clone().unwrap_or_default();
59 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 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 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 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}