1use crate::api::requests::request_api;
2use crate::error::{Result, TwitterError};
3use crate::models::Profile;
4use chrono::{DateTime, Utc};
5use lazy_static::lazy_static;
6use reqwest::header::HeaderMap;
7use reqwest::Method;
8use serde::{Deserialize, Serialize};
9use serde_json::json;
10use std::collections::HashMap;
11use std::sync::Mutex;
12use crate::api::client::TwitterClient;
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 user.entities
120 .as_ref()
121 .and_then(|entities| entities.url.as_ref())
122 .and_then(|url_entity| url_entity.urls.as_ref())
123 .and_then(|urls| urls.first())
124 .and_then(|first_url| first_url.expanded_url.as_ref())
125 .map(|expanded_url| profile.url = Some(expanded_url.clone()));
126
127 profile
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct UserResults {
132 pub result: UserResult,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
136#[serde(tag = "__typename")]
137pub enum UserResult {
138 User(UserData),
139 UserUnavailable(UserUnavailable),
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct UserData {
144 pub id: String,
145 pub rest_id: String,
146 pub affiliates_highlighted_label: Option<serde_json::Value>,
147 pub has_graduated_access: bool,
148 pub is_blue_verified: bool,
149 pub profile_image_shape: String,
150 pub legacy: LegacyUserRaw,
151 pub smart_blocked_by: bool,
152 pub smart_blocking: bool,
153 pub legacy_extended_profile: Option<serde_json::Value>,
154 pub is_profile_translatable: bool,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct UserUnavailable {
159 pub reason: String,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct UserRaw {
164 pub data: UserRawData,
165 pub errors: Option<Vec<TwitterApiErrorRaw>>,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct UserRawData {
170 pub user: UserRawUser,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct UserRawUser {
175 pub result: UserRawResult,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct UserRawResult {
180 pub rest_id: Option<String>,
181 pub is_blue_verified: Option<bool>,
182 pub legacy: LegacyUserRaw,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct TwitterApiErrorRaw {
187 pub message: String,
188 pub code: i32,
189}
190
191pub async fn get_profile(client: &TwitterClient, screen_name: &str) -> Result<Profile> {
192 let mut headers = HeaderMap::new();
193 client.auth.install_headers(&mut headers).await?;
194
195 let variables = json!({
196 "screen_name": screen_name,
197 "withSafetyModeUserFields": true
198 });
199
200 let features = json!({
201 "hidden_profile_likes_enabled": false,
202 "hidden_profile_subscriptions_enabled": false,
203 "responsive_web_graphql_exclude_directive_enabled": true,
204 "verified_phone_label_enabled": false,
205 "subscriptions_verification_info_is_identity_verified_enabled": false,
206 "subscriptions_verification_info_verified_since_enabled": true,
207 "highlights_tweets_tab_ui_enabled": true,
208 "creator_subscriptions_tweet_preview_api_enabled": true,
209 "responsive_web_graphql_skip_user_profile_image_extensions_enabled": false,
210 "responsive_web_graphql_timeline_navigation_enabled": true
211 });
212
213 let field_toggles = json!({
214 "withAuxiliaryUserLabels": false
215 });
216
217 let (response, _) = request_api::<UserRaw>(
218 &client.client,
219 "https://twitter.com/i/api/graphql/G3KGOASz96M-Qu0nwmGXNg/UserByScreenName",
220 headers,
221 Method::GET,
222 Some(json!({
223 "variables": variables,
224 "features": features,
225 "fieldToggles": field_toggles
226 })),
227 )
228 .await?;
229
230 if let Some(errors) = response.errors {
231 if !errors.is_empty() {
232 return Err(TwitterError::Api(errors[0].message.clone()));
233 }
234 }
235 let user_raw_result = &response.data.user.result;
236 let mut legacy = user_raw_result.legacy.clone();
237 let rest_id = user_raw_result.rest_id.clone();
238 let is_blue_verified = user_raw_result.is_blue_verified;
239 legacy.user_id = rest_id;
240 if legacy.screen_name.is_none() || legacy.screen_name.as_ref().unwrap().is_empty() {
241 return Err(TwitterError::Api(format!(
242 "Either {} does not exist or is private.",
243 screen_name
244 )));
245 }
246 Ok(parse_profile(&legacy, is_blue_verified))
247}
248
249pub async fn get_screen_name_by_user_id(client: &TwitterClient, user_id: &str) -> Result<String> {
250 let mut headers = HeaderMap::new();
251 client.auth.install_headers(&mut headers).await?;
252
253 let variables = json!({
254 "userId": user_id,
255 "withSafetyModeUserFields": true
256 });
257
258 let features = json!({
259 "hidden_profile_subscriptions_enabled": true,
260 "rweb_tipjar_consumption_enabled": true,
261 "responsive_web_graphql_exclude_directive_enabled": true,
262 "verified_phone_label_enabled": false,
263 "highlights_tweets_tab_ui_enabled": true,
264 "responsive_web_twitter_article_notes_tab_enabled": true,
265 "subscriptions_feature_can_gift_premium": false,
266 "creator_subscriptions_tweet_preview_api_enabled": true,
267 "responsive_web_graphql_skip_user_profile_image_extensions_enabled": false,
268 "responsive_web_graphql_timeline_navigation_enabled": true
269 });
270
271 let (response, _) = request_api::<UserRaw>(
272 &client.client,
273 "https://twitter.com/i/api/graphql/xf3jd90KKBCUxdlI_tNHZw/UserByRestId",
274 headers,
275 Method::GET,
276 Some(json!({
277 "variables": variables,
278 "features": features
279 })),
280 )
281 .await?;
282
283 if let Some(errors) = response.errors {
284 if !errors.is_empty() {
285 return Err(TwitterError::Api(errors[0].message.clone()));
286 }
287 }
288
289 if let Some(user) = response.data.user.result.legacy.screen_name {
290 Ok(user)
291 } else {
292 Err(TwitterError::Api(format!(
293 "Either user with ID {} does not exist or is private.",
294 user_id
295 )))
296 }
297}
298
299pub async fn get_user_id_by_screen_name(
300 client: &TwitterClient,
301 screen_name: &str,
302) -> Result<String> {
303 if let Some(cached_id) = ID_CACHE.lock().unwrap().get(screen_name) {
304 return Ok(cached_id.clone());
305 }
306
307 let profile = get_profile(client, screen_name).await?;
308 if let Some(user_id) = Some(profile.id) {
309 ID_CACHE
310 .lock()
311 .unwrap()
312 .insert(screen_name.to_string(), user_id.clone());
313 Ok(user_id)
314 } else {
315 Err(TwitterError::Api("User ID is undefined".into()))
316 }
317}