1use crate::api::requests::request_api;
2use crate::auth::TwitterAuth;
3use crate::error::{Result, TwitterError};
4use crate::models::Profile;
5use chrono::{DateTime, Utc};
6use lazy_static::lazy_static;
7use reqwest::header::HeaderMap;
8use reqwest::Method;
9use serde::{Deserialize, Serialize};
10use serde_json::json;
11use std::collections::HashMap;
12use std::sync::Mutex;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct UserProfile {
16 pub id: String,
17 pub id_str: String,
18 pub name: String,
19 pub screen_name: String,
20 pub location: Option<String>,
21 pub description: Option<String>,
22 pub url: Option<String>,
23 pub protected: bool,
24 pub followers_count: i32,
25 pub friends_count: i32,
26 pub listed_count: i32,
27 pub created_at: String,
28 pub favourites_count: i32,
29 pub verified: bool,
30 pub statuses_count: i32,
31 pub profile_image_url_https: String,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct LegacyUserRaw {
36 pub created_at: Option<String>,
37 pub description: Option<String>,
38 pub entities: Option<UserEntitiesRaw>,
39 pub favourites_count: Option<i32>,
40 pub followers_count: Option<i32>,
41 pub friends_count: Option<i32>,
42 pub media_count: Option<i32>,
43 pub statuses_count: Option<i32>,
44 pub id_str: Option<String>,
45 pub listed_count: Option<i32>,
46 pub name: Option<String>,
47 pub location: String,
48 pub geo_enabled: Option<bool>,
49 pub pinned_tweet_ids_str: Option<Vec<String>>,
50 pub profile_background_color: Option<String>,
51 pub profile_banner_url: Option<String>,
52 pub profile_image_url_https: Option<String>,
53 pub protected: Option<bool>,
54 pub screen_name: Option<String>,
55 pub verified: Option<bool>,
56 pub has_custom_timelines: Option<bool>,
57 pub has_extended_profile: Option<bool>,
58 pub url: Option<String>,
59 pub can_dm: Option<bool>,
60 #[serde(rename = "userId")]
61 pub user_id: Option<String>,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct UserEntitiesRaw {
66 pub url: Option<UserUrlEntity>,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct UserUrlEntity {
71 pub urls: Option<Vec<ExpandedUrl>>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct ExpandedUrl {
76 pub expanded_url: Option<String>,
77}
78
79lazy_static! {
80 static ref ID_CACHE: Mutex<HashMap<String, String>> = Mutex::new(HashMap::new());
81}
82
83pub fn parse_profile(user: &LegacyUserRaw, is_blue_verified: Option<bool>) -> Profile {
84 let mut profile = Profile {
85 id: user.user_id.clone().unwrap_or_default(),
86 username: user.screen_name.clone().unwrap_or_default(),
87 name: user.name.clone().unwrap_or_default(),
88 description: user.description.clone(),
89 location: Some(user.location.clone()),
90 url: user.url.clone(),
91 protected: user.protected.unwrap_or(false),
92 verified: user.verified.unwrap_or(false),
93 followers_count: user.followers_count.unwrap_or(0),
94 following_count: user.friends_count.unwrap_or(0),
95 tweets_count: user.statuses_count.unwrap_or(0),
96 listed_count: user.listed_count.unwrap_or(0),
97 is_blue_verified: Some(is_blue_verified.unwrap_or(false)),
98 created_at: user
99 .created_at
100 .as_ref()
101 .and_then(|date_str| {
102 DateTime::parse_from_str(date_str, "%a %b %d %H:%M:%S %z %Y")
103 .ok()
104 .map(|dt| dt.with_timezone(&Utc))
105 })
106 .unwrap_or_else(Utc::now),
107 profile_image_url: user
108 .profile_image_url_https
109 .as_ref()
110 .map(|url| url.replace("_normal", "")),
111 profile_banner_url: user.profile_banner_url.clone(),
112 pinned_tweet_id: user
113 .pinned_tweet_ids_str
114 .as_ref()
115 .and_then(|ids| ids.first().cloned()),
116 };
117
118 if let Some(entities) = &user.entities {
120 if let Some(url_entity) = &entities.url {
121 if let Some(urls) = &url_entity.urls {
122 if let Some(first_url) = urls.first() {
123 if let Some(expanded_url) = &first_url.expanded_url {
124 profile.url = Some(expanded_url.clone());
125 }
126 }
127 }
128 }
129 }
130
131 profile
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct UserResults {
136 pub result: UserResult,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
140#[serde(tag = "__typename")]
141pub enum UserResult {
142 User(UserData),
143 UserUnavailable(UserUnavailable),
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct UserData {
148 pub id: String,
149 pub rest_id: String,
150 pub affiliates_highlighted_label: Option<serde_json::Value>,
151 pub has_graduated_access: bool,
152 pub is_blue_verified: bool,
153 pub profile_image_shape: String,
154 pub legacy: LegacyUserRaw,
155 pub smart_blocked_by: bool,
156 pub smart_blocking: bool,
157 pub legacy_extended_profile: Option<serde_json::Value>,
158 pub is_profile_translatable: bool,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct UserUnavailable {
163 pub reason: String,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct UserRaw {
168 pub data: UserRawData,
169 pub errors: Option<Vec<TwitterApiErrorRaw>>,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct UserRawData {
174 pub user: UserRawUser,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct UserRawUser {
179 pub result: UserRawResult,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct UserRawResult {
184 pub rest_id: Option<String>,
185 pub is_blue_verified: Option<bool>,
186 pub legacy: LegacyUserRaw,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct TwitterApiErrorRaw {
191 pub message: String,
192 pub code: i32,
193}
194
195pub async fn get_profile(screen_name: &str, auth: &dyn TwitterAuth) -> Result<Profile> {
196 let mut headers = HeaderMap::new();
197 auth.install_headers(&mut headers).await?;
198
199 let variables = json!({
200 "screen_name": screen_name,
201 "withSafetyModeUserFields": true
202 });
203
204 let features = json!({
205 "hidden_profile_likes_enabled": false,
206 "hidden_profile_subscriptions_enabled": false,
207 "responsive_web_graphql_exclude_directive_enabled": true,
208 "verified_phone_label_enabled": false,
209 "subscriptions_verification_info_is_identity_verified_enabled": false,
210 "subscriptions_verification_info_verified_since_enabled": true,
211 "highlights_tweets_tab_ui_enabled": true,
212 "creator_subscriptions_tweet_preview_api_enabled": true,
213 "responsive_web_graphql_skip_user_profile_image_extensions_enabled": false,
214 "responsive_web_graphql_timeline_navigation_enabled": true
215 });
216
217 let field_toggles = json!({
218 "withAuxiliaryUserLabels": false
219 });
220
221 let (response, _) = request_api::<UserRaw>(
222 "https://twitter.com/i/api/graphql/G3KGOASz96M-Qu0nwmGXNg/UserByScreenName",
223 headers,
224 Method::GET,
225 Some(json!({
226 "variables": variables,
227 "features": features,
228 "fieldToggles": field_toggles
229 })),
230 )
231 .await?;
232
233 if let Some(errors) = response.errors {
235 if !errors.is_empty() {
236 return Err(TwitterError::Api(errors[0].message.clone()));
237 }
238 }
239 let user_raw_result = &response.data.user.result;
240 let mut legacy = user_raw_result.legacy.clone();
241 let rest_id = user_raw_result.rest_id.clone();
242 let is_blue_verified = user_raw_result.is_blue_verified;
243 legacy.user_id = rest_id;
244 if legacy.screen_name.is_none() || legacy.screen_name.as_ref().unwrap().is_empty() {
245 return Err(TwitterError::Api(format!(
246 "Either {} does not exist or is private.",
247 screen_name
248 )));
249 }
250 Ok(parse_profile(&legacy, is_blue_verified))
251}
252
253pub async fn get_screen_name_by_user_id(user_id: &str, auth: &dyn TwitterAuth) -> Result<String> {
254 let mut headers = HeaderMap::new();
255 auth.install_headers(&mut headers).await?;
256
257 let variables = json!({
258 "userId": user_id,
259 "withSafetyModeUserFields": true
260 });
261
262 let features = json!({
263 "hidden_profile_subscriptions_enabled": true,
264 "rweb_tipjar_consumption_enabled": true,
265 "responsive_web_graphql_exclude_directive_enabled": true,
266 "verified_phone_label_enabled": false,
267 "highlights_tweets_tab_ui_enabled": true,
268 "responsive_web_twitter_article_notes_tab_enabled": true,
269 "subscriptions_feature_can_gift_premium": false,
270 "creator_subscriptions_tweet_preview_api_enabled": true,
271 "responsive_web_graphql_skip_user_profile_image_extensions_enabled": false,
272 "responsive_web_graphql_timeline_navigation_enabled": true
273 });
274
275 let (response, _) = request_api::<UserRaw>(
276 "https://twitter.com/i/api/graphql/xf3jd90KKBCUxdlI_tNHZw/UserByRestId",
277 headers,
278 Method::GET,
279 Some(json!({
280 "variables": variables,
281 "features": features
282 })),
283 )
284 .await?;
285
286 if let Some(errors) = response.errors {
287 if !errors.is_empty() {
288 return Err(TwitterError::Api(errors[0].message.clone()));
289 }
290 }
291
292 if let Some(user) = response.data.user.result.legacy.screen_name {
293 Ok(user)
294 } else {
295 Err(TwitterError::Api(format!(
296 "Either user with ID {} does not exist or is private.",
297 user_id
298 )))
299 }
300}
301
302pub async fn get_user_id_by_screen_name(
303 screen_name: &str,
304 auth: &dyn TwitterAuth,
305) -> Result<String> {
306 if let Some(cached_id) = ID_CACHE.lock().unwrap().get(screen_name) {
308 return Ok(cached_id.clone());
309 }
310
311 let profile = get_profile(screen_name, auth).await?;
312 println!("profile: {:?}", profile);
313 if let Some(user_id) = Some(profile.id) {
314 ID_CACHE
316 .lock()
317 .unwrap()
318 .insert(screen_name.to_string(), user_id.clone());
319 Ok(user_id)
320 } else {
321 Err(TwitterError::Api("User ID is undefined".into()))
322 }
323}