1use crate::models::tweets::Mention;
2use crate::models::tweets::PlaceRaw;
3use crate::models::{Profile, Tweet};
4use crate::profile::LegacyUserRaw;
5use crate::timeline::tweet_utils::{parse_media_groups, reconstruct_tweet_html};
6use chrono::DateTime;
7use chrono::Utc;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[derive(Debug, Deserialize, Serialize)]
12pub struct Hashtag {
13 pub text: Option<String>,
14}
15
16#[derive(Debug, Deserialize, Serialize)]
17pub struct TimelineUserMentionBasicRaw {
18 pub id_str: Option<String>,
19 pub name: Option<String>,
20 pub screen_name: Option<String>,
21}
22
23#[derive(Debug, Deserialize, Serialize)]
24pub struct TimelineMediaBasicRaw {
25 pub media_url_https: Option<String>,
26 pub r#type: Option<String>,
27 pub url: Option<String>,
28}
29
30#[derive(Debug, Deserialize, Serialize)]
31pub struct TimelineUrlBasicRaw {
32 pub expanded_url: Option<String>,
33 pub url: Option<String>,
34}
35
36#[derive(Debug, Deserialize, Serialize)]
37pub struct ExtSensitiveMediaWarningRaw {
38 pub adult_content: Option<bool>,
39 pub graphic_violence: Option<bool>,
40 pub other: Option<bool>,
41}
42
43#[derive(Debug, Deserialize, Serialize)]
44pub struct VideoVariant {
45 pub bitrate: Option<i32>,
46 pub url: Option<String>,
47}
48
49#[derive(Debug, Deserialize, Serialize)]
50pub struct VideoInfo {
51 pub variants: Option<Vec<VideoVariant>>,
52}
53
54#[derive(Debug, Deserialize, Serialize)]
55pub struct TimelineMediaExtendedRaw {
56 pub id_str: Option<String>,
57 pub media_url_https: Option<String>,
58 pub ext_sensitive_media_warning: Option<ExtSensitiveMediaWarningRaw>,
59 pub r#type: Option<String>,
60 pub url: Option<String>,
61 pub video_info: Option<VideoInfo>,
62 pub ext_alt_text: Option<String>,
63}
64
65#[derive(Debug, Deserialize, Serialize)]
66pub struct SearchResultRaw {
67 pub rest_id: Option<String>,
68 pub __typename: Option<String>,
69 pub core: Option<UserResultsCore>,
70 pub views: Option<Views>,
71 pub note_tweet: Option<NoteTweet>,
72 pub quoted_status_result: Option<QuotedStatusResult>,
73 pub legacy: Option<LegacyTweetRaw>,
74}
75
76#[derive(Debug, Deserialize, Serialize)]
77pub struct UserResultsCore {
78 pub user_results: Option<UserResults>,
79}
80
81#[derive(Debug, Deserialize, Serialize)]
82pub struct UserResults {
83 pub result: Option<UserResult>,
84}
85
86#[derive(Debug, Deserialize, Serialize)]
87pub struct UserResult {
88 pub is_blue_verified: Option<bool>,
89 pub legacy: Option<LegacyUserRaw>,
90}
91
92#[derive(Debug, Deserialize, Serialize)]
93pub struct Views {
94 pub count: Option<String>,
95}
96
97#[derive(Debug, Deserialize, Serialize)]
98pub struct NoteTweet {
99 pub note_tweet_results: Option<NoteTweetResults>,
100}
101
102#[derive(Debug, Deserialize, Serialize)]
103pub struct NoteTweetResults {
104 pub result: Option<NoteTweetResult>,
105}
106
107#[derive(Debug, Deserialize, Serialize)]
108pub struct NoteTweetResult {
109 pub text: Option<String>,
110}
111
112#[derive(Debug, Deserialize, Serialize)]
113pub struct QuotedStatusResult {
114 pub result: Option<Box<SearchResultRaw>>,
115}
116
117#[derive(Debug, Deserialize, Serialize)]
118pub struct TimelineResultRaw {
119 pub result: Option<Box<TimelineResultRaw>>,
120 pub rest_id: Option<String>,
121 pub __typename: Option<String>,
122 pub core: Option<TimelineCore>,
123 pub views: Option<TimelineViews>,
124 pub note_tweet: Option<TimelineNoteTweet>,
125 pub quoted_status_result: Option<Box<TimelineQuotedStatus>>,
126 pub legacy: Option<Box<LegacyTweetRaw>>,
127 pub tweet: Option<Box<TimelineResultRaw>>,
128}
129
130#[derive(Debug, Deserialize, Serialize)]
131pub struct TimelineCore {
132 pub user_results: Option<TimelineUserResults>,
133}
134
135#[derive(Debug, Deserialize, Serialize)]
136pub struct TimelineUserResults {
137 pub result: Option<TimelineUserResult>,
138}
139
140#[derive(Debug, Deserialize, Serialize)]
141pub struct TimelineUserResult {
142 pub is_blue_verified: Option<bool>,
143 pub legacy: Option<LegacyUserRaw>,
144}
145
146#[derive(Debug, Deserialize, Serialize)]
147pub struct TimelineViews {
148 pub count: Option<String>,
149}
150
151#[derive(Debug, Deserialize, Serialize)]
152pub struct TimelineNoteTweet {
153 pub note_tweet_results: Option<TimelineNoteTweetResults>,
154}
155
156#[derive(Debug, Deserialize, Serialize)]
157pub struct TimelineNoteTweetResults {
158 pub result: Option<TimelineNoteTweetResult>,
159}
160
161#[derive(Debug, Deserialize, Serialize)]
162pub struct TimelineNoteTweetResult {
163 pub text: Option<String>,
164}
165
166#[derive(Debug, Deserialize, Serialize)]
167pub struct TimelineQuotedStatus {
168 pub result: Option<Box<TimelineResultRaw>>,
169}
170
171#[derive(Debug, Deserialize, Serialize)]
172pub struct LegacyTweetRaw {
173 pub bookmark_count: Option<i32>,
174 pub conversation_id_str: Option<String>,
175 pub created_at: Option<String>,
176 pub favorite_count: Option<i32>,
177 pub full_text: Option<String>,
178 pub entities: Option<TweetEntities>,
179 pub extended_entities: Option<TweetExtendedEntities>,
180 pub id_str: Option<String>,
181 pub in_reply_to_status_id_str: Option<String>,
182 pub place: Option<PlaceRaw>,
183 pub reply_count: Option<i32>,
184 pub retweet_count: Option<i32>,
185 pub retweeted_status_id_str: Option<String>,
186 pub retweeted_status_result: Option<TimelineRetweetedStatus>,
187 pub quoted_status_id_str: Option<String>,
188 pub time: Option<String>,
189 pub user_id_str: Option<String>,
190 pub ext_views: Option<TweetExtViews>,
191}
192
193#[derive(Debug, Deserialize, Serialize)]
194pub struct TweetEntities {
195 pub hashtags: Option<Vec<Hashtag>>,
196 pub media: Option<Vec<TimelineMediaBasicRaw>>,
197 pub urls: Option<Vec<TimelineUrlBasicRaw>>,
198 pub user_mentions: Option<Vec<TimelineUserMentionBasicRaw>>,
199}
200
201#[derive(Debug, Deserialize, Serialize)]
202pub struct TweetExtendedEntities {
203 pub media: Option<Vec<TimelineMediaExtendedRaw>>,
204}
205
206#[derive(Debug, Deserialize, Serialize)]
207pub struct TimelineRetweetedStatus {
208 pub result: Option<TimelineResultRaw>,
209}
210
211#[derive(Debug, Deserialize, Serialize)]
212pub struct TweetExtViews {
213 pub state: Option<String>,
214 pub count: Option<String>,
215}
216
217#[derive(Debug, Deserialize, Serialize)]
218pub struct TimelineGlobalObjectsRaw {
219 pub tweets: Option<HashMap<String, Option<LegacyTweetRaw>>>,
220 pub users: Option<HashMap<String, Option<LegacyUserRaw>>>,
221}
222
223#[derive(Debug, Deserialize, Serialize)]
224pub struct TimelineDataRawCursor {
225 pub value: Option<String>,
226 pub cursor_type: Option<String>,
227}
228
229#[derive(Debug, Deserialize, Serialize)]
230pub struct TimelineDataRawEntity {
231 pub id: Option<String>,
232}
233
234#[derive(Debug, Deserialize, Serialize)]
235pub struct TimelineDataRawModuleItem {
236 pub client_event_info: Option<ClientEventInfo>,
237}
238
239#[derive(Debug, Deserialize, Serialize)]
240pub struct ClientEventInfo {
241 pub details: Option<ClientEventDetails>,
242}
243
244#[derive(Debug, Deserialize, Serialize)]
245pub struct ClientEventDetails {
246 pub guide_details: Option<GuideDetails>,
247}
248
249#[derive(Debug, Deserialize, Serialize)]
250pub struct GuideDetails {
251 pub transparent_guide_details: Option<TransparentGuideDetails>,
252}
253
254#[derive(Debug, Deserialize, Serialize)]
255pub struct TransparentGuideDetails {
256 pub trend_metadata: Option<TrendMetadata>,
257}
258
259#[derive(Debug, Deserialize, Serialize)]
260pub struct TrendMetadata {
261 pub trend_name: Option<String>,
262}
263
264#[derive(Debug, Deserialize, Serialize)]
265pub struct TimelineDataRawAddEntry {
266 pub content: Option<TimelineEntryContent>,
267}
268
269#[derive(Debug, Deserialize, Serialize)]
270pub struct TimelineDataRawPinEntry {
271 pub content: Option<TimelinePinContent>,
272}
273
274#[derive(Debug, Deserialize, Serialize)]
275pub struct TimelinePinContent {
276 pub item: Option<TimelineItem>,
277}
278
279#[derive(Debug, Deserialize, Serialize)]
280pub struct TimelineDataRawReplaceEntry {
281 pub content: Option<TimelineReplaceContent>,
282}
283
284#[derive(Debug, Deserialize, Serialize)]
285pub struct TimelineReplaceContent {
286 pub operation: Option<TimelineOperation>,
287}
288
289#[derive(Debug, Deserialize, Serialize)]
290pub struct TimelineDataRawInstruction {
291 pub add_entries: Option<TimelineAddEntries>,
292 pub pin_entry: Option<TimelineDataRawPinEntry>,
293 pub replace_entry: Option<TimelineDataRawReplaceEntry>,
294}
295
296#[derive(Debug, Deserialize, Serialize)]
297pub struct TimelineAddEntries {
298 pub entries: Option<Vec<TimelineDataRawAddEntry>>,
299}
300
301#[derive(Debug, Deserialize, Serialize)]
302pub struct TimelineDataRaw {
303 pub instructions: Option<Vec<TimelineDataRawInstruction>>,
304}
305
306#[derive(Debug, Deserialize, Serialize)]
307pub struct TimelineV1 {
308 pub global_objects: Option<TimelineGlobalObjectsRaw>,
309 pub timeline: Option<TimelineDataRaw>,
310}
311
312#[derive(Debug)]
313pub enum ParseTweetResult {
314 Success { tweet: Tweet },
315 Error { err: String },
316}
317
318#[derive(Debug, Serialize, Deserialize)]
319pub struct QueryTweetsResponse {
320 pub tweets: Vec<Tweet>,
321 pub next: Option<String>,
322 pub previous: Option<String>,
323}
324
325#[derive(Debug, Deserialize, Serialize)]
326pub struct QueryProfilesResponse {
327 pub profiles: Vec<Profile>,
328 pub next: Option<String>,
329 pub previous: Option<String>,
330}
331
332#[derive(Debug, Deserialize, Serialize)]
333pub struct TimelineEntryContent {
334 pub item: Option<TimelineItem>,
335 pub operation: Option<TimelineOperation>,
336 pub timeline_module: Option<TimelineModule>,
337}
338
339#[derive(Debug, Deserialize, Serialize)]
340pub struct TimelineItem {
341 pub content: Option<TimelineContent>,
342}
343
344#[derive(Debug, Deserialize, Serialize)]
345pub struct TimelineContent {
346 pub tweet: Option<TimelineDataRawEntity>,
347 pub user: Option<TimelineDataRawEntity>,
348}
349
350#[derive(Debug, Deserialize, Serialize)]
351pub struct TimelineOperation {
352 pub cursor: Option<TimelineDataRawCursor>,
353}
354
355#[derive(Debug, Deserialize, Serialize)]
356pub struct TimelineModule {
357 pub items: Option<Vec<TimelineModuleItemWrapper>>,
358}
359
360#[derive(Debug, Deserialize, Serialize)]
361pub struct TimelineModuleItemWrapper {
362 pub item: Option<TimelineDataRawModuleItem>,
363}
364
365#[derive(Debug)]
366pub struct UserMention {
367 pub id: String,
368 pub username: String,
369 pub name: String,
370}
371
372pub fn parse_timeline_tweet(timeline: &TimelineV1, id: &str) -> ParseTweetResult {
373 let empty_tweets = HashMap::new();
374 let tweets = match &timeline.global_objects {
375 Some(go) => go.tweets.as_ref().unwrap_or(&empty_tweets),
376 None => {
377 return ParseTweetResult::Error {
378 err: "No global objects found".to_string(),
379 }
380 }
381 };
382
383 let tweet = match tweets.get(id) {
384 Some(Some(t)) => t,
385 _ => {
386 return ParseTweetResult::Error {
387 err: format!("Tweet \"{}\" was not found in the timeline object.", id),
388 }
389 }
390 };
391
392 let user_id = match &tweet.user_id_str {
393 Some(id) => id,
394 None => {
395 return ParseTweetResult::Error {
396 err: "Tweet has no user ID".to_string(),
397 }
398 }
399 };
400
401 let empty_users = HashMap::new();
402 let users = match &timeline.global_objects {
403 Some(go) => go.users.as_ref().unwrap_or(&empty_users),
404 None => {
405 return ParseTweetResult::Error {
406 err: "No users found".to_string(),
407 }
408 }
409 };
410
411 let user = match users.get(user_id) {
412 Some(Some(u)) => u,
413 _ => {
414 return ParseTweetResult::Error {
415 err: format!("User \"{}\" has no username data.", user_id),
416 }
417 }
418 };
419
420 let hashtags = tweet
421 .entities
422 .as_ref()
423 .and_then(|e| e.hashtags.as_ref())
424 .map(|h| h.iter().filter_map(|tag| tag.text.clone()).collect())
425 .unwrap_or_default();
426
427 let mentions = tweet
428 .entities
429 .as_ref()
430 .and_then(|e| e.user_mentions.as_ref())
431 .map(|m| {
432 m.iter()
433 .filter_map(|mention| {
434 if let (Some(id), Some(screen_name), Some(name)) =
435 (&mention.id_str, &mention.screen_name, &mention.name)
436 {
437 Some(Mention {
438 id: id.clone(),
439 username: Some(screen_name.clone()),
440 name: Some(name.clone()),
441 })
442 } else {
443 None
444 }
445 })
446 .collect()
447 })
448 .unwrap_or_default();
449
450 let empty_media = Vec::new();
451 let media = tweet
452 .extended_entities
453 .as_ref()
454 .and_then(|e| e.media.as_ref())
455 .unwrap_or(&empty_media);
456
457 let urls = tweet
458 .entities
459 .as_ref()
460 .and_then(|e| e.urls.as_ref())
461 .map(|u| {
462 u.iter()
463 .filter_map(|url| url.expanded_url.clone())
464 .collect()
465 })
466 .unwrap_or_default();
467
468 let (photos, videos, sensitive_content) = parse_media_groups(media);
469
470 let mut tweet_obj = Tweet {
471 conversation_id: tweet.conversation_id_str.clone(),
472 id: Some(id.to_string()),
473 hashtags,
474 likes: tweet.favorite_count,
475 mentions,
476 name: user.name.clone(),
477 permanent_url: Some(format!(
478 "https://twitter.com/{}/status/{}",
479 user.screen_name.as_ref().unwrap_or(&String::new()),
480 id
481 )),
482 photos,
483 replies: tweet.reply_count,
484 retweets: tweet.retweet_count,
485 text: tweet.full_text.clone(),
486 thread: Vec::new(),
487 urls,
488 user_id: tweet.user_id_str.clone(),
489 username: user.screen_name.clone(),
490 videos,
491 time_parsed: None,
492 timestamp: None,
493 place: None,
494 is_quoted: Some(false),
495 quoted_status_id: None,
496 quoted_status: None,
497 is_reply: Some(false),
498 in_reply_to_status_id: None,
499 in_reply_to_status: None,
500 is_retweet: Some(false),
501 retweeted_status_id: None,
502 retweeted_status: None,
503 views: None,
504 is_pin: Some(false),
505 sensitive_content: Some(sensitive_content),
506 html: None,
507 bookmark_count: None,
508 is_self_thread: None,
509 poll: None,
510 created_at: None,
511 ext_views: None,
512 quote_count: None,
513 reply_count: None,
514 retweet_count: None,
515 screen_name: None,
516 thread_id: None,
517 };
518
519 if let Some(created_at) = &tweet.created_at {
520 if let Ok(parsed_time) = DateTime::parse_from_str(created_at, "%a %b %d %H:%M:%S %z %Y") {
521 tweet_obj.time_parsed = Some(parsed_time.with_timezone(&Utc));
522 tweet_obj.timestamp = Some(parsed_time.timestamp());
523 }
524 }
525
526 if let Some(place) = &tweet.place {
527 tweet_obj.place = Some(place.clone());
528 }
529
530 if let Some(quoted_id) = &tweet.quoted_status_id_str {
531 tweet_obj.is_quoted = Some(true);
532 tweet_obj.quoted_status_id = Some(quoted_id.clone());
533
534 if let ParseTweetResult::Success {
535 tweet: quoted_tweet,
536 } = parse_timeline_tweet(timeline, quoted_id)
537 {
538 tweet_obj.quoted_status = Some(Box::new(quoted_tweet));
539 }
540 }
541
542 if let Some(ext_views) = &tweet.ext_views {
543 if let Some(count) = &ext_views.count {
544 if let Ok(views) = count.parse::<i32>() {
545 tweet_obj.views = Some(views);
546 }
547 }
548 }
549
550 tweet_obj.html = reconstruct_tweet_html(tweet, &tweet_obj.photos, &tweet_obj.videos);
551
552 ParseTweetResult::Success { tweet: tweet_obj }
553}