1#![cfg_attr(test, deny(warnings))]
33#![cfg_attr(test, deny(missing_docs))]
34
35#[macro_use] extern crate serde_derive;
36#[macro_use] extern crate doc_comment;
37#[macro_use] extern crate serde_json as json;
38
39pub mod apps;
41pub mod status_builder;
43pub mod entities;
45pub mod registration;
47pub mod page;
49pub mod media_builder;
51
52use std::borrow::Cow;
53use std::error::Error as StdError;
54use std::fmt;
55use std::io::Error as IoError;
56use std::ops;
57
58use json::Error as SerdeError;
59use reqwest::Error as HttpError;
60use reqwest::header::ToStrError as HeaderToStrError;
61use reqwest::{Client, Response, StatusCode};
62use reqwest::header::{self, HeaderMap, HeaderValue};
63use url::ParseError as UrlError;
64use hyperx::Error as HyperxError;
65use log::debug;
66
67use entities::prelude::*;
68pub use status_builder::StatusBuilder;
69use page::Page;
70pub use media_builder::MediaBuilder;
71
72pub use registration::Registration;
73pub type Result<T> = std::result::Result<T, Error>;
75
76macro_rules! methods {
77    ($($method:ident,)+) => {
78        $(
79            fn $method<T: for<'de> serde::Deserialize<'de>>(&self, url: String)
80            -> Result<T>
81            {
82                let request = self.client.$method(&url)
83                    .headers(self.headers.clone());
84                debug!("REQUEST: {:?}", request);
85
86                let response = request.send()?;
87                debug!("RESPONSE: {:?}", response);
88
89                deserialise(response)
90            }
91         )+
92    };
93}
94
95macro_rules! paged_routes {
96
97    (($method:ident) $name:ident: $url:expr => $ret:ty, $($rest:tt)*) => {
98        doc_comment! {
99            concat!(
100                "Equivalent to `/api/v1/",
101                $url,
102                "`\n# Errors\nIf `access_token` is not set."),
103            pub fn $name(&self) -> Result<Page<$ret>> {
104                let url = self.route(concat!("/api/v1/", $url));
105                let response = self.client.$method(&url)
106                    .headers(self.headers.clone())
107                    .send()?;
108
109                Page::new(self, response)
110            }
111
112        }
113
114        paged_routes!{$($rest)*}
115    };
116
117    () => {}
118}
119
120macro_rules! route {
121
122    (($method:ident ($($param:ident: $typ:ty,)*)) $name:ident: $url:expr => $ret:ty, $($rest:tt)*) => {
123        doc_comment! {
124            concat!(
125                "Equivalent to `/api/v1/",
126                $url,
127                "`\n# Errors\nIf `access_token` is not set."),
128
129            pub fn $name(&self, $($param: $typ,)*) -> Result<$ret> {
130
131                let form_data = json!({
132                    $(
133                        stringify!($param): $param,
134                    )*
135                });
136
137                let response = self.client.$method(&self.route(concat!("/api/v1/", $url)))
138                    .headers(self.headers.clone())
139                    .json(&form_data)
140                    .send()?;
141
142                let status = response.status().clone();
143
144                if status.is_client_error() {
145                    return Err(Error::Client(status));
146                } else if status.is_server_error() {
147                    return Err(Error::Server(status));
148                }
149
150                deserialise(response)
151            }
152        }
153
154        route!{$($rest)*}
155    };
156
157    (($method:ident) $name:ident: $url:expr => $ret:ty, $($rest:tt)*) => {
158        doc_comment! {
159            concat!(
160                "Equivalent to `/api/v1/",
161                $url,
162                "`\n# Errors\nIf `access_token` is not set."),
163            pub fn $name(&self) -> Result<$ret> {
164                self.$method(self.route(concat!("/api/v1/", $url)))
165            }
166        }
167
168        route!{$($rest)*}
169    };
170
171    () => {}
172}
173
174macro_rules! route_id {
175
176    ($(($method:ident) $name:ident: $url:expr => $ret:ty,)*) => {
177        $(
178            doc_comment! {
179                concat!(
180                    "Equivalent to `/api/v1/",
181                    $url,
182                    "`\n# Errors\nIf `access_token` is not set."),
183                pub fn $name(&self, id: &str) -> Result<$ret> {
184                    self.$method(self.route(&format!(concat!("/api/v1/", $url), id)))
185                }
186            }
187         )*
188    }
189
190}
191macro_rules! paged_routes_with_id {
192
193    (($method:ident) $name:ident: $url:expr => $ret:ty, $($rest:tt)*) => {
194        doc_comment! {
195            concat!(
196                "Equivalent to `/api/v1/",
197                $url,
198                "`\n# Errors\nIf `access_token` is not set."),
199            pub fn $name(&self, id: &str) -> Result<Page<$ret>> {
200                let url = self.route(&format!(concat!("/api/v1/", $url), id));
201                let response = self.client.$method(&url)
202                    .headers(self.headers.clone())
203                    .send()?;
204
205                Page::new(self, response)
206            }
207        }
208
209        paged_routes_with_id!{$($rest)*}
210    };
211
212    () => {}
213}
214
215
216#[derive(Clone, Debug)]
218pub struct Mastodon {
219    client: Client,
220    headers: HeaderMap,
221    pub data: Data
223}
224
225#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
228pub struct Data {
229    pub base: Cow<'static, str>,
231    pub client_id: Cow<'static, str>,
233    pub client_secret: Cow<'static, str>,
235    pub redirect: Cow<'static, str>,
237    pub token: Cow<'static, str>,
239}
240
241#[derive(Debug, Deserialize)]
243#[serde(untagged)]
244pub enum Error {
245    Api(ApiError),
248    #[serde(skip_deserializing)]
251    Serde(SerdeError),
252    #[serde(skip_deserializing)]
254    Http(HttpError),
255    #[serde(skip_deserializing)]
257    Io(IoError),
258    #[serde(skip_deserializing)]
260    Url(UrlError),
261    #[serde(skip_deserializing)]
263    ClientIdRequired,
264    #[serde(skip_deserializing)]
266    ClientSecretRequired,
267    #[serde(skip_deserializing)]
269    AccessTokenRequired,
270    #[serde(skip_deserializing)]
272    Client(StatusCode),
273    #[serde(skip_deserializing)]
275    Server(StatusCode),
276    #[serde(skip_deserializing)]
278    Header(HeaderToStrError),
279    #[serde(skip_deserializing)]
281    Hyperx(HyperxError),
282}
283
284impl fmt::Display for Error {
285    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
286        write!(f, "{:?}", self)
287    }
288}
289
290impl StdError for Error {
291    fn description(&self) -> &str {
292        match *self {
293            Error::Api(ref e) => {
294                e.error_description.as_ref().map(|i| &**i)
295                    .or(e.error.as_ref().map(|i| &**i))
296                    .unwrap_or("Unknown API Error")
297            },
298            Error::Serde(ref e) => e.description(),
299            Error::Http(ref e) => e.description(),
300            Error::Io(ref e) => e.description(),
301            Error::Url(ref e) => e.description(),
302            Error::Client(ref status) | Error::Server(ref status) => {
303                status.canonical_reason().unwrap_or("Unknown Status code")
304            },
305            Error::Hyperx(ref e) => e.description(),
306            Error::Header(ref e) => e.description(),
307            Error::ClientIdRequired => "ClientIdRequired",
308            Error::ClientSecretRequired => "ClientSecretRequired",
309            Error::AccessTokenRequired => "AccessTokenRequired",
310        }
311    }
312}
313
314impl From<HyperxError> for Error {
315    fn from(error: HyperxError) -> Self {
316        Error::Hyperx(error)
317    }
318}
319
320impl From<HeaderToStrError> for Error {
321    fn from(error: HeaderToStrError) -> Self {
322        Error::Header(error)
323    }
324}
325
326#[derive(Clone, Debug, Deserialize)]
328pub struct ApiError {
329    pub error: Option<String>,
331    pub error_description: Option<String>,
333}
334
335#[derive(Clone, Debug, Default)]
347pub struct StatusesRequest<'a> {
348    only_media: bool,
349    exclude_replies: bool,
350    pinned: bool,
351    max_id: Option<Cow<'a, str>>,
352    since_id: Option<Cow<'a, str>>,
353    limit: Option<usize>,
354}
355
356impl<'a> StatusesRequest<'a> {
357    pub fn new() -> Self {
358        Self::default()
359    }
360
361    pub fn only_media(mut self) -> Self {
362        self.only_media = true;
363        self
364    }
365
366    pub fn exclude_replies(mut self) -> Self {
367        self.exclude_replies = true;
368        self
369    }
370
371    pub fn pinned(mut self) -> Self {
372        self.pinned = true;
373        self
374    }
375
376    pub fn max_id<S: Into<Cow<'a, str>>>(mut self, max_id: S) -> Self {
377        self.max_id = Some(max_id.into());
378        self
379    }
380
381    pub fn since_id<S: Into<Cow<'a, str>>>(mut self, since_id: S) -> Self {
382        self.since_id = Some(since_id.into());
383        self
384    }
385
386    pub fn limit(mut self, limit: usize) -> Self {
387        self.limit = Some(limit);
388        self
389    }
390
391    pub fn to_querystring(&self) -> String {
392        let mut opts = vec![];
393
394        if self.only_media {
395            opts.push("only_media=1".into());
396        }
397
398        if self.exclude_replies {
399            opts.push("exclude_replies=1".into());
400        }
401
402        if self.pinned {
403            opts.push("pinned=1".into());
404        }
405
406        if let Some(ref max_id) = self.max_id {
407            opts.push(format!("max_id={}", max_id));
408        }
409
410        if let Some(ref since_id) = self.since_id {
411            opts.push(format!("since_id={}", since_id));
412        }
413
414        if let Some(limit) = self.limit {
415            opts.push(format!("limit={}", limit));
416        }
417
418        if opts.is_empty() {
419            String::new()
420        } else {
421            format!("?{}", opts.join("&"))
422        }
423    }
424}
425
426impl Mastodon {
427    fn from_registration<I>(base: I,
428                         client_id: I,
429                         client_secret: I,
430                         redirect: I,
431                         token: I,
432                         client: Client)
433        -> Self
434        where I: Into<Cow<'static, str>>
435        {
436            let data = Data {
437                base: base.into(),
438                client_id: client_id.into(),
439                client_secret: client_secret.into(),
440                redirect: redirect.into(),
441                token: token.into(),
442
443            };
444
445            let mut headers = HeaderMap::new();
446            let auth = HeaderValue::from_str(&format!("Bearer {}", data.token));
447            headers.insert(header::AUTHORIZATION, auth.unwrap());
448
449            Mastodon {
450                client: client,
451                headers: headers,
452                data: data,
453            }
454        }
455
456    pub fn from_data(data: Data) -> Self {
458        let mut headers = HeaderMap::new();
459        let auth = HeaderValue::from_str(&format!("Bearer {}", data.token));
460        headers.insert(header::AUTHORIZATION, auth.unwrap());
461
462        Mastodon {
463            client: Client::new(),
464            headers: headers,
465            data: data,
466        }
467    }
468
469    paged_routes! {
470        (get) favourites: "favourites" => Status,
471        (get) blocks: "blocks" => Account,
472        (get) domain_blocks: "domain_blocks" => String,
473        (get) follow_requests: "follow_requests" => Account,
474        (get) get_home_timeline: "timelines/home" => Status,
475        (get) get_emojis: "custom_emojis" => Emoji,
476        (get) mutes: "mutes" => Account,
477        (get) notifications: "notifications" => Notification,
478        (get) reports: "reports" => Report,
479    }
480
481    paged_routes_with_id! {
482        (get) followers: "accounts/{}/followers" => Account,
483        (get) following: "accounts/{}/following" => Account,
484        (get) reblogged_by: "statuses/{}/reblogged_by" => Account,
485        (get) favourited_by: "statuses/{}/favourited_by" => Account,
486    }
487
488    route! {
489        (delete (domain: String,)) unblock_domain: "domain_blocks" => Empty,
490        (get) instance: "instance" => Instance,
491        (get) verify_credentials: "accounts/verify_credentials" => Account,
492        (post (account_id: &str, status_ids: Vec<&str>, comment: String,)) report: "reports" => Report,
493        (post (domain: String,)) block_domain: "domain_blocks" => Empty,
494        (post (id: &str,)) authorize_follow_request: "accounts/follow_requests/authorize" => Empty,
495        (post (id: &str,)) reject_follow_request: "accounts/follow_requests/reject" => Empty,
496        (post (q: String, resolve: bool,)) search: "search" => SearchResult,
497        (post (uri: Cow<'static, str>,)) follows: "follows" => Account,
498        (post) clear_notifications: "notifications/clear" => Empty,
499    }
500
501    route_id! {
502        (get) get_account: "accounts/{}" => Account,
503        (post) follow: "accounts/{}/follow" => Account,
504        (post) unfollow: "accounts/{}/unfollow" => Account,
505        (get) block: "accounts/{}/block" => Account,
506        (get) unblock: "accounts/{}/unblock" => Account,
507        (get) mute: "accounts/{}/mute" => Account,
508        (get) unmute: "accounts/{}/unmute" => Account,
509        (get) get_notification: "notifications/{}" => Notification,
510        (get) get_status: "statuses/{}" => Status,
511        (get) get_context: "statuses/{}/context" => Context,
512        (get) get_card: "statuses/{}/card" => Card,
513        (post) reblog: "statuses/{}/reblog" => Status,
514        (post) unreblog: "statuses/{}/unreblog" => Status,
515        (post) favourite: "statuses/{}/favourite" => Status,
516        (post) unfavourite: "statuses/{}/unfavourite" => Status,
517        (delete) delete_status: "statuses/{}" => Empty,
518    }
519
520    pub fn update_credentials(&self, changes: CredientialsBuilder)
521        -> Result<Account>
522    {
523
524        let url = self.route("/api/v1/accounts/update_credentials");
525        let response = self.client.patch(&url)
526            .headers(self.headers.clone())
527            .multipart(changes.into_form()?)
528            .send()?;
529
530        let status = response.status().clone();
531
532        if status.is_client_error() {
533            return Err(Error::Client(status));
534        } else if status.is_server_error() {
535            return Err(Error::Server(status));
536        }
537
538        deserialise(response)
539    }
540
541    pub fn new_status(&self, status: StatusBuilder) -> Result<Status> {
543
544        let response = self.client.post(&self.route("/api/v1/statuses"))
545            .headers(self.headers.clone())
546            .json(&status)
547            .send()?;
548
549        deserialise(response)
550    }
551
552    pub fn get_public_timeline(&self, local: bool) -> Result<Vec<Status>> {
554        let mut url = self.route("/api/v1/timelines/public");
555
556        if local {
557            url += "?local=1";
558        }
559
560        self.get(url)
561    }
562
563    pub fn get_tagged_timeline(&self, hashtag: String, local: bool) -> Result<Vec<Status>> {
566        let mut url = self.route("/api/v1/timelines/tag/");
567        url += &hashtag;
568
569        if local {
570            url += "?local=1";
571        }
572
573        self.get(url)
574    }
575
576    pub fn statuses<'a, S>(&self, id: &str, request: S) -> Result<Page<Status>>
619            where S: Into<Option<StatusesRequest<'a>>>
620    {
621        let mut url = format!("{}/api/v1/accounts/{}/statuses", self.base, id);
622
623        if let Some(request) = request.into() {
624            url = format!("{}{}", url, request.to_querystring());
625        }
626
627        let response = self.client.get(&url)
628            .headers(self.headers.clone())
629            .send()?;
630
631        Page::new(self, response)
632    }
633
634    pub fn relationships(&self, ids: &[&str]) -> Result<Page<Relationship>> {
637        let mut url = self.route("/api/v1/accounts/relationships?");
638
639        if ids.len() == 1 {
640            url += "id=";
641            url += &ids[0];
642        } else {
643            for id in ids {
644                url += "id[]=";
645                url += &id;
646                url += "&";
647            }
648            url.pop();
649        }
650
651        let response = self.client.get(&url)
652            .headers(self.headers.clone())
653            .send()?;
654
655        Page::new(self, response)
656    }
657
658    pub fn search_accounts(&self,
662                           query: &str,
663                           limit: Option<u64>,
664                           following: bool)
665        -> Result<Page<Account>>
666    {
667        let url = format!("{}/api/v1/accounts/search?q={}&limit={}&following={}",
668                          self.base,
669                          query,
670                          limit.unwrap_or(40),
671                          following);
672
673        let response = self.client.get(&url)
674            .headers(self.headers.clone())
675            .send()?;
676
677        Page::new(self, response)
678    }
679
680    methods![get, post, delete,];
681
682    fn route(&self, url: &str) -> String {
683        let mut s = (*self.base).to_owned();
684        s += url;
685        s
686    }
687
688    pub fn media(&self, media_builder: MediaBuilder) -> Result<Attachment>
690    {
691        use reqwest::multipart::Form;
692
693        let mut form_data = Form::new()
694            .file("file", media_builder.file.as_ref())?;
695
696        if let Some(description) = media_builder.description {
697            form_data = form_data.text("description", description);
698        }
699
700        if let Some(focus) = media_builder.focus {
701            let string = format!("{},{}", focus.0, focus.1);
702            form_data = form_data.text("focus", string);
703        }
704
705        let response = self.client.post(&self.route("/api/v1/media"))
706            .headers(self.headers.clone())
707            .multipart(form_data)
708            .send()?;
709
710        let status = response.status().clone();
711
712        if status.is_client_error() {
713            return Err(Error::Client(status));
714        } else if status.is_server_error() {
715            return Err(Error::Server(status));
716        }
717
718        deserialise(response)
719    }
720}
721
722impl ops::Deref for Mastodon {
723    type Target = Data;
724
725    fn deref(&self) -> &Self::Target {
726        &self.data
727    }
728}
729
730macro_rules! from {
731    ($($typ:ident, $variant:ident,)*) => {
732        $(
733            impl From<$typ> for Error {
734                fn from(from: $typ) -> Self {
735                    use Error::*;
736                    $variant(from)
737                }
738            }
739        )*
740    }
741}
742
743from! {
744    HttpError, Http,
745    IoError, Io,
746    SerdeError, Serde,
747    UrlError, Url,
748}
749
750fn deserialise<T: for<'de> serde::Deserialize<'de>>(mut response: Response)
753    -> Result<T>
754{
755    use std::io::Read;
756
757    let mut vec = Vec::new();
758    response.read_to_end(&mut vec)?;
759
760    match json::from_slice(&vec) {
761        Ok(t) => Ok(t),
762        Err(e) => {
765            if let Ok(error) = json::from_slice(&vec) {
766                return Err(Error::Api(error));
767            }
768            Err(e.into())
769        },
770    }
771}