mastodon_api/
lib.rs

1//! # Mastodon API Wrapper
2//!
3//! A comprehensive, asynchronous Rust wrapper for the Mastodon API, designed for advanced bot development.
4//!
5//! ## Core Concepts
6//!
7//! ### 1. Timelines
8//! A **Timeline** is a chronological list of statuses (posts).
9//! - **Public Timeline**: Every public post known to your instance ("The Federated Timeline").
10//! - **Local Timeline**: Public posts from users specifically on your instance.
11//! - **Home Timeline**: Personalized feed from accounts you follow.
12//!
13//! ### 2. Statuses
14//! A **Status** is the technical name for a post. It contains text, media attachments, mentions, etc.
15//! Statuses can be regular posts, replies, or reblogs (boosts).
16//!
17//! ### 3. Streaming
18//! **Streaming** provides real-time updates via WebSockets. Instead of polling, the server
19//! "pushes" events (new posts, mentions) to you as they happen.
20//!
21//! ### 4. Accounts
22//! Represents a user. Identified by a local `id` and a unique `acct` string (`user@domain`).
23//!
24//! ### 5. Media Attachments
25//! Media must be uploaded separately to get an ID before being attached to a new status.
26//!
27//! # Example
28//! ```no_run
29//! use mastodon_api::MastodonClient;
30//!
31//! #[tokio::main]
32//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
33//!     let client = MastodonClient::new("https://mastodon.social").with_token("your_token");
34//!     client.statuses().create_simple("Hello from Rust!").await?;
35//!     Ok(())
36//! }
37//! ```
38
39pub 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/// The main entry point for interacting with the Mastodon API.
55///
56/// Use `MastodonClient::new` to create a new instance, and `with_token` to authenticate.
57#[derive(Clone)]
58pub struct MastodonClient {
59    base_url: String,
60    client: Client,
61    access_token: Option<String>,
62}
63
64impl MastodonClient {
65    /// Creates a new `MastodonClient` for the given instance URL.
66    ///
67    /// # Example
68    /// ```
69    /// use mastodon_api::MastodonClient;
70    /// let client = MastodonClient::new("https://mastodon.social");
71    /// ```
72    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    /// Sets the access token for the client, enabling authenticated requests.
81    pub fn with_token(mut self, token: &str) -> Self {
82        self.access_token = Some(token.to_string());
83        self
84    }
85
86    /// Returns the access token used by the client, if any.
87    pub fn access_token(&self) -> Option<&str> {
88        self.access_token.as_deref()
89    }
90
91    /// Returns the base URL of the Mastodon instance.
92    pub fn base_url(&self) -> &str {
93        &self.base_url
94    }
95
96    /// Returns a reference to the underlying HTTP client.
97    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            // Handle Rate Limiting
119            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            // Handle transient server errors (5xx)
133            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    /// Access account-related endpoints.
150    pub fn accounts(&self) -> methods::accounts::AccountsHandler<'_> {
151        methods::accounts::AccountsHandler::new(self)
152    }
153
154    /// Access follow request-related endpoints.
155    pub fn follow_requests(&self) -> methods::follow_requests::FollowRequestsHandler<'_> {
156        methods::follow_requests::FollowRequestsHandler::new(self)
157    }
158
159    /// Access server-wide announcements.
160    pub fn announcements(&self) -> methods::announcements::AnnouncementsHandler<'_> {
161        methods::announcements::AnnouncementsHandler::new(self)
162    }
163
164    /// Access status-related (posting) endpoints.
165    pub fn statuses(&self) -> methods::statuses::StatusesHandler<'_> {
166        methods::statuses::StatusesHandler::new(self)
167    }
168
169    /// Access timeline-related endpoints (Home, Public, etc.).
170    pub fn timelines(&self) -> methods::timelines::TimelinesHandler<'_> {
171        methods::timelines::TimelinesHandler::new(self)
172    }
173
174    /// Access application registration and OAuth endpoints.
175    pub fn apps(&self) -> methods::apps::AppsHandler<'_> {
176        methods::apps::AppsHandler::new(self)
177    }
178
179    /// Access media upload and management endpoints.
180    pub fn media(&self) -> methods::media::MediaHandler<'_> {
181        methods::media::MediaHandler::new(self)
182    }
183
184    /// Access content filter management endpoints.
185    pub fn filters(&self) -> methods::filters::FiltersHandler<'_> {
186        methods::filters::FiltersHandler::new(self)
187    }
188
189    /// Access tag management endpoints (followed and featured tags).
190    pub fn tags(&self) -> methods::tags::TagsHandler<'_> {
191        methods::tags::TagsHandler::new(self)
192    }
193
194    /// Access reading position sync (markers).
195    pub fn markers(&self) -> methods::markers::MarkersHandler<'_> {
196        methods::markers::MarkersHandler::new(self)
197    }
198
199    /// Access reports made by the authenticated user.
200    pub fn reports(&self) -> methods::reports::ReportsHandler<'_> {
201        methods::reports::ReportsHandler::new(self)
202    }
203
204    /// Access accounts endorsed (pinned) by the authenticated user.
205    pub fn endorsements(&self) -> methods::endorsements::EndorsementsHandler<'_> {
206        methods::endorsements::EndorsementsHandler::new(self)
207    }
208
209    /// Access domain blocks for the authenticated user.
210    pub fn domain_blocks(&self) -> methods::domain_blocks::DomainBlocksHandler<'_> {
211        methods::domain_blocks::DomainBlocksHandler::new(self)
212    }
213
214    /// Access user preferences.
215    pub fn preferences(&self) -> methods::preferences::PreferencesHandler<'_> {
216        methods::preferences::PreferencesHandler::new(self)
217    }
218
219    /// Access Web Push API subscriptions.
220    pub fn push(&self) -> methods::push::PushHandler<'_> {
221        methods::push::PushHandler::new(self)
222    }
223
224    /// Access administrative moderation and management endpoints.
225    pub fn admin(&self) -> methods::admin::AdminHandler<'_> {
226        methods::admin::AdminHandler::new(self)
227    }
228
229    /// Access list management endpoints.
230    pub fn lists(&self) -> methods::lists::ListsHandler<'_> {
231        methods::lists::ListsHandler::new(self)
232    }
233
234    /// Access direct conversation (DM) endpoints.
235    pub fn conversations(&self) -> methods::conversations::ConversationsHandler<'_> {
236        methods::conversations::ConversationsHandler::new(self)
237    }
238
239    /// Access notification management endpoints.
240    pub fn notifications(&self) -> methods::notifications::NotificationsHandler<'_> {
241        methods::notifications::NotificationsHandler::new(self)
242    }
243
244    /// Access global search endpoints.
245    pub fn search(&self) -> methods::search::SearchHandler<'_> {
246        methods::search::SearchHandler::new(self)
247    }
248
249    /// Access account suggestions for follow.
250    pub fn suggestions(&self) -> methods::suggestions::SuggestionsHandler<'_> {
251        methods::suggestions::SuggestionsHandler::new(self)
252    }
253
254    /// Access trending content endpoints.
255    pub fn trends(&self) -> methods::trends::TrendsHandler<'_> {
256        methods::trends::TrendsHandler::new(self)
257    }
258
259    /// Access custom emoji endpoints.
260    pub fn emojis(&self) -> methods::emojis::EmojisHandler<'_> {
261        methods::emojis::EmojisHandler::new(self)
262    }
263
264    /// Access instance metadata endpoints.
265    pub fn instance(&self) -> methods::instance::InstanceHandler<'_> {
266        methods::instance::InstanceHandler::new(self)
267    }
268
269    /// Create a new streaming client for real-time events.
270    pub fn streaming(&self) -> streaming::StreamingClient {
271        streaming::StreamingClient::new(&self.base_url, self.access_token.clone())
272    }
273}