Revamp registration & auth process
This commit is contained in:
@@ -17,6 +17,7 @@ serde = "1"
|
|||||||
serde_derive = "1"
|
serde_derive = "1"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
url = "1"
|
url = "1"
|
||||||
|
try_from = "0.2.2"
|
||||||
|
|
||||||
[dependencies.chrono]
|
[dependencies.chrono]
|
||||||
version = "0.4"
|
version = "0.4"
|
||||||
|
|||||||
+9
-11
@@ -11,7 +11,7 @@ use std::{
|
|||||||
|
|
||||||
use self::elefren::{
|
use self::elefren::{
|
||||||
apps::{
|
apps::{
|
||||||
AppBuilder,
|
App,
|
||||||
Scopes
|
Scopes
|
||||||
},
|
},
|
||||||
Mastodon,
|
Mastodon,
|
||||||
@@ -36,23 +36,21 @@ pub fn get_mastodon_data() -> Result<Mastodon, Box<Error>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn register() -> Result<Mastodon, Box<Error>> {
|
pub fn register() -> Result<Mastodon, Box<Error>> {
|
||||||
let app = AppBuilder {
|
let mut app = App::builder();
|
||||||
client_name: "elefren-examples",
|
app.client_name("elefren-examples")
|
||||||
redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
|
.scopes(Scopes::All)
|
||||||
scopes: Scopes::All,
|
.website("https://github.com/pwoolcoc/elefren");
|
||||||
website: Some("https://github.com/pwoolcoc/elefren"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let website = read_line("Please enter your mastodon instance url:")?;
|
let website = read_line("Please enter your mastodon instance url:")?;
|
||||||
let mut registration = Registration::new(website.trim());
|
let registration = Registration::new(website.trim());
|
||||||
registration.register(app)?;
|
let registered = registration.register(app)?;
|
||||||
let url = registration.authorise()?;
|
let url = registered.authorize_url()?;
|
||||||
|
|
||||||
println!("Click this link to authorize on Mastodon: {}", url);
|
println!("Click this link to authorize on Mastodon: {}", url);
|
||||||
let input = read_line("Paste the returned authorization code: ")?;
|
let input = read_line("Paste the returned authorization code: ")?;
|
||||||
|
|
||||||
let code = input.trim();
|
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.
|
// Save app data for using on the next run.
|
||||||
let toml = toml::to_string(&*mastodon)?;
|
let toml = toml::to_string(&*mastodon)?;
|
||||||
|
|||||||
+104
-13
@@ -1,39 +1,130 @@
|
|||||||
use std::fmt;
|
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 mod prelude {
|
||||||
pub use {
|
pub use {
|
||||||
apps::{
|
apps::{
|
||||||
AppBuilder,
|
App,
|
||||||
Scopes
|
Scopes
|
||||||
},
|
},
|
||||||
registration::Registration
|
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.
|
/// Builder struct for defining your application.
|
||||||
/// ```
|
/// ```
|
||||||
|
/// use std::error::Error;
|
||||||
/// use elefren::apps::prelude::*;
|
/// use elefren::apps::prelude::*;
|
||||||
///
|
///
|
||||||
/// let app = AppBuilder {
|
/// # fn main() -> Result<(), Box<Error>> {
|
||||||
/// client_name: "elefren_test",
|
/// let mut builder = App::builder();
|
||||||
/// redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
|
/// builder.client_name("elefren_test");
|
||||||
/// scopes: Scopes::Read,
|
/// let app = builder.build()?;
|
||||||
/// website: None,
|
/// # Ok(())
|
||||||
/// };
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug, Default, Serialize)]
|
#[derive(Debug, Default, Serialize)]
|
||||||
pub struct AppBuilder<'a> {
|
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
|
/// Name of the application. Will be displayed when the user is deciding to
|
||||||
/// grant permission.
|
/// 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
|
/// 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.
|
/// 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.
|
/// URL to the homepage of your application.
|
||||||
#[serde(skip_serializing_if="Option::is_none")]
|
pub fn website<I: Into<Cow<'a, str>>>(&mut self, website: I) -> &mut Self {
|
||||||
pub website: Option<&'a str>,
|
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.
|
/// Permission scope of the application.
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ pub enum Error {
|
|||||||
Server(StatusCode),
|
Server(StatusCode),
|
||||||
/// MastodonBuilder error.
|
/// MastodonBuilder error.
|
||||||
DataMissing,
|
DataMissing,
|
||||||
|
/// AppBuilder error
|
||||||
|
MissingField(&'static str),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Error {
|
impl fmt::Display for Error {
|
||||||
@@ -66,6 +68,7 @@ impl error::Error for Error {
|
|||||||
Error::ClientSecretRequired => "ClientSecretRequired",
|
Error::ClientSecretRequired => "ClientSecretRequired",
|
||||||
Error::AccessTokenRequired => "AccessTokenRequired",
|
Error::AccessTokenRequired => "AccessTokenRequired",
|
||||||
Error::DataMissing => "DataMissing",
|
Error::DataMissing => "DataMissing",
|
||||||
|
Error::MissingField(_) => "MissingField",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-38
@@ -12,20 +12,16 @@
|
|||||||
//! use elefren::prelude::*;
|
//! use elefren::prelude::*;
|
||||||
//! use elefren::apps::prelude::*;
|
//! use elefren::apps::prelude::*;
|
||||||
//!
|
//!
|
||||||
//! let app = AppBuilder {
|
//! let mut app = App::builder();
|
||||||
//! client_name: "elefren_test",
|
//! app.client_name("elefren_test");
|
||||||
//! redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
|
|
||||||
//! scopes: Scopes::Read,
|
|
||||||
//! website: None,
|
|
||||||
//! };
|
|
||||||
//!
|
//!
|
||||||
//! let mut registration = Registration::new("https://mastodon.social");
|
//! let registration = Registration::new("https://mastodon.social")
|
||||||
//! registration.register(app)?;
|
//! .register(app)?;
|
||||||
//! let url = registration.authorise()?;
|
//! let url = registration.authorize_url()?;
|
||||||
//! // Here you now need to open the url in the browser
|
//! // Here you now need to open the url in the browser
|
||||||
//! // And handle a the redirect url coming back with the code.
|
//! // And handle a the redirect url coming back with the code.
|
||||||
//! let code = String::from("RETURNED_FROM_BROWSER");
|
//! 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);
|
//! println!("{:?}", mastodon.get_home_timeline()?.initial_items);
|
||||||
//! # Ok(())
|
//! # Ok(())
|
||||||
@@ -41,6 +37,7 @@
|
|||||||
extern crate chrono;
|
extern crate chrono;
|
||||||
extern crate reqwest;
|
extern crate reqwest;
|
||||||
extern crate serde;
|
extern crate serde;
|
||||||
|
extern crate try_from;
|
||||||
extern crate url;
|
extern crate url;
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
@@ -156,34 +153,6 @@ pub trait MastodonClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Mastodon {
|
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,];
|
methods![get, post, delete,];
|
||||||
|
|
||||||
fn route(&self, url: &str) -> String {
|
fn route(&self, url: &str) -> String {
|
||||||
|
|||||||
+56
-59
@@ -1,17 +1,14 @@
|
|||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
use try_from::TryInto;
|
||||||
|
|
||||||
use super::{Error, Mastodon, Result};
|
use {Data, Error, Mastodon, MastodonBuilder, Result};
|
||||||
use apps::{AppBuilder, Scopes};
|
use apps::{App, Scopes};
|
||||||
|
|
||||||
/// Handles registering your mastodon app to your instance. It is recommended
|
/// Handles registering your mastodon app to your instance. It is recommended
|
||||||
/// you cache your data struct to avoid registering on every run.
|
/// you cache your data struct to avoid registering on every run.
|
||||||
pub struct Registration {
|
pub struct Registration {
|
||||||
base: String,
|
base: String,
|
||||||
client: Client,
|
client: Client,
|
||||||
client_id: Option<String>,
|
|
||||||
client_secret: Option<String>,
|
|
||||||
redirect: Option<String>,
|
|
||||||
scopes: Scopes,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -42,10 +39,6 @@ impl Registration {
|
|||||||
Registration {
|
Registration {
|
||||||
base: base.into(),
|
base: base.into(),
|
||||||
client: Client::new(),
|
client: Client::new(),
|
||||||
client_id: None,
|
|
||||||
client_secret: None,
|
|
||||||
redirect: None,
|
|
||||||
scopes: Scopes::Read,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,92 +46,96 @@ impl Registration {
|
|||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// # extern crate elefren;
|
/// # extern crate elefren;
|
||||||
/// # fn main() {
|
/// # fn main () -> elefren::Result<()> {
|
||||||
/// # try().unwrap();
|
|
||||||
/// # }
|
|
||||||
/// # fn try() -> elefren::Result<()> {
|
|
||||||
/// use elefren::prelude::*;
|
/// use elefren::prelude::*;
|
||||||
/// use elefren::apps::prelude::*;
|
/// use elefren::apps::prelude::*;
|
||||||
///
|
///
|
||||||
/// let app = AppBuilder {
|
/// let mut builder = App::builder();
|
||||||
/// client_name: "elefren_test",
|
/// builder.client_name("elefren_test");
|
||||||
/// redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
|
/// let app = builder.build()?;
|
||||||
/// scopes: Scopes::Read,
|
|
||||||
/// website: None,
|
|
||||||
/// };
|
|
||||||
///
|
///
|
||||||
/// let mut registration = Registration::new("https://mastodon.social");
|
/// let registration = Registration::new("https://mastodon.social");
|
||||||
/// registration.register(app)?;
|
/// let registered = registration.register(app)?;
|
||||||
/// let url = registration.authorise()?;
|
/// let url = registered.authorize_url()?;
|
||||||
/// // Here you now need to open the url in the browser
|
/// // Here you now need to open the url in the browser
|
||||||
/// // And handle a the redirect url coming back with the code.
|
/// // And handle a the redirect url coming back with the code.
|
||||||
/// let code = String::from("RETURNED_FROM_BROWSER");
|
/// 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);
|
/// println!("{:?}", mastodon.get_home_timeline()?.initial_items);
|
||||||
/// # Ok(())
|
/// # 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);
|
let url = format!("{}/api/v1/apps", self.base);
|
||||||
self.scopes = app_builder.scopes;
|
let oauth: OAuth = self.client
|
||||||
let app: OAuth = self.client.post(&url).form(&app_builder).send()?.json()?;
|
.post(&url)
|
||||||
|
.form(&app)
|
||||||
|
.send()?
|
||||||
|
.json()?;
|
||||||
|
|
||||||
self.client_id = Some(app.client_id);
|
Ok(Registered {
|
||||||
self.client_secret = Some(app.client_secret);
|
base: self.base,
|
||||||
self.redirect = Some(app.redirect_uri);
|
client: self.client,
|
||||||
|
client_id: oauth.client_id,
|
||||||
Ok(())
|
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
|
/// Returns the full url needed for authorisation. This needs to be opened
|
||||||
/// in a browser.
|
/// in a browser.
|
||||||
pub fn authorise(&mut self) -> Result<String> {
|
pub fn authorize_url(&self) -> Result<String> {
|
||||||
self.is_registered()?;
|
|
||||||
|
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"{}/oauth/authorize?client_id={}&redirect_uri={}&scope={}&response_type=code",
|
"{}/oauth/authorize?client_id={}&redirect_uri={}&scope={}&response_type=code",
|
||||||
self.base,
|
self.base,
|
||||||
self.client_id.clone().unwrap(),
|
self.client_id,
|
||||||
self.redirect.clone().unwrap(),
|
self.redirect,
|
||||||
self.scopes,
|
self.scopes,
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(url)
|
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
|
/// Create an access token from the client id, client secret, and code
|
||||||
/// provided by the authorisation url.
|
/// provided by the authorisation url.
|
||||||
pub fn create_access_token(self, code: String) -> Result<Mastodon> {
|
pub fn complete(self, code: String) -> Result<Mastodon> {
|
||||||
self.is_registered()?;
|
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"{}/oauth/token?client_id={}&client_secret={}&code={}&grant_type=authorization_code&redirect_uri={}",
|
"{}/oauth/token?client_id={}&client_secret={}&code={}&grant_type=authorization_code&redirect_uri={}",
|
||||||
self.base,
|
self.base,
|
||||||
self.client_id.clone().unwrap(),
|
self.client_id,
|
||||||
self.client_secret.clone().unwrap(),
|
self.client_secret,
|
||||||
code,
|
code,
|
||||||
self.redirect.clone().unwrap()
|
self.redirect
|
||||||
);
|
);
|
||||||
|
|
||||||
let token: AccessToken = self.client.post(&url).send()?.json()?;
|
let token: AccessToken = self.client.post(&url).send()?.json()?;
|
||||||
|
|
||||||
Ok(Mastodon::from_registration(self.base,
|
let data = Data {
|
||||||
self.client_id.unwrap(),
|
base: self.base.into(),
|
||||||
self.client_secret.unwrap(),
|
client_id: self.client_id.into(),
|
||||||
self.redirect.unwrap(),
|
client_secret: self.client_secret.into(),
|
||||||
token.access_token,
|
redirect: self.redirect.into(),
|
||||||
self.client))
|
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