Revamp registration & auth process
This commit is contained in:
@@ -17,6 +17,7 @@ serde = "1"
|
||||
serde_derive = "1"
|
||||
serde_json = "1"
|
||||
url = "1"
|
||||
try_from = "0.2.2"
|
||||
|
||||
[dependencies.chrono]
|
||||
version = "0.4"
|
||||
|
||||
+9
-11
@@ -11,7 +11,7 @@ use std::{
|
||||
|
||||
use self::elefren::{
|
||||
apps::{
|
||||
AppBuilder,
|
||||
App,
|
||||
Scopes
|
||||
},
|
||||
Mastodon,
|
||||
@@ -36,23 +36,21 @@ pub fn get_mastodon_data() -> Result<Mastodon, Box<Error>> {
|
||||
}
|
||||
|
||||
pub fn register() -> Result<Mastodon, Box<Error>> {
|
||||
let app = AppBuilder {
|
||||
client_name: "elefren-examples",
|
||||
redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
|
||||
scopes: Scopes::All,
|
||||
website: Some("https://github.com/pwoolcoc/elefren"),
|
||||
};
|
||||
let mut app = App::builder();
|
||||
app.client_name("elefren-examples")
|
||||
.scopes(Scopes::All)
|
||||
.website("https://github.com/pwoolcoc/elefren");
|
||||
|
||||
let website = read_line("Please enter your mastodon instance url:")?;
|
||||
let mut registration = Registration::new(website.trim());
|
||||
registration.register(app)?;
|
||||
let url = registration.authorise()?;
|
||||
let registration = Registration::new(website.trim());
|
||||
let registered = registration.register(app)?;
|
||||
let url = registered.authorize_url()?;
|
||||
|
||||
println!("Click this link to authorize on Mastodon: {}", url);
|
||||
let input = read_line("Paste the returned authorization code: ")?;
|
||||
|
||||
let code = input.trim();
|
||||
let mastodon = registration.create_access_token(code.to_string())?;
|
||||
let mastodon = registered.complete(code.to_string())?;
|
||||
|
||||
// Save app data for using on the next run.
|
||||
let toml = toml::to_string(&*mastodon)?;
|
||||
|
||||
+104
-13
@@ -1,39 +1,130 @@
|
||||
use std::fmt;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use try_from::TryInto;
|
||||
|
||||
use errors::{Error, Result};
|
||||
|
||||
/// Provides the necessary types for registering an App and getting the
|
||||
/// necessary auth information
|
||||
pub mod prelude {
|
||||
pub use {
|
||||
apps::{
|
||||
AppBuilder,
|
||||
App,
|
||||
Scopes
|
||||
},
|
||||
registration::Registration
|
||||
};
|
||||
}
|
||||
|
||||
/// Represents an application that can be registered with a mastodon instance
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct App {
|
||||
client_name: String,
|
||||
redirect_uris: String,
|
||||
scopes: Scopes,
|
||||
#[serde(skip_serializing_if="Option::is_none")]
|
||||
website: Option<String>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn builder<'a>() -> AppBuilder<'a> {
|
||||
AppBuilder::new()
|
||||
}
|
||||
|
||||
pub fn scopes(&self) -> Scopes {
|
||||
self.scopes
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder struct for defining your application.
|
||||
/// ```
|
||||
/// use std::error::Error;
|
||||
/// use elefren::apps::prelude::*;
|
||||
///
|
||||
/// let app = AppBuilder {
|
||||
/// client_name: "elefren_test",
|
||||
/// redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
|
||||
/// scopes: Scopes::Read,
|
||||
/// website: None,
|
||||
/// };
|
||||
/// # fn main() -> Result<(), Box<Error>> {
|
||||
/// let mut builder = App::builder();
|
||||
/// builder.client_name("elefren_test");
|
||||
/// let app = builder.build()?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct AppBuilder<'a> {
|
||||
client_name: Option<Cow<'a, str>>,
|
||||
redirect_uris: Option<Cow<'a, str>>,
|
||||
scopes: Option<Scopes>,
|
||||
website: Option<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
impl<'a> AppBuilder<'a> {
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// Name of the application. Will be displayed when the user is deciding to
|
||||
/// grant permission.
|
||||
pub client_name: &'a str,
|
||||
///
|
||||
/// In order to turn this builder into an App, this needs to be provided
|
||||
pub fn client_name<I: Into<Cow<'a, str>>>(&mut self, name: I) -> &mut Self {
|
||||
self.client_name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Where the user should be redirected after authorization
|
||||
/// (for no redirect, use `urn:ietf:wg:oauth:2.0:oob`)
|
||||
pub redirect_uris: &'a str,
|
||||
///
|
||||
/// If none is specified, the default is `urn:ietf:wg:oauth:2.0:oob`
|
||||
pub fn redirect_uris<I: Into<Cow<'a, str>>>(&mut self, uris: I) -> &mut Self {
|
||||
self.redirect_uris = Some(uris.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Permission scope of the application.
|
||||
pub scopes: Scopes,
|
||||
///
|
||||
/// IF none is specified, the default is Scopes::Read
|
||||
pub fn scopes(&mut self, scopes: Scopes) -> &mut Self {
|
||||
self.scopes = Some(scopes);
|
||||
self
|
||||
}
|
||||
|
||||
/// URL to the homepage of your application.
|
||||
#[serde(skip_serializing_if="Option::is_none")]
|
||||
pub website: Option<&'a str>,
|
||||
pub fn website<I: Into<Cow<'a, str>>>(&mut self, website: I) -> &mut Self {
|
||||
self.website = Some(website.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Attempts to convert this build into an `App`
|
||||
///
|
||||
/// Will fail if no `client_name` was provided
|
||||
pub fn build(self) -> Result<App> {
|
||||
Ok(App {
|
||||
client_name: self.client_name
|
||||
.ok_or_else(|| Error::MissingField("client_name"))?
|
||||
.into(),
|
||||
redirect_uris: self.redirect_uris
|
||||
.unwrap_or_else(|| "urn:ietf:wg:oauth:2.0:oob".into())
|
||||
.into(),
|
||||
scopes: self.scopes
|
||||
.unwrap_or_else(|| Scopes::Read),
|
||||
website: self.website.map(|s| s.into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<App> for App {
|
||||
type Err = Error;
|
||||
|
||||
fn try_into(self) -> Result<App> {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryInto<App> for AppBuilder<'a> {
|
||||
type Err = Error;
|
||||
|
||||
fn try_into(self) -> Result<App> {
|
||||
Ok(self.build()?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Permission scope of the application.
|
||||
|
||||
@@ -39,6 +39,8 @@ pub enum Error {
|
||||
Server(StatusCode),
|
||||
/// MastodonBuilder error.
|
||||
DataMissing,
|
||||
/// AppBuilder error
|
||||
MissingField(&'static str),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
@@ -66,6 +68,7 @@ impl error::Error for Error {
|
||||
Error::ClientSecretRequired => "ClientSecretRequired",
|
||||
Error::AccessTokenRequired => "AccessTokenRequired",
|
||||
Error::DataMissing => "DataMissing",
|
||||
Error::MissingField(_) => "MissingField",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-38
@@ -12,20 +12,16 @@
|
||||
//! use elefren::prelude::*;
|
||||
//! use elefren::apps::prelude::*;
|
||||
//!
|
||||
//! let app = AppBuilder {
|
||||
//! client_name: "elefren_test",
|
||||
//! redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
|
||||
//! scopes: Scopes::Read,
|
||||
//! website: None,
|
||||
//! };
|
||||
//! let mut app = App::builder();
|
||||
//! app.client_name("elefren_test");
|
||||
//!
|
||||
//! let mut registration = Registration::new("https://mastodon.social");
|
||||
//! registration.register(app)?;
|
||||
//! let url = registration.authorise()?;
|
||||
//! let registration = Registration::new("https://mastodon.social")
|
||||
//! .register(app)?;
|
||||
//! let url = registration.authorize_url()?;
|
||||
//! // 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)?;
|
||||
//! let mastodon = registration.complete(code)?;
|
||||
//!
|
||||
//! println!("{:?}", mastodon.get_home_timeline()?.initial_items);
|
||||
//! # Ok(())
|
||||
@@ -41,6 +37,7 @@
|
||||
extern crate chrono;
|
||||
extern crate reqwest;
|
||||
extern crate serde;
|
||||
extern crate try_from;
|
||||
extern crate url;
|
||||
|
||||
use std::borrow::Cow;
|
||||
@@ -156,34 +153,6 @@ pub trait MastodonClient {
|
||||
}
|
||||
|
||||
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 = Headers::new();
|
||||
headers.set(Authorization(Bearer { token: (*data.token).to_owned() }));
|
||||
|
||||
Mastodon {
|
||||
client: client,
|
||||
headers: headers,
|
||||
data: data,
|
||||
}
|
||||
}
|
||||
|
||||
methods![get, post, delete,];
|
||||
|
||||
fn route(&self, url: &str) -> String {
|
||||
|
||||
+56
-59
@@ -1,17 +1,14 @@
|
||||
use reqwest::Client;
|
||||
use try_from::TryInto;
|
||||
|
||||
use super::{Error, Mastodon, Result};
|
||||
use apps::{AppBuilder, Scopes};
|
||||
use {Data, Error, Mastodon, MastodonBuilder, Result};
|
||||
use apps::{App, Scopes};
|
||||
|
||||
/// 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>,
|
||||
scopes: Scopes,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -42,10 +39,6 @@ impl Registration {
|
||||
Registration {
|
||||
base: base.into(),
|
||||
client: Client::new(),
|
||||
client_id: None,
|
||||
client_secret: None,
|
||||
redirect: None,
|
||||
scopes: Scopes::Read,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,92 +46,96 @@ impl Registration {
|
||||
///
|
||||
/// ```no_run
|
||||
/// # extern crate elefren;
|
||||
/// # fn main() {
|
||||
/// # try().unwrap();
|
||||
/// # }
|
||||
/// # fn try() -> elefren::Result<()> {
|
||||
/// # fn main () -> elefren::Result<()> {
|
||||
/// use elefren::prelude::*;
|
||||
/// use elefren::apps::prelude::*;
|
||||
///
|
||||
/// let app = AppBuilder {
|
||||
/// client_name: "elefren_test",
|
||||
/// redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
|
||||
/// scopes: Scopes::Read,
|
||||
/// website: None,
|
||||
/// };
|
||||
/// let mut builder = App::builder();
|
||||
/// builder.client_name("elefren_test");
|
||||
/// let app = builder.build()?;
|
||||
///
|
||||
/// let mut registration = Registration::new("https://mastodon.social");
|
||||
/// registration.register(app)?;
|
||||
/// let url = registration.authorise()?;
|
||||
/// let registration = Registration::new("https://mastodon.social");
|
||||
/// let registered = registration.register(app)?;
|
||||
/// let url = registered.authorize_url()?;
|
||||
/// // 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)?;
|
||||
/// let mastodon = registered.complete(code)?;
|
||||
///
|
||||
/// println!("{:?}", mastodon.get_home_timeline()?.initial_items);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn register(&mut self, app_builder: AppBuilder) -> Result<()> {
|
||||
pub fn register<I: TryInto<App>>(self, app: I) -> Result<Registered>
|
||||
where Error: From<<I as TryInto<App>>::Err>
|
||||
{
|
||||
let app = app.try_into()?;
|
||||
let url = format!("{}/api/v1/apps", self.base);
|
||||
self.scopes = app_builder.scopes;
|
||||
let app: OAuth = self.client.post(&url).form(&app_builder).send()?.json()?;
|
||||
let oauth: OAuth = self.client
|
||||
.post(&url)
|
||||
.form(&app)
|
||||
.send()?
|
||||
.json()?;
|
||||
|
||||
self.client_id = Some(app.client_id);
|
||||
self.client_secret = Some(app.client_secret);
|
||||
self.redirect = Some(app.redirect_uri);
|
||||
|
||||
Ok(())
|
||||
Ok(Registered {
|
||||
base: self.base,
|
||||
client: self.client,
|
||||
client_id: oauth.client_id,
|
||||
client_secret: oauth.client_secret,
|
||||
redirect: oauth.redirect_uri,
|
||||
scopes: app.scopes(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Registered {
|
||||
/// 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()?;
|
||||
|
||||
pub fn authorize_url(&self) -> Result<String> {
|
||||
let url = format!(
|
||||
"{}/oauth/authorize?client_id={}&redirect_uri={}&scope={}&response_type=code",
|
||||
self.base,
|
||||
self.client_id.clone().unwrap(),
|
||||
self.redirect.clone().unwrap(),
|
||||
self.client_id,
|
||||
self.redirect,
|
||||
self.scopes,
|
||||
);
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an access token from the client id, client secret, and code
|
||||
/// provided by the authorisation url.
|
||||
pub fn create_access_token(self, code: String) -> Result<Mastodon> {
|
||||
self.is_registered()?;
|
||||
pub fn complete(self, code: String) -> Result<Mastodon> {
|
||||
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(),
|
||||
self.client_id,
|
||||
self.client_secret,
|
||||
code,
|
||||
self.redirect.clone().unwrap()
|
||||
self.redirect
|
||||
);
|
||||
|
||||
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))
|
||||
let data = Data {
|
||||
base: self.base.into(),
|
||||
client_id: self.client_id.into(),
|
||||
client_secret: self.client_secret.into(),
|
||||
redirect: self.redirect.into(),
|
||||
token: token.access_token.into(),
|
||||
};
|
||||
|
||||
let mut builder = MastodonBuilder::new();
|
||||
builder.client(self.client).data(data);
|
||||
Ok(builder.build()?)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct Registered {
|
||||
base: String,
|
||||
client: Client,
|
||||
client_id: String,
|
||||
client_secret: String,
|
||||
redirect: String,
|
||||
scopes: Scopes,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user