0.3.0 Redone registration api, added debug/clone

master
Aaron Power 8 years ago
parent 686c5129f3
commit a528624dc3
  1. 2
      Cargo.toml
  2. 29
      README.md
  3. 2
      src/entities/account.rs
  4. 2
      src/entities/card.rs
  5. 2
      src/entities/context.rs
  6. 2
      src/entities/instance.rs
  7. 4
      src/entities/notification.rs
  8. 2
      src/entities/relationship.rs
  9. 2
      src/entities/report.rs
  10. 2
      src/entities/search_result.rs
  11. 10
      src/entities/status.rs
  12. 234
      src/lib.rs
  13. 128
      src/registration.rs

@ -1,6 +1,6 @@
[package] [package]
name = "mammut" name = "mammut"
version = "0.2.0" version = "0.3.0"
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>"]
license = "MIT/Apache-2.0" license = "MIT/Apache-2.0"

@ -3,3 +3,32 @@
### [Documentation](https://docs.rs/mammut/) ### [Documentation](https://docs.rs/mammut/)
A wrapper around the [API](https://github.com/tootsuite/mastodon/blob/master/docs/Using-the-API/API.md#tag) for [Mastodon](https://mastodon.social/) A wrapper around the [API](https://github.com/tootsuite/mastodon/blob/master/docs/Using-the-API/API.md#tag) for [Mastodon](https://mastodon.social/)
```rust
# extern crate mammut;
# fn main() {
# try().unwrap();
# }
# fn try() -> mammut::Result<()> {
use mammut::Registration;
use mammut::apps::{AppBuilder, Scope};
let app = AppBuilder {
client_name: "mammut_test",
redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
scopes: Scope::Read,
website: None,
};
let mut registration = Registration::new("https://mastodon.social")?;
registration.register(app)?;
let url = registration.authorise()?;
// Here you now need to open the url in the browser
// And handle a the redirect url coming back with the code.
let code = String::from("RETURNED_FROM_BROWSER");
let mastodon = registration.create_access_token(code)?;
println!("{:?}", mastodon.get_home_timeline()?);
# Ok(())
# }
```

@ -1,5 +1,5 @@
use chrono::prelude::*; use chrono::prelude::*;
#[derive(Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct Account { pub struct Account {
pub id: u64, pub id: u64,
pub username: String, pub username: String,

@ -1,4 +1,4 @@
#[derive(Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct Card { pub struct Card {
pub url: String, pub url: String,
pub title: String, pub title: String,

@ -1,6 +1,6 @@
use super::status::Status; use super::status::Status;
#[derive(Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct Context { pub struct Context {
pub ancestors: Vec<Status>, pub ancestors: Vec<Status>,
pub descendants: Vec<Status>, pub descendants: Vec<Status>,

@ -1,4 +1,4 @@
#[derive(Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct Instance { pub struct Instance {
pub uri: String, pub uri: String,
pub title: String, pub title: String,

@ -2,7 +2,7 @@ use chrono::prelude::*;
use super::account::Account; use super::account::Account;
use super::status::Status; use super::status::Status;
#[derive(Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct Notification { pub struct Notification {
pub id: u64, pub id: u64,
pub notification_type: NotificationType, pub notification_type: NotificationType,
@ -11,7 +11,7 @@ pub struct Notification {
pub status: Option<Status>, pub status: Option<Status>,
} }
#[derive(Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub enum NotificationType { pub enum NotificationType {
Mention, Mention,
Reblog, Reblog,

@ -1,4 +1,4 @@
#[derive(Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct Relationship { pub struct Relationship {
pub following: bool, pub following: bool,
pub followed_by: bool, pub followed_by: bool,

@ -1,4 +1,4 @@
#[derive(Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct Report { pub struct Report {
pub id: u64, pub id: u64,
pub action_taken: String, pub action_taken: String,

@ -1,6 +1,6 @@
use super::prelude::{Account, Status}; use super::prelude::{Account, Status};
#[derive(Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct SearchResult { pub struct SearchResult {
pub accounts: Vec<Account>, pub accounts: Vec<Account>,
pub statuses: Vec<Status>, pub statuses: Vec<Status>,

@ -2,7 +2,7 @@ use chrono::prelude::*;
use super::prelude::*; use super::prelude::*;
use status_builder::Visibility; use status_builder::Visibility;
#[derive(Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct Status { pub struct Status {
pub id: i64, pub id: i64,
pub uri: String, pub uri: String,
@ -26,7 +26,7 @@ pub struct Status {
pub application: Application, pub application: Application,
} }
#[derive(Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct Mention { pub struct Mention {
pub url: String, pub url: String,
pub username: String, pub username: String,
@ -34,16 +34,14 @@ pub struct Mention {
pub id: u64, pub id: u64,
} }
#[derive(Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct Tag { pub struct Tag {
pub name: String, pub name: String,
pub url: String, pub url: String,
} }
#[derive(Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct Application { pub struct Application {
pub name: String, pub name: String,
pub website: String, pub website: String,
} }

@ -2,6 +2,35 @@
//! //!
//! Most of the api is documented on [Mastodon's //! Most of the api is documented on [Mastodon's
//! github](https://github.com/tootsuite/mastodon/blob/master/docs/Using-the-API/API.md#tag) //! github](https://github.com/tootsuite/mastodon/blob/master/docs/Using-the-API/API.md#tag)
//!
//! ```no_run
//! # extern crate mammut;
//! # fn main() {
//! # try().unwrap();
//! # }
//! # fn try() -> mammut::Result<()> {
//! use mammut::Registration;
//! use mammut::apps::{AppBuilder, Scope};
//!
//! let app = AppBuilder {
//! client_name: "mammut_test",
//! redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
//! scopes: Scope::Read,
//! website: None,
//! };
//!
//! let mut registration = Registration::new("https://mastodon.social")?;
//! registration.register(app)?;
//! let url = registration.authorise()?;
//! // Here you now need to open the url in the browser
//! // And handle a the redirect url coming back with the code.
//! let code = String::from("RETURNED_FROM_BROWSER");
//! let mastodon = registration.create_access_token(code)?;
//!
//! println!("{:?}", mastodon.get_home_timeline()?);
//! # Ok(())
//! # }
//! ```
#[cfg_attr(test, deny(warnings))] #[cfg_attr(test, deny(warnings))]
@ -17,6 +46,10 @@ pub mod apps;
pub mod status_builder; pub mod status_builder;
/// Entities returned from the API /// Entities returned from the API
pub mod entities; pub mod entities;
/// Registering your app.
pub mod registration;
use std::ops;
use json::Error as SerdeError; use json::Error as SerdeError;
use reqwest::Error as HttpError; use reqwest::Error as HttpError;
@ -26,6 +59,7 @@ use reqwest::header::{Authorization, Bearer, Headers};
use entities::prelude::*; use entities::prelude::*;
use status_builder::StatusBuilder; use status_builder::StatusBuilder;
pub use registration::Registration;
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
macro_rules! methods { macro_rules! methods {
@ -35,7 +69,7 @@ macro_rules! methods {
-> Result<T> -> Result<T>
{ {
Ok(self.client.$method(&url) Ok(self.client.$method(&url)
.headers(self.access_token.clone().unwrap()) .headers(self.headers.clone())
.send()? .send()?
.json()?) .json()?)
} }
@ -53,7 +87,6 @@ macro_rules! route {
#[doc = "# Errors"] #[doc = "# Errors"]
/// If `access_token` is not set. /// If `access_token` is not set.
pub fn $name(&self, $($param: $typ,)*) -> Result<$ret> { pub fn $name(&self, $($param: $typ,)*) -> Result<$ret> {
self.has_access_token()?;
let form_data = json!({ let form_data = json!({
$( $(
@ -62,7 +95,7 @@ macro_rules! route {
}); });
Ok(self.client.post(&self.route(concat!("/api/v1/", $url))) Ok(self.client.post(&self.route(concat!("/api/v1/", $url)))
.headers(self.access_token.clone().unwrap()) .headers(self.headers.clone())
.form(&form_data) .form(&form_data)
.send()? .send()?
.json()?) .json()?)
@ -78,8 +111,6 @@ macro_rules! route {
#[doc = "# Errors"] #[doc = "# Errors"]
/// If `access_token` is not set. /// If `access_token` is not set.
pub fn $name(&self) -> Result<$ret> { pub fn $name(&self) -> Result<$ret> {
self.has_access_token()?;
self.$method(self.route(concat!("/api/v1/", $url))) self.$method(self.route(concat!("/api/v1/", $url)))
} }
@ -100,9 +131,6 @@ macro_rules! route_id {
#[doc = "# Errors"] #[doc = "# Errors"]
/// If `access_token` is not set. /// If `access_token` is not set.
pub fn $name(&self, id: u64) -> Result<$ret> { pub fn $name(&self, id: u64) -> Result<$ret> {
self.has_access_token()?;
self.$method(self.route(&format!(concat!("/api/v1/", $url), id))) self.$method(self.route(&format!(concat!("/api/v1/", $url), id)))
} }
)* )*
@ -112,26 +140,21 @@ macro_rules! route_id {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Mastodon { pub struct Mastodon {
base_url: String,
client: Client, client: Client,
client_id: Option<String>, headers: Headers,
client_secret: Option<String>, /// Raw data about your mastodon instance.
redirect_uri: Option<String>, pub data: Data
access_token: Option<Headers>,
id: Option<u64>,
} }
#[derive(Deserialize)] /// Raw data about mastodon app. Save `Data` using `serde` to prevent needing
struct OAuth { /// to authenticate on every run.
client_id: String, #[derive(Clone, Debug, Deserialize, Serialize)]
client_secret: String, pub struct Data {
id: u64, pub base: String,
redirect_uri: String, pub client_id: String,
} pub client_secret: String,
pub redirect: String,
#[derive(Deserialize)] pub token: String,
struct AccessToken {
access_token: String,
} }
#[derive(Debug)] #[derive(Debug)]
@ -144,104 +167,43 @@ pub enum Error {
} }
impl Mastodon { impl Mastodon {
/// Inits new Mastodon object. `base_url` is expected in the following fn from_registration(base: String,
/// format `https://mastodon.social` with no leading forward slash. client_id: String,
/// client_secret: String,
/// ``` redirect: String,
/// use mammut::Mastodon; token: String,
/// client: Client)
/// let mastodon = Mastodon::new("https://mastodon.social").unwrap(); -> Self
/// ``` {
pub fn new<I: Into<String>>(base_url: I) -> Result<Self> { let data = Data {
Ok(Mastodon { base: base,
base_url: base_url.into(), client_id: client_id,
client: Client::new()?, client_secret: client_secret,
client_id: None, redirect: redirect,
client_secret: None, token: token,
redirect_uri: None,
access_token: None,
id: None,
})
}
/// Register the application with the server from `base_url`.
///
/// ```
/// # extern crate mammut;
/// # fn main() {
/// # try().unwrap();
/// # }
///
/// # fn try() -> mammut::Result<()> {
/// use mammut::Mastodon;
/// use mammut::apps::{AppBuilder, Scope};
///
/// let app = AppBuilder {
/// client_name: "mammut_test",
/// redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
/// scopes: Scope::Read,
/// website: None,
/// };
///
/// let mut mastodon = Mastodon::new("https://mastodon.social")?;
/// mastodon.register(app)?;
/// # Ok(())
/// # }
/// ```
pub fn register(&mut self, app_builder: apps::AppBuilder) -> Result<()> {
let url = self.route("/api/v1/apps");
let app: OAuth = self.client.post(&url).form(&app_builder).send()?.json()?;
self.id = Some(app.id);
self.client_id = Some(app.client_id);
self.client_secret = Some(app.client_secret);
self.redirect_uri = Some(app.redirect_uri);
Ok(())
}
/// Returns the full url needed for authorisation. This needs to be opened };
/// in a browser.
pub fn authorise(&mut self) -> Result<String> {
self.is_registered()?;
let url = format!( let mut headers = Headers::new();
"{}/oauth/authorize?client_id={}&redirect_uri={}&response_type=code", headers.set(Authorization(Bearer { token: data.token.clone() }));
self.base_url,
self.client_id.clone().unwrap(),
self.redirect_uri.clone().unwrap(),
);
Ok(url) Mastodon {
client: client,
headers: headers,
data: data,
} }
/// Requires the authorisation code returned from the `redirect_url`
pub fn get_access_token(&mut self, code: String) -> Result<()> {
self.is_registered()?;
let url = format!(
"{}/oauth/token?client_id={}&client_secret={}&code={}&grant_type=authorization_code&redirect_uri={}",
self.base_url,
self.client_id.clone().unwrap(),
self.client_secret.clone().unwrap(),
code,
self.redirect_uri.clone().unwrap(),
);
let access_token: AccessToken = self.client.post(&url).send()?.json()?;
self.set_access_token(access_token.access_token);
Ok(())
} }
/// Set `access_token` required to use any method about the user. /// Creates a mastodon instance from the data struct.
fn set_access_token(&mut self, access_token: String) { pub fn from_data(data: Data) -> Result<Self> {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.set(Authorization(Bearer { token: data.token.clone() }));
headers.set(Authorization(Bearer { token: access_token })); Ok(Mastodon {
client: Client::new()?,
self.access_token = Some(headers); headers: headers,
data: data,
})
} }
route! { route! {
@ -287,8 +249,6 @@ impl Mastodon {
} }
pub fn get_public_timeline(&self, local: bool) -> Result<Vec<Status>> { pub fn get_public_timeline(&self, local: bool) -> Result<Vec<Status>> {
self.has_access_token()?;
let mut url = self.route("/api/v1/timelines/public"); let mut url = self.route("/api/v1/timelines/public");
if local { if local {
@ -299,8 +259,6 @@ impl Mastodon {
} }
pub fn get_tagged_timeline(&self, hashtag: String, local: bool) -> Result<Vec<Status>> { pub fn get_tagged_timeline(&self, hashtag: String, local: bool) -> Result<Vec<Status>> {
self.has_access_token()?;
let mut url = self.route("/api/v1/timelines/tag/"); let mut url = self.route("/api/v1/timelines/tag/");
url += &hashtag; url += &hashtag;
@ -314,8 +272,7 @@ impl Mastodon {
pub fn statuses(&self, id: u64, only_media: bool, exclude_replies: bool) pub fn statuses(&self, id: u64, only_media: bool, exclude_replies: bool)
-> Result<Vec<Status>> -> Result<Vec<Status>>
{ {
self.has_access_token()?; let mut url = format!("{}/api/v1/accounts/{}/statuses", self.base, id);
let mut url = format!("{}/api/v1/accounts/{}/statuses", self.base_url, id);
if only_media { if only_media {
url += "?only_media=1"; url += "?only_media=1";
@ -336,8 +293,6 @@ impl Mastodon {
pub fn relationships(&self, ids: &[u64]) -> Result<Vec<Relationship>> { pub fn relationships(&self, ids: &[u64]) -> Result<Vec<Relationship>> {
self.has_access_token()?;
let mut url = self.route("/api/v1/accounts/relationships?"); let mut url = self.route("/api/v1/accounts/relationships?");
if ids.len() == 1 { if ids.len() == 1 {
@ -357,44 +312,29 @@ impl Mastodon {
// TODO: Add a limit fn // TODO: Add a limit fn
pub fn search_accounts(&self, query: &str) -> Result<Vec<Account>> { pub fn search_accounts(&self, query: &str) -> Result<Vec<Account>> {
self.has_access_token()?; self.get(format!("{}/api/v1/accounts/search?q={}", self.base, query))
self.get(format!("{}/api/v1/accounts/search?q={}", self.base_url, query))
} }
pub fn instance(&self) -> Result<Instance> { pub fn instance(&self) -> Result<Instance> {
self.is_registered()?;
self.get(self.route("/api/v1/instance")) self.get(self.route("/api/v1/instance"))
} }
fn has_access_token(&self) -> Result<()> {
if self.access_token.is_none() {
Err(Error::AccessTokenRequired)
} else {
Ok(())
}
}
fn is_registered(&self) -> Result<()> {
if self.client_id.is_none() {
Err(Error::ClientIdRequired)
} else if self.client_secret.is_none() {
Err(Error::ClientSecretRequired)
} else {
Ok(())
}
}
methods![get, post, delete,]; methods![get, post, delete,];
fn route(&self, url: &str) -> String { fn route(&self, url: &str) -> String {
let mut s = self.base_url.clone(); let mut s = self.base.clone();
s += url; s += url;
s s
} }
} }
impl ops::Deref for Mastodon {
type Target = Data;
fn deref(&self) -> &Self::Target {
&self.data
}
}
macro_rules! from { macro_rules! from {
($($typ:ident, $variant:ident,)*) => { ($($typ:ident, $variant:ident,)*) => {

@ -0,0 +1,128 @@
use reqwest::Client;
use super::{Error, Mastodon, Result};
use apps::AppBuilder;
/// Handles registering your mastodon app to your instance. It is recommended
/// you cache your data struct to avoid registering on every run.
pub struct Registration {
base: String,
client: Client,
client_id: Option<String>,
client_secret: Option<String>,
redirect: Option<String>,
}
#[derive(Deserialize)]
struct OAuth {
client_id: String,
client_secret: String,
redirect_uri: String,
}
#[derive(Deserialize)]
struct AccessToken {
access_token: String,
}
impl Registration {
pub fn new<I: Into<String>>(base: I) -> Result<Self> {
Ok(Registration {
base: base.into(),
client: Client::new()?,
client_id: None,
client_secret: None,
redirect: None,
})
}
/// Register the application with the server from the `base` url.
///
/// ```no_run
/// # extern crate mammut;
/// # fn main() {
/// # try().unwrap();
/// # }
/// # fn try() -> mammut::Result<()> {
/// use mammut::Registration;
/// use mammut::apps::{AppBuilder, Scope};
///
/// let app = AppBuilder {
/// client_name: "mammut_test",
/// redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
/// scopes: Scope::Read,
/// website: None,
/// };
///
/// let mut registration = Registration::new("https://mastodon.social")?;
/// registration.register(app)?;
/// let url = registration.authorise()?;
/// // Here you now need to open the url in the browser
/// // And handle a the redirect url coming back with the code.
/// let code = String::from("RETURNED_FROM_BROWSER");
/// let mastodon = registration.create_access_token(code)?;
///
/// println!("{:?}", mastodon.get_home_timeline()?);
/// # Ok(())
/// # }
/// ```
pub fn register(&mut self, app_builder: AppBuilder) -> Result<()> {
let url = format!("{}/api/v1/apps", self.base);
let app: OAuth = self.client.post(&url).form(&app_builder).send()?.json()?;
self.client_id = Some(app.client_id);
self.client_secret = Some(app.client_secret);
self.redirect = Some(app.redirect_uri);
Ok(())
}
/// Returns the full url needed for authorisation. This needs to be opened
/// in a browser.
pub fn authorise(&mut self) -> Result<String> {
self.is_registered()?;
let url = format!(
"{}/oauth/authorize?client_id={}&redirect_uri={}&response_type=code",
self.base,
self.client_id.clone().unwrap(),
self.redirect.clone().unwrap(),
);
Ok(url)
}
fn is_registered(&self) -> Result<()> {
if self.client_id.is_none() {
Err(Error::ClientIdRequired)
} else if self.client_secret.is_none() {
Err(Error::ClientSecretRequired)
} else {
Ok(())
}
}
pub fn create_access_token(self, code: String) -> Result<Mastodon> {
self.is_registered()?;
let url = format!(
"{}/oauth/token?client_id={}&client_secret={}&code={}&grant_type=authorization_code&redirect_uri={}",
self.base,
self.client_id.clone().unwrap(),
self.client_secret.clone().unwrap(),
code,
self.redirect.clone().unwrap()
);
let token: AccessToken = self.client.post(&url).send()?.json()?;
Ok(Mastodon::from_registration(self.base,
self.client_id.unwrap(),
self.client_secret.unwrap(),
self.redirect.unwrap(),
token.access_token,
self.client))
}
}
Loading…
Cancel
Save