1use crate::fields::{
2 media_fields::MediaFields, place_fields::PlaceFields, poll_fields::PollFields,
3 tweet_fields::TweetFields, user_fields::UserFields,
4};
5use crate::responses::{errors::Errors, includes::Includes, meta::Meta, tweets::Tweets};
6use crate::{
7 api::{Authentication, TwapiOptions, execute_twitter, make_url},
8 error::Error,
9 headers::Headers,
10};
11use chrono::prelude::*;
12use itertools::Itertools;
13use reqwest::RequestBuilder;
14use serde::{Deserialize, Serialize};
15use std::collections::HashSet;
16
17const URL: &str = "/2/users/:id/timelines/reverse_chronological";
18
19#[derive(Serialize, Deserialize, Debug, Eq, Hash, PartialEq, Clone)]
20#[derive(Default)]
21pub enum Exclude {
22 #[serde(rename = "retweets")]
23 #[default]
24 Retweets,
25 #[serde(rename = "replies")]
26 Replies,
27}
28
29impl Exclude {
30 pub fn all() -> HashSet<Self> {
31 let mut result = HashSet::new();
32 result.insert(Self::Retweets);
33 result.insert(Self::Replies);
34 result
35 }
36}
37
38impl std::fmt::Display for Exclude {
39 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
40 match self {
41 Self::Retweets => write!(f, "retweets"),
42 Self::Replies => write!(f, "replies"),
43 }
44 }
45}
46
47
48#[derive(Serialize, Deserialize, Debug, Eq, Hash, PartialEq, Clone)]
49#[derive(Default)]
50pub enum Expansions {
51 #[serde(rename = "article.cover_media")]
52 #[default]
53 ArticleCoverMedia,
54 #[serde(rename = "article.media_entities")]
55 ArticleMediaEntities,
56 #[serde(rename = "attachments.media_keys")]
57 AttachmentsMediaKeys,
58 #[serde(rename = "attachments.media_source_tweet")]
59 AttachmentsMediaSourceTweet,
60 #[serde(rename = "attachments.poll_ids")]
61 AttachmentsPollIds,
62 #[serde(rename = "author_id")]
63 AuthorId,
64 #[serde(rename = "edit_history_tweet_ids")]
65 EditHistoryTweetIds,
66 #[serde(rename = "entities.mentions.username")]
67 EntitiesMentionsUsername,
68 #[serde(rename = "geo.place_id")]
69 GeoPlaceId,
70 #[serde(rename = "in_reply_to_user_id")]
71 InReplyToUserId,
72 #[serde(rename = "entities.note.mentions.username")]
73 EntitiesNoteMentionsUsername,
74 #[serde(rename = "referenced_tweets.id")]
75 ReferencedTweetsId,
76 #[serde(rename = "referenced_tweets.id.author_id")]
77 ReferencedTweetsIdAuthorId,
78}
79
80impl Expansions {
81 pub fn all() -> HashSet<Self> {
82 let mut result = HashSet::new();
83 result.insert(Self::ArticleCoverMedia);
84 result.insert(Self::ArticleMediaEntities);
85 result.insert(Self::AttachmentsMediaKeys);
86 result.insert(Self::AttachmentsMediaSourceTweet);
87 result.insert(Self::AttachmentsPollIds);
88 result.insert(Self::AuthorId);
89 result.insert(Self::EditHistoryTweetIds);
90 result.insert(Self::EntitiesMentionsUsername);
91 result.insert(Self::GeoPlaceId);
92 result.insert(Self::InReplyToUserId);
93 result.insert(Self::EntitiesNoteMentionsUsername);
94 result.insert(Self::ReferencedTweetsId);
95 result.insert(Self::ReferencedTweetsIdAuthorId);
96 result
97 }
98}
99
100impl std::fmt::Display for Expansions {
101 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
102 match self {
103 Self::ArticleCoverMedia => write!(f, "article.cover_media"),
104 Self::ArticleMediaEntities => write!(f, "article.media_entities"),
105 Self::AttachmentsMediaKeys => write!(f, "attachments.media_keys"),
106 Self::AttachmentsMediaSourceTweet => write!(f, "attachments.media_source_tweet"),
107 Self::AttachmentsPollIds => write!(f, "attachments.poll_ids"),
108 Self::AuthorId => write!(f, "author_id"),
109 Self::EditHistoryTweetIds => write!(f, "edit_history_tweet_ids"),
110 Self::EntitiesMentionsUsername => write!(f, "entities.mentions.username"),
111 Self::GeoPlaceId => write!(f, "geo.place_id"),
112 Self::InReplyToUserId => write!(f, "in_reply_to_user_id"),
113 Self::EntitiesNoteMentionsUsername => write!(f, "entities.note.mentions.username"),
114 Self::ReferencedTweetsId => write!(f, "referenced_tweets.id"),
115 Self::ReferencedTweetsIdAuthorId => write!(f, "referenced_tweets.id.author_id"),
116 }
117 }
118}
119
120
121#[derive(Debug, Clone, Default)]
122pub struct Api {
123 id: String,
124 end_time: Option<DateTime<Utc>>,
125 exclude: Option<HashSet<Exclude>>,
126 expansions: Option<HashSet<Expansions>>,
127 max_results: Option<usize>,
128 media_fields: Option<HashSet<MediaFields>>,
129 pagination_token: Option<String>,
130 place_fields: Option<HashSet<PlaceFields>>,
131 poll_fields: Option<HashSet<PollFields>>,
132 since_id: Option<String>,
133 start_time: Option<DateTime<Utc>>,
134 tweet_fields: Option<HashSet<TweetFields>>,
135 until_id: Option<String>,
136 user_fields: Option<HashSet<UserFields>>,
137 twapi_options: Option<TwapiOptions>,
138}
139
140impl Api {
141 pub fn new(id: &str) -> Self {
142 Self {
143 id: id.to_owned(),
144 ..Default::default()
145 }
146 }
147
148 pub fn all(id: &str) -> Self {
149 Self {
150 id: id.to_owned(),
151 exclude: Some(Exclude::all()),
152 expansions: Some(Expansions::all()),
153 media_fields: Some(MediaFields::all()),
154 place_fields: Some(PlaceFields::all()),
155 poll_fields: Some(PollFields::all()),
156 tweet_fields: Some(TweetFields::organic()),
157 user_fields: Some(UserFields::all()),
158 max_results: Some(100),
159 ..Default::default()
160 }
161 }
162
163 pub fn open(id: &str) -> Self {
164 Self {
165 id: id.to_owned(),
166 exclude: Some(Exclude::all()),
167 expansions: Some(Expansions::all()),
168 media_fields: Some(MediaFields::open()),
169 place_fields: Some(PlaceFields::all()),
170 poll_fields: Some(PollFields::all()),
171 tweet_fields: Some(TweetFields::open()),
172 user_fields: Some(UserFields::all()),
173 max_results: Some(100),
174 ..Default::default()
175 }
176 }
177
178 pub fn end_time(mut self, value: DateTime<Utc>) -> Self {
179 self.end_time = Some(value);
180 self
181 }
182
183 pub fn exclude(mut self, value: HashSet<Exclude>) -> Self {
184 self.exclude = Some(value);
185 self
186 }
187
188 pub fn expansions(mut self, value: HashSet<Expansions>) -> Self {
189 self.expansions = Some(value);
190 self
191 }
192
193 pub fn max_results(mut self, value: usize) -> Self {
194 self.max_results = Some(value);
195 self
196 }
197
198 pub fn media_fields(mut self, value: HashSet<MediaFields>) -> Self {
199 self.media_fields = Some(value);
200 self
201 }
202
203 pub fn pagination_token(mut self, value: &str) -> Self {
204 self.pagination_token = Some(value.to_owned());
205 self
206 }
207
208 pub fn place_fields(mut self, value: HashSet<PlaceFields>) -> Self {
209 self.place_fields = Some(value);
210 self
211 }
212
213 pub fn poll_fields(mut self, value: HashSet<PollFields>) -> Self {
214 self.poll_fields = Some(value);
215 self
216 }
217
218 pub fn since_id(mut self, value: &str) -> Self {
219 self.since_id = Some(value.to_owned());
220 self
221 }
222
223 pub fn start_time(mut self, value: DateTime<Utc>) -> Self {
224 self.start_time = Some(value);
225 self
226 }
227
228 pub fn tweet_fields(mut self, value: HashSet<TweetFields>) -> Self {
229 self.tweet_fields = Some(value);
230 self
231 }
232
233 pub fn until_id(mut self, value: &str) -> Self {
234 self.until_id = Some(value.to_owned());
235 self
236 }
237
238 pub fn user_fields(mut self, value: HashSet<UserFields>) -> Self {
239 self.user_fields = Some(value);
240 self
241 }
242
243 pub fn twapi_options(mut self, value: TwapiOptions) -> Self {
244 self.twapi_options = Some(value);
245 self
246 }
247
248 pub fn build(&self, authentication: &impl Authentication) -> RequestBuilder {
249 let mut query_parameters = vec![];
250 if let Some(end_time) = self.end_time.as_ref() {
251 query_parameters.push((
252 "end_time",
253 end_time.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
254 ));
255 }
256 if let Some(exclude) = self.exclude.as_ref() {
257 query_parameters.push(("exclude", exclude.iter().join(",")));
258 }
259 if let Some(expansions) = self.expansions.as_ref() {
260 query_parameters.push(("expansions", expansions.iter().join(",")));
261 }
262 if let Some(max_results) = self.max_results.as_ref() {
263 query_parameters.push(("max_results", max_results.to_string()));
264 }
265 if let Some(media_fields) = self.media_fields.as_ref() {
266 query_parameters.push(("media.fields", media_fields.iter().join(",")));
267 }
268 if let Some(pagination_token) = self.pagination_token.as_ref() {
269 query_parameters.push(("pagination_token", pagination_token.to_string()));
270 }
271 if let Some(place_fields) = self.place_fields.as_ref() {
272 query_parameters.push(("place.fields", place_fields.iter().join(",")));
273 }
274 if let Some(poll_fields) = self.poll_fields.as_ref() {
275 query_parameters.push(("poll.fields", poll_fields.iter().join(",")));
276 }
277 if let Some(since_id) = self.since_id.as_ref() {
278 query_parameters.push(("since_id", since_id.to_string()));
279 }
280 if let Some(start_time) = self.start_time.as_ref() {
281 query_parameters.push((
282 "start_time",
283 start_time.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
284 ));
285 }
286 if let Some(tweet_fields) = self.tweet_fields.as_ref() {
287 query_parameters.push(("tweet.fields", tweet_fields.iter().join(",")));
288 }
289 if let Some(until_id) = self.until_id.as_ref() {
290 query_parameters.push(("until_id", until_id.to_string()));
291 }
292 if let Some(user_fields) = self.user_fields.as_ref() {
293 query_parameters.push(("user.fields", user_fields.iter().join(",")));
294 }
295 let client = reqwest::Client::new();
296 let url = make_url(&self.twapi_options, &URL.replace(":id", &self.id));
297 let builder = client.get(&url).query(&query_parameters);
298 authentication.execute(
299 builder,
300 "GET",
301 &url,
302 &query_parameters
303 .iter()
304 .map(|it| (it.0, it.1.as_str()))
305 .collect::<Vec<_>>(),
306 )
307 }
308
309 pub async fn execute(
310 &self,
311 authentication: &impl Authentication,
312 ) -> Result<(Response, Headers), Error> {
313 execute_twitter(|| self.build(authentication), &self.twapi_options).await
314 }
315}
316
317#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
318pub struct Response {
319 #[serde(skip_serializing_if = "Option::is_none")]
320 pub data: Option<Vec<Tweets>>,
321 #[serde(skip_serializing_if = "Option::is_none")]
322 pub errors: Option<Vec<Errors>>,
323 #[serde(skip_serializing_if = "Option::is_none")]
324 pub includes: Option<Includes>,
325 #[serde(skip_serializing_if = "Option::is_none")]
326 pub meta: Option<Meta>,
327 #[serde(flatten)]
328 pub extra: std::collections::HashMap<String, serde_json::Value>,
329}
330
331impl Response {
332 pub fn is_empty_extra(&self) -> bool {
333 let res = self.extra.is_empty()
334 && self
335 .data
336 .as_ref()
337 .map(|it| it.iter().all(|item| item.is_empty_extra()))
338 .unwrap_or(true)
339 && self
340 .errors
341 .as_ref()
342 .map(|it| it.iter().all(|item| item.is_empty_extra()))
343 .unwrap_or(true)
344 && self
345 .includes
346 .as_ref()
347 .map(|it| it.is_empty_extra())
348 .unwrap_or(true)
349 && self
350 .meta
351 .as_ref()
352 .map(|it| it.is_empty_extra())
353 .unwrap_or(true);
354 if !res {
355 println!("Response {:?}", self.extra);
356 }
357 res
358 }
359}