Skip to main content

elefren/
lib.rs

1//! # Elefren: API Wrapper around the Mastodon API.
2//!
3//! Most of the api is documented on [Mastodon's website](https://docs.joinmastodon.org/client/intro/)
4//!
5//! ```no_run
6//! # extern crate elefren;
7//! # fn main() {
8//! #    try().unwrap();
9//! # }
10//! # fn try() -> elefren::Result<()> {
11//! use elefren::{helpers::cli, prelude::*};
12//!
13//! let registration = Registration::new("https://mastodon.social")
14//!     .client_name("elefren_test")
15//!     .build()?;
16//! let mastodon = cli::authenticate(registration)?;
17//!
18//! println!(
19//!     "{:?}",
20//!     mastodon
21//!         .get_home_timeline()?
22//!         .items_iter()
23//!         .take(100)
24//!         .collect::<Vec<_>>()
25//! );
26//! # Ok(())
27//! # }
28//! ```
29//!
30//! Elefren also supports Mastodon's Streaming API:
31//!
32//! # Example
33//!
34//! ```no_run
35//! # extern crate elefren;
36//! # use elefren::prelude::*;
37//! # use std::error::Error;
38//! use elefren::entities::event::Event;
39//! # fn main() -> Result<(), Box<Error>> {
40//! # let data = Data {
41//! #   base: "".into(),
42//! #   client_id: "".into(),
43//! #   client_secret: "".into(),
44//! #   redirect: "".into(),
45//! #   token: "".into(),
46//! # };
47//! let client = Mastodon::from(data);
48//! for event in client.streaming_user()? {
49//!     match event {
50//!         Event::Update(ref status) => { /* .. */ },
51//!         Event::Notification(ref notification) => { /* .. */ },
52//!         Event::Delete(ref id) => { /* .. */ },
53//!         Event::FiltersChanged => { /* .. */ },
54//!     }
55//! }
56//! # Ok(())
57//! # }
58//! ```
59
60#![deny(
61    missing_docs,
62    warnings,
63    missing_debug_implementations,
64    missing_copy_implementations,
65    trivial_casts,
66    trivial_numeric_casts,
67    unsafe_code,
68    unstable_features,
69    unused_import_braces,
70    unused_qualifications
71)]
72#![allow(intra_doc_link_resolution_failure)]
73
74#[macro_use]
75extern crate log;
76#[macro_use]
77extern crate serde_derive;
78#[macro_use]
79extern crate doc_comment;
80extern crate hyper_old_types;
81extern crate isolang;
82#[macro_use]
83extern crate serde_json;
84extern crate chrono;
85extern crate reqwest;
86extern crate serde;
87extern crate serde_qs;
88extern crate serde_urlencoded;
89extern crate tap_reader;
90extern crate try_from;
91extern crate url;
92extern crate tungstenite;
93
94#[cfg(feature = "env")]
95extern crate envy;
96
97#[cfg(feature = "toml")]
98extern crate toml as tomlcrate;
99
100#[cfg(test)]
101extern crate tempfile;
102
103#[cfg(test)]
104#[cfg_attr(all(test, any(feature = "toml", feature = "json")), macro_use)]
105extern crate indoc;
106
107use std::{
108    borrow::Cow,
109    io::BufRead,
110    ops,
111};
112
113use reqwest::{Client, RequestBuilder, Response};
114use tap_reader::Tap;
115use tungstenite::client::AutoStream;
116
117use entities::prelude::*;
118use http_send::{HttpSend, HttpSender};
119use page::Page;
120
121pub use data::Data;
122pub use errors::{ApiError, Error, Result};
123pub use isolang::Language;
124pub use mastodon_client::{MastodonClient, MastodonUnauthenticated};
125pub use registration::Registration;
126pub use requests::{
127    AddFilterRequest,
128    AddPushRequest,
129    StatusesRequest,
130    UpdateCredsRequest,
131    UpdatePushRequest,
132};
133pub use status_builder::{NewStatus, StatusBuilder};
134
135/// Registering your App
136pub mod apps;
137/// Contains the struct that holds the client auth data
138pub mod data;
139/// Entities returned from the API
140pub mod entities;
141/// Errors
142pub mod errors;
143/// Collection of helpers for serializing/deserializing `Data` objects
144pub mod helpers;
145/// Contains trait for converting `reqwest::Request`s to `reqwest::Response`s
146pub mod http_send;
147mod mastodon_client;
148/// Handling multiple pages of entities.
149pub mod page;
150/// Registering your app.
151pub mod registration;
152/// Requests
153pub mod requests;
154/// OAuth Scopes
155pub mod scopes;
156/// Constructing a status
157pub mod status_builder;
158#[macro_use]
159mod macros;
160/// Automatically import the things you need
161pub mod prelude {
162    pub use scopes::Scopes;
163    pub use Data;
164    pub use Mastodon;
165    pub use MastodonClient;
166    pub use NewStatus;
167    pub use Registration;
168    pub use StatusBuilder;
169    pub use StatusesRequest;
170}
171
172/// Your mastodon application client, handles all requests to and from Mastodon.
173#[derive(Clone, Debug)]
174pub struct Mastodon<H: HttpSend = HttpSender> {
175    client: Client,
176    http_sender: H,
177    /// Raw data about your mastodon instance.
178    pub data: Data,
179}
180
181impl<H: HttpSend> Mastodon<H> {
182    methods![get, post, delete,];
183
184    fn route(&self, url: &str) -> String {
185        format!("{}{}", self.base, url)
186    }
187
188    pub(crate) fn send(&self, req: RequestBuilder) -> Result<Response> {
189        Ok(self
190            .http_sender
191            .send(&self.client, req.bearer_auth(&self.token))?)
192    }
193}
194
195impl From<Data> for Mastodon<HttpSender> {
196    /// Creates a mastodon instance from the data struct.
197    fn from(data: Data) -> Mastodon<HttpSender> {
198        let mut builder = MastodonBuilder::new(HttpSender);
199        builder.data(data);
200        builder
201            .build()
202            .expect("We know `data` is present, so this should be fine")
203    }
204}
205
206impl<H: HttpSend> MastodonClient<H> for Mastodon<H> {
207    type Stream = EventReader<WebSocket>;
208
209    paged_routes! {
210        (get) favourites: "favourites" => Status,
211        (get) blocks: "blocks" => Account,
212        (get) domain_blocks: "domain_blocks" => String,
213        (get) follow_requests: "follow_requests" => Account,
214        (get) get_home_timeline: "timelines/home" => Status,
215        (get) get_emojis: "custom_emojis" => Emoji,
216        (get) mutes: "mutes" => Account,
217        (get) notifications: "notifications" => Notification,
218        (get) reports: "reports" => Report,
219        (get (q: &'a str, #[serde(skip_serializing_if = "Option::is_none")] limit: Option<u64>, following: bool,)) search_accounts: "accounts/search" => Account,
220        (get) get_endorsements: "endorsements" => Account,
221    }
222
223    paged_routes_with_id! {
224        (get) followers: "accounts/{}/followers" => Account,
225        (get) following: "accounts/{}/following" => Account,
226        (get) reblogged_by: "statuses/{}/reblogged_by" => Account,
227        (get) favourited_by: "statuses/{}/favourited_by" => Account,
228    }
229
230    route! {
231        (delete (domain: String,)) unblock_domain: "domain_blocks" => Empty,
232        (get) instance: "instance" => Instance,
233        (get) verify_credentials: "accounts/verify_credentials" => Account,
234        (post (account_id: &str, status_ids: Vec<&str>, comment: String,)) report: "reports" => Report,
235        (post (domain: String,)) block_domain: "domain_blocks" => Empty,
236        (post (id: &str,)) authorize_follow_request: "accounts/follow_requests/authorize" => Empty,
237        (post (id: &str,)) reject_follow_request: "accounts/follow_requests/reject" => Empty,
238        (get  (q: &'a str, resolve: bool,)) search: "search" => SearchResult,
239        (get  (local: bool,)) get_public_timeline: "timelines/public" => Vec<Status>,
240        (post (uri: Cow<'static, str>,)) follows: "follows" => Account,
241        (post multipart (file: Cow<'static, str>,)) media: "media" => Attachment,
242        (post) clear_notifications: "notifications/clear" => Empty,
243        (post (id: &str,)) dismiss_notification: "notifications/dismiss" => Empty,
244        (get) get_push_subscription: "push/subscription" => Subscription,
245        (delete) delete_push_subscription: "push/subscription" => Empty,
246        (get) get_filters: "filters" => Vec<Filter>,
247        (get) get_follow_suggestions: "suggestions" => Vec<Account>,
248    }
249
250    route_v2! {
251        (get (q: &'a str, resolve: bool,)) search_v2: "search" => SearchResultV2,
252    }
253
254    route_id! {
255        (get) get_account: "accounts/{}" => Account,
256        (post) follow: "accounts/{}/follow" => Relationship,
257        (post) unfollow: "accounts/{}/unfollow" => Relationship,
258        (post) block: "accounts/{}/block" => Relationship,
259        (post) unblock: "accounts/{}/unblock" => Relationship,
260        (get) mute: "accounts/{}/mute" => Relationship,
261        (get) unmute: "accounts/{}/unmute" => Relationship,
262        (get) get_notification: "notifications/{}" => Notification,
263        (get) get_status: "statuses/{}" => Status,
264        (get) get_context: "statuses/{}/context" => Context,
265        (get) get_card: "statuses/{}/card" => Card,
266        (post) reblog: "statuses/{}/reblog" => Status,
267        (post) unreblog: "statuses/{}/unreblog" => Status,
268        (post) favourite: "statuses/{}/favourite" => Status,
269        (post) unfavourite: "statuses/{}/unfavourite" => Status,
270        (delete) delete_status: "statuses/{}" => Empty,
271        (get) get_filter: "filters/{}" => Filter,
272        (delete) delete_filter: "filters/{}" => Empty,
273        (delete) delete_from_suggestions: "suggestions/{}" => Empty,
274        (post) endorse_user: "accounts/{}/pin" => Relationship,
275        (post) unendorse_user: "accounts/{}/unpin" => Relationship,
276    }
277
278    fn add_filter(&self, request: &mut AddFilterRequest) -> Result<Filter> {
279        let url = self.route("/api/v1/filters");
280        let response = self.send(self.client.post(&url).json(&request))?;
281
282        let status = response.status();
283
284        if status.is_client_error() {
285            return Err(Error::Client(status.clone()));
286        } else if status.is_server_error() {
287            return Err(Error::Server(status.clone()));
288        }
289
290        deserialise(response)
291    }
292
293    /// PUT /api/v1/filters/:id
294    fn update_filter(&self, id: &str, request: &mut AddFilterRequest) -> Result<Filter> {
295        let url = self.route(&format!("/api/v1/filters/{}", id));
296        let response = self.send(self.client.put(&url).json(&request))?;
297
298        let status = response.status();
299
300        if status.is_client_error() {
301            return Err(Error::Client(status.clone()));
302        } else if status.is_server_error() {
303            return Err(Error::Server(status.clone()));
304        }
305
306        deserialise(response)
307    }
308
309    fn update_credentials(&self, builder: &mut UpdateCredsRequest) -> Result<Account> {
310        let changes = builder.build()?;
311        let url = self.route("/api/v1/accounts/update_credentials");
312        let response = self.send(self.client.patch(&url).json(&changes))?;
313
314        let status = response.status();
315
316        if status.is_client_error() {
317            return Err(Error::Client(status.clone()));
318        } else if status.is_server_error() {
319            return Err(Error::Server(status.clone()));
320        }
321
322        deserialise(response)
323    }
324
325    /// Post a new status to the account.
326    fn new_status(&self, status: NewStatus) -> Result<Status> {
327        let response = self.send(
328            self.client
329                .post(&self.route("/api/v1/statuses"))
330                .json(&status),
331        )?;
332
333        deserialise(response)
334    }
335
336    /// Get timeline filtered by a hashtag(eg. `#coffee`) either locally or
337    /// federated.
338    fn get_tagged_timeline(&self, hashtag: String, local: bool) -> Result<Vec<Status>> {
339        let base = "/api/v1/timelines/tag/";
340        let url = if local {
341            self.route(&format!("{}{}?local=1", base, hashtag))
342        } else {
343            self.route(&format!("{}{}", base, hashtag))
344        };
345
346        self.get(url)
347    }
348
349    /// Get statuses of a single account by id. Optionally only with pictures
350    /// and or excluding replies.
351    ///
352    /// # Example
353    ///
354    /// ```no_run
355    /// # extern crate elefren;
356    /// # use elefren::prelude::*;
357    /// # use std::error::Error;
358    /// # fn main() -> Result<(), Box<Error>> {
359    /// # let data = Data {
360    /// #   base: "".into(),
361    /// #   client_id: "".into(),
362    /// #   client_secret: "".into(),
363    /// #   redirect: "".into(),
364    /// #   token: "".into(),
365    /// # };
366    /// let client = Mastodon::from(data);
367    /// let statuses = client.statuses("user-id", None)?;
368    /// # Ok(())
369    /// # }
370    /// ```
371    ///
372    /// ```no_run
373    /// # extern crate elefren;
374    /// # use elefren::prelude::*;
375    /// # use std::error::Error;
376    /// # fn main() -> Result<(), Box<Error>> {
377    /// # let data = Data {
378    /// #   base: "".into(),
379    /// #   client_id: "".into(),
380    /// #   client_secret: "".into(),
381    /// #   redirect: "".into(),
382    /// #   token: "".into(),
383    /// # };
384    /// let client = Mastodon::from(data);
385    /// let mut request = StatusesRequest::new();
386    /// request.only_media();
387    /// let statuses = client.statuses("user-id", request)?;
388    /// # Ok(())
389    /// # }
390    /// ```
391    fn statuses<'a, 'b: 'a, S>(&'b self, id: &'b str, request: S) -> Result<Page<Status, H>>
392    where
393        S: Into<Option<StatusesRequest<'a>>>,
394    {
395        let mut url = format!("{}/api/v1/accounts/{}/statuses", self.base, id);
396
397        if let Some(request) = request.into() {
398            url = format!("{}{}", url, request.to_querystring()?);
399        }
400
401        let response = self.send(self.client.get(&url))?;
402
403        Page::new(self, response)
404    }
405
406    /// Returns the client account's relationship to a list of other accounts.
407    /// Such as whether they follow them or vice versa.
408    fn relationships(&self, ids: &[&str]) -> Result<Page<Relationship, H>> {
409        let mut url = self.route("/api/v1/accounts/relationships?");
410
411        if ids.len() == 1 {
412            url += "id=";
413            url += &ids[0];
414        } else {
415            for id in ids {
416                url += "id[]=";
417                url += &id;
418                url += "&";
419            }
420            url.pop();
421        }
422
423        let response = self.send(self.client.get(&url))?;
424
425        Page::new(self, response)
426    }
427
428    /// Add a push notifications subscription
429    fn add_push_subscription(&self, request: &AddPushRequest) -> Result<Subscription> {
430        let request = request.build()?;
431        let response = self.send(
432            self.client
433                .post(&self.route("/api/v1/push/subscription"))
434                .json(&request),
435        )?;
436
437        deserialise(response)
438    }
439
440    /// Update the `data` portion of the push subscription associated with this
441    /// access token
442    fn update_push_data(&self, request: &UpdatePushRequest) -> Result<Subscription> {
443        let request = request.build();
444        let response = self.send(
445            self.client
446                .put(&self.route("/api/v1/push/subscription"))
447                .json(&request),
448        )?;
449
450        deserialise(response)
451    }
452
453    /// Get all accounts that follow the authenticated user
454    fn follows_me(&self) -> Result<Page<Account, H>> {
455        let me = self.verify_credentials()?;
456        Ok(self.followers(&me.id)?)
457    }
458
459    /// Get all accounts that the authenticated user follows
460    fn followed_by_me(&self) -> Result<Page<Account, H>> {
461        let me = self.verify_credentials()?;
462        Ok(self.following(&me.id)?)
463    }
464
465    /// returns events that are relevant to the authorized user, i.e. home
466    /// timeline & notifications
467    ///
468    /// # Example
469    ///
470    /// ```no_run
471    /// # extern crate elefren;
472    /// # use elefren::prelude::*;
473    /// # use std::error::Error;
474    /// use elefren::entities::event::Event;
475    /// # fn main() -> Result<(), Box<Error>> {
476    /// # let data = Data {
477    /// #   base: "".into(),
478    /// #   client_id: "".into(),
479    /// #   client_secret: "".into(),
480    /// #   redirect: "".into(),
481    /// #   token: "".into(),
482    /// # };
483    /// let client = Mastodon::from(data);
484    /// for event in client.streaming_user()? {
485    ///     match event {
486    ///         Event::Update(ref status) => { /* .. */ },
487    ///         Event::Notification(ref notification) => { /* .. */ },
488    ///         Event::Delete(ref id) => { /* .. */ },
489    ///         Event::FiltersChanged => { /* .. */ },
490    ///     }
491    /// }
492    /// # Ok(())
493    /// # }
494    /// ```
495    fn streaming_user(&self) -> Result<Self::Stream> {
496        let mut url: url::Url = self.route("/api/v1/streaming").parse()?;
497        url.query_pairs_mut()
498            .append_pair("access_token", &self.token)
499            .append_pair("stream", "user");
500        let mut url: url::Url = reqwest::get(url.as_str())?.url().as_str().parse()?;
501        let new_scheme = match url.scheme() {
502            "http" => "ws",
503            "https" => "wss",
504            x => return Err(Error::Other(format!("Bad URL scheme: {}", x))),
505        };
506        url.set_scheme(new_scheme).map_err(|_| Error::Other("Bad URL scheme!".to_string()))?;
507
508        let client = tungstenite::connect(url.as_str())?.0;
509
510        Ok(EventReader(WebSocket(client)))
511    }
512
513    /// returns all public statuses
514    fn streaming_public(&self) -> Result<Self::Stream> {
515        let mut url: url::Url = self.route("/api/v1/streaming").parse()?;
516        url.query_pairs_mut()
517            .append_pair("access_token", &self.token)
518            .append_pair("stream", "public");
519        let mut url: url::Url = reqwest::get(url.as_str())?.url().as_str().parse()?;
520        let new_scheme = match url.scheme() {
521            "http" => "ws",
522            "https" => "wss",
523            x => return Err(Error::Other(format!("Bad URL scheme: {}", x))),
524        };
525        url.set_scheme(new_scheme).map_err(|_| Error::Other("Bad URL scheme!".to_string()))?;
526
527        let client = tungstenite::connect(url.as_str())?.0;
528
529        Ok(EventReader(WebSocket(client)))
530    }
531
532    /// Returns all local statuses
533    fn streaming_local(&self) -> Result<Self::Stream> {
534        let mut url: url::Url = self.route("/api/v1/streaming").parse()?;
535        url.query_pairs_mut()
536            .append_pair("access_token", &self.token)
537            .append_pair("stream", "public:local");
538        let mut url: url::Url = reqwest::get(url.as_str())?.url().as_str().parse()?;
539        let new_scheme = match url.scheme() {
540            "http" => "ws",
541            "https" => "wss",
542            x => return Err(Error::Other(format!("Bad URL scheme: {}", x))),
543        };
544        url.set_scheme(new_scheme).map_err(|_| Error::Other("Bad URL scheme!".to_string()))?;
545
546        let client = tungstenite::connect(url.as_str())?.0;
547
548        Ok(EventReader(WebSocket(client)))
549    }
550
551    /// Returns all public statuses for a particular hashtag
552    fn streaming_public_hashtag(&self, hashtag: &str) -> Result<Self::Stream> {
553        let mut url: url::Url = self.route("/api/v1/streaming").parse()?;
554        url.query_pairs_mut()
555            .append_pair("access_token", &self.token)
556            .append_pair("stream", "hashtag")
557            .append_pair("tag", hashtag);
558        let mut url: url::Url = reqwest::get(url.as_str())?.url().as_str().parse()?;
559        let new_scheme = match url.scheme() {
560            "http" => "ws",
561            "https" => "wss",
562            x => return Err(Error::Other(format!("Bad URL scheme: {}", x))),
563        };
564        url.set_scheme(new_scheme).map_err(|_| Error::Other("Bad URL scheme!".to_string()))?;
565
566        let client = tungstenite::connect(url.as_str())?.0;
567
568        Ok(EventReader(WebSocket(client)))
569    }
570
571    /// Returns all local statuses for a particular hashtag
572    fn streaming_local_hashtag(&self, hashtag: &str) -> Result<Self::Stream> {
573        let mut url: url::Url = self.route("/api/v1/streaming").parse()?;
574        url.query_pairs_mut()
575            .append_pair("access_token", &self.token)
576            .append_pair("stream", "hashtag:local")
577            .append_pair("tag", hashtag);
578        let mut url: url::Url = reqwest::get(url.as_str())?.url().as_str().parse()?;
579        let new_scheme = match url.scheme() {
580            "http" => "ws",
581            "https" => "wss",
582            x => return Err(Error::Other(format!("Bad URL scheme: {}", x))),
583        };
584        url.set_scheme(new_scheme).map_err(|_| Error::Other("Bad URL scheme!".to_string()))?;
585
586        let client = tungstenite::connect(url.as_str())?.0;
587
588        Ok(EventReader(WebSocket(client)))
589    }
590
591    /// Returns statuses for a list
592    fn streaming_list(&self, list_id: &str) -> Result<Self::Stream> {
593        let mut url: url::Url = self.route("/api/v1/streaming").parse()?;
594        url.query_pairs_mut()
595            .append_pair("access_token", &self.token)
596            .append_pair("stream", "list")
597            .append_pair("list", list_id);
598        let mut url: url::Url = reqwest::get(url.as_str())?.url().as_str().parse()?;
599        let new_scheme = match url.scheme() {
600            "http" => "ws",
601            "https" => "wss",
602            x => return Err(Error::Other(format!("Bad URL scheme: {}", x))),
603        };
604        url.set_scheme(new_scheme).map_err(|_| Error::Other("Bad URL scheme!".to_string()))?;
605
606        let client = tungstenite::connect(url.as_str())?.0;
607
608        Ok(EventReader(WebSocket(client)))
609    }
610
611    /// Returns all direct messages
612    fn streaming_direct(&self) -> Result<Self::Stream> {
613        let mut url: url::Url = self.route("/api/v1/streaming").parse()?;
614        url.query_pairs_mut()
615            .append_pair("access_token", &self.token)
616            .append_pair("stream", "direct");
617        let mut url: url::Url = reqwest::get(url.as_str())?.url().as_str().parse()?;
618        let new_scheme = match url.scheme() {
619            "http" => "ws",
620            "https" => "wss",
621            x => return Err(Error::Other(format!("Bad URL scheme: {}", x))),
622        };
623        url.set_scheme(new_scheme).map_err(|_| Error::Other("Bad URL scheme!".to_string()))?;
624
625        let client = tungstenite::connect(url.as_str())?.0;
626
627        Ok(EventReader(WebSocket(client)))
628    }
629}
630
631#[derive(Debug)]
632/// WebSocket newtype so that EventStream can be implemented without coherency issues
633pub struct WebSocket(tungstenite::protocol::WebSocket<AutoStream>);
634
635/// A type that streaming events can be read from
636pub trait EventStream {
637    /// Read a message from this stream
638    fn read_message(&mut self) -> Result<String>;
639}
640
641impl<R: BufRead> EventStream for R {
642    fn read_message(&mut self) -> Result<String> {
643        let mut buf = String::new();
644        self.read_line(&mut buf)?;
645        Ok(buf)
646    }
647}
648
649impl EventStream for WebSocket {
650    fn read_message(&mut self) -> Result<String> {
651        Ok(self.0.read_message()?.into_text()?)
652    }
653}
654
655#[derive(Debug)]
656/// Iterator that produces events from a mastodon streaming API event stream
657pub struct EventReader<R: EventStream>(R);
658impl<R: EventStream> Iterator for EventReader<R> {
659    type Item = Event;
660
661    fn next(&mut self) -> Option<Self::Item> {
662        let mut lines = Vec::new();
663        loop {
664            if let Ok(line) = self.0.read_message() {
665                let line = line.trim().to_string();
666                if line.starts_with(":") || line.is_empty() {
667                    continue;
668                }
669                lines.push(line);
670                if let Ok(event) = self.make_event(&lines) {
671                    lines.clear();
672                    return Some(event);
673                } else {
674                    continue;
675                }
676            }
677        }
678    }
679}
680
681impl<R: EventStream> EventReader<R> {
682    fn make_event(&self, lines: &[String]) -> Result<Event> {
683        let event;
684        let data;
685        if let Some(event_line) = lines
686            .iter()
687            .find(|line| line.starts_with("event:"))
688        {
689            event = event_line[6..].trim().to_string();
690            data = lines.iter().find(|line| line.starts_with("data:")).map(|x| x[5..].trim().to_string());
691        } else {
692            #[derive(Deserialize)]
693            struct Message {
694                pub event: String,
695                pub payload: Option<String>,
696            }
697            let message = serde_json::from_str::<Message>(&lines[0])?;
698            event = message.event;
699            data = message.payload;
700        }
701        let event: &str = &event;
702        Ok(match event {
703            "notification" => {
704                let data = data.ok_or_else(|| {
705                    Error::Other("Missing `data` line for notification".to_string())
706                })?;
707                let notification = serde_json::from_str::<Notification>(&data)?;
708                Event::Notification(notification)
709            },
710            "update" => {
711                let data =
712                    data.ok_or_else(|| Error::Other("Missing `data` line for update".to_string()))?;
713                let status = serde_json::from_str::<Status>(&data)?;
714                Event::Update(status)
715            },
716            "delete" => {
717                let data =
718                    data.ok_or_else(|| Error::Other("Missing `data` line for delete".to_string()))?;
719                Event::Delete(data)
720            },
721            "filters_changed" => Event::FiltersChanged,
722            _ => return Err(Error::Other(format!("Unknown event `{}`", event))),
723        })
724    }
725}
726
727impl<H: HttpSend> ops::Deref for Mastodon<H> {
728    type Target = Data;
729
730    fn deref(&self) -> &Self::Target {
731        &self.data
732    }
733}
734
735struct MastodonBuilder<H: HttpSend> {
736    client: Option<Client>,
737    http_sender: H,
738    data: Option<Data>,
739}
740
741impl<H: HttpSend> MastodonBuilder<H> {
742    pub fn new(sender: H) -> Self {
743        MastodonBuilder {
744            http_sender: sender,
745            client: None,
746            data: None,
747        }
748    }
749
750    pub fn client(&mut self, client: Client) -> &mut Self {
751        self.client = Some(client);
752        self
753    }
754
755    pub fn data(&mut self, data: Data) -> &mut Self {
756        self.data = Some(data);
757        self
758    }
759
760    pub fn build(self) -> Result<Mastodon<H>> {
761        Ok(if let Some(data) = self.data {
762            Mastodon {
763                client: self.client.unwrap_or_else(|| Client::new()),
764                http_sender: self.http_sender,
765                data,
766            }
767        } else {
768            return Err(Error::MissingField("missing field 'data'"));
769        })
770    }
771}
772
773/// Client that can make unauthenticated calls to a mastodon instance
774#[derive(Clone, Debug)]
775pub struct MastodonUnauth<H: HttpSend = HttpSender> {
776    client: Client,
777    http_sender: H,
778    base: url::Url,
779}
780
781impl MastodonUnauth<HttpSender> {
782    /// Create a new unauthenticated client
783    pub fn new(base: &str) -> Result<MastodonUnauth<HttpSender>> {
784        let base = if base.starts_with("https://") {
785            base.to_string()
786        } else {
787            format!("https://{}", base)
788        };
789        Ok(MastodonUnauth {
790            client: Client::new(),
791            http_sender: HttpSender,
792            base: url::Url::parse(&base)?,
793        })
794    }
795}
796
797impl<H: HttpSend> MastodonUnauth<H> {
798    fn route(&self, url: &str) -> Result<url::Url> {
799        Ok(self.base.join(url)?)
800    }
801
802    fn send(&self, req: RequestBuilder) -> Result<Response> {
803        Ok(self.http_sender.send(&self.client, req)?)
804    }
805}
806
807impl<H: HttpSend> MastodonUnauthenticated<H> for MastodonUnauth<H> {
808    /// GET /api/v1/statuses/:id
809    fn get_status(&self, id: &str) -> Result<Status> {
810        let route = self.route("/api/v1/statuses")?;
811        let route = route.join(id)?;
812        let response = self.send(self.client.get(route))?;
813        deserialise(response)
814    }
815
816    /// GET /api/v1/statuses/:id/context
817    fn get_context(&self, id: &str) -> Result<Context> {
818        let route = self.route("/api/v1/statuses")?;
819        let route = route.join(id)?;
820        let route = route.join("context")?;
821        let response = self.send(self.client.get(route))?;
822        deserialise(response)
823    }
824
825    /// GET /api/v1/statuses/:id/card
826    fn get_card(&self, id: &str) -> Result<Card> {
827        let route = self.route("/api/v1/statuses")?;
828        let route = route.join(id)?;
829        let route = route.join("card")?;
830        let response = self.send(self.client.get(route))?;
831        deserialise(response)
832    }
833}
834
835// Convert the HTTP response body from JSON. Pass up deserialization errors
836// transparently.
837fn deserialise<T: for<'de> serde::Deserialize<'de>>(response: Response) -> Result<T> {
838    let mut reader = Tap::new(response);
839
840    match serde_json::from_reader(&mut reader) {
841        Ok(t) => {
842            debug!("{}", String::from_utf8_lossy(&reader.bytes));
843            Ok(t)
844        },
845        // If deserializing into the desired type fails try again to
846        // see if this is an error response.
847        Err(e) => {
848            error!("{}", String::from_utf8_lossy(&reader.bytes));
849            if let Ok(error) = serde_json::from_slice(&reader.bytes) {
850                return Err(Error::Api(error));
851            }
852            Err(e.into())
853        },
854    }
855}