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}