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}