#![cfg_attr(test, deny(warnings))]
#![cfg_attr(test, deny(missing_docs))]
#[macro_use] extern crate serde_derive;
#[macro_use] extern crate doc_comment;
#[macro_use] extern crate serde_json as json;
pub mod apps;
pub mod status_builder;
pub mod entities;
pub mod registration;
pub mod page;
pub mod media_builder;
use std::borrow::Cow;
use std::error::Error as StdError;
use std::fmt;
use std::io::Error as IoError;
use std::ops;
use json::Error as SerdeError;
use reqwest::Error as HttpError;
use reqwest::header::ToStrError as HeaderToStrError;
use reqwest::{Client, Response, StatusCode};
use reqwest::header::{self, HeaderMap, HeaderValue};
use url::ParseError as UrlError;
use hyperx::Error as HyperxError;
use log::debug;
use entities::prelude::*;
pub use status_builder::StatusBuilder;
use page::Page;
pub use media_builder::MediaBuilder;
pub use registration::Registration;
pub type Result<T> = std::result::Result<T, Error>;
macro_rules! methods {
($($method:ident,)+) => {
$(
fn $method<T: for<'de> serde::Deserialize<'de>>(&self, url: String)
-> Result<T>
{
let request = self.client.$method(&url)
.headers(self.headers.clone());
debug!("REQUEST: {:?}", request);
let response = request.send()?;
debug!("RESPONSE: {:?}", response);
deserialise(response)
}
)+
};
}
macro_rules! paged_routes {
(($method:ident) $name:ident: $url:expr => $ret:ty, $($rest:tt)*) => {
doc_comment! {
concat!(
"Equivalent to `/api/v1/",
$url,
"`\n# Errors\nIf `access_token` is not set."),
pub fn $name(&self) -> Result<Page<$ret>> {
let url = self.route(concat!("/api/v1/", $url));
let response = self.client.$method(&url)
.headers(self.headers.clone())
.send()?;
Page::new(self, response)
}
}
paged_routes!{$($rest)*}
};
() => {}
}
macro_rules! route {
(($method:ident ($($param:ident: $typ:ty,)*)) $name:ident: $url:expr => $ret:ty, $($rest:tt)*) => {
doc_comment! {
concat!(
"Equivalent to `/api/v1/",
$url,
"`\n# Errors\nIf `access_token` is not set."),
pub fn $name(&self, $($param: $typ,)*) -> Result<$ret> {
let form_data = json!({
$(
stringify!($param): $param,
)*
});
let response = self.client.$method(&self.route(concat!("/api/v1/", $url)))
.headers(self.headers.clone())
.json(&form_data)
.send()?;
let status = response.status().clone();
if status.is_client_error() {
return Err(Error::Client(status));
} else if status.is_server_error() {
return Err(Error::Server(status));
}
deserialise(response)
}
}
route!{$($rest)*}
};
(($method:ident) $name:ident: $url:expr => $ret:ty, $($rest:tt)*) => {
doc_comment! {
concat!(
"Equivalent to `/api/v1/",
$url,
"`\n# Errors\nIf `access_token` is not set."),
pub fn $name(&self) -> Result<$ret> {
self.$method(self.route(concat!("/api/v1/", $url)))
}
}
route!{$($rest)*}
};
() => {}
}
macro_rules! route_id {
($(($method:ident) $name:ident: $url:expr => $ret:ty,)*) => {
$(
doc_comment! {
concat!(
"Equivalent to `/api/v1/",
$url,
"`\n# Errors\nIf `access_token` is not set."),
pub fn $name(&self, id: &str) -> Result<$ret> {
self.$method(self.route(&format!(concat!("/api/v1/", $url), id)))
}
}
)*
}
}
macro_rules! paged_routes_with_id {
(($method:ident) $name:ident: $url:expr => $ret:ty, $($rest:tt)*) => {
doc_comment! {
concat!(
"Equivalent to `/api/v1/",
$url,
"`\n# Errors\nIf `access_token` is not set."),
pub fn $name(&self, id: &str) -> Result<Page<$ret>> {
let url = self.route(&format!(concat!("/api/v1/", $url), id));
let response = self.client.$method(&url)
.headers(self.headers.clone())
.send()?;
Page::new(self, response)
}
}
paged_routes_with_id!{$($rest)*}
};
() => {}
}
#[derive(Clone, Debug)]
pub struct Mastodon {
client: Client,
headers: HeaderMap,
pub data: Data
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct Data {
pub base: Cow<'static, str>,
pub client_id: Cow<'static, str>,
pub client_secret: Cow<'static, str>,
pub redirect: Cow<'static, str>,
pub token: Cow<'static, str>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum Error {
Api(ApiError),
#[serde(skip_deserializing)]
Serde(SerdeError),
#[serde(skip_deserializing)]
Http(HttpError),
#[serde(skip_deserializing)]
Io(IoError),
#[serde(skip_deserializing)]
Url(UrlError),
#[serde(skip_deserializing)]
ClientIdRequired,
#[serde(skip_deserializing)]
ClientSecretRequired,
#[serde(skip_deserializing)]
AccessTokenRequired,
#[serde(skip_deserializing)]
Client(StatusCode),
#[serde(skip_deserializing)]
Server(StatusCode),
#[serde(skip_deserializing)]
Header(HeaderToStrError),
#[serde(skip_deserializing)]
Hyperx(HyperxError),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self)
}
}
impl StdError for Error {
fn description(&self) -> &str {
match *self {
Error::Api(ref e) => {
e.error_description.as_ref().map(|i| &**i)
.or(e.error.as_ref().map(|i| &**i))
.unwrap_or("Unknown API Error")
},
Error::Serde(ref e) => e.description(),
Error::Http(ref e) => e.description(),
Error::Io(ref e) => e.description(),
Error::Url(ref e) => e.description(),
Error::Client(ref status) | Error::Server(ref status) => {
status.canonical_reason().unwrap_or("Unknown Status code")
},
Error::Hyperx(ref e) => e.description(),
Error::Header(ref e) => e.description(),
Error::ClientIdRequired => "ClientIdRequired",
Error::ClientSecretRequired => "ClientSecretRequired",
Error::AccessTokenRequired => "AccessTokenRequired",
}
}
}
impl From<HyperxError> for Error {
fn from(error: HyperxError) -> Self {
Error::Hyperx(error)
}
}
impl From<HeaderToStrError> for Error {
fn from(error: HeaderToStrError) -> Self {
Error::Header(error)
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct ApiError {
pub error: Option<String>,
pub error_description: Option<String>,
}
#[derive(Clone, Debug, Default)]
pub struct StatusesRequest<'a> {
only_media: bool,
exclude_replies: bool,
pinned: bool,
max_id: Option<Cow<'a, str>>,
since_id: Option<Cow<'a, str>>,
limit: Option<usize>,
}
impl<'a> StatusesRequest<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn only_media(mut self) -> Self {
self.only_media = true;
self
}
pub fn exclude_replies(mut self) -> Self {
self.exclude_replies = true;
self
}
pub fn pinned(mut self) -> Self {
self.pinned = true;
self
}
pub fn max_id<S: Into<Cow<'a, str>>>(mut self, max_id: S) -> Self {
self.max_id = Some(max_id.into());
self
}
pub fn since_id<S: Into<Cow<'a, str>>>(mut self, since_id: S) -> Self {
self.since_id = Some(since_id.into());
self
}
pub fn limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
pub fn to_querystring(&self) -> String {
let mut opts = vec![];
if self.only_media {
opts.push("only_media=1".into());
}
if self.exclude_replies {
opts.push("exclude_replies=1".into());
}
if self.pinned {
opts.push("pinned=1".into());
}
if let Some(ref max_id) = self.max_id {
opts.push(format!("max_id={}", max_id));
}
if let Some(ref since_id) = self.since_id {
opts.push(format!("since_id={}", since_id));
}
if let Some(limit) = self.limit {
opts.push(format!("limit={}", limit));
}
if opts.is_empty() {
String::new()
} else {
format!("?{}", opts.join("&"))
}
}
}
impl Mastodon {
fn from_registration<I>(base: I,
client_id: I,
client_secret: I,
redirect: I,
token: I,
client: Client)
-> Self
where I: Into<Cow<'static, str>>
{
let data = Data {
base: base.into(),
client_id: client_id.into(),
client_secret: client_secret.into(),
redirect: redirect.into(),
token: token.into(),
};
let mut headers = HeaderMap::new();
let auth = HeaderValue::from_str(&format!("Bearer {}", data.token));
headers.insert(header::AUTHORIZATION, auth.unwrap());
Mastodon {
client: client,
headers: headers,
data: data,
}
}
pub fn from_data(data: Data) -> Self {
let mut headers = HeaderMap::new();
let auth = HeaderValue::from_str(&format!("Bearer {}", data.token));
headers.insert(header::AUTHORIZATION, auth.unwrap());
Mastodon {
client: Client::new(),
headers: headers,
data: data,
}
}
paged_routes! {
(get) favourites: "favourites" => Status,
(get) blocks: "blocks" => Account,
(get) domain_blocks: "domain_blocks" => String,
(get) follow_requests: "follow_requests" => Account,
(get) get_home_timeline: "timelines/home" => Status,
(get) get_emojis: "custom_emojis" => Emoji,
(get) mutes: "mutes" => Account,
(get) notifications: "notifications" => Notification,
(get) reports: "reports" => Report,
}
paged_routes_with_id! {
(get) followers: "accounts/{}/followers" => Account,
(get) following: "accounts/{}/following" => Account,
(get) reblogged_by: "statuses/{}/reblogged_by" => Account,
(get) favourited_by: "statuses/{}/favourited_by" => Account,
}
route! {
(delete (domain: String,)) unblock_domain: "domain_blocks" => Empty,
(get) instance: "instance" => Instance,
(get) verify_credentials: "accounts/verify_credentials" => Account,
(post (account_id: &str, status_ids: Vec<&str>, comment: String,)) report: "reports" => Report,
(post (domain: String,)) block_domain: "domain_blocks" => Empty,
(post (id: &str,)) authorize_follow_request: "accounts/follow_requests/authorize" => Empty,
(post (id: &str,)) reject_follow_request: "accounts/follow_requests/reject" => Empty,
(post (q: String, resolve: bool,)) search: "search" => SearchResult,
(post (uri: Cow<'static, str>,)) follows: "follows" => Account,
(post) clear_notifications: "notifications/clear" => Empty,
}
route_id! {
(get) get_account: "accounts/{}" => Account,
(post) follow: "accounts/{}/follow" => Account,
(post) unfollow: "accounts/{}/unfollow" => Account,
(get) block: "accounts/{}/block" => Account,
(get) unblock: "accounts/{}/unblock" => Account,
(get) mute: "accounts/{}/mute" => Account,
(get) unmute: "accounts/{}/unmute" => Account,
(get) get_notification: "notifications/{}" => Notification,
(get) get_status: "statuses/{}" => Status,
(get) get_context: "statuses/{}/context" => Context,
(get) get_card: "statuses/{}/card" => Card,
(post) reblog: "statuses/{}/reblog" => Status,
(post) unreblog: "statuses/{}/unreblog" => Status,
(post) favourite: "statuses/{}/favourite" => Status,
(post) unfavourite: "statuses/{}/unfavourite" => Status,
(delete) delete_status: "statuses/{}" => Empty,
}
pub fn update_credentials(&self, changes: CredientialsBuilder)
-> Result<Account>
{
let url = self.route("/api/v1/accounts/update_credentials");
let response = self.client.patch(&url)
.headers(self.headers.clone())
.multipart(changes.into_form()?)
.send()?;
let status = response.status().clone();
if status.is_client_error() {
return Err(Error::Client(status));
} else if status.is_server_error() {
return Err(Error::Server(status));
}
deserialise(response)
}
pub fn new_status(&self, status: StatusBuilder) -> Result<Status> {
let response = self.client.post(&self.route("/api/v1/statuses"))
.headers(self.headers.clone())
.json(&status)
.send()?;
deserialise(response)
}
pub fn get_public_timeline(&self, local: bool) -> Result<Vec<Status>> {
let mut url = self.route("/api/v1/timelines/public");
if local {
url += "?local=1";
}
self.get(url)
}
pub fn get_tagged_timeline(&self, hashtag: String, local: bool) -> Result<Vec<Status>> {
let mut url = self.route("/api/v1/timelines/tag/");
url += &hashtag;
if local {
url += "?local=1";
}
self.get(url)
}
pub fn statuses<'a, S>(&self, id: &str, request: S) -> Result<Page<Status>>
where S: Into<Option<StatusesRequest<'a>>>
{
let mut url = format!("{}/api/v1/accounts/{}/statuses", self.base, id);
if let Some(request) = request.into() {
url = format!("{}{}", url, request.to_querystring());
}
let response = self.client.get(&url)
.headers(self.headers.clone())
.send()?;
Page::new(self, response)
}
pub fn relationships(&self, ids: &[&str]) -> Result<Page<Relationship>> {
let mut url = self.route("/api/v1/accounts/relationships?");
if ids.len() == 1 {
url += "id=";
url += &ids[0];
} else {
for id in ids {
url += "id[]=";
url += &id;
url += "&";
}
url.pop();
}
let response = self.client.get(&url)
.headers(self.headers.clone())
.send()?;
Page::new(self, response)
}
pub fn search_accounts(&self,
query: &str,
limit: Option<u64>,
following: bool)
-> Result<Page<Account>>
{
let url = format!("{}/api/v1/accounts/search?q={}&limit={}&following={}",
self.base,
query,
limit.unwrap_or(40),
following);
let response = self.client.get(&url)
.headers(self.headers.clone())
.send()?;
Page::new(self, response)
}
methods![get, post, delete,];
fn route(&self, url: &str) -> String {
let mut s = (*self.base).to_owned();
s += url;
s
}
pub fn media(&self, media_builder: MediaBuilder) -> Result<Attachment>
{
use reqwest::multipart::Form;
let mut form_data = Form::new()
.file("file", media_builder.file.as_ref())?;
if let Some(description) = media_builder.description {
form_data = form_data.text("description", description);
}
if let Some(focus) = media_builder.focus {
let string = format!("{},{}", focus.0, focus.1);
form_data = form_data.text("focus", string);
}
let response = self.client.post(&self.route("/api/v1/media"))
.headers(self.headers.clone())
.multipart(form_data)
.send()?;
let status = response.status().clone();
if status.is_client_error() {
return Err(Error::Client(status));
} else if status.is_server_error() {
return Err(Error::Server(status));
}
deserialise(response)
}
}
impl ops::Deref for Mastodon {
type Target = Data;
fn deref(&self) -> &Self::Target {
&self.data
}
}
macro_rules! from {
($($typ:ident, $variant:ident,)*) => {
$(
impl From<$typ> for Error {
fn from(from: $typ) -> Self {
use Error::*;
$variant(from)
}
}
)*
}
}
from! {
HttpError, Http,
IoError, Io,
SerdeError, Serde,
UrlError, Url,
}
fn deserialise<T: for<'de> serde::Deserialize<'de>>(mut response: Response)
-> Result<T>
{
use std::io::Read;
let mut vec = Vec::new();
response.read_to_end(&mut vec)?;
match json::from_slice(&vec) {
Ok(t) => Ok(t),
Err(e) => {
if let Ok(error) = json::from_slice(&vec) {
return Err(Error::Api(error));
}
Err(e.into())
},
}
}