x_api_rs/
types.rs

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// Mention type.
162#[derive(Debug, Deserialize)]
163pub struct Mention {
164    pub id: String,
165    pub username: String,
166    pub name: String,
167}
168
169// Photo type.
170#[derive(Debug, Deserialize)]
171pub struct Photo {
172    pub id: String,
173    pub url: Option<String>,
174}
175
176// Video type.
177#[derive(Debug, Deserialize)]
178pub struct Video {
179    pub id: String,
180    pub preview: String,
181    pub url: Option<String>,
182}
183
184// GIF type.
185#[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}