1use crate::api::TwitterApi;
2use crate::api_result::ApiResult;
3use crate::authorization::Authorization;
4use crate::data::{ReplySettings, Tweet};
5use crate::id::{IntoNumericId, IntoStringId, StringId};
6use reqwest::Method;
7use serde::{Deserialize, Serialize};
8use std::time::Duration;
9use url::Url;
10
11#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
12struct DraftTweetGeo {
13 pub place_id: StringId,
14}
15
16#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
17struct DraftTweetMedia {
18 pub media_ids: Vec<String>,
19 pub tagged_user_ids: Vec<String>,
20}
21
22#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
23struct DraftTweetPoll {
24 pub options: Vec<String>,
25 pub duration_minutes: u64,
26}
27
28#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
29struct DraftTweetReply {
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub exclude_reply_user_ids: Option<Vec<String>>,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 pub in_reply_to_tweet_id: Option<String>,
34}
35
36#[derive(Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)]
37struct DraftTweet {
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub direct_message_deep_link: Option<String>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub for_super_followers_only: Option<bool>,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub geo: Option<DraftTweetGeo>,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub media: Option<DraftTweetMedia>,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub poll: Option<DraftTweetPoll>,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub quote_tweet_id: Option<String>,
50 #[serde(skip_serializing_if = "Option::is_none")]
51 pub reply: Option<DraftTweetReply>,
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub reply_settings: Option<ReplySettings>,
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub text: Option<String>,
56}
57
58#[derive(Debug)]
59pub struct TweetBuilder<A> {
60 client: TwitterApi<A>,
61 url: Url,
62 tweet: DraftTweet,
63}
64
65impl<A> TweetBuilder<A>
66where
67 A: Authorization,
68{
69 pub(crate) fn new(client: &TwitterApi<A>, url: Url) -> Self {
70 Self {
71 client: client.clone(),
72 url,
73 tweet: Default::default(),
74 }
75 }
76 pub fn text(&mut self, text: String) -> &mut Self {
77 self.tweet.text = Some(text);
78 self
79 }
80 pub fn direct_message_deep_link(&mut self, direct_message_deep_link: String) -> &mut Self {
81 self.tweet.direct_message_deep_link = Some(direct_message_deep_link);
82 self
83 }
84 pub fn for_super_followers_only(&mut self, for_super_followers_only: bool) -> &mut Self {
85 self.tweet.for_super_followers_only = Some(for_super_followers_only);
86 self
87 }
88 pub fn place_id(&mut self, place_id: impl IntoStringId) -> &mut Self {
89 if let Some(geo) = self.tweet.geo.as_mut() {
90 geo.place_id = place_id.into_id();
91 } else {
92 self.tweet.geo = Some(DraftTweetGeo {
93 place_id: place_id.into_id(),
94 });
95 }
96 self
97 }
98 pub fn add_media(
99 &mut self,
100 media_ids: impl IntoIterator<Item = impl IntoNumericId>,
101 tagged_user_ids: impl IntoIterator<Item = impl IntoNumericId>,
102 ) -> &mut Self {
103 if let Some(media) = self.tweet.media.as_mut() {
104 media
105 .media_ids
106 .extend(media_ids.into_iter().map(|id| id.to_string()));
107 media
108 .tagged_user_ids
109 .extend(tagged_user_ids.into_iter().map(|id| id.to_string()));
110 } else {
111 self.tweet.media = Some(DraftTweetMedia {
112 media_ids: media_ids.into_iter().map(|id| id.to_string()).collect(),
113 tagged_user_ids: tagged_user_ids
114 .into_iter()
115 .map(|id| id.to_string())
116 .collect(),
117 });
118 }
119 self
120 }
121 pub fn poll(
122 &mut self,
123 options: impl IntoIterator<Item = impl ToString>,
124 duration: Duration,
125 ) -> &mut Self {
126 self.tweet.poll = Some(DraftTweetPoll {
127 options: options
128 .into_iter()
129 .map(|option| option.to_string())
130 .collect::<Vec<_>>(),
131 duration_minutes: duration.as_secs() / 60,
132 });
133 self
134 }
135 pub fn quote_tweet_id(&mut self, id: impl IntoNumericId) -> &mut Self {
136 self.tweet.quote_tweet_id = Some(id.to_string());
137 self
138 }
139 pub fn add_exclude_reply_user_id(&mut self, user_id: impl IntoNumericId) -> &mut Self {
140 self.add_exclude_reply_user_ids([user_id])
141 }
142 pub fn add_exclude_reply_user_ids(
143 &mut self,
144 user_ids: impl IntoIterator<Item = impl IntoNumericId>,
145 ) -> &mut Self {
146 let mut user_ids = user_ids
147 .into_iter()
148 .map(|id| id.to_string())
149 .collect::<Vec<_>>();
150 if let Some(reply) = self.tweet.reply.as_mut() {
151 if let Some(exclude_reply_user_ids) = reply.exclude_reply_user_ids.as_mut() {
152 exclude_reply_user_ids.append(&mut user_ids)
153 } else {
154 reply.exclude_reply_user_ids = Some(user_ids);
155 }
156 } else {
157 self.tweet.reply = Some(DraftTweetReply {
158 exclude_reply_user_ids: Some(user_ids),
159 in_reply_to_tweet_id: None,
160 });
161 }
162 self
163 }
164 pub fn in_reply_to_tweet_id(&mut self, tweet_id: impl IntoNumericId) -> &mut Self {
165 if let Some(reply) = self.tweet.reply.as_mut() {
166 reply.in_reply_to_tweet_id = Some(tweet_id.to_string());
167 } else {
168 self.tweet.reply = Some(DraftTweetReply {
169 exclude_reply_user_ids: None,
170 in_reply_to_tweet_id: Some(tweet_id.to_string()),
171 });
172 }
173 self
174 }
175 pub fn reply_settings(&mut self, reply_settings: ReplySettings) -> &mut Self {
176 self.tweet.reply_settings = Some(reply_settings);
177 self
178 }
179 pub async fn send(&self) -> ApiResult<A, Tweet, ()> {
180 self.client
181 .send(
182 self.client
183 .request(Method::POST, self.url.clone())
184 .json(&self.tweet),
185 )
186 .await
187 }
188}
189
190impl<A> Clone for TweetBuilder<A> {
191 fn clone(&self) -> Self {
192 Self {
193 client: self.client.clone(),
194 url: self.url.clone(),
195 tweet: self.tweet.clone(),
196 }
197 }
198}