1use std::collections::HashMap;
2
3use chrono::prelude::*;
4use serde::Deserialize;
5
6pub type TweetModel = crate::api::get_2_tweets_id::Response;
7
8fn str_to_utc(src: &str) -> Option<DateTime<Utc>> {
9 DateTime::parse_from_str(src, "%a %b %d %T %z %Y")
10 .ok()
11 .map(|it| it.into())
12}
13
14fn millis_to_utc(millis: i64) -> Option<DateTime<Utc>> {
15 match Utc.timestamp_millis_opt(millis) {
16 chrono::LocalResult::Single(v) => Some(v),
17 _ => None,
18 }
19}
20
21fn from_v1_indicies(src: &serde_json::Value) -> (Option<i64>, Option<i64>) {
22 match src["indices"].as_array() {
23 Some(indices) => (
24 #[allow(clippy::get_first)]
25 indices.get(0).and_then(|it| it.as_i64()),
26 indices.get(1).and_then(|it| it.as_i64()),
27 ),
28 None => (None, None),
29 }
30}
31
32fn from_v1_menthions(src: &serde_json::Value) -> Option<Vec<crate::responses::mentions::Mentions>> {
33 match src.as_array() {
34 Some(targets) => {
35 if targets.is_empty() {
36 None
37 } else {
38 Some(
39 targets
40 .iter()
41 .map(|it| {
42 let indices = from_v1_indicies(it);
43 crate::responses::mentions::Mentions {
44 username: it["screen_name"].as_str().map(|it| it.to_owned()),
45 id: it["id_str"].as_str().map(|it| it.to_owned()),
46 start: indices.0,
47 end: indices.1,
48 ..Default::default()
49 }
50 })
51 .collect(),
52 )
53 }
54 }
55 None => None,
56 }
57}
58
59fn from_v1_hashtags(src: &serde_json::Value) -> Option<Vec<crate::responses::hashtags::Hashtags>> {
60 match src.as_array() {
61 Some(targets) => {
62 if targets.is_empty() {
63 None
64 } else {
65 Some(
66 targets
67 .iter()
68 .map(|it| {
69 let indices = from_v1_indicies(it);
70 crate::responses::hashtags::Hashtags {
71 tag: it["text"].as_str().map(|it| it.to_owned()),
72 start: indices.0,
73 end: indices.1,
74 ..Default::default()
75 }
76 })
77 .collect(),
78 )
79 }
80 }
81 None => None,
82 }
83}
84
85fn from_v1_urls(src: &serde_json::Value) -> Vec<crate::responses::urls::Urls> {
86 match src.as_array() {
87 Some(targets) => targets
88 .iter()
89 .map(|it| {
90 let indices = from_v1_indicies(it);
91 crate::responses::urls::Urls {
92 url: it["url"].as_str().map(|it| it.to_owned()),
93 display_url: it["display_url"].as_str().map(|it| it.to_owned()),
94 expanded_url: it["expanded_url"].as_str().map(|it| it.to_owned()),
95 title: it["unwound"]["title"].as_str().map(|it| it.to_owned()),
96 description: it["unwound"]["description"]
97 .as_str()
98 .map(|it| it.to_owned()),
99 status: it["unwound"]["status"].as_i64(),
100 start: indices.0,
101 end: indices.1,
102 ..Default::default()
103 }
104 })
105 .collect(),
106 None => vec![],
107 }
108}
109
110fn from_v1_edit_controls(
111 src: &serde_json::Value,
112) -> Option<crate::responses::edit_controls::EditControls> {
113 if src.is_object() {
114 Some(crate::responses::edit_controls::EditControls {
115 editable_until: millis_to_utc(
116 src["edit_controls"]["editable_until_ms"]
117 .as_i64()
118 .unwrap_or_default(),
119 ),
120 edits_remaining: src["edit_controls"]["edits_remaining"].as_i64(),
121 is_edit_eligible: src["editable"].as_bool(),
122 ..Default::default()
123 })
124 } else {
125 None
126 }
127}
128
129fn from_v1_edit_history_tweet_ids(src: &serde_json::Value) -> Vec<String> {
130 if let Some(value) = src.as_array() {
131 value
132 .iter()
133 .map(|v| v.as_str().unwrap_or_default().to_owned())
134 .collect()
135 } else {
136 vec![]
137 }
138}
139
140fn from_v1_media_key(src: &serde_json::Value) -> String {
141 let media_type = match src["type"].as_str() {
142 Some("photo") => "3",
144 Some("video") => "7",
145 Some("animated_gif") => "16",
146 _ => "0",
147 };
148 format!(
149 "{}_{}",
150 media_type,
151 src["id_str"].as_str().unwrap_or_default()
152 )
153}
154
155fn from_v1_media_url(
156 src: &serde_json::Value,
157 media_map: &mut HashMap<String, crate::responses::media::Media>,
158) -> (
159 Vec<crate::responses::urls::Urls>,
160 Option<crate::responses::attachments::Attachments>,
161) {
162 let mut media_keys = vec![];
163
164 let urls = if let Some(targets) = src.as_array() {
165 targets
166 .iter()
167 .map(|it| {
168 let indices = from_v1_indicies(it);
169 let id = it["id_str"].as_str().unwrap_or_default().to_owned();
170 let media_key = if let Some(media) = media_map.get(&id) {
171 if let Some(media_key) = &media.media_key {
172 media_key.clone()
173 } else {
174 from_v1_media_key(it)
175 }
176 } else {
177 from_v1_media_key(it)
178 };
179 media_keys.push(media_key.clone());
180 crate::responses::urls::Urls {
181 url: it["url"].as_str().map(|it| it.to_owned()),
182 display_url: it["display_url"].as_str().map(|it| it.to_owned()),
183 expanded_url: it["expanded_url"].as_str().map(|it| it.to_owned()),
184 media_key: Some(media_key),
185 start: indices.0,
186 end: indices.1,
187 ..Default::default()
188 }
189 })
190 .collect()
191 } else {
192 vec![]
193 };
194
195 let attachments = if !media_keys.is_empty() {
196 Some(crate::responses::attachments::Attachments {
197 media_keys: Some(media_keys),
198 ..Default::default()
199 })
200 } else {
201 None
202 };
203 (urls, attachments)
204}
205
206fn from_v1_entities(
207 src: &serde_json::Value,
208 media_map: &mut HashMap<String, crate::responses::media::Media>,
209) -> (
210 Option<crate::responses::entities::Entities>,
211 Option<crate::responses::attachments::Attachments>,
212) {
213 if src.is_object() {
214 let (mut urls1, attachments) = from_v1_media_url(&src["media"], media_map);
215 let mut urls2 = from_v1_urls(&src["urls"]);
216 urls1.append(&mut urls2);
217 (
218 Some(crate::responses::entities::Entities {
219 mentions: from_v1_menthions(&src["user_mentions"]),
220 hashtags: from_v1_hashtags(&src["hashtags"]),
221 urls: Some(urls1),
222 ..Default::default()
223 }),
224 attachments,
225 )
226 } else {
227 (None, None)
228 }
229}
230
231#[derive(Deserialize)]
232struct BoundingBox {
233 #[allow(dead_code)]
234 r#type: String,
235 coordinates: Vec<Vec<Vec<f64>>>,
236}
237
238impl BoundingBox {
239 fn coordinates(src: &serde_json::Value) -> Vec<f64> {
240 match serde_json::from_value::<BoundingBox>(src["bounding_box"].clone()) {
241 Ok(it) => {
242 let mut x0 = it.coordinates[0][0][0];
243 let mut y0 = it.coordinates[0][0][1];
244 let mut x1 = it.coordinates[0][0][0];
245 let mut y1 = it.coordinates[0][0][1];
246 for i in 1..4 {
247 if x0 > it.coordinates[0][i][0] {
248 x0 = it.coordinates[0][i][0];
249 }
250 if x1 < it.coordinates[0][i][0] {
251 x1 = it.coordinates[0][i][0];
252 }
253 if y0 > it.coordinates[0][i][1] {
254 y0 = it.coordinates[0][i][1];
255 }
256 if y1 < it.coordinates[0][i][1] {
257 y1 = it.coordinates[0][i][1];
258 }
259 }
260 vec![x0, y0, x1, y1]
261 }
262 Err(_) => vec![],
263 }
264 }
265}
266
267fn from_v1_place(
268 src: &serde_json::Value,
269) -> (
270 Option<crate::responses::places::Places>,
271 Option<crate::responses::geo::Geo>,
272) {
273 if src.is_object() {
274 let place_geo = crate::responses::geo::Geo {
275 r#type: Some("Feature".to_owned()),
276 bbox: Some(BoundingBox::coordinates(src)),
277 ..Default::default()
278 };
279 let place = crate::responses::places::Places {
280 id: src["id"].as_str().unwrap_or_default().to_owned(),
281 full_name: src["full_name"].as_str().unwrap_or_default().to_owned(),
282 country: src["country"].as_str().map(|it| it.to_owned()),
283 country_code: src["country_code"].as_str().map(|it| it.to_owned()),
284 name: src["name"].as_str().map(|it| it.to_owned()),
285 place_type: src["place_type"].as_str().map(|it| it.to_owned()),
286 geo: Some(place_geo),
287 ..Default::default()
288 };
289 let geo = crate::responses::geo::Geo {
290 place_id: Some(place.id.clone()),
291 ..Default::default()
292 };
293 (Some(place), Some(geo))
294 } else {
295 (None, None)
296 }
297}
298
299fn from_v1_public_metrics(
300 src: &serde_json::Value,
301) -> Option<crate::responses::public_metrics::PublicMetrics> {
302 Some(crate::responses::public_metrics::PublicMetrics {
303 retweet_count: src["retweet_count"].as_i64(),
304 quote_count: src["quote_count"].as_i64(),
305 reply_count: src["reply_count"].as_i64(),
306 like_count: src["favorite_count"].as_i64(),
307 ..Default::default()
308 })
309}
310
311fn from_v1_users(src: &serde_json::Value) -> crate::responses::users::Users {
312 let public_metrics = crate::responses::users::PublicMetrics {
313 followers_count: src["followers_count"].as_i64(),
314 following_count: src["friends_count"].as_i64(),
315 tweet_count: src["statuses_count"].as_i64(),
316 listed_count: src["listed_count"].as_i64(),
317 ..Default::default()
318 };
319
320 crate::responses::users::Users {
321 created_at: str_to_utc(src["created_at"].as_str().unwrap_or_default()),
322 description: src["description"].as_str().map(|it| it.to_owned()),
323 id: src["id_str"].as_str().unwrap_or_default().to_owned(),
324 location: src["location"].as_str().map(|it| it.to_owned()),
325 name: src["name"].as_str().unwrap_or_default().to_owned(),
326 profile_image_url: src["profile_image_url_https"]
327 .as_str()
328 .map(|it| it.to_owned()),
329 protected: src["protected"].as_bool(),
330 public_metrics: Some(public_metrics),
331 url: src["url"].as_str().map(|it| it.to_owned()),
332 username: src["screen_name"].as_str().unwrap_or_default().to_owned(),
333 verified: src["verified"].as_bool(),
334 verified_type: src["verified_type"].as_str().map(|it| it.to_owned()),
335 ..Default::default()
336 }
337}
338
339fn from_v1_exetend_entities_media_size(src: &serde_json::Value) -> (Option<i64>, Option<i64>) {
340 if let Some(sizes) = src["medium"].as_object() {
341 (sizes["w"].as_i64(), sizes["h"].as_i64())
342 } else if let Some(sizes) = src["small"].as_object() {
343 (sizes["w"].as_i64(), sizes["h"].as_i64())
344 } else if let Some(sizes) = src["large"].as_object() {
345 (sizes["w"].as_i64(), sizes["h"].as_i64())
346 } else if let Some(sizes) = src["thumb"].as_object() {
347 (sizes["w"].as_i64(), sizes["h"].as_i64())
348 } else {
349 (None, None)
350 }
351}
352
353fn from_v1_variants(src: &serde_json::Value) -> Option<Vec<crate::responses::variants::Variants>> {
354 src["video_info"]["variants"].as_array().map(|targets| {
355 targets
356 .iter()
357 .map(|it| crate::responses::variants::Variants {
358 bit_rate: it["bitrate"].as_i64(),
359 content_type: it["content_type"].as_str().map(|it| it.to_owned()),
360 url: it["url"].as_str().map(|it| it.to_owned()),
361 ..Default::default()
362 })
363 .collect()
364 })
365}
366
367fn from_v1_exetend_entities_media(
368 src: &serde_json::Value,
369 media_map: &mut HashMap<String, crate::responses::media::Media>,
370) {
371 if let Some(targets) = src["extended_entities"]["media"].as_array() {
372 targets.iter().for_each(|it| {
373 let (width, height) = from_v1_exetend_entities_media_size(&it["sizes"]);
374 let media = crate::responses::media::Media {
375 duration_ms: it["video_info"]["duration_millis"].as_i64(),
376 media_key: Some(from_v1_media_key(it)),
377 width,
378 height,
379 preview_image_url: it["media_url_https"].as_str().map(|it| it.to_owned()),
380 r#type: it["type"].as_str().map(|it| it.to_owned()),
381 variants: from_v1_variants(it),
382 ..Default::default()
383 };
384 media_map.insert(it["id_str"].as_str().unwrap_or_default().to_owned(), media);
385 })
386 }
387}
388
389fn from_v1_tweets(
390 src: &serde_json::Value,
391 tweet_map: &mut HashMap<String, crate::responses::tweets::Tweets>,
392 user_map: &mut HashMap<String, crate::responses::users::Users>,
393 place_map: &mut HashMap<String, crate::responses::places::Places>,
394 media_map: &mut HashMap<String, crate::responses::media::Media>,
395) -> Option<crate::responses::tweets::Tweets> {
396 if src.is_object() {
397 from_v1_exetend_entities_media(src, media_map);
400
401 let (entities, attachments) = from_v1_entities(&src["entities"], media_map);
402 let (places, geo) = from_v1_place(&src["place"]);
403
404 let mut data = crate::responses::tweets::Tweets {
405 id: src["id_str"].as_str().unwrap_or_default().to_owned(),
406 text: src["text"].as_str().unwrap_or_default().to_owned(),
407 attachments,
408 source: src["source"].as_str().map(|it| it.to_owned()),
409 author_id: src["user"]["id_str"].as_str().map(|it| it.to_owned()),
410 conversation_id: src["edit_history"]["initial_tweet_id"]
411 .as_str()
412 .map(|it| it.to_owned()),
413 created_at: str_to_utc(src["created_at"].as_str().unwrap_or_default()),
414 edit_controls: from_v1_edit_controls(src),
415 edit_history_tweet_ids: Some(from_v1_edit_history_tweet_ids(
416 &src["edit_history"]["edit_tweet_ids"],
417 )),
418 entities,
419 geo,
420 in_reply_to_user_id: src["in_reply_to_user_id_str"]
421 .as_str()
422 .map(|it| it.to_owned()),
423 lang: src["lang"].as_str().map(|it| it.to_owned()),
424 possibly_sensitive: src["possibly_sensitive"].as_bool(),
425 public_metrics: from_v1_public_metrics(src),
426 ..Default::default()
427 };
428
429 if let Some(places) = places {
430 place_map.insert(places.id.clone(), places);
431 }
432
433 let mut referenced_tweets = vec![];
434
435 if src["retweeted_status"].is_object() {
436 if let Some(tweet) = from_v1_tweets(
437 &src["retweeted_status"],
438 tweet_map,
439 user_map,
440 place_map,
441 media_map,
442 ) {
443 referenced_tweets.push(crate::responses::referenced_tweets::ReferencedTweets {
444 id: Some(tweet.id.clone()),
445 r#type: Some(crate::responses::referenced_tweets::Type::Retweeted),
446 ..Default::default()
447 });
448 tweet_map.insert(tweet.id.clone(), tweet);
449 }
450 }
451
452 if src["quoted_status"].is_object() {
453 if let Some(tweet) = from_v1_tweets(
454 &src["quoted_status"],
455 tweet_map,
456 user_map,
457 place_map,
458 media_map,
459 ) {
460 if !src["retweeted_status"].is_object() {
463 referenced_tweets.push(crate::responses::referenced_tweets::ReferencedTweets {
464 id: Some(tweet.id.clone()),
465 r#type: Some(crate::responses::referenced_tweets::Type::Quoted),
466 ..Default::default()
467 });
468 }
469 tweet_map.insert(tweet.id.clone(), tweet);
470 }
471 }
472
473 if let Some(id) = src["in_reply_to_status_id_str"].as_str() {
476 referenced_tweets.push(crate::responses::referenced_tweets::ReferencedTweets {
477 id: Some(id.to_owned()),
478 r#type: Some(crate::responses::referenced_tweets::Type::RepliedTo),
479 ..Default::default()
480 });
481 }
482
483 if !referenced_tweets.is_empty() {
484 data.referenced_tweets = Some(referenced_tweets);
485 }
486
487 let user = from_v1_users(&src["user"]);
488 user_map.insert(user.id.clone(), user);
489
490 Some(data)
493 } else {
494 None
495 }
496}
497
498impl TweetModel {
499 pub fn from_v1(src: &serde_json::Value) -> Self {
500 let mut tweet_map = HashMap::new();
501 let mut user_map = HashMap::new();
502 let mut place_map = HashMap::new();
503 let mut media_map = HashMap::new();
504 let data = from_v1_tweets(
505 src,
506 &mut tweet_map,
507 &mut user_map,
508 &mut place_map,
509 &mut media_map,
510 );
511 Self {
512 data,
513 includes: Some(crate::responses::includes::Includes {
514 tweets: if tweet_map.is_empty() {
515 None
516 } else {
517 Some(tweet_map.into_values().collect())
518 },
519 users: if user_map.is_empty() {
520 None
521 } else {
522 Some(user_map.into_values().collect())
523 },
524 places: if place_map.is_empty() {
525 None
526 } else {
527 Some(place_map.into_values().collect())
528 },
529 media: if media_map.is_empty() {
530 None
531 } else {
532 Some(media_map.into_values().collect())
533 },
534 ..Default::default()
535 }),
536 ..Default::default()
537 }
538 }
539}