1use chrono::{DateTime, Utc};
2use reqwest::Client;
3use std::sync::Arc;
4use url::Url;
5
6use thiserror::Error;
7
8use crate::{
9 consts::BASE_URL,
10 model::{BaseResponse, Metric, Platform, Post, Tracking, User, UserTrackings},
11};
12
13#[derive(Error, Debug)]
14pub enum XTrackerError {
15 #[error("URL parsing failed: {0}")]
16 Url(#[from] url::ParseError),
17 #[error("HTTP request failed: {0}")]
18 Request(#[from] reqwest::Error),
19 #[error("Server returned an error: {0}")]
20 Server(String),
21}
22
23#[derive(Debug, Clone)]
24pub struct XTrackerClient(Arc<ClientInner>);
25
26#[derive(Debug)]
27struct ClientInner {
28 base_url: Url,
29 client: Client,
30}
31
32impl Default for XTrackerClient {
33 fn default() -> Self {
34 Self::new()
35 }
36}
37
38impl XTrackerClient {
39 pub fn new() -> Self {
40 Self(Arc::new(ClientInner {
41 base_url: Url::parse(BASE_URL).unwrap(),
42 client: Client::new(),
43 }))
44 }
45
46 pub fn new_with_url(base_url: impl Into<Url>) -> Self {
47 Self(Arc::new(ClientInner {
48 base_url: base_url.into(),
49 client: Client::new(),
50 }))
51 }
52}
53
54impl XTrackerClient {
55 pub async fn get_users(
56 &self,
57 platform: impl Into<Option<Platform>>,
58 active_only: bool,
59 ) -> Result<Vec<User>, XTrackerError> {
60 let mut url = self.0.base_url.join("users")?;
61
62 {
63 let mut query = url.query_pairs_mut();
64
65 if let Some(platform) = platform.into() {
66 query.append_pair("platform", &platform.to_string());
67 }
68
69 query.append_pair("includeInactive", &(!active_only).to_string());
70 }
71
72 let request = self.0.client.get(url);
73
74 let response = request
75 .send()
76 .await?
77 .error_for_status()?
78 .json::<BaseResponse<Vec<User>>>()
79 .await?
80 .into_result()?;
81
82 Ok(response)
83 }
84
85 pub async fn get_user_by_handle(
86 &self,
87 handle: &str,
88 platform: impl Into<Option<Platform>>,
89 ) -> Result<User, XTrackerError> {
90 let mut url = self.0.base_url.join(&format!("users/{handle}"))?;
91
92 if let Some(platform) = platform.into() {
93 url.set_query(Some(&format!("platform={}", &platform.to_string())));
94 }
95
96 let request = self.0.client.get(url);
97
98 let response = request
99 .send()
100 .await?
101 .error_for_status()?
102 .json::<BaseResponse<User>>()
103 .await?
104 .into_result()?;
105
106 Ok(response)
107 }
108}
109
110impl XTrackerClient {
111 pub async fn get_posts_by_handle(
112 &self,
113 handle: &str,
114 platform: impl Into<Option<Platform>>,
115 start: impl Into<Option<DateTime<Utc>>>,
116 end: impl Into<Option<DateTime<Utc>>>,
117 ) -> Result<Vec<Post>, XTrackerError> {
118 let mut url = self.0.base_url.join(&format!("users/{handle}/posts"))?;
119
120 {
121 let mut query = url.query_pairs_mut();
122
123 if let Some(platform) = platform.into() {
124 query.append_pair("platform", &platform.to_string());
125 }
126
127 if let Some(start) = start.into() {
128 query.append_pair("startDate", &start.to_rfc3339());
129 }
130
131 if let Some(end) = end.into() {
132 query.append_pair("endDate", &end.to_rfc3339());
133 }
134 }
135 let request = self.0.client.get(url);
136
137 let response = request
138 .send()
139 .await?
140 .error_for_status()?
141 .json::<BaseResponse<Vec<Post>>>()
142 .await?
143 .into_result()?;
144
145 Ok(response)
146 }
147}
148
149impl XTrackerClient {
150 pub async fn get_trackings(&self, active_only: bool) -> Result<Vec<Tracking>, XTrackerError> {
151 let mut url = self.0.base_url.join("trackings")?;
152
153 url.set_query(Some(&format!("activeOnly={active_only}")));
154
155 let request = self.0.client.get(url);
156
157 let response = request
158 .send()
159 .await?
160 .error_for_status()?
161 .json::<BaseResponse<Vec<Tracking>>>()
162 .await?
163 .into_result()?;
164
165 Ok(response)
166 }
167
168 pub async fn get_tracking_by_id(
169 &self,
170 tracking_id: &str,
171 include_stats: bool,
172 ) -> Result<Tracking, XTrackerError> {
173 let mut url = self.0.base_url.join(&format!("trackings/{tracking_id}"))?;
174
175 url.set_query(Some(&format!("includeStats={include_stats}")));
176
177 let request = self.0.client.get(url);
178
179 let response = request
180 .send()
181 .await?
182 .error_for_status()?
183 .json::<BaseResponse<Tracking>>()
184 .await?
185 .into_result()?;
186
187 Ok(response)
188 }
189
190 pub async fn get_trackings_by_handle(
191 &self,
192 handle: &str,
193 platform: impl Into<Option<Platform>>,
194 active_only: bool,
195 ) -> Result<Vec<UserTrackings>, XTrackerError> {
196 let mut url = self.0.base_url.join(&format!("users/{handle}/trackings"))?;
197
198 {
199 let mut query = url.query_pairs_mut();
200
201 if let Some(platform) = platform.into() {
202 query.append_pair("platform", &platform.to_string());
203 }
204
205 query.append_pair("includeInactive", &(!active_only).to_string());
206 }
207
208 let request = self.0.client.get(url);
209
210 let response = request
211 .send()
212 .await?
213 .error_for_status()?
214 .json::<BaseResponse<Vec<UserTrackings>>>()
215 .await?
216 .into_result()?;
217
218 Ok(response)
219 }
220}
221
222impl XTrackerClient {
223 pub async fn get_metrics_by_user(
224 &self,
225 user_id: &str,
226 start: impl Into<Option<DateTime<Utc>>>,
227 end: impl Into<Option<DateTime<Utc>>>,
228 ) -> Result<Vec<Metric>, XTrackerError> {
229 let mut url = self.0.base_url.join(&format!("metrics/{user_id}"))?;
230
231 {
232 let mut query = url.query_pairs_mut();
233
234 if let Some(start) = start.into() {
235 query.append_pair("startDate", &start.to_rfc3339());
236 }
237
238 if let Some(end) = end.into() {
239 query.append_pair("endDate", &end.to_rfc3339());
240 }
241 }
242
243 let request = self.0.client.get(url);
244
245 let response = request
246 .send()
247 .await?
248 .error_for_status()?
249 .json::<BaseResponse<Vec<Metric>>>()
250 .await?
251 .into_result()?;
252
253 Ok(response)
254 }
255}