tuitbot_core/automation/adapters/
x_api.rs1use std::sync::Arc;
4
5use super::super::analytics_loop::{AnalyticsError, EngagementFetcher, ProfileFetcher};
6use super::super::loop_helpers::{
7 ContentLoopError, LoopError, LoopTweet, MentionsFetcher, ThreadPoster, TweetSearcher,
8};
9use super::super::posting_queue::PostExecutor;
10use super::super::target_loop::{TargetTweetFetcher, TargetUserManager};
11use super::helpers::{
12 search_response_to_loop_tweets, toolkit_to_analytics_error, toolkit_to_content_error,
13 toolkit_to_loop_error,
14};
15use crate::x_api::XApiClient;
16
17pub struct XApiSearchAdapter {
19 client: Arc<dyn XApiClient>,
20}
21
22impl XApiSearchAdapter {
23 pub fn new(client: Arc<dyn XApiClient>) -> Self {
24 Self { client }
25 }
26}
27
28#[async_trait::async_trait]
29impl TweetSearcher for XApiSearchAdapter {
30 async fn search_tweets(&self, query: &str) -> Result<Vec<LoopTweet>, LoopError> {
31 let response = crate::toolkit::read::search_tweets(&*self.client, query, 20, None, None)
32 .await
33 .map_err(toolkit_to_loop_error)?;
34 Ok(search_response_to_loop_tweets(response))
35 }
36}
37
38pub struct XApiMentionsAdapter {
40 client: Arc<dyn XApiClient>,
41 own_user_id: String,
42}
43
44impl XApiMentionsAdapter {
45 pub fn new(client: Arc<dyn XApiClient>, own_user_id: String) -> Self {
46 Self {
47 client,
48 own_user_id,
49 }
50 }
51}
52
53#[async_trait::async_trait]
54impl MentionsFetcher for XApiMentionsAdapter {
55 async fn get_mentions(&self, since_id: Option<&str>) -> Result<Vec<LoopTweet>, LoopError> {
56 let response =
57 crate::toolkit::read::get_mentions(&*self.client, &self.own_user_id, since_id, None)
58 .await
59 .map_err(toolkit_to_loop_error)?;
60 Ok(search_response_to_loop_tweets(response))
61 }
62}
63
64pub struct XApiTargetAdapter {
66 client: Arc<dyn XApiClient>,
67}
68
69impl XApiTargetAdapter {
70 pub fn new(client: Arc<dyn XApiClient>) -> Self {
71 Self { client }
72 }
73}
74
75#[async_trait::async_trait]
76impl TargetTweetFetcher for XApiTargetAdapter {
77 async fn fetch_user_tweets(&self, user_id: &str) -> Result<Vec<LoopTweet>, LoopError> {
78 let response = crate::toolkit::read::get_user_tweets(&*self.client, user_id, 10, None)
79 .await
80 .map_err(toolkit_to_loop_error)?;
81 Ok(search_response_to_loop_tweets(response))
82 }
83}
84
85#[async_trait::async_trait]
86impl TargetUserManager for XApiTargetAdapter {
87 async fn lookup_user(&self, username: &str) -> Result<(String, String), LoopError> {
88 let user = crate::toolkit::read::get_user_by_username(&*self.client, username)
89 .await
90 .map_err(toolkit_to_loop_error)?;
91 Ok((user.id, user.username))
92 }
93}
94
95pub struct XApiProfileAdapter {
97 client: Arc<dyn XApiClient>,
98}
99
100impl XApiProfileAdapter {
101 pub fn new(client: Arc<dyn XApiClient>) -> Self {
102 Self { client }
103 }
104}
105
106#[async_trait::async_trait]
107impl ProfileFetcher for XApiProfileAdapter {
108 async fn get_profile_metrics(
109 &self,
110 ) -> Result<super::super::analytics_loop::ProfileMetrics, AnalyticsError> {
111 let user = crate::toolkit::read::get_me(&*self.client)
112 .await
113 .map_err(toolkit_to_analytics_error)?;
114 Ok(super::super::analytics_loop::ProfileMetrics {
115 follower_count: user.public_metrics.followers_count as i64,
116 following_count: user.public_metrics.following_count as i64,
117 tweet_count: user.public_metrics.tweet_count as i64,
118 })
119 }
120}
121
122#[async_trait::async_trait]
123impl EngagementFetcher for XApiProfileAdapter {
124 async fn get_tweet_metrics(
125 &self,
126 tweet_id: &str,
127 ) -> Result<super::super::analytics_loop::TweetMetrics, AnalyticsError> {
128 let tweet = crate::toolkit::read::get_tweet(&*self.client, tweet_id)
129 .await
130 .map_err(toolkit_to_analytics_error)?;
131 Ok(super::super::analytics_loop::TweetMetrics {
132 likes: tweet.public_metrics.like_count as i64,
133 retweets: tweet.public_metrics.retweet_count as i64,
134 replies: tweet.public_metrics.reply_count as i64,
135 impressions: tweet.public_metrics.impression_count as i64,
136 })
137 }
138}
139
140pub struct XApiPostExecutorAdapter {
142 client: Arc<dyn XApiClient>,
143}
144
145impl XApiPostExecutorAdapter {
146 pub fn new(client: Arc<dyn XApiClient>) -> Self {
147 Self { client }
148 }
149}
150
151#[async_trait::async_trait]
152impl PostExecutor for XApiPostExecutorAdapter {
153 async fn execute_reply(
154 &self,
155 tweet_id: &str,
156 content: &str,
157 media_ids: &[String],
158 ) -> Result<String, String> {
159 let media = if media_ids.is_empty() {
160 None
161 } else {
162 Some(media_ids)
163 };
164 crate::toolkit::write::reply_to_tweet(&*self.client, content, tweet_id, media)
165 .await
166 .map(|posted| posted.id)
167 .map_err(|e| e.to_string())
168 }
169
170 async fn execute_tweet(&self, content: &str, media_ids: &[String]) -> Result<String, String> {
171 let media = if media_ids.is_empty() {
172 None
173 } else {
174 Some(media_ids)
175 };
176 crate::toolkit::write::post_tweet(&*self.client, content, media)
177 .await
178 .map(|posted| posted.id)
179 .map_err(|e| e.to_string())
180 }
181}
182
183pub struct XApiThreadPosterAdapter {
185 client: Arc<dyn XApiClient>,
186}
187
188impl XApiThreadPosterAdapter {
189 pub fn new(client: Arc<dyn XApiClient>) -> Self {
190 Self { client }
191 }
192}
193
194#[async_trait::async_trait]
195impl ThreadPoster for XApiThreadPosterAdapter {
196 async fn post_tweet(&self, content: &str) -> Result<String, ContentLoopError> {
197 crate::toolkit::write::post_tweet(&*self.client, content, None)
198 .await
199 .map(|posted| posted.id)
200 .map_err(toolkit_to_content_error)
201 }
202
203 async fn reply_to_tweet(
204 &self,
205 in_reply_to: &str,
206 content: &str,
207 ) -> Result<String, ContentLoopError> {
208 crate::toolkit::write::reply_to_tweet(&*self.client, content, in_reply_to, None)
209 .await
210 .map(|posted| posted.id)
211 .map_err(toolkit_to_content_error)
212 }
213}