twitter_archive/structs/
tweets.rs

1#!/usr/bin/env rust
2
3//! Tweeter archives as of 2023-08-31 have public tweets found under;
4//!
5//!   twitter-<DATE>-<UID>.zip:data/tweets.js
6//!   twitter-<DATE>-<UID>.zip:data/deleted-tweets.js
7//!
8//! ## Example file reader for `data/tweets.js`
9//!
10//! ```no_build
11//! use std::io::Read;
12//! use std::{fs, path};
13//! use zip::read::ZipArchive;
14//!
15//! use twitter_archive::structs::tweets;
16//!
17//! fn main() {
18//!     let input_file = "~/Downloads/twitter-archive.zip";
19//!
20//!     let file_descriptor = fs::File::open(input_file).expect("Unable to read --input-file");
21//!     let mut zip_archive = ZipArchive::new(file_descriptor).unwrap();
22//!     let mut zip_file = zip_archive.by_name("data/tweets.js").unwrap();
23//!     let mut buff = String::new();
24//!     zip_file.read_to_string(&mut buff).unwrap();
25//!
26//!     let json = buff.replacen("window.YTD.tweets.part0 = ", "", 1);
27//!     let data: Vec<tweets::TweetObject> = serde_json::from_str(&json).expect("Unable to parse");
28//!
29//!     for (index, object) in data.iter().enumerate() {
30//!         /* Do stuff with each Tweet */
31//!         println!("Index: {index}");
32//!         println!("Created at: {}", object.tweet.created_at);
33//!         println!("vvv Content\n{}\n^^^ Content", object.tweet.full_text);
34//!     }
35//! }
36//! ```
37//!
38//! ## Example file reader for `deleted-tweets.js`
39//!
40//! ```no_build
41//! use std::io::Read;
42//! use std::{fs, path};
43//! use zip::read::ZipArchive;
44//!
45//! use twitter_archive::structs::tweets;
46//!
47//! fn main() {
48//!     let input_file = "~/Downloads/twitter-archive.zip";
49//!
50//!     let file_descriptor = fs::File::open(input_file).expect("Unable to read --input-file");
51//!     let mut zip_archive = ZipArchive::new(file_descriptor).unwrap();
52//!     let mut zip_file = zip_archive.by_name("data/deleted-tweets.js").unwrap();
53//!     let mut buff = String::new();
54//!     zip_file.read_to_string(&mut buff).unwrap();
55//!
56//!     let json = buff.replacen("window.YTD.deleted_tweets.part0 = ", "", 1);
57//!     let data: Vec<tweets::TweetObject> = serde_json::from_str(&json).expect("Unable to parse");
58//!
59//!     for (index, object) in data.iter().enumerate() {
60//!         /* Do stuff with each Tweet */
61//!         println!("Index: {index}");
62//!         println!("Created at: {}", object.tweet.created_at);
63//!         println!("vvv Content\n{}\n^^^ Content", object.tweet.full_text);
64//!     }
65//! }
66//! ```
67//!
68//! ## Example content for `twitter-<DATE>-<UID>.zip:data/tweets.js`
69//!
70//! ```javascript
71//! window.YTD.tweets.part0 = [
72//!   {
73//!     "tweet": {
74//!       "edit_info": {
75//!         "initial": {
76//!           "editTweetIds": ["1690395372546301952"],
77//!           "editableUntil": "2023-08-12T17:10:37.000Z",
78//!           "editsRemaining": "5",
79//!           "isEditEligible": true
80//!         }
81//!       },
82//!       "retweeted": false,
83//!       "source": "<a href=\"https://mobile.twitter.com\" rel=\"nofollow\">Twitter Web App</a>",
84//!       "entities": {
85//!         "hashtags": [],
86//!         "symbols": [],
87//!         "user_mentions": [
88//!           {
89//!             "name": "ThePrimeagen",
90//!             "screen_name": "ThePrimeagen",
91//!             "indices": ["0", "13"],
92//!             "id_str": "291797158",
93//!             "id": "291797158"
94//!           }
95//!         ],
96//!         "urls": [
97//!           {
98//!             "url": "https://t.co/4LBPKIGBzf",
99//!             "expanded_url": "https://www.youtube.com/watch?v=J7bX5dPUw0g",
100//!             "display_url": "youtube.com/watch?v=J7bX5d…",
101//!             "indices": ["132", "155"]
102//!           }
103//!         ]
104//!       },
105//!       "display_text_range": ["0", "276"],
106//!       "favorite_count": "0",
107//!       "id_str": "1690395372546301952",
108//!       "in_reply_to_user_id": "291797158",
109//!       "truncated": false,
110//!       "retweet_count": "0",
111//!       "id": "1690395372546301952",
112//!       "possibly_sensitive": false,
113//!       "created_at": "Sat Aug 12 16:10:37 +0000 2023",
114//!       "favorited": false,
115//!       "full_text": "@ThePrimeagen to answer your question about when writing interfaces, without the intention to change or test, is a good idea from;\n\nhttps://t.co/4LBPKIGBzf\n\n... Solidity interfaces are cheaper to store (S3), and pass over-the-wire, than shipping full contract(s) to consumers.",
116//!       "lang": "en",
117//!       "in_reply_to_screen_name": "ThePrimeagen",
118//!       "in_reply_to_user_id_str": "291797158"
119//!     }
120//!   }
121//! ]
122//! ```
123//!
124//! Tip, to parse deleted tweets only requires one change in preparation;
125//!
126//! ```diff
127//! -window.YTD.tweets.part0
128//! +window.YTD.deleted_tweets.part0
129//! ```
130
131use chrono::{DateTime, Utc};
132use derive_more::Display;
133use serde::{Deserialize, Serialize};
134
135use crate::convert;
136
137/// ## Example
138///
139/// ```
140/// use chrono::{DateTime, NaiveDateTime, Utc};
141///
142/// use twitter_archive::convert::{created_at, date_time_iso_8601};
143///
144/// use twitter_archive::structs::tweets::TweetObject;
145///
146/// let editable_until_string = "2023-08-12T17:10:37.000Z";
147/// let editable_until_native_time = NaiveDateTime::parse_from_str(&editable_until_string, date_time_iso_8601::FORMAT).unwrap();
148/// let editable_until_date_time = DateTime::<Utc>::from_naive_utc_and_offset(editable_until_native_time, Utc);
149///
150/// let created_at_string = "Sat Aug 12 16:10:37 +0000 2023";
151/// let created_at_date_time: DateTime<Utc> = DateTime::parse_from_str(&created_at_string, created_at::FORMAT)
152///     .unwrap()
153///     .into();
154///
155/// let json = format!(r#"{{
156///   "tweet": {{
157///     "edit_info": {{
158///       "initial": {{
159///         "editTweetIds": [
160///           "1690395372546301952"
161///         ],
162///         "editableUntil": "{editable_until_string}",
163///         "editsRemaining": "5",
164///         "isEditEligible": true
165///       }}
166///     }},
167///     "retweeted": false,
168///     "source": "<a href=\"https://mobile.twitter.com\" rel=\"nofollow\">Twitter Web App</a>",
169///     "entities": {{
170///       "hashtags": [],
171///       "symbols": [],
172///       "user_mentions": [
173///         {{
174///           "name": "ThePrimeagen",
175///           "screen_name": "ThePrimeagen",
176///           "indices": [
177///             "0",
178///             "13"
179///           ],
180///           "id_str": "291797158",
181///           "id": "291797158"
182///         }}
183///       ],
184///       "urls": [
185///         {{
186///           "url": "https://t.co/4LBPKIGBzf",
187///           "expanded_url": "https://www.youtube.com/watch?v=J7bX5dPUw0g",
188///           "display_url": "youtube.com/watch?v=J7bX5d…",
189///           "indices": [
190///             "132",
191///             "155"
192///           ]
193///         }}
194///       ]
195///     }},
196///     "display_text_range": [
197///       "0",
198///       "276"
199///     ],
200///     "favorite_count": "0",
201///     "id_str": "1690395372546301952",
202///     "in_reply_to_user_id": "291797158",
203///     "truncated": false,
204///     "retweet_count": "0",
205///     "id": "1690395372546301952",
206///     "possibly_sensitive": false,
207///     "created_at": "{created_at_string}",
208///     "favorited": false,
209///     "full_text": "@ThePrimeagen to answer your question about when writing interfaces, without the intention to change or test, is a good idea from;\n\nhttps://t.co/4LBPKIGBzf\n\n... Solidity interfaces are cheaper to store (S3), and pass over-the-wire, than shipping full contract(s) to consumers.",
210///     "lang": "en",
211///     "in_reply_to_screen_name": "ThePrimeagen",
212///     "in_reply_to_user_id_str": "291797158"
213///   }}
214/// }}"#);
215///
216/// let data: TweetObject = serde_json::from_str(&json).unwrap();
217///
218/// // De-serialized properties
219/// assert_eq!(data.tweet.retweeted, false);
220/// assert_eq!(data.tweet.source, "<a href=\"https://mobile.twitter.com\" rel=\"nofollow\">Twitter Web App</a>");
221/// assert_eq!(data.tweet.display_text_range, [0, 276]);
222/// assert_eq!(data.tweet.favorite_count, 0);
223/// assert_eq!(data.tweet.id_str, "1690395372546301952");
224/// assert_eq!(data.tweet.in_reply_to_user_id, Some("291797158".to_string()));
225/// assert_eq!(data.tweet.truncated, false);
226/// assert_eq!(data.tweet.retweet_count, 0);
227/// assert_eq!(data.tweet.id, "1690395372546301952");
228/// assert_eq!(data.tweet.possibly_sensitive, Some(false));
229/// assert_eq!(data.tweet.created_at, created_at_date_time);
230/// assert_eq!(data.tweet.favorited, false);
231/// assert_eq!(data.tweet.full_text, "@ThePrimeagen to answer your question about when writing interfaces, without the intention to change or test, is a good idea from;\n\nhttps://t.co/4LBPKIGBzf\n\n... Solidity interfaces are cheaper to store (S3), and pass over-the-wire, than shipping full contract(s) to consumers.");
232/// assert_eq!(data.tweet.lang, "en");
233/// assert_eq!(data.tweet.in_reply_to_screen_name, Some("ThePrimeagen".to_string()));
234/// assert_eq!(data.tweet.in_reply_to_user_id_str, Some("291797158".to_string()));
235///
236/// // Re-serialize is equivalent to original data
237/// assert_eq!(serde_json::to_string_pretty(&data).unwrap(), json);
238/// ```
239#[derive(Deserialize, Serialize, Debug, Clone, Display)]
240#[display(fmt = "{}", "serde_json::to_value(self).unwrap()")]
241pub struct TweetObject {
242	/// Why they wrapped a list of Tweets within unnecessary object label is anyone's guess
243	///
244	/// ## Example JSON data
245	///
246	/// ```json
247	/// {
248	///   "tweet": {
249	///     "edit_info": {
250	///       "initial": {
251	///         "editTweetIds": ["1690395372546301952"],
252	///         "editableUntil": "2023-08-12T17:10:37.000Z",
253	///         "editsRemaining": "5",
254	///         "isEditEligible": true
255	///       }
256	///     },
257	///     "retweeted": false,
258	///     "source": "<a href=\"https://mobile.twitter.com\" rel=\"nofollow\">Twitter Web App</a>",
259	///     "entities": {
260	///       "hashtags": [],
261	///       "symbols": [],
262	///       "user_mentions": [
263	///         {
264	///           "name": "ThePrimeagen",
265	///           "screen_name": "ThePrimeagen",
266	///           "indices": ["0", "13"],
267	///           "id_str": "291797158",
268	///           "id": "291797158"
269	///         }
270	///       ],
271	///       "urls": [
272	///         {
273	///           "url": "https://t.co/4LBPKIGBzf",
274	///           "expanded_url": "https://www.youtube.com/watch?v=J7bX5dPUw0g",
275	///           "display_url": "youtube.com/watch?v=J7bX5d…",
276	///           "indices": ["132", "155"]
277	///         }
278	///       ]
279	///     },
280	///     "display_text_range": ["0", "276"],
281	///     "favorite_count": "0",
282	///     "id_str": "1690395372546301952",
283	///     "in_reply_to_user_id": "291797158",
284	///     "truncated": false,
285	///     "retweet_count": "0",
286	///     "id": "1690395372546301952",
287	///     "possibly_sensitive": false,
288	///     "created_at": "Sat Aug 12 16:10:37 +0000 2023",
289	///     "favorited": false,
290	///     "full_text": "@ThePrimeagen to answer your question about when writing interfaces, without the intention to change or test, is a good idea from;\n\nhttps://t.co/4LBPKIGBzf\n\n... Solidity interfaces are cheaper to store (S3), and pass over-the-wire, than shipping full contract(s) to consumers.",
291	///     "lang": "en",
292	///     "in_reply_to_screen_name": "ThePrimeagen",
293	///     "in_reply_to_user_id_str": "291797158"
294	///   }
295	/// }
296	/// ```
297	pub tweet: Tweet,
298}
299
300/// ```
301/// use chrono::{DateTime, NaiveDateTime, Utc};
302///
303/// use twitter_archive::convert::{created_at, date_time_iso_8601};
304///
305/// use twitter_archive::structs::tweets::Tweet;
306///
307/// let editable_until_string = "2023-08-12T17:10:37.000Z";
308/// let editable_until_native_time = NaiveDateTime::parse_from_str(&editable_until_string, date_time_iso_8601::FORMAT).unwrap();
309/// let editable_until_date_time = DateTime::<Utc>::from_naive_utc_and_offset(editable_until_native_time, Utc);
310///
311/// let created_at_string = "Sat Aug 12 16:10:37 +0000 2023";
312/// let created_at_date_time: DateTime<Utc> = DateTime::parse_from_str(&created_at_string, created_at::FORMAT)
313///     .unwrap()
314///     .into();
315///
316/// let json = format!(r#"{{
317///   "edit_info": {{
318///     "initial": {{
319///       "editTweetIds": [
320///         "1690395372546301952"
321///       ],
322///       "editableUntil": "{editable_until_string}",
323///       "editsRemaining": "5",
324///       "isEditEligible": true
325///     }}
326///   }},
327///   "retweeted": false,
328///   "source": "<a href=\"https://mobile.twitter.com\" rel=\"nofollow\">Twitter Web App</a>",
329///   "entities": {{
330///     "hashtags": [],
331///     "symbols": [],
332///     "user_mentions": [
333///       {{
334///         "name": "ThePrimeagen",
335///         "screen_name": "ThePrimeagen",
336///         "indices": [
337///           "0",
338///           "13"
339///         ],
340///         "id_str": "291797158",
341///         "id": "291797158"
342///       }}
343///     ],
344///     "urls": [
345///       {{
346///         "url": "https://t.co/4LBPKIGBzf",
347///         "expanded_url": "https://www.youtube.com/watch?v=J7bX5dPUw0g",
348///         "display_url": "youtube.com/watch?v=J7bX5d…",
349///         "indices": [
350///           "132",
351///           "155"
352///         ]
353///       }}
354///     ]
355///   }},
356///   "display_text_range": [
357///     "0",
358///     "276"
359///   ],
360///   "favorite_count": "0",
361///   "id_str": "1690395372546301952",
362///   "in_reply_to_user_id": "291797158",
363///   "truncated": false,
364///   "retweet_count": "0",
365///   "id": "1690395372546301952",
366///   "possibly_sensitive": false,
367///   "created_at": "{created_at_string}",
368///   "favorited": false,
369///   "full_text": "@ThePrimeagen to answer your question about when writing interfaces, without the intention to change or test, is a good idea from;\n\nhttps://t.co/4LBPKIGBzf\n\n... Solidity interfaces are cheaper to store (S3), and pass over-the-wire, than shipping full contract(s) to consumers.",
370///   "lang": "en",
371///   "in_reply_to_screen_name": "ThePrimeagen",
372///   "in_reply_to_user_id_str": "291797158"
373/// }}"#);
374///
375/// let data: Tweet = serde_json::from_str(&json).unwrap();
376///
377/// // De-serialized properties
378/// assert_eq!(data.retweeted, false);
379/// assert_eq!(data.source, "<a href=\"https://mobile.twitter.com\" rel=\"nofollow\">Twitter Web App</a>");
380/// assert_eq!(data.display_text_range, [0, 276]);
381/// assert_eq!(data.favorite_count, 0);
382/// assert_eq!(data.id_str, "1690395372546301952");
383/// assert_eq!(data.in_reply_to_user_id, Some("291797158".to_string()));
384/// assert_eq!(data.truncated, false);
385/// assert_eq!(data.retweet_count, 0);
386/// assert_eq!(data.id, "1690395372546301952");
387/// assert_eq!(data.possibly_sensitive, Some(false));
388/// assert_eq!(data.created_at, created_at_date_time);
389/// assert_eq!(data.favorited, false);
390/// assert_eq!(data.full_text, "@ThePrimeagen to answer your question about when writing interfaces, without the intention to change or test, is a good idea from;\n\nhttps://t.co/4LBPKIGBzf\n\n... Solidity interfaces are cheaper to store (S3), and pass over-the-wire, than shipping full contract(s) to consumers.");
391/// assert_eq!(data.lang, "en");
392/// assert_eq!(data.in_reply_to_screen_name, Some("ThePrimeagen".to_string()));
393/// assert_eq!(data.in_reply_to_user_id_str, Some("291797158".to_string()));
394///
395/// // Re-serialize is equivalent to original data
396/// assert_eq!(serde_json::to_string_pretty(&data).unwrap(), json);
397/// ```
398#[derive(Deserialize, Serialize, Debug, Clone, Display)]
399#[display(fmt = "{}", "serde_json::to_value(self).unwrap()")]
400pub struct Tweet {
401	/// Data about edit history and availability for further edits
402	///
403	/// ## Example JSON data
404	///
405	/// ```json
406	/// {
407	///   "edit_info": {
408	///     "initial": {
409	///       "editTweetIds": ["1690395372546301952"],
410	///       "editableUntil": "2023-08-12T17:10:37.000Z",
411	///       "editsRemaining": "5",
412	///       "isEditEligible": true
413	///     }
414	///   }
415	/// }
416	/// ```
417	pub edit_info: TweetEditInfo,
418
419	/// Is or is not retweeted
420	///
421	/// ## Example JSON data
422	///
423	/// ```json
424	/// { "retweeted": false }
425	/// ```
426	pub retweeted: bool,
427
428	/// URL that almost, if not, always points to `"<a href=\"https://mobile.twitter.com\" rel=\"nofollow\">Twitter Web App</a>"`
429	///
430	/// ## Example JSON data
431	///
432	/// ```json
433	/// { "source": "<a href=\"https://mobile.twitter.com\" rel=\"nofollow\">Twitter Web App</a>" }
434	/// ```
435	pub source: String,
436
437	/// Additional data within Tweet such as hashtags and URLs
438	///
439	/// ## Example JSON data
440	///
441	/// ```json
442	/// {
443	///   "entries": {
444	///     "hashtags": [],
445	///     "symbols": [],
446	///     "user_mentions": [
447	///       {
448	///         "name": "ThePrimeagen",
449	///         "screen_name": "ThePrimeagen",
450	///         "indices": ["0", "13"],
451	///         "id_str": "291797158",
452	///         "id": "291797158"
453	///       }
454	///     ],
455	///     "urls": [
456	///       {
457	///         "url": "https://t.co/4LBPKIGBzf",
458	///         "expanded_url": "https://www.youtube.com/watch?v=J7bX5dPUw0g",
459	///         "display_url": "youtube.com/watch?v=J7bX5d…",
460	///         "indices": ["132", "155"]
461	///       }
462	///     ]
463	///   }
464	/// }
465	/// ```
466	pub entities: TweetEntities,
467
468	/// Indexes of beginning and end of Tweeted text
469	///
470	/// ## Example JSON data
471	///
472	/// ```json
473	/// {
474	///   "display_text_range": ["0", "276"]
475	/// }
476	/// ```
477	#[serde(with = "convert::indices")]
478	pub display_text_range: [usize; 2],
479
480	/// How many hearts have been clicked for Tweet
481	///
482	/// ## Example JSON data
483	///
484	/// ```json
485	/// { "favorite_count": "0" }
486	/// ```
487	#[serde(with = "convert::number_like_string")]
488	pub favorite_count: usize,
489
490	/// URL formats;
491	///
492	/// - Desktop: `https://twitter.com/i/web/status/{in_reply_to_status_id_str}`
493	/// - Mobile: `https://mobile.twitter.com/i/web/status/{in_reply_to_status_id_str}`
494	///
495	/// ## Example JSON data
496	///
497	/// ```json
498	/// { "in_reply_to_status_id_str": "1111111111111111111" }
499	/// ```
500	#[serde(skip_serializing_if = "Option::is_none")]
501	pub in_reply_to_status_id_str: Option<String>,
502
503	/// URL formats;
504	///
505	/// - Desktop: `https://twitter.com/i/web/status/{id_str}`
506	/// - Mobile: `https://mobile.twitter.com/i/web/status/{id_str}`
507	///
508	/// ## Example JSON data
509	///
510	/// ```json
511	/// { "id_str": "1690395372546301952" }
512	/// ```
513	pub id_str: String,
514
515	/// URL formats;
516	///
517	/// - Desktop: `https://twitter.com/i/web/status/{in_reply_to_user_id}`
518	/// - Mobile: `https://mobile.twitter.com/i/web/status/{in_reply_to_user_id}`
519	///
520	/// ## Example JSON data
521	///
522	/// ```json
523	/// { "in_reply_to_user_id": "291797158" }
524	/// ```
525	#[serde(skip_serializing_if = "Option::is_none")]
526	pub in_reply_to_user_id: Option<String>,
527
528	/// Is Tweet too long for most Twitter readers to wanna read?
529	///
530	/// ## Example JSON data
531	///
532	/// ```json
533	/// { "truncated": false, }
534	/// ```
535	pub truncated: bool,
536
537	/// How many felt Tweet worthy to re-Tweet?
538	///
539	/// ## Example JSON data
540	///
541	/// ```json
542	/// { "retweet_count": "0" }
543	/// ```
544	#[serde(with = "convert::number_like_string")]
545	pub retweet_count: usize,
546
547	/// URL formats;
548	///
549	/// - Desktop: `https://twitter.com/i/web/status/{id}`
550	/// - Mobile: `https://mobile.twitter.com/i/web/status/{id}`
551	///
552	/// ## Example JSON data
553	///
554	/// ```json
555	/// { "id": "1690395372546301952" }
556	/// ```
557	pub id: String,
558
559	/// URL formats;
560	///
561	/// - Desktop: `https://twitter.com/i/web/status/{in_reply_to_status_id}`
562	/// - Mobile: `https://mobile.twitter.com/i/web/status/{in_reply_to_status_id}`
563	///
564	/// ## Example JSON data
565	///
566	/// ```json
567	/// { "in_reply_to_status_id": "1111111111111111111" }
568	/// ```
569	#[serde(skip_serializing_if = "Option::is_none")]
570	pub in_reply_to_status_id: Option<String>,
571
572	/// Is the Tweet maybe ticklish?
573	///
574	/// ## Example JSON data
575	///
576	/// ```json
577	/// { "possibly_sensitive": false }
578	/// ```
579	#[serde(skip_serializing_if = "Option::is_none")]
580	pub possibly_sensitive: Option<bool>,
581
582	/// Date time-stamp of when Tweet was originally tweeted
583	///
584	/// ## Example JSON data
585	///
586	/// ```json
587	/// { "created_at": "Sat Aug 12 16:10:37 +0000 2023" }
588	/// ```
589	#[serde(with = "convert::created_at")]
590	pub created_at: DateTime<Utc>,
591
592	/// Is the Tweet a for sure favored Tweet?
593	///
594	/// ## Example JSON data
595	///
596	/// ```json
597	/// { "favorited": false }
598	/// ```
599	pub favorited: bool,
600
601	/// Content of Tweet with embedded newlines `\n` where applicable
602	///
603	/// ## Example JSON data
604	///
605	/// ```json
606	/// {
607	///   "full_text": "@ThePrimeagen to answer your question about when writing interfaces, without the intention to change or test, is a good idea from;\n\nhttps://t.co/4LBPKIGBzf\n\n... Solidity interfaces are cheaper to store (S3), and pass over-the-wire, than shipping full contract(s) to consumers."
608	/// }
609	/// ```
610	pub full_text: String,
611
612	/// Two letter string representing language Tweet was authored in (e.g. "en")
613	///
614	/// ## Example JSON data
615	///
616	/// ```json
617	/// { "lang": "en" }
618	/// ```
619	pub lang: String,
620
621	/// Same value as is found in `.tweets[].tweet.entries.user_mentions[].screen_name`
622	///
623	/// URL formats;
624	///
625	/// - Desktop: `https://twitter.com/{in_reply_to_screen_name}`
626	///
627	/// > Note; redirects to log-in if not logged in, and redirections may be broken.  Thanks be to
628	/// > Mr. Musk !-D
629	///
630	/// ## Example JSON data
631	///
632	/// ```json
633	/// { "in_reply_to_screen_name": "ThePrimeagen" }
634	/// ```
635	#[serde(skip_serializing_if = "Option::is_none")]
636	pub in_reply_to_screen_name: Option<String>,
637
638	/// URL formats;
639	///
640	/// - Desktop: `https://twitter.com/i/user/{in_reply_to_user_id_str}`
641	///
642	/// > Note; does **not** work if not logged-in.  Thanks be to Mr. Musk !-D
643	///
644	/// ## Example JSON data
645	///
646	/// ```json
647	/// { "in_reply_to_user_id_str": "291797158" }
648	/// ```
649	#[serde(skip_serializing_if = "Option::is_none")]
650	pub in_reply_to_user_id_str: Option<String>,
651}
652
653/// ## Example
654///
655/// ```
656/// use chrono::{DateTime, NaiveDateTime, Utc};
657///
658/// use twitter_archive::structs::tweets::TweetEditInfo;
659///
660/// use twitter_archive::convert::date_time_iso_8601::FORMAT;
661///
662/// let editable_until_string = "2023-08-12T17:10:37.000Z";
663/// let editable_until_native_time = NaiveDateTime::parse_from_str(&editable_until_string, FORMAT).unwrap();
664/// let editable_until_date_time = DateTime::<Utc>::from_naive_utc_and_offset(editable_until_native_time, Utc);
665///
666/// let json = format!(r#"{{
667///   "initial": {{
668///     "editTweetIds": [
669///       "1690395372546301952"
670///     ],
671///     "editableUntil": "{editable_until_string}",
672///     "editsRemaining": "5",
673///     "isEditEligible": true
674///   }}
675/// }}"#);
676///
677/// let data: TweetEditInfo = serde_json::from_str(&json).unwrap();
678///
679/// // De-serialized properties
680/// assert_eq!(data.initial.edit_tweet_ids, ["1690395372546301952"]);
681/// assert_eq!(data.initial.editable_until, editable_until_date_time);
682/// assert_eq!(data.initial.edits_remaining, 5);
683/// assert_eq!(data.initial.is_edit_eligible, true);
684///
685/// // Re-serialize is equivalent to original data
686/// assert_eq!(serde_json::to_string_pretty(&data).unwrap(), json);
687/// ```
688#[derive(Deserialize, Serialize, Debug, Clone, Display)]
689#[display(fmt = "{}", "serde_json::to_value(self).unwrap()")]
690pub struct TweetEditInfo {
691	/// Object/data-structure containing information about edited tweets
692	///
693	/// ## Example JSON data
694	///
695	/// ```json
696	/// {
697	///  "initial": {
698	///    "editTweetIds": ["1690395372546301952"],
699	///    "editableUntil": "2023-08-12T17:10:37.000Z",
700	///    "editsRemaining": "5",
701	///    "isEditEligible": true
702	///  }
703	/// }
704	/// ```
705	pub initial: TweetEditInfoInitial,
706}
707
708/// Whom-ever originally added the edit feature seems to have said, "F existing conventions, we're
709/// doing this camel style" X-D
710///
711/// ```
712/// use chrono::{DateTime, NaiveDateTime, Utc};
713///
714/// use twitter_archive::convert::date_time_iso_8601::FORMAT;
715/// use twitter_archive::structs::tweets::TweetEditInfoInitial;
716///
717/// let time = "2023-08-12T17:10:37.000Z";
718/// let date_time = NaiveDateTime::parse_from_str(&time, FORMAT).unwrap();
719/// let editable_until = DateTime::<Utc>::from_naive_utc_and_offset(date_time, Utc);
720///
721/// let json = format!(r#"{{
722///   "editTweetIds": [
723///     "1690395372546301952"
724///   ],
725///   "editableUntil": "{time}",
726///   "editsRemaining": "5",
727///   "isEditEligible": true
728/// }}"#);
729///
730/// let data: TweetEditInfoInitial = serde_json::from_str(&json).unwrap();
731///
732/// // De-serialized properties
733/// assert_eq!(data.edit_tweet_ids, ["1690395372546301952"]);
734/// assert_eq!(data.editable_until, editable_until);
735/// assert_eq!(data.edits_remaining, 5);
736/// assert_eq!(data.is_edit_eligible, true);
737///
738/// // Re-serialize is equivalent to original data
739/// assert_eq!(serde_json::to_string_pretty(&data).unwrap(), json);
740/// ```
741#[derive(Deserialize, Serialize, Debug, Clone, Display)]
742#[display(fmt = "{}", "serde_json::to_value(self).unwrap()")]
743#[serde(rename_all = "camelCase")]
744pub struct TweetEditInfoInitial {
745	/// URL formats;
746	///
747	/// - Desktop: `https://twitter.com/i/web/status/{edit_tweet_ids}`
748	/// - Mobile: `https://mobile.twitter.com/i/web/status/{edit_tweet_ids}`
749	///
750	/// ## Example JSON data
751	///
752	/// ```json
753	/// {
754	///    "editTweetIds": ["1690395372546301952"]
755	/// }
756	/// ```
757	pub edit_tweet_ids: Vec<String>,
758
759	/// Date time stamp until editing is no longer allowed, even if paying for Mr. Musk perks
760	///
761	/// ## Example JSON data
762	///
763	/// ```json
764	/// { "editableUntil": "2023-08-12T17:10:37.000Z" }
765	/// ```
766	#[serde(with = "convert::date_time_iso_8601")]
767	pub editable_until: DateTime<Utc>,
768
769	/// Remaining edits available, if account is currently paying Mr. Musk for check-mark parks
770	///
771	/// ## Example JSON data
772	///
773	/// ```json
774	/// { "editsRemaining": "5" }
775	/// ```
776	#[serde(with = "convert::number_like_string")]
777	pub edits_remaining: usize,
778
779	/// State is a lie unless user of this data structure is paying member.  Thanks be to Mr. Musk
780	///
781	/// ## Example JSON data
782	///
783	/// ```json
784	/// { "isEditEligible": true }
785	/// ```
786	pub is_edit_eligible: bool,
787}
788
789/// ## Example
790///
791/// ```
792/// use twitter_archive::structs::tweets::TweetEntities;
793///
794/// let json = r#"{
795///   "hashtags": [],
796///   "symbols": [],
797///   "user_mentions": [
798///     {
799///       "name": "ThePrimeagen",
800///       "screen_name": "ThePrimeagen",
801///       "indices": [
802///         "0",
803///         "13"
804///       ],
805///       "id_str": "291797158",
806///       "id": "291797158"
807///     }
808///   ],
809///   "urls": [
810///     {
811///       "url": "https://t.co/4LBPKIGBzf",
812///       "expanded_url": "https://www.youtube.com/watch?v=J7bX5dPUw0g",
813///       "display_url": "youtube.com/watch?v=J7bX5d…",
814///       "indices": [
815///         "132",
816///         "155"
817///       ]
818///     }
819///   ]
820/// }"#;
821///
822/// let data: TweetEntities = serde_json::from_str(&json).unwrap();
823///
824/// // De-serialized properties
825/// assert_eq!(data.hashtags.len(), 0);
826/// assert_eq!(data.symbols.len(), 0);
827/// assert_eq!(data.user_mentions.len(), 1);
828/// assert_eq!(data.urls.len(), 1);
829///
830/// // Re-serialize is equivalent to original data
831/// assert_eq!(serde_json::to_string_pretty(&data).unwrap(), json);
832/// ```
833#[derive(Deserialize, Serialize, Debug, Clone, Display)]
834#[display(fmt = "{}", "serde_json::to_value(self).unwrap()")]
835pub struct TweetEntities {
836	/// List of hashtags (string prefixed by `#`) data within Tweet
837	///
838	/// TODO: Add example JSON data
839	pub hashtags: Vec<TweetEntitiesEntry>,
840
841	/// List of symbols (string prefixed by `$`) data within Tweet
842	///
843	/// TODO: Add example JSON data
844	pub symbols: Vec<TweetEntitiesEntry>,
845
846	/// List of user data mentioned by Tweet
847	/// ## Example JSON data
848	///
849	/// ```json
850	/// {
851	///   "user_mentions": [
852	///     {
853	///       "name": "ThePrimeagen",
854	///       "screen_name": "ThePrimeagen",
855	///       "indices": ["0", "13"],
856	///       "id_str": "291797158",
857	///       "id": "291797158"
858	///     }
859	///   ]
860	/// }
861	/// ```
862	pub user_mentions: Vec<TweetEntitiesUserMention>,
863
864	/// List of URL data within Tweet
865	///
866	/// ## Example JSON data
867	///
868	/// ```json
869	/// {
870	///   "urls" [
871	///     {
872	///       "url": "https://t.co/4LBPKIGBzf",
873	///       "expanded_url": "https://www.youtube.com/watch?v=J7bX5dPUw0g",
874	///       "display_url": "youtube.com/watch?v=J7bX5d…",
875	///       "indices": ["132", "155"]
876	///     }
877	///   ]
878	/// }
879	/// ```
880	pub urls: Vec<TweetEntitiesUserUrl>,
881}
882
883/// Common structure for;
884///
885/// - `tweets[].tweet.entities.hashtags[]`
886/// - `tweets[].tweet.entities.symbols[]`
887///
888/// TODO: Add doc-tests
889#[derive(Deserialize, Serialize, Debug, Clone, Display)]
890#[display(fmt = "{}", "serde_json::to_value(self).unwrap()")]
891pub struct TweetEntitiesEntry {
892	/// String representation of hashtag or symbol entry
893	///
894	/// TODO: Add example JSON data
895	pub text: String,
896
897	/// Start and stop indexes within `.tweets[].tweet.full_text`
898	///
899	/// TODO: Add example JSON data
900	#[serde(with = "convert::indices")]
901	pub indices: [usize; 2],
902}
903
904/// ## Example
905///
906/// ```
907/// use twitter_archive::structs::tweets::TweetEntitiesUserMention;
908///
909/// let json = r#"{
910///   "name": "ThePrimeagen",
911///   "screen_name": "ThePrimeagen",
912///   "indices": [
913///     "0",
914///     "13"
915///   ],
916///   "id_str": "291797158",
917///   "id": "291797158"
918/// }"#;
919///
920/// let data: TweetEntitiesUserMention = serde_json::from_str(&json).unwrap();
921///
922/// // De-serialized properties
923/// assert_eq!(data.name, "ThePrimeagen");
924/// assert_eq!(data.screen_name, "ThePrimeagen");
925/// assert_eq!(data.indices, [0, 13]);
926/// assert_eq!(data.id_str, "291797158");
927/// assert_eq!(data.id, "291797158");
928///
929/// // Re-serialize is equivalent to original data
930/// assert_eq!(serde_json::to_string_pretty(&data).unwrap(), json);
931/// ```
932#[derive(Deserialize, Serialize, Debug, Clone, Display)]
933#[display(fmt = "{}", "serde_json::to_value(self).unwrap()")]
934pub struct TweetEntitiesUserMention {
935	/// Who to _@_ when mentioning a user
936	///
937	/// URL formats;
938	///
939	/// - Desktop: `https://twitter.com/{name}`
940	///
941	/// > Note; redirects to log-in if not logged in, and redirections may be broken.  Thanks be to
942	/// > Mr. Musk !-D
943	///
944	/// ## Example JSON data
945	///
946	/// ```json
947	/// { "name": "ThePrimeagen" }
948	/// ```
949	pub name: String,
950
951	/// Contains one value identical to `.tweets[].tweet.in_reply_to_screen_name`
952	///
953	/// URL formats;
954	///
955	/// - Desktop: `https://twitter.com/{screen_name}`
956	///
957	/// > Note; redirects to log-in if not logged in, and redirections may be broken.  Thanks be to
958	/// > Mr. Musk !-D
959	///
960	/// ## Example JSON data
961	///
962	/// ```json
963	/// { "screen_name": "ThePrimeagen" }
964	/// ```
965	pub screen_name: String,
966
967	/// Start and stop indexes within `.tweets[].tweet.full_text`
968	///
969	/// ## Example JSON data
970	///
971	/// ```json
972	/// {
973	///   "indices": ["0", "13"]
974	/// }
975	/// ```
976	#[serde(with = "convert::indices")]
977	pub indices: [usize; 2],
978
979	/// URL formats;
980	///
981	/// - Desktop: `https://twitter.com/i/user/{id_str}`
982	///
983	/// > Note; does **not** work if not logged-in.  Thanks be to Mr. Musk !-D
984	///
985	/// ## Example JSON data
986	///
987	/// ```json
988	/// { "id_str": "291797158" }
989	/// ```
990	pub id_str: String,
991
992	/// URL formats;
993	///
994	/// - Desktop: `https://twitter.com/i/user/{id}`
995	///
996	/// > Note; does **not** work if not logged-in.  Thanks be to Mr. Musk !-D
997	///
998	/// ## Example JSON data
999	///
1000	/// ```json
1001	/// { "id": "291797158" }
1002	/// ```
1003	pub id: String,
1004}
1005
1006/// ## Example
1007///
1008/// ```
1009/// use twitter_archive::structs::tweets::TweetEntitiesUserUrl;
1010///
1011/// let json = r#"{
1012///   "url": "https://t.co/4LBPKIGBzf",
1013///   "expanded_url": "https://www.youtube.com/watch?v=J7bX5dPUw0g",
1014///   "display_url": "youtube.com/watch?v=J7bX5d…",
1015///   "indices": [
1016///     "132",
1017///     "155"
1018///   ]
1019/// }"#;
1020///
1021/// let data: TweetEntitiesUserUrl = serde_json::from_str(&json).unwrap();
1022///
1023/// // De-serialized properties
1024/// assert_eq!(data.url, "https://t.co/4LBPKIGBzf");
1025/// assert_eq!(data.expanded_url, "https://www.youtube.com/watch?v=J7bX5dPUw0g");
1026/// assert_eq!(data.display_url, "youtube.com/watch?v=J7bX5d…");
1027/// assert_eq!(data.indices, [132, 155]);
1028///
1029/// // Re-serialize is equivalent to original data
1030/// assert_eq!(serde_json::to_string_pretty(&data).unwrap(), json);
1031/// ```
1032#[derive(Deserialize, Serialize, Debug, Clone, Display)]
1033#[display(fmt = "{}", "serde_json::to_value(self).unwrap()")]
1034pub struct TweetEntitiesUserUrl {
1035	/// Twitter shortened, and tracking, URL
1036	///
1037	/// ## Example JSON data
1038	///
1039	/// ```json
1040	/// { "url": "https://t.co/4LBPKIGBzf" }
1041	/// ```
1042	pub url: String,
1043
1044	/// The _real_ URL
1045	///
1046	/// ## Example JSON data
1047	///
1048	/// ```json
1049	/// { "expanded_url": "https://www.youtube.com/watch?v=J7bX5dPUw0g" }
1050	/// ```
1051	pub expanded_url: String,
1052
1053	/// What clients are able to view of URL within text
1054	///
1055	/// ## Example JSON data
1056	///
1057	/// ```json
1058	/// { "display_url": "youtube.com/watch?v=J7bX5d…" }
1059	/// ```
1060	pub display_url: String,
1061
1062	/// Start and stop indexes within `.tweets[].tweet.full_text`
1063	///
1064	/// ## Example JSON data
1065	///
1066	/// ```json
1067	/// {
1068	///   "indices": ["132", "155"]
1069	/// }
1070	/// ```
1071	#[serde(with = "convert::indices")]
1072	pub indices: [usize; 2],
1073}