Version 0.10.0-rc1

- Added the ability to handle paged entities like favourites and such.
  (Only favourites in prerelease)
- Added optional `source` and `moved` fields to `Account`.
- Added `Source` struct to match with the `Account.source` field.
- Added `CredientialsBuilder` struct for updating profile using
  `verify_credientials`.
- Attachment now handles being sent an empty object, which is converted
  to `None`.
- Added ombed data fields to `Card`.
- Added `version` and `urls` fields to `Instance`.
- Added `id`, `muting_notifications`, and `domain_blocking` to `Relationship`.
- Added `emojis`, `language`, and `pinned` fields to `Status`
- Added `Emoji` struct.
- Added `List` and `Mention` structs(matching routes not added yet).
- Added example that prints your profile.
master
Aaron Power 7 years ago
parent ab1e5f86f0
commit 4984dfaacf
  1. 1
      .gitignore
  2. 16
      CHANGELOG.md
  3. 4
      Cargo.toml
  4. 52
      examples/print_profile.rs
  5. 58
      src/entities/account.rs
  6. 19
      src/entities/attachment.rs
  7. 16
      src/entities/card.rs
  8. 4
      src/entities/instance.rs
  9. 5
      src/entities/list.rs
  10. 10
      src/entities/mention.rs
  11. 8
      src/entities/mod.rs
  12. 6
      src/entities/relationship.rs
  13. 17
      src/entities/status.rs
  14. 98
      src/lib.rs
  15. 76
      src/page.rs

1
.gitignore vendored

@ -1,3 +1,4 @@
target target
Cargo.lock Cargo.lock
.env .env
mastodon-data.toml

@ -0,0 +1,16 @@
# 0.10 (prerelease)
- Added the ability to handle paged entities like favourites and such.(Only favourites in prerelease)
- Added optional `source` and `moved` fields to `Account`.
- Added `Source` struct to match with the `Account.source` field.
- Added `CredientialsBuilder` struct for updating profile using
`verify_credientials`.
- Attachment now handles being sent an empty object, which is converted
to `None`.
- Added ombed data fields to `Card`.
- Added `version` and `urls` fields to `Instance`.
- Added `id`, `muting_notifications`, and `domain_blocking` to `Relationship`.
- Added `emojis`, `language`, and `pinned` fields to `Status`
- Added `Emoji` struct.
- Added `List` and `Mention` structs(matching routes not added yet).
- Added example that prints your profile.

@ -1,6 +1,6 @@
[package] [package]
name = "mammut" name = "mammut"
version = "0.9.1" version = "0.10.0-rc1"
description = "A wrapper around the Mastodon API." description = "A wrapper around the Mastodon API."
authors = ["Aaron Power <theaaronepower@gmail.com>"] authors = ["Aaron Power <theaaronepower@gmail.com>"]
@ -15,6 +15,7 @@ reqwest = "0.8"
serde = "1" serde = "1"
serde_json = "1" serde_json = "1"
serde_derive = "1" serde_derive = "1"
url = "1"
[dependencies.chrono] [dependencies.chrono]
version = "0.4" version = "0.4"
@ -22,3 +23,4 @@ features = ["serde"]
[dev-dependencies] [dev-dependencies]
dotenv = "0.10" dotenv = "0.10"
toml = "0.4"

@ -0,0 +1,52 @@
extern crate mammut;
extern crate toml;
use mammut::{Data, Mastodon, Registration};
use mammut::apps::{AppBuilder, Scopes};
use std::io;
use std::fs::File;
use std::io::prelude::*;
fn main() {
let mastodon = match File::open("mastodon-data.toml") {
Ok(mut file) => {
let mut config = String::new();
file.read_to_string(&mut config).unwrap();
let data: Data = toml::from_str(&config).unwrap();
Mastodon::from_data(data)
},
Err(_) => register(),
};
let you = mastodon.verify_credentials().unwrap();
println!("{:#?}", you);
}
fn register() -> Mastodon {
let app = AppBuilder {
client_name: "mammut-examples",
redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
scopes: Scopes::Read,
website: Some("https://github.com/Aaronepower/mammut"),
};
let mut registration = Registration::new("https://mastodon.social");
registration.register(app).unwrap();;
let url = registration.authorise().unwrap();
println!("Click this link to authorize on Mastodon: {}", url);
println!("Paste the returned authorization code: ");
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
let code = input.trim();
let mastodon = registration.create_access_token(code.to_string()).unwrap();
// Save app data for using on the next run.
let toml = toml::to_string(&*mastodon).unwrap();
let mut file = File::create("mastodon-data.toml").unwrap();
file.write_all(toml.as_bytes()).unwrap();
mastodon
}

@ -1,6 +1,9 @@
//! A module containing everything relating to a account returned from the api. //! A module containing everything relating to a account returned from the api.
use chrono::prelude::*; use chrono::prelude::*;
use reqwest::multipart::Form;
use ::Result;
use std::path::Path;
/// A struct representing an Account. /// A struct representing an Account.
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
@ -36,4 +39,59 @@ pub struct Account {
pub url: String, pub url: String,
/// The username of the account. /// The username of the account.
pub username: String, pub username: String,
/// An extra attribute given from `verify_credentials` giving defaults about
/// a user
pub source: Option<Source>,
/// If the owner decided to switch accounts, new account is in
/// this attribute
pub moved: Option<String>,
}
/// An extra object given from `verify_credentials` giving defaults about a user
#[derive(Debug, Clone, Deserialize)]
pub struct Source {
privacy: ::status_builder::Visibility,
sensitive: bool,
note: String,
}
pub struct CredientialsBuilder<'a> {
display_name: Option<&'a str>,
note: Option<&'a str>,
avatar: Option<&'a Path>,
header: Option<&'a Path>,
}
impl<'a> CredientialsBuilder<'a> {
pub fn into_form(self) -> Result<Form> {
let mut form = Form::new();
macro_rules! add_to_form {
($key:ident : Text; $($rest:tt)*) => {{
if let Some(val) = self.$key {
form = form.text(stringify!($key), val.to_owned());
}
add_to_form!{$($rest)*}
}};
($key:ident : File; $($rest:tt)*) => {{
if let Some(val) = self.$key {
form = form.file(stringify!($key), val)?;
}
add_to_form!{$($rest)*}
}};
() => {}
}
add_to_form! {
display_name: Text;
note: Text;
avatar: File;
header: File;
}
Ok(form)
}
} }

@ -1,4 +1,6 @@
//! Module containing everything related to media attachements. //! Module containing everything related to media attachements.
use serde::{Deserialize, Deserializer};
use super::Empty;
/// A struct representing a media attachment. /// A struct representing a media attachment.
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
@ -18,11 +20,28 @@ pub struct Attachment {
/// (only present on local images) /// (only present on local images)
pub text_url: Option<String>, pub text_url: Option<String>,
/// Meta information about the attachment. /// Meta information about the attachment.
#[serde(deserialize_with="empty_as_none")]
pub meta: Option<Meta>, pub meta: Option<Meta>,
/// Noop will be removed. /// Noop will be removed.
pub description: Option<String>, pub description: Option<String>,
} }
fn empty_as_none<'de, D: Deserializer<'de>>(val: D)
-> Result<Option<Meta>, D::Error>
{
#[derive(Deserialize)]
#[serde(untagged)]
enum EmptyOrMeta {
Empty(Empty),
Meta(Meta),
}
Ok(match EmptyOrMeta::deserialize(val)? {
EmptyOrMeta::Empty(_) => None,
EmptyOrMeta::Meta(m) => Some(m),
})
}
/// Information about the attachment itself. /// Information about the attachment itself.
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
pub struct Meta { pub struct Meta {

@ -10,5 +10,19 @@ pub struct Card {
/// The card description. /// The card description.
pub description: String, pub description: String,
/// The image associated with the card, if any. /// The image associated with the card, if any.
pub image: String, pub image: Option<String>,
/// OEmbed data
author_name: Option<String>,
/// OEmbed data
author_url: Option<String>,
/// OEmbed data
provider_name: Option<String>,
/// OEmbed data
provider_url: Option<String>,
/// OEmbed data
html: Option<String>,
/// OEmbed data
width: Option<String>,
/// OEmbed data
height: Option<String>,
} }

@ -12,4 +12,8 @@ pub struct Instance {
/// An email address which can be used to contact the /// An email address which can be used to contact the
/// instance administrator. /// instance administrator.
pub email: String, pub email: String,
/// The Mastodon version used by instance.
pub version: String,
/// `streaming_api`
pub urls: Vec<String>,
} }

@ -0,0 +1,5 @@
#[derive(Clone, Debug, Deserialize)]
pub struct List {
id: String,
title: String,
}

@ -0,0 +1,10 @@
pub struct Mention {
/// URL of user's profile (can be remote)
pub url: String,
/// The username of the account
pub username: String,
/// Equals username for local users, includes `@domain` for remote ones
pub acct: String,
/// Account ID
pub id: String,
}

@ -3,6 +3,8 @@ pub mod attachment;
pub mod card; pub mod card;
pub mod context; pub mod context;
pub mod instance; pub mod instance;
pub mod list;
pub mod mention;
pub mod notification; pub mod notification;
pub mod relationship; pub mod relationship;
pub mod report; pub mod report;
@ -17,14 +19,16 @@ pub mod prelude {
//! The purpose of this module is to alleviate imports of many common structs //! The purpose of this module is to alleviate imports of many common structs
//! by adding a glob import to the top of mastodon heavy modules: //! by adding a glob import to the top of mastodon heavy modules:
pub use super::Empty; pub use super::Empty;
pub use super::account::Account; pub use super::account::{Account, CredientialsBuilder, Source};
pub use super::attachment::{Attachment, MediaType}; pub use super::attachment::{Attachment, MediaType};
pub use super::card::Card; pub use super::card::Card;
pub use super::context::Context; pub use super::context::Context;
pub use super::instance::Instance; pub use super::instance::Instance;
pub use super::list::List;
pub use super::mention::Mention;
pub use super::notification::Notification; pub use super::notification::Notification;
pub use super::relationship::Relationship; pub use super::relationship::Relationship;
pub use super::report::Report; pub use super::report::Report;
pub use super::search_result::SearchResult; pub use super::search_result::SearchResult;
pub use super::status::{Status, Application}; pub use super::status::{Application, Emoji, Status};
} }

@ -4,6 +4,8 @@
/// A struct containing information about a relationship with another account. /// A struct containing information about a relationship with another account.
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct Relationship { pub struct Relationship {
/// Target account id
pub id: String,
/// Whether the application client follows the account. /// Whether the application client follows the account.
pub following: bool, pub following: bool,
/// Whether the account follows the application client. /// Whether the account follows the application client.
@ -14,4 +16,8 @@ pub struct Relationship {
pub muting: bool, pub muting: bool,
/// Whether the application client has requested to follow the account. /// Whether the application client has requested to follow the account.
pub requested: bool, pub requested: bool,
/// Whether the user is also muting notifications
pub muting_notifications: bool,
/// Whether the user is currently blocking the accounts's domain
pub domain_blocking: bool,
} }

@ -28,6 +28,8 @@ pub struct Status {
pub content: String, pub content: String,
/// The time the status was created. /// The time the status was created.
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
/// An array of Emoji
pub emojis: Vec<Emoji>,
/// The number of reblogs for the status. /// The number of reblogs for the status.
pub reblogs_count: u64, pub reblogs_count: u64,
/// The number of favourites for the status. /// The number of favourites for the status.
@ -51,6 +53,10 @@ pub struct Status {
pub tags: Vec<Tag>, pub tags: Vec<Tag>,
/// Name of application used to post status. /// Name of application used to post status.
pub application: Option<Application>, pub application: Option<Application>,
/// The detected language for the status, if detected.
pub language: Option<String>,
/// Whether this is the pinned status for the account that posted it.
pub pinned: Option<bool>,
} }
/// A mention of another user. /// A mention of another user.
@ -66,6 +72,17 @@ pub struct Mention {
pub id: String, pub id: String,
} }
/// Struct representing an emoji within text.
#[derive(Clone, Debug, Deserialize)]
pub struct Emoji {
/// The shortcode of the emoji
pub shortcode: String,
/// URL to the emoji static image
pub static_url: String,
/// URL to the emoji image
pub url: String,
}
/// Hashtags in the status. /// Hashtags in the status.
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct Tag { pub struct Tag {

@ -40,6 +40,7 @@
extern crate chrono; extern crate chrono;
extern crate reqwest; extern crate reqwest;
extern crate serde; extern crate serde;
extern crate url;
/// Registering your App /// Registering your App
pub mod apps; pub mod apps;
@ -49,6 +50,8 @@ pub mod status_builder;
pub mod entities; pub mod entities;
/// Registering your app. /// Registering your app.
pub mod registration; pub mod registration;
/// Handling multiple pages of entities.
pub mod page;
use std::borrow::Cow; use std::borrow::Cow;
use std::error::Error as StdError; use std::error::Error as StdError;
@ -60,9 +63,11 @@ use json::Error as SerdeError;
use reqwest::Error as HttpError; use reqwest::Error as HttpError;
use reqwest::{Client, Response, StatusCode}; use reqwest::{Client, Response, StatusCode};
use reqwest::header::{Authorization, Bearer, Headers}; use reqwest::header::{Authorization, Bearer, Headers};
use url::ParseError as UrlError;
use entities::prelude::*; use entities::prelude::*;
pub use status_builder::StatusBuilder; pub use status_builder::StatusBuilder;
use page::Page;
pub use registration::Registration; pub use registration::Registration;
/// Convience type over `std::result::Result` with `Error` as the error type. /// Convience type over `std::result::Result` with `Error` as the error type.
@ -78,12 +83,34 @@ macro_rules! methods {
.headers(self.headers.clone()) .headers(self.headers.clone())
.send()?; .send()?;
json_convert_response(response) deserialise(response)
} }
)+ )+
}; };
} }
macro_rules! paged_routes {
(($method:ident) $name:ident: $url:expr => $ret:ty, $($rest:tt)*) => {
/// Equivalent to `/api/v1/
#[doc = $url]
/// `
///
#[doc = "# Errors"]
/// If `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)
}
route!{$($rest)*}
};
}
macro_rules! route { macro_rules! route {
((post multipart ($($param:ident: $typ:ty,)*)) $name:ident: $url:expr => $ret:ty, $($rest:tt)*) => { ((post multipart ($($param:ident: $typ:ty,)*)) $name:ident: $url:expr => $ret:ty, $($rest:tt)*) => {
@ -114,13 +141,13 @@ macro_rules! route {
return Err(Error::Server(status)); return Err(Error::Server(status));
} }
json_convert_response(response) deserialise(response)
} }
route!{$($rest)*} route!{$($rest)*}
}; };
((post ($($param:ident: $typ:ty,)*)) $name:ident: $url:expr => $ret:ty, $($rest:tt)*) => { (($method:ident ($($param:ident: $typ:ty,)*)) $name:ident: $url:expr => $ret:ty, $($rest:tt)*) => {
/// Equivalent to `/api/v1/ /// Equivalent to `/api/v1/
#[doc = $url] #[doc = $url]
/// ` /// `
@ -135,7 +162,7 @@ macro_rules! route {
)* )*
}); });
let response = self.client.post(&self.route(concat!("/api/v1/", $url))) let response = self.client.$method(&self.route(concat!("/api/v1/", $url)))
.headers(self.headers.clone()) .headers(self.headers.clone())
.json(&form_data) .json(&form_data)
.send()?; .send()?;
@ -148,7 +175,7 @@ macro_rules! route {
return Err(Error::Server(status)); return Err(Error::Server(status));
} }
json_convert_response(response) deserialise(response)
} }
route!{$($rest)*} route!{$($rest)*}
@ -231,6 +258,9 @@ pub enum Error {
/// Wrapper around the `std::io::Error` struct. /// Wrapper around the `std::io::Error` struct.
#[serde(skip_deserializing)] #[serde(skip_deserializing)]
Io(IoError), Io(IoError),
/// Wrapper around the `url::ParseError` struct.
#[serde(skip_deserializing)]
Url(UrlError),
/// Missing Client Id. /// Missing Client Id.
#[serde(skip_deserializing)] #[serde(skip_deserializing)]
ClientIdRequired, ClientIdRequired,
@ -263,6 +293,7 @@ impl StdError for Error {
Error::Serde(ref e) => e.description(), Error::Serde(ref e) => e.description(),
Error::Http(ref e) => e.description(), Error::Http(ref e) => e.description(),
Error::Io(ref e) => e.description(), Error::Io(ref e) => e.description(),
Error::Url(ref e) => e.description(),
Error::Client(ref status) | Error::Server(ref status) => { Error::Client(ref status) | Error::Server(ref status) => {
status.canonical_reason().unwrap_or("Unknown Status code") status.canonical_reason().unwrap_or("Unknown Status code")
}, },
@ -323,22 +354,30 @@ impl Mastodon {
} }
} }
paged_routes! {
(get) favourites: "favourites" => Status,
}
route! { route! {
(get) verify: "accounts/verify_credentials" => Account, (delete (domain: String,)) unblock_domain: "domain_blocks" => Empty,
(get) blocks: "blocks" => Vec<Account>, (get) blocks: "blocks" => Vec<Account>,
(get) domain_blocks: "domain_blocks" => Vec<String>,
(get) follow_requests: "follow_requests" => Vec<Account>, (get) follow_requests: "follow_requests" => Vec<Account>,
(get) get_home_timeline: "timelines/home" => Vec<Status>,
(get) instance: "instance" => Instance,
(get) get_emojis: "custom_emojis" => Vec<Emoji>,
(get) mutes: "mutes" => Vec<Account>, (get) mutes: "mutes" => Vec<Account>,
(get) notifications: "notifications" => Vec<Notification>, (get) notifications: "notifications" => Vec<Notification>,
(get) reports: "reports" => Vec<Report>, (get) reports: "reports" => Vec<Report>,
(get) get_home_timeline: "timelines/home" => Vec<Status>, (get) verify_credentials: "accounts/verify_credentials" => Account,
(post (id: u64,)) allow_follow_request: "accounts/follow_requests/authorize" => Empty, (post (account_id: u64, status_ids: Vec<u64>, comment: String,)) report: "reports" => Report,
(post (domain: String,)) block_domain: "domain_blocks" => Empty,
(post (id: u64,)) authorize_follow_request: "accounts/follow_requests/authorize" => Empty,
(post (id: u64,)) reject_follow_request: "accounts/follow_requests/reject" => Empty, (post (id: u64,)) 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 (uri: Cow<'static, str>,)) follows: "follows" => Account,
(post) clear_notifications: "notifications/clear" => Empty,
(post multipart (file: Cow<'static, str>,)) media: "media" => Attachment, (post multipart (file: Cow<'static, str>,)) media: "media" => Attachment,
(post (account_id: u64, status_ids: Vec<u64>, comment: String,)) report: (post) clear_notifications: "notifications/clear" => Empty,
"reports" => Report,
(post (q: String, resolve: bool,)) search: "search" => SearchResult,
} }
route_id! { route_id! {
@ -364,6 +403,27 @@ impl Mastodon {
(delete) delete_status: "statuses/{}" => Empty, (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)
}
/// Post a new status to the account. /// Post a new status to the account.
pub fn new_status(&self, status: StatusBuilder) -> Result<Status> { pub fn new_status(&self, status: StatusBuilder) -> Result<Status> {
@ -372,7 +432,7 @@ impl Mastodon {
.json(&status) .json(&status)
.send()?; .send()?;
json_convert_response(response) deserialise(response)
} }
/// Get the federated timeline for the instance. /// Get the federated timeline for the instance.
@ -452,11 +512,6 @@ impl Mastodon {
self.get(format!("{}/api/v1/accounts/search?q={}", self.base, query)) self.get(format!("{}/api/v1/accounts/search?q={}", self.base, query))
} }
/// Returns the current Instance.
pub fn instance(&self) -> Result<Instance> {
self.get(self.route("/api/v1/instance"))
}
methods![get, post, delete,]; methods![get, post, delete,];
fn route(&self, url: &str) -> String { fn route(&self, url: &str) -> String {
@ -488,14 +543,17 @@ macro_rules! from {
} }
from! { from! {
SerdeError, Serde,
HttpError, Http, HttpError, Http,
IoError, Io, IoError, Io,
SerdeError, Serde,
UrlError, Url,
} }
// Convert the HTTP response body from JSON. Pass up deserialization errors // Convert the HTTP response body from JSON. Pass up deserialization errors
// transparently. // transparently.
fn json_convert_response<T: for<'de> serde::Deserialize<'de>>(mut response: Response) -> Result<T> { fn deserialise<T: for<'de> serde::Deserialize<'de>>(mut response: Response)
-> Result<T>
{
use std::io::Read; use std::io::Read;
let mut vec = Vec::new(); let mut vec = Vec::new();

@ -0,0 +1,76 @@
use super::{Mastodon, Result, deserialise};
use reqwest::Response;
use reqwest::header::{Link, RelationType};
use serde::Deserialize;
use url::Url;
pub struct Page<'a, T: for<'de> Deserialize<'de>> {
mastodon: &'a Mastodon,
next: Option<Url>,
prev: Option<Url>,
/// Initial set of items
pub initial_items: Vec<T>,
}
macro_rules! pages {
($($direction:ident: $fun:ident),*) => {
$(
pub fn $fun(&mut self) -> Result<Option<Vec<T>>> {
let url = match self.$direction.take() {
Some(s) => s,
None => return Ok(None),
};
let response = self.mastodon.client.get(url)
.headers(self.mastodon.headers.clone())
.send()?;
let (prev, next) = get_links(&response)?;
self.next = next;
self.prev = prev;
deserialise(response)
}
)*
}
}
impl<'a, T: for<'de> Deserialize<'de>> Page<'a, T> {
pub fn new(mastodon: &'a Mastodon, response: Response) -> Result<Self> {
let (prev, next) = get_links(&response)?;
Ok(Page {
initial_items: deserialise(response)?,
next,
prev,
mastodon
})
}
pages! {
next: next_page,
prev: prev_page
}
}
fn get_links(response: &Response) -> Result<(Option<Url>, Option<Url>)> {
let mut prev = None;
let mut next = None;
if let Some(link_header) = response.headers().get::<Link>() {
for value in link_header.values() {
if let Some(relations) = value.rel() {
if relations.contains(&RelationType::Next) {
next = Some(Url::parse(value.link())?);
}
if relations.contains(&RelationType::Prev) {
prev = Some(Url::parse(value.link())?);
}
}
}
}
Ok((prev, next))
}
Loading…
Cancel
Save