1pub mod error;
40pub mod methods;
41pub mod models;
42pub mod paging;
43pub mod streaming;
44
45pub use error::{MastodonError, Result};
46pub use models::{
47 Account, Announcement, AnnouncementReaction, FeaturedTag, Marker, Preferences, Relationship,
48 Report, Status, Suggestion, Tag, WebPushAlerts, WebPushSubscription,
49};
50
51use reqwest::{Client, RequestBuilder};
52use serde::de::DeserializeOwned;
53
54#[derive(Clone)]
58pub struct MastodonClient {
59 base_url: String,
60 client: Client,
61 access_token: Option<String>,
62}
63
64impl MastodonClient {
65 pub fn new(instance_url: &str) -> Self {
73 Self {
74 base_url: instance_url.trim_end_matches('/').to_string(),
75 client: Client::new(),
76 access_token: None,
77 }
78 }
79
80 pub fn with_token(mut self, token: &str) -> Self {
82 self.access_token = Some(token.to_string());
83 self
84 }
85
86 pub fn access_token(&self) -> Option<&str> {
88 self.access_token.as_deref()
89 }
90
91 pub fn base_url(&self) -> &str {
93 &self.base_url
94 }
95
96 pub fn http_client(&self) -> &Client {
98 &self.client
99 }
100
101 pub(crate) async fn send<T: DeserializeOwned>(&self, builder: RequestBuilder) -> Result<T> {
102 let mut retries = 0;
103 let max_retries = 3;
104
105 loop {
106 let mut current_builder = builder.try_clone().ok_or(MastodonError::ApiError {
107 status: reqwest::StatusCode::INTERNAL_SERVER_ERROR,
108 message: "Failed to clone request for retry".to_string(),
109 })?;
110
111 if let Some(token) = &self.access_token {
112 current_builder = current_builder.bearer_auth(token);
113 }
114
115 let response = current_builder.send().await?;
116 let status = response.status();
117
118 if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
120 if retries < max_retries {
121 if let Some(reset) = response.headers().get("X-RateLimit-Reset") {
122 if let Ok(_) = reset.to_str() {
123 let wait_secs = 2u64.pow(retries);
124 tokio::time::sleep(std::time::Duration::from_secs(wait_secs)).await;
125 retries += 1;
126 continue;
127 }
128 }
129 }
130 }
131
132 if status.is_server_error() && retries < max_retries {
134 let wait_secs = 2u64.pow(retries);
135 tokio::time::sleep(std::time::Duration::from_secs(wait_secs)).await;
136 retries += 1;
137 continue;
138 }
139
140 if !status.is_success() {
141 let message = response.text().await.unwrap_or_default();
142 return Err(MastodonError::ApiError { status, message });
143 }
144
145 return Ok(response.json().await?);
146 }
147 }
148
149 pub fn accounts(&self) -> methods::accounts::AccountsHandler<'_> {
151 methods::accounts::AccountsHandler::new(self)
152 }
153
154 pub fn follow_requests(&self) -> methods::follow_requests::FollowRequestsHandler<'_> {
156 methods::follow_requests::FollowRequestsHandler::new(self)
157 }
158
159 pub fn announcements(&self) -> methods::announcements::AnnouncementsHandler<'_> {
161 methods::announcements::AnnouncementsHandler::new(self)
162 }
163
164 pub fn statuses(&self) -> methods::statuses::StatusesHandler<'_> {
166 methods::statuses::StatusesHandler::new(self)
167 }
168
169 pub fn timelines(&self) -> methods::timelines::TimelinesHandler<'_> {
171 methods::timelines::TimelinesHandler::new(self)
172 }
173
174 pub fn apps(&self) -> methods::apps::AppsHandler<'_> {
176 methods::apps::AppsHandler::new(self)
177 }
178
179 pub fn media(&self) -> methods::media::MediaHandler<'_> {
181 methods::media::MediaHandler::new(self)
182 }
183
184 pub fn filters(&self) -> methods::filters::FiltersHandler<'_> {
186 methods::filters::FiltersHandler::new(self)
187 }
188
189 pub fn tags(&self) -> methods::tags::TagsHandler<'_> {
191 methods::tags::TagsHandler::new(self)
192 }
193
194 pub fn markers(&self) -> methods::markers::MarkersHandler<'_> {
196 methods::markers::MarkersHandler::new(self)
197 }
198
199 pub fn reports(&self) -> methods::reports::ReportsHandler<'_> {
201 methods::reports::ReportsHandler::new(self)
202 }
203
204 pub fn endorsements(&self) -> methods::endorsements::EndorsementsHandler<'_> {
206 methods::endorsements::EndorsementsHandler::new(self)
207 }
208
209 pub fn domain_blocks(&self) -> methods::domain_blocks::DomainBlocksHandler<'_> {
211 methods::domain_blocks::DomainBlocksHandler::new(self)
212 }
213
214 pub fn preferences(&self) -> methods::preferences::PreferencesHandler<'_> {
216 methods::preferences::PreferencesHandler::new(self)
217 }
218
219 pub fn push(&self) -> methods::push::PushHandler<'_> {
221 methods::push::PushHandler::new(self)
222 }
223
224 pub fn admin(&self) -> methods::admin::AdminHandler<'_> {
226 methods::admin::AdminHandler::new(self)
227 }
228
229 pub fn lists(&self) -> methods::lists::ListsHandler<'_> {
231 methods::lists::ListsHandler::new(self)
232 }
233
234 pub fn conversations(&self) -> methods::conversations::ConversationsHandler<'_> {
236 methods::conversations::ConversationsHandler::new(self)
237 }
238
239 pub fn notifications(&self) -> methods::notifications::NotificationsHandler<'_> {
241 methods::notifications::NotificationsHandler::new(self)
242 }
243
244 pub fn search(&self) -> methods::search::SearchHandler<'_> {
246 methods::search::SearchHandler::new(self)
247 }
248
249 pub fn suggestions(&self) -> methods::suggestions::SuggestionsHandler<'_> {
251 methods::suggestions::SuggestionsHandler::new(self)
252 }
253
254 pub fn trends(&self) -> methods::trends::TrendsHandler<'_> {
256 methods::trends::TrendsHandler::new(self)
257 }
258
259 pub fn emojis(&self) -> methods::emojis::EmojisHandler<'_> {
261 methods::emojis::EmojisHandler::new(self)
262 }
263
264 pub fn instance(&self) -> methods::instance::InstanceHandler<'_> {
266 methods::instance::InstanceHandler::new(self)
267 }
268
269 pub fn streaming(&self) -> streaming::StreamingClient {
271 streaming::StreamingClient::new(&self.base_url, self.access_token.clone())
272 }
273}