From a528624dc3609bfd230bb4e1dd2f8fa6767eccca Mon Sep 17 00:00:00 2001 From: Aaron Power Date: Fri, 21 Apr 2017 12:06:30 +0100 Subject: [PATCH] 0.3.0 Redone registration api, added debug/clone --- Cargo.toml | 2 +- README.md | 29 +++++ src/entities/account.rs | 2 +- src/entities/card.rs | 2 +- src/entities/context.rs | 2 +- src/entities/instance.rs | 2 +- src/entities/notification.rs | 4 +- src/entities/relationship.rs | 2 +- src/entities/report.rs | 2 +- src/entities/search_result.rs | 2 +- src/entities/status.rs | 10 +- src/lib.rs | 236 +++++++++++++--------------------- src/registration.rs | 128 ++++++++++++++++++ 13 files changed, 259 insertions(+), 164 deletions(-) create mode 100644 src/registration.rs diff --git a/Cargo.toml b/Cargo.toml index 7e54b24..c9ac861 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mammut" -version = "0.2.0" +version = "0.3.0" description = "A wrapper around the Mastodon API." authors = ["Aaron Power "] license = "MIT/Apache-2.0" diff --git a/README.md b/README.md index fa197d8..8883673 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,32 @@ ### [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/) + +```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(()) +# } +``` diff --git a/src/entities/account.rs b/src/entities/account.rs index 82af4f7..57ed9aa 100644 --- a/src/entities/account.rs +++ b/src/entities/account.rs @@ -1,5 +1,5 @@ use chrono::prelude::*; -#[derive(Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct Account { pub id: u64, pub username: String, diff --git a/src/entities/card.rs b/src/entities/card.rs index ea34f70..b2a4e8b 100644 --- a/src/entities/card.rs +++ b/src/entities/card.rs @@ -1,4 +1,4 @@ -#[derive(Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct Card { pub url: String, pub title: String, diff --git a/src/entities/context.rs b/src/entities/context.rs index dc8c3ce..518a02b 100644 --- a/src/entities/context.rs +++ b/src/entities/context.rs @@ -1,6 +1,6 @@ use super::status::Status; -#[derive(Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct Context { pub ancestors: Vec, pub descendants: Vec, diff --git a/src/entities/instance.rs b/src/entities/instance.rs index 5979c8d..994e1ad 100644 --- a/src/entities/instance.rs +++ b/src/entities/instance.rs @@ -1,4 +1,4 @@ -#[derive(Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct Instance { pub uri: String, pub title: String, diff --git a/src/entities/notification.rs b/src/entities/notification.rs index bdb3ce2..2a6690d 100644 --- a/src/entities/notification.rs +++ b/src/entities/notification.rs @@ -2,7 +2,7 @@ use chrono::prelude::*; use super::account::Account; use super::status::Status; -#[derive(Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct Notification { pub id: u64, pub notification_type: NotificationType, @@ -11,7 +11,7 @@ pub struct Notification { pub status: Option, } -#[derive(Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub enum NotificationType { Mention, Reblog, diff --git a/src/entities/relationship.rs b/src/entities/relationship.rs index 24879b1..cf3c7ed 100644 --- a/src/entities/relationship.rs +++ b/src/entities/relationship.rs @@ -1,4 +1,4 @@ -#[derive(Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct Relationship { pub following: bool, pub followed_by: bool, diff --git a/src/entities/report.rs b/src/entities/report.rs index a2fbdbc..b397a62 100644 --- a/src/entities/report.rs +++ b/src/entities/report.rs @@ -1,4 +1,4 @@ -#[derive(Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct Report { pub id: u64, pub action_taken: String, diff --git a/src/entities/search_result.rs b/src/entities/search_result.rs index a92bd08..2c75f36 100644 --- a/src/entities/search_result.rs +++ b/src/entities/search_result.rs @@ -1,6 +1,6 @@ use super::prelude::{Account, Status}; -#[derive(Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct SearchResult { pub accounts: Vec, pub statuses: Vec, diff --git a/src/entities/status.rs b/src/entities/status.rs index 5d472c2..12e6406 100644 --- a/src/entities/status.rs +++ b/src/entities/status.rs @@ -2,7 +2,7 @@ use chrono::prelude::*; use super::prelude::*; use status_builder::Visibility; -#[derive(Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct Status { pub id: i64, pub uri: String, @@ -26,7 +26,7 @@ pub struct Status { pub application: Application, } -#[derive(Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct Mention { pub url: String, pub username: String, @@ -34,16 +34,14 @@ pub struct Mention { pub id: u64, } -#[derive(Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct Tag { pub name: String, pub url: String, } -#[derive(Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct Application { pub name: String, pub website: String, } - - diff --git a/src/lib.rs b/src/lib.rs index c4ab9a1..1bad27e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,35 @@ //! //! Most of the api is documented on [Mastodon's //! 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))] @@ -17,6 +46,10 @@ pub mod apps; pub mod status_builder; /// Entities returned from the API pub mod entities; +/// Registering your app. +pub mod registration; + +use std::ops; use json::Error as SerdeError; use reqwest::Error as HttpError; @@ -26,6 +59,7 @@ use reqwest::header::{Authorization, Bearer, Headers}; use entities::prelude::*; use status_builder::StatusBuilder; +pub use registration::Registration; pub type Result = std::result::Result; macro_rules! methods { @@ -35,7 +69,7 @@ macro_rules! methods { -> Result { Ok(self.client.$method(&url) - .headers(self.access_token.clone().unwrap()) + .headers(self.headers.clone()) .send()? .json()?) } @@ -53,7 +87,6 @@ macro_rules! route { #[doc = "# Errors"] /// If `access_token` is not set. pub fn $name(&self, $($param: $typ,)*) -> Result<$ret> { - self.has_access_token()?; let form_data = json!({ $( @@ -62,7 +95,7 @@ macro_rules! route { }); Ok(self.client.post(&self.route(concat!("/api/v1/", $url))) - .headers(self.access_token.clone().unwrap()) + .headers(self.headers.clone()) .form(&form_data) .send()? .json()?) @@ -78,8 +111,6 @@ macro_rules! route { #[doc = "# Errors"] /// If `access_token` is not set. pub fn $name(&self) -> Result<$ret> { - self.has_access_token()?; - self.$method(self.route(concat!("/api/v1/", $url))) } @@ -100,9 +131,6 @@ macro_rules! route_id { #[doc = "# Errors"] /// If `access_token` is not set. pub fn $name(&self, id: u64) -> Result<$ret> { - self.has_access_token()?; - - self.$method(self.route(&format!(concat!("/api/v1/", $url), id))) } )* @@ -112,26 +140,21 @@ macro_rules! route_id { #[derive(Clone, Debug)] pub struct Mastodon { - base_url: String, client: Client, - client_id: Option, - client_secret: Option, - redirect_uri: Option, - access_token: Option, - id: Option, + headers: Headers, + /// Raw data about your mastodon instance. + pub data: Data } -#[derive(Deserialize)] -struct OAuth { - client_id: String, - client_secret: String, - id: u64, - redirect_uri: String, -} - -#[derive(Deserialize)] -struct AccessToken { - access_token: String, +/// Raw data about mastodon app. Save `Data` using `serde` to prevent needing +/// to authenticate on every run. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Data { + pub base: String, + pub client_id: String, + pub client_secret: String, + pub redirect: String, + pub token: String, } #[derive(Debug)] @@ -144,104 +167,43 @@ pub enum Error { } impl Mastodon { - /// Inits new Mastodon object. `base_url` is expected in the following - /// format `https://mastodon.social` with no leading forward slash. - /// - /// ``` - /// use mammut::Mastodon; - /// - /// let mastodon = Mastodon::new("https://mastodon.social").unwrap(); - /// ``` - pub fn new>(base_url: I) -> Result { - Ok(Mastodon { - base_url: base_url.into(), - client: Client::new()?, - client_id: None, - client_secret: None, - 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 { - self.is_registered()?; - - let url = format!( - "{}/oauth/authorize?client_id={}&redirect_uri={}&response_type=code", - self.base_url, - self.client_id.clone().unwrap(), - self.redirect_uri.clone().unwrap(), - ); - - Ok(url) - } - - /// Requires the authorisation code returned from the `redirect_url` - pub fn get_access_token(&mut self, code: String) -> Result<()> { - self.is_registered()?; + fn from_registration(base: String, + client_id: String, + client_secret: String, + redirect: String, + token: String, + client: Client) + -> Self + { + let data = Data { + base: base, + client_id: client_id, + client_secret: client_secret, + redirect: redirect, + token: token, - 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()?; + let mut headers = Headers::new(); + headers.set(Authorization(Bearer { token: data.token.clone() })); - self.set_access_token(access_token.access_token); - Ok(()) + Mastodon { + client: client, + headers: headers, + data: data, + } } - /// Set `access_token` required to use any method about the user. - fn set_access_token(&mut self, access_token: String) { + /// Creates a mastodon instance from the data struct. + pub fn from_data(data: Data) -> Result { let mut headers = Headers::new(); + headers.set(Authorization(Bearer { token: data.token.clone() })); - headers.set(Authorization(Bearer { token: access_token })); - - self.access_token = Some(headers); + Ok(Mastodon { + client: Client::new()?, + headers: headers, + data: data, + }) } route! { @@ -287,8 +249,6 @@ impl Mastodon { } pub fn get_public_timeline(&self, local: bool) -> Result> { - self.has_access_token()?; - let mut url = self.route("/api/v1/timelines/public"); if local { @@ -299,8 +259,6 @@ impl Mastodon { } pub fn get_tagged_timeline(&self, hashtag: String, local: bool) -> Result> { - self.has_access_token()?; - let mut url = self.route("/api/v1/timelines/tag/"); url += &hashtag; @@ -314,8 +272,7 @@ impl Mastodon { pub fn statuses(&self, id: u64, only_media: bool, exclude_replies: bool) -> Result> { - self.has_access_token()?; - let mut url = format!("{}/api/v1/accounts/{}/statuses", self.base_url, id); + let mut url = format!("{}/api/v1/accounts/{}/statuses", self.base, id); if only_media { url += "?only_media=1"; @@ -336,8 +293,6 @@ impl Mastodon { pub fn relationships(&self, ids: &[u64]) -> Result> { - self.has_access_token()?; - let mut url = self.route("/api/v1/accounts/relationships?"); if ids.len() == 1 { @@ -357,44 +312,29 @@ impl Mastodon { // TODO: Add a limit fn pub fn search_accounts(&self, query: &str) -> Result> { - self.has_access_token()?; - self.get(format!("{}/api/v1/accounts/search?q={}", self.base_url, query)) + self.get(format!("{}/api/v1/accounts/search?q={}", self.base, query)) } pub fn instance(&self) -> Result { - self.is_registered()?; - 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,]; fn route(&self, url: &str) -> String { - let mut s = self.base_url.clone(); + let mut s = self.base.clone(); s += url; s } } +impl ops::Deref for Mastodon { + type Target = Data; + + fn deref(&self) -> &Self::Target { + &self.data + } +} macro_rules! from { ($($typ:ident, $variant:ident,)*) => { diff --git a/src/registration.rs b/src/registration.rs new file mode 100644 index 0000000..8b58f57 --- /dev/null +++ b/src/registration.rs @@ -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, + client_secret: Option, + redirect: Option, +} + +#[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>(base: I) -> Result { + 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 { + 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 { + 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)) + } +} + +