1use chrono::{DateTime, FixedOffset};
2use serde::Deserialize;
3use eyre::{bail, ContextCompat, Result};
4
5#[derive(Debug, Deserialize)]
6pub struct ExpandedURL {
7 pub expanded_url: String,
8}
9
10#[derive(Debug, Deserialize)]
11pub struct Url {
12 pub urls: Vec<ExpandedURL>,
13}
14
15#[derive(Debug, Deserialize)]
16pub struct EntitiesOfLegacyUser {
17 pub url: Option<Url>,
18}
19
20#[derive(Debug, Deserialize)]
21pub struct LegacyUser {
22 pub created_at: String,
23 pub description: String,
24 pub entities: EntitiesOfLegacyUser,
25 pub favourites_count: i128,
26 pub followers_count: i128,
27 pub friends_count: i128,
28 pub id_str: Option<String>,
29 pub listed_count: i128,
30 pub name: String,
31 pub location: String,
32 pub pinned_tweet_ids_str: Vec<String>,
33 pub profile_banner_url: Option<String>,
34 pub profile_image_url_https: String,
35 pub protected: Option<bool>,
36 pub screen_name: String,
37 pub statuses_count: i128,
38 pub verified: bool,
39}
40
41#[derive(Debug, Deserialize)]
42pub struct UserResult {
43 pub is_blue_verified: bool,
44 pub legacy: Option<LegacyUser>,
45}
46
47#[derive(Debug, Deserialize)]
48pub struct UserResults {
49 pub result: UserResult,
50}
51
52#[derive(Debug, Deserialize)]
53pub struct ResultCore {
54 pub user_results: UserResults,
55}
56
57#[derive(Debug, Deserialize)]
58pub struct Views {
59 pub count: String,
60}
61
62#[derive(Debug, Deserialize)]
63pub struct NoteTweetResult {
64 pub text: String,
65}
66
67#[derive(Debug, Deserialize)]
68pub struct NoteTweetResults {
69 pub result: NoteTweetResult,
70}
71
72#[derive(Debug, Deserialize)]
73pub struct NoteTweet {
74 pub note_tweet_results: NoteTweetResults,
75}
76
77#[derive(Debug, Deserialize)]
78pub struct QuotedStatusResult {
79 pub result: Box<TweetResult>,
80}
81
82#[derive(Debug, Deserialize)]
83pub struct TweetResult {
84 pub __typename: String,
85 pub core: Option<ResultCore>,
86 pub views: Views,
87 pub note_tweet: Option<NoteTweet>,
88 pub quoted_status_result: Option<QuotedStatusResult>,
89 pub legacy: LegacyTweet,
90}
91
92#[derive(Debug, Deserialize)]
93pub struct TweetRresults {
94 pub result: TweetResult,
95}
96
97#[derive(Debug, Deserialize)]
98pub struct ItemContent {
99 #[serde(rename(serialize = "tweetDisplayType", deserialize = "tweetDisplayType"))]
100 pub tweet_display_type: String,
101 pub tweet_results: TweetRresults,
102 #[serde(rename(serialize = "userDisplayType", deserialize = "userDisplayType"))]
103 pub user_display_yype: Option<String>,
104 pub user_results: Option<TweetRresults>,
105}
106
107#[derive(Debug, Deserialize)]
108pub struct Item {
109 #[serde(rename(serialize = "itemContent", deserialize = "itemContent"))]
110 pub item_content: ItemContent,
111}
112
113#[derive(Debug, Deserialize)]
114pub struct EntryContent {
115 #[serde(rename(serialize = "cursorType", deserialize = "cursorType"))]
116 pub cursor_type: Option<String>,
117 pub value: Option<String>,
118 pub items: Option<Vec<Item>>,
119 #[serde(rename(serialize = "itemContent", deserialize = "itemContent"))]
120 pub item_content: Option<ItemContent>,
121}
122
123#[derive(Debug, Deserialize)]
124pub struct Entry {
125 pub content: EntryContent,
126}
127
128#[derive(Debug, Deserialize)]
129pub struct Instruction {
130 #[serde(rename(serialize = "type", deserialize = "type"))]
131 pub instruction_type: String,
132 pub entries: Vec<Entry>,
133 pub entry: Option<Entry>,
134}
135
136#[derive(Debug, Deserialize)]
137pub struct Timeline {
138 pub instructions: Option<Vec<Instruction>>,
139}
140
141#[derive(Debug, Deserialize)]
142pub struct TimelineResult {
143 pub timeline: Timeline,
144}
145
146#[derive(Debug, Deserialize)]
147pub struct SearchTimeline {
148 pub search_timeline: TimelineResult,
149}
150
151#[derive(Debug, Deserialize)]
152pub struct SearchByRawQuery {
153 pub search_by_raw_query: SearchTimeline,
154}
155
156#[derive(Debug, Deserialize)]
157pub struct Data {
158 pub data: SearchByRawQuery,
159}
160
161#[derive(Debug, Deserialize)]
163pub struct Mention {
164 pub id: String,
165 pub username: String,
166 pub name: String,
167}
168
169#[derive(Debug, Deserialize)]
171pub struct Photo {
172 pub id: String,
173 pub url: Option<String>,
174}
175
176#[derive(Debug, Deserialize)]
178pub struct Video {
179 pub id: String,
180 pub preview: String,
181 pub url: Option<String>,
182}
183
184#[derive(Debug, Deserialize)]
186pub struct GIF {
187 pub id: String,
188 pub preview: String,
189 pub url: Option<String>,
190}
191
192#[derive(Debug, Clone, Deserialize)]
193pub struct BoundingBox {
194 pub _type: String,
195 pub coordinates: Vec<Vec<Vec<f64>>>,
196}
197
198#[derive(Debug, Clone, Deserialize)]
199pub struct Place {
200 pub id: String,
201 pub place_type: String,
202 pub name: String,
203 pub full_name: String,
204 pub country_code: String,
205 pub country: String,
206 pub bounding_box: BoundingBox,
207}
208
209#[derive(Debug)]
210pub struct Tweet {
211 pub converation_id: String,
212 pub gifs: Vec<GIF>,
213 pub hash_tags: Vec<String>,
214 pub html: String,
215 pub id: String,
216 pub in_reply_to_status: Option<Box<Tweet>>,
217 pub in_reply_to_status_id: Option<String>,
218 pub is_quoted: bool,
219 pub is_pin: bool,
220 pub is_reply: bool,
221 pub is_retweet: bool,
222 pub is_self_thread: bool,
223 pub likes: i128,
224 pub name: String,
225 pub mentions: Vec<Mention>,
226 pub permanent_url: String,
227 pub photos: Vec<Photo>,
228 pub place: Option<Place>,
229 pub quoted_status: Option<Box<Tweet>>,
230 pub quoted_status_id: Option<String>,
231 pub replies: i128,
232 pub retweets: i128,
233 pub retweeted_status: Option<Box<Tweet>>,
234 pub retweeted_status_id: String,
235 pub text: String,
236 pub thread: Vec<Box<Tweet>>,
237 pub time_parsed: DateTime<FixedOffset>,
238 pub timestamp: i64,
239 pub urls: Vec<String>,
240 pub user_id: String,
241 pub username: String,
242 pub videos: Vec<Video>,
243 pub views: i128,
244 pub sensitive_content: bool,
245}
246
247#[derive(Debug, Deserialize)]
248pub struct HashTag {
249 pub text: String,
250}
251
252#[derive(Debug, Deserialize)]
253pub struct TweetMedia {
254 pub id_str: String,
255 pub media_url_https: String,
256 #[serde(rename(serialize = "type", deserialize = "type"))]
257 pub media_type: String,
258 pub url: Option<String>,
259 pub ext_sensitive_media_warning: Option<ExtSensitiveMediaWarning>,
260 pub video_info: Option<VideoInfo>,
261}
262
263#[derive(Debug, Deserialize)]
264pub struct Urls {
265 pub expanded_url: String,
266 pub url: Option<String>,
267}
268
269#[derive(Debug, Deserialize)]
270pub struct UserMentions {
271 pub id_str: String,
272 pub name: String,
273 pub screen_name: String,
274}
275
276#[derive(Debug, Deserialize)]
277pub struct Entities {
278 pub hashtags: Vec<HashTag>,
279 pub media: Option<Vec<TweetMedia>>,
280 pub urls: Vec<Urls>,
281 pub user_mentions: Vec<UserMentions>,
282}
283
284#[derive(Debug, Deserialize)]
285pub struct ExtSensitiveMediaWarning {
286 pub adult_content: bool,
287 pub graphic_violence: bool,
288 pub other: bool,
289}
290
291#[derive(Debug, Deserialize)]
292pub struct Variant {
293 pub bitrate: Option<i64>,
294 pub url: String,
295}
296
297#[derive(Debug, Deserialize)]
298pub struct VideoInfo {
299 pub variants: Vec<Variant>,
300}
301
302#[derive(Debug, Deserialize)]
303pub struct ExtendedMedia {
304 pub id_str: String,
305 pub media_url_https: String,
306 pub ext_sensitive_media_warning: Option<ExtSensitiveMediaWarning>,
307 #[serde(rename(serialize = "type", deserialize = "type"))]
308 pub ext_type: String,
309 pub url: Option<String>,
310 pub video_info: VideoInfo,
311}
312
313#[derive(Debug, Deserialize)]
314pub struct ExtendedEntities {
315 pub media: Vec<ExtendedMedia>,
316}
317
318#[derive(Debug, Deserialize)]
319pub struct RetweetedStatusResult {
320 pub result: Option<Box<TweetResult>>,
321}
322
323#[derive(Debug, Deserialize)]
324pub struct SelfThread {
325 pub id_str: String,
326}
327
328#[derive(Debug, Deserialize)]
329pub struct ExtViews {
330 pub state: String,
331 pub count: String,
332}
333
334#[derive(Debug, Deserialize)]
335pub struct LegacyTweet {
336 pub conversation_id_str: String,
337 pub created_at: String,
338 pub favorite_count: i128,
339 pub full_text: String,
340 pub entities: Entities,
341 pub extended_entities: Option<ExtendedEntities>,
342 pub id_str: String,
343 pub in_reply_to_status_id_str: Option<String>,
344 pub place: Option<Place>,
345 pub reply_count: i128,
346 pub retweet_count: i128,
347 pub retweeted_status_id_str: Option<String>,
348 pub retweeted_status_result: Option<RetweetedStatusResult>,
349 pub quoted_status_id_str: Option<String>,
350 pub self_thread: Option<SelfThread>,
351 pub time: Option<String>,
352 pub user_id_str: String,
353 pub ext_views: Option<ExtViews>,
354}
355
356pub fn parse_legacy_tweet(u: &LegacyUser, t: &LegacyTweet) -> Result<Tweet> {
357 let tweet_id = &t.id_str;
358 if tweet_id.eq("") {
359 bail!("Tweet id is empty")
360 }
361 let id = t.id_str.to_owned();
362 let name = u.name.to_owned();
363 let likes = t.favorite_count;
364 let user_id = t.user_id_str.to_owned();
365 let username = u.screen_name.to_owned();
366 let converation_id = t.conversation_id_str.to_owned();
367 let permanent_url = format!("https://twitter.com/{}/status/{}", username, tweet_id);
368 let replies = t.reply_count;
369 let retweets = t.retweet_count;
370 let text = t.full_text.to_owned();
371 let is_quoted = t
372 .quoted_status_id_str
373 .as_ref()
374 .unwrap_or(&"".to_string())
375 .ne("");
376 let quoted_status_id = t.quoted_status_id_str.to_owned();
377 let is_reply = t
378 .in_reply_to_status_id_str
379 .as_ref()
380 .unwrap_or(&"".to_string())
381 .ne("");
382 let in_reply_to_status_id = t.in_reply_to_status_id_str.to_owned();
383 let is_retweet = (t.in_reply_to_status_id_str.is_some()
384 && t.in_reply_to_status_id_str
385 .as_ref()
386 .unwrap_or(&"".to_string())
387 .ne(""))
388 || (t.retweeted_status_result.is_some()
389 && t.retweeted_status_result.as_ref().context("retweet status result is none")?.result.is_some());
390 let retweeted_status_id = t
391 .retweeted_status_id_str
392 .as_ref()
393 .unwrap_or(&String::from(""))
394 .to_string();
395 let mut views = 0i128;
396
397 if t.ext_views.is_some() {
398 views = t.ext_views.as_ref().context("ext_views is none")?.count.parse::<i128>()?;
399 }
400
401 let hash_tags: Vec<String> = t
402 .entities
403 .hashtags
404 .iter()
405 .map(|i| i.text.to_owned())
406 .collect();
407 let mentions: Vec<Mention> = t
408 .entities
409 .user_mentions
410 .iter()
411 .map(|i| Mention {
412 id: i.id_str.to_owned(),
413 username: i.screen_name.to_owned(),
414 name: i.name.to_owned(),
415 })
416 .collect();
417 let mut photos: Vec<Photo> = vec![];
418 let mut videos: Vec<Video> = vec![];
419 let mut gifs: Vec<GIF> = vec![];
420 let mut sensitive_content = false;
421 for i in t.entities.media.as_ref().unwrap_or(&vec![]).iter() {
422 match i.media_type.as_str() {
423 "photo" => photos.push(Photo {
424 id: i.id_str.to_owned(),
425 url: Some(i.media_url_https.to_owned()),
426 }),
427 "animated_gif" | "video" => {
428 let mut url = "";
429 let mut max_bitrate = 0;
430 if i.video_info.is_some() {
431 let video_info = i.video_info.as_ref().context("video_info is none")?;
432 for variant in &video_info.variants {
433 let bitrate = variant.bitrate.unwrap_or(0);
434 if bitrate > max_bitrate {
435 max_bitrate = bitrate;
436 url = variant.url.strip_suffix("?tag=10").context("cant strip video suffix")?;
437 }
438 }
439 }
440 if i.media_type.as_str().eq("video") {
441 videos.push(Video {
442 id: i.id_str.to_owned(),
443 preview: i.media_url_https.to_owned(),
444 url: Some(url.to_string()),
445 })
446 } else {
447 gifs.push(GIF {
448 id: i.id_str.to_owned(),
449 preview: i.media_url_https.to_owned(),
450 url: Some(url.to_string()),
451 })
452 }
453 }
454 _ => {}
455 }
456 if !sensitive_content && i.ext_sensitive_media_warning.is_some() {
457 let warning = i.ext_sensitive_media_warning.as_ref().context("sensitive content but warning is none")?;
458 sensitive_content = warning.adult_content || warning.graphic_violence || warning.other;
459 }
460 }
461 let mut urls: Vec<String> = vec![];
462 for i in t.entities.urls.iter() {
463 urls.push(i.expanded_url.to_owned());
464 }
465
466 let mut time_parsed = chrono::offset::Utc::now().fixed_offset();
467 if t.time.is_some() {
468 time_parsed = DateTime::parse_from_rfc2822(&t.time.as_ref().context("time is none")?)?;
469 }
470 let timestamp = time_parsed.timestamp();
471 let html = t.full_text.to_owned();
472
473 let mut retweeted_status: Option<Box<Tweet>> = Option::None;
474 if t.retweeted_status_result.is_some() {
475 let core = &t
476 .retweeted_status_result
477 .as_ref()
478 .context("retweet status is none")?
479 .result
480 .as_ref()
481 .context("retweet status result is none")?
482 .core;
483 if let Some(core) = core {
484 let legacy_u = core.user_results.result.legacy.as_ref().context("legacy is none")?;
485 let legacy_t = &t
486 .retweeted_status_result
487 .as_ref()
488 .context("retweeted status result is none")?
489 .result
490 .as_ref()
491 .context("retweeted status result is none")?
492 .legacy;
493 retweeted_status = Some(Box::new(parse_legacy_tweet(&legacy_u, &legacy_t)?));
494 }
495 }
496 let tweet = Tweet {
497 converation_id,
498 id,
499 likes,
500 name,
501 permanent_url,
502 replies,
503 retweets,
504 text,
505 user_id,
506 username,
507 place: t.place.clone(),
508 is_pin: false,
509 is_self_thread: false,
510 thread: vec![],
511 is_quoted,
512 quoted_status_id,
513 quoted_status: Option::None,
514 is_reply,
515 in_reply_to_status_id,
516 in_reply_to_status: Option::None,
517 is_retweet,
518 retweeted_status_id,
519 retweeted_status,
520 views,
521 hash_tags,
522 mentions,
523 gifs,
524 videos,
525 photos,
526 urls,
527 time_parsed,
528 timestamp,
529 sensitive_content,
530 html,
531 };
532 Ok(tweet)
533}