tweet/model/tweet.rs
1use chrono::{DateTime, Utc};
2use serde_derive::{Deserialize, Serialize};
3use std::str::FromStr;
4
5use crate::model::coordinates::Coordinates;
6use crate::model::entity::{Entity, ExtendedEntity};
7use crate::model::place::Place;
8use crate::model::url::LegacyUrl;
9use crate::model::user::User;
10use crate::util::datetime::{datefmt_de, datefmt_ser};
11
12impl FromStr for Tweet {
13 type Err = serde_json::error::Error;
14
15 fn from_str(s: &str) -> Result<Self, Self::Err> {
16 serde_json::from_str(s)
17 }
18}
19
20/// Represents a tweet
21#[derive(Debug, Deserialize, Serialize)]
22pub struct Tweet {
23 /// When the tweet was posted
24 #[serde(deserialize_with="datefmt_de", serialize_with="datefmt_ser")]
25 pub created_at: DateTime<Utc>,
26 /// The unique id for the tweet
27 pub id: u64,
28 /// String version of `id`
29 pub id_str: String,
30 /// The possibly truncated text of the tweet
31 pub text: String,
32 /// The client that posted the tweet
33 pub source: String,
34 /// Whether or not the text field is truncated to 140 characters
35 pub truncated: bool,
36 /// If this tweet is a reply, this will contain the original tweet id
37 pub in_reply_to_status_id: Option<u64>,
38 /// Same as `in_reply_to_status_id`, but a String
39 pub in_reply_to_status_id_str: Option<String>,
40 /// If this tweet is a reply, this will contain the original author id
41 pub in_reply_to_user_id: Option<u64>,
42 /// Same as `in_reply_to_user_id`, but a String
43 pub in_reply_to_user_id_str: Option<String>,
44 /// If this tweet is a reply, contains the original user's screen name
45 pub in_reply_to_screen_name: Option<String>,
46 /// The user who posted this tweet
47 pub user: User,
48 /// If the tweet was truncated because it was longer than 140 chars, this contains the rest of the text
49 pub extended_tweet: Option<ExtendedTweet>,
50 /// Represents the geographic location of this tweet as reported by user/client
51 pub coordinates: Option<Coordinates>,
52 /// The place that this tweet is associated with
53 pub place: Option<Place>,
54 /// If this tweet is a quote, it contains the of the quoted tweet id
55 pub quoted_status_id: Option<u64>,
56 /// Same as `quoted_status_id_str`, but a String
57 pub quoted_status_id_str: Option<String>,
58 /// Whether this is a quoted tweet or not
59 pub is_quote_status: bool,
60 /// When the tweet is a quote tweet, this contains the quoted tweet
61 pub quoted_status: Option<Box<Tweet>>,
62 /// When the tweet is a retweet, this contains the retweeted tweet
63 pub retweeted_status: Option<Box<Tweet>>,
64 /// Approximate count of times tweet was quoted
65 pub quote_count: Option<u32>,
66 /// How many times this tweet has been replied to
67 pub reply_count: u32,
68 /// How many times this tweet has been retweeted
69 pub retweet_count: u32,
70 /// How many times this tweet has been favorited
71 pub favorite_count: Option<u32>,
72 /// Entities that have been parsed from the tweet
73 pub entities: Option<Entity>,
74 /// If there are media entities, this field contains them all
75 pub extended_entities: Option<ExtendedEntity>,
76 /// Whether the authenticated user favorited this tweet
77 pub favorited: Option<bool>,
78 /// Whether the authenticated user retweeted this tweet
79 pub retweeted: bool,
80 /// Whether a link in this tweet (including media) is potentially sensitive
81 pub possibly_sensitive: Option<bool>,
82 /// What filter level is associated with this tweet. Can be none, low, or medium.
83 pub filter_level: String,
84 /// BCP 47 language identifier corresponding to machine-detected language of tweet
85 pub lang: Option<String>,
86 // matching_rules: Vec<Rule>,
87 // current_user_retweet:
88 // scopes
89 /// Indicates whether content was removed via DMCA
90 pub withheld_copyright: Option<bool>,
91 /// Indicates what countries this tweet is unavailable
92 pub withheld_in_countries: Option<Vec<String>>,
93 /// Indicates whether content is being withheld because of "status" or "user"
94 pub withheld_scope: Option<String>,
95
96 #[deprecated(since="0.2.0", note="Deprecated in the Twitter API, but kept here for completion.")]
97 pub contributors: Option<String>,
98 #[deprecated(since="0.2.0", note="Deprecated in the Twitter API, but kept here for completion.")]
99 pub display_text_range: Option<(u32, u32)>,
100 #[deprecated(since="0.2.0", note="Deprecated in the Twitter API, but kept here for completion. Use coordinates instead.")]
101 pub geo: Option<Coordinates>,
102 #[deprecated(since="0.2.0", note="Deprecated in the Twitter API, but kept here for completion.")]
103 pub quoted_status_permalink: Option<LegacyUrl>,
104 #[deprecated(since="0.2.0", note="Deprecated in the Twitter API, but kept here for completion. Used created_at instead.")]
105 pub timestamp_ms: Option<String>,
106}
107
108impl Tweet {
109 /// Determine whether Twitter thinks this post is sensitive or not.
110 /// For tweets that do not have this attribute, the default return
111 /// value is false.
112 pub fn is_sensitive(&self) -> bool {
113 self.possibly_sensitive.unwrap_or(false)
114 }
115
116 /// Determine whether this is a retweet or not.
117 pub fn is_retweet(&self) -> bool {
118 self.retweeted_status.is_some()
119 }
120
121 /// Determine whether the tweet is extended or not
122 pub fn is_extended(&self) -> bool {
123 self.extended_tweet.is_some()
124 }
125
126 /// Returns true when the tweet has a photo, gif, or video.
127 pub fn has_media(&self) -> bool {
128 if self.extended_entities.is_some() {
129 return true;
130 }
131
132 if let Some(ext) = &self.extended_tweet {
133 if ext.entities.media.is_some() {
134 return true;
135 }
136 }
137
138 if let Some(rt) = &self.retweeted_status {
139 if rt.has_media() {
140 return true;
141 }
142 }
143
144 return false;
145 }
146
147 /// If it's a retweet, the original tweet id is returned. Otherwise
148 /// the current tweet id is returned.
149 pub fn base_id(&self) -> u64 {
150 if let Some(rt) = &self.retweeted_status {
151 return rt.id;
152 }
153
154 return self.id;
155 }
156
157 /// Creates a direct URL to the status
158 pub fn url(&self) -> String {
159 format!("https://twitter.com/{}/status/{}", self.user.screen_name, self.id)
160 }
161
162 /// When a tweet is extended it can go over the 140 character limit.
163 /// In such cases, the tweet text field is truncated as noted by the
164 /// truncated flag. This method will check the extended tweet data
165 /// and return either it, the full retweet text if it was truncated
166 /// or the original text field depending on what's appropriate.
167 pub fn full_text(&self) -> String {
168 if let Some(ex) = &self.extended_tweet {
169 // If base tweet is extended, get that text
170 ex.full_text.clone()
171 } else if let Some(rt) = &self.retweeted_status {
172 // If this is a retweet, get its full text
173 rt.full_text().clone()
174 } else {
175 // Otherwise return base text
176 self.text.clone()
177 }
178 }
179
180 /// Gathers all media urls from the post into a `Vec`.
181 /// For videos and gifs this will always have a single
182 /// url, but for photos it can be up to 4 max.
183 pub fn media_urls(&self) -> Vec<String> {
184 use std::collections::HashSet;
185
186 // Sometimes the original tweet media and current tweet
187 // media are different. This combines them all into a
188 // set so that we don't miss anything.
189 let mut urls = if let Some(rt) = &self.retweeted_status {
190 rt.media_urls().iter().cloned().collect::<HashSet<_>>()
191 } else {
192 HashSet::new()
193 };
194
195 // Use extended entities to get media
196 if let Some(ent) = &self.extended_entities {
197 for media in &ent.media {
198 if let Some(url) = media.url() {
199 urls.insert(url);
200 }
201 }
202 }
203
204 // If this is an extended tweet, check its own media entries
205 if let Some(ext) = &self.extended_tweet {
206 if let Some(media_entries) = &ext.entities.media {
207 for media in media_entries {
208 if let Some(url) = media.url() {
209 urls.insert(url);
210 }
211 }
212 }
213 }
214
215 urls.iter().cloned().collect::<Vec<_>>()
216 }
217
218 /// Gets a list of hashtags associated with the tweet
219 pub fn hashtags(&self) -> Vec<String> {
220 match &self.entities {
221 Some(entities) => {
222 entities.hashtags
223 .iter()
224 .map(|ht| ht.text.clone())
225 .collect::<Vec<_>>()
226 }
227 None => Vec::new(),
228 }
229 }
230}
231
232/// Represents a full tweet text and entities when going over 140 characters
233#[derive(Debug, Deserialize, Serialize)]
234pub struct ExtendedTweet {
235 full_text: String,
236 display_text_range: (u32, u32),
237 entities: Entity,
238}