mastodon-async/src/registration.rs

419 lines
13 KiB
Rust
Raw Normal View History

2022-11-27 14:44:43 +00:00
use std::borrow::Cow;
use reqwest::Client;
2022-11-27 14:44:43 +00:00
use url::percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET};
use crate::{
apps::{App, AppBuilder},
scopes::Scopes,
Data,
Error,
Mastodon,
Result,
};
2022-11-27 14:44:43 +00:00
const DEFAULT_REDIRECT_URI: &'static str = "urn:ietf:wg:oauth:2.0:oob";
/// Handles registering your mastodon app to your instance. It is recommended
/// you cache your data struct to avoid registering on every run.
#[derive(Debug, Clone)]
pub struct Registration<'a> {
2022-11-27 14:44:43 +00:00
base: String,
client: Client,
app_builder: AppBuilder<'a>,
force_login: bool,
}
#[derive(Deserialize)]
struct OAuth {
client_id: String,
client_secret: String,
#[serde(default = "default_redirect_uri")]
redirect_uri: String,
}
fn default_redirect_uri() -> String {
DEFAULT_REDIRECT_URI.to_string()
}
#[derive(Deserialize)]
struct AccessToken {
access_token: String,
}
impl<'a> Registration<'a> {
2022-11-27 14:44:43 +00:00
/// Construct a new registration process to the instance of the `base` url.
/// ```
/// use elefren::prelude::*;
///
/// let registration = Registration::new("https://botsin.space");
2022-11-27 14:44:43 +00:00
/// ```
pub fn new<I: Into<String>>(base: I) -> Self {
Registration {
base: base.into(),
client: Client::new(),
app_builder: AppBuilder::new(),
force_login: false,
}
}
}
impl<'a> Registration<'a> {
2022-11-27 14:44:43 +00:00
#[allow(dead_code)]
pub(crate) fn with_sender<I: Into<String>>(base: I) -> Self {
2022-11-27 14:44:43 +00:00
Registration {
base: base.into(),
client: Client::new(),
app_builder: AppBuilder::new(),
force_login: false,
}
}
/// Sets the name of this app
///
/// This is required, and if this isn't set then the AppBuilder::build
/// method will fail
pub fn client_name<I: Into<Cow<'a, str>>>(&mut self, name: I) -> &mut Self {
self.app_builder.client_name(name.into());
self
}
/// Sets the redirect uris that this app uses
pub fn redirect_uris<I: Into<Cow<'a, str>>>(&mut self, uris: I) -> &mut Self {
self.app_builder.redirect_uris(uris);
self
}
/// Sets the scopes that this app requires
///
/// The default for an app is Scopes::Read
pub fn scopes(&mut self, scopes: Scopes) -> &mut Self {
self.app_builder.scopes(scopes);
self
}
/// Sets the optional "website" to register the app with
pub fn website<I: Into<Cow<'a, str>>>(&mut self, website: I) -> &mut Self {
self.app_builder.website(website);
self
}
/// Forces the user to re-login (useful if you need to re-auth as a
/// different user on the same instance
pub fn force_login(&mut self, force_login: bool) -> &mut Self {
self.force_login = force_login;
self
}
/// Register the given application
///
/// ```no_run
/// use elefren::{apps::App, prelude::*};
///
/// tokio_test::block_on(async {
/// let mut app = App::builder();
/// app.client_name("elefren_test");
2022-11-27 14:44:43 +00:00
///
/// let registration = Registration::new("https://botsin.space")
/// .register(app)
/// .await
/// .unwrap();
/// let url = registration.authorize_url().unwrap();
/// // 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.complete(&code).await.unwrap();
2022-11-27 14:44:43 +00:00
///
/// println!("{:?}", mastodon.get_home_timeline().await.unwrap().initial_items);
/// });
2022-11-27 14:44:43 +00:00
/// ```
pub async fn register<I: TryInto<App>>(&mut self, app: I) -> Result<Registered>
2022-11-27 14:44:43 +00:00
where
Error: From<<I as TryInto<App>>::Error>,
2022-11-27 14:44:43 +00:00
{
let app = app.try_into()?;
let oauth = self.send_app(&app).await?;
2022-11-27 14:44:43 +00:00
Ok(Registered {
base: self.base.clone(),
client: self.client.clone(),
client_id: oauth.client_id,
client_secret: oauth.client_secret,
redirect: oauth.redirect_uri,
scopes: app.scopes().clone(),
force_login: self.force_login,
})
}
/// Register the application with the server from the `base` url.
///
/// ```no_run
/// use elefren::prelude::*;
///
/// tokio_test::block_on(async {
/// let registration = Registration::new("https://botsin.space")
/// .client_name("elefren_test")
/// .build()
/// .await
/// .unwrap();
/// let url = registration.authorize_url().unwrap();
/// // 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.complete(&code).await.unwrap();
2022-11-27 14:44:43 +00:00
///
/// println!("{:?}", mastodon.get_home_timeline().await.unwrap().initial_items);
/// });
2022-11-27 14:44:43 +00:00
/// ```
pub async fn build(&mut self) -> Result<Registered> {
2022-11-27 14:44:43 +00:00
let app: App = self.app_builder.clone().build()?;
let oauth = self.send_app(&app).await?;
2022-11-27 14:44:43 +00:00
Ok(Registered {
base: self.base.clone(),
client: self.client.clone(),
client_id: oauth.client_id,
client_secret: oauth.client_secret,
redirect: oauth.redirect_uri,
scopes: app.scopes().clone(),
force_login: self.force_login,
})
}
async fn send_app(&self, app: &App) -> Result<OAuth> {
2022-11-27 14:44:43 +00:00
let url = format!("{}/api/v1/apps", self.base);
let response = self.client.post(&url).json(&app).send().await?;
Ok(response.json().await?)
2022-11-27 14:44:43 +00:00
}
}
impl Registered {
2022-11-27 14:44:43 +00:00
/// Skip having to retrieve the client id and secret from the server by
/// creating a `Registered` struct directly
///
/// // Example
2022-11-27 14:44:43 +00:00
///
/// ```no_run
/// use elefren::{prelude::*, registration::Registered};
///
/// tokio_test::block_on(async {
/// let registration = Registered::from_parts(
/// "https://example.com",
/// "the-client-id",
/// "the-client-secret",
/// "https://example.com/redirect",
/// Scopes::read_all(),
/// false,
/// );
/// let url = registration.authorize_url().unwrap();
/// // 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.complete(&code).await.unwrap();
2022-11-27 14:44:43 +00:00
///
/// println!("{:?}", mastodon.get_home_timeline().await.unwrap().initial_items);
/// });
2022-11-27 14:44:43 +00:00
/// ```
pub fn from_parts(
base: &str,
client_id: &str,
client_secret: &str,
redirect: &str,
scopes: Scopes,
force_login: bool,
) -> Registered {
2022-11-27 14:44:43 +00:00
Registered {
base: base.to_string(),
client: Client::new(),
client_id: client_id.to_string(),
client_secret: client_secret.to_string(),
redirect: redirect.to_string(),
scopes,
force_login,
}
}
}
impl Registered {
2022-11-27 14:44:43 +00:00
/// Returns the parts of the `Registered` struct that can be used to
/// recreate another `Registered` struct
///
/// // Example
2022-11-27 14:44:43 +00:00
///
/// ```
/// use elefren::{prelude::*, registration::Registered};
///
/// let orig_base = "https://example.social";
/// let orig_client_id = "some-client_id";
/// let orig_client_secret = "some-client-secret";
/// let orig_redirect = "https://example.social/redirect";
/// let orig_scopes = Scopes::all();
/// let orig_force_login = false;
2022-11-27 14:44:43 +00:00
///
/// let registered = Registered::from_parts(
/// orig_base,
/// orig_client_id,
/// orig_client_secret,
/// orig_redirect,
/// orig_scopes.clone(),
/// orig_force_login,
2022-11-27 14:44:43 +00:00
/// );
///
/// let (base, client_id, client_secret, redirect, scopes, force_login) = registered.into_parts();
///
/// assert_eq!(orig_base, &base);
/// assert_eq!(orig_client_id, &client_id);
/// assert_eq!(orig_client_secret, &client_secret);
/// assert_eq!(orig_redirect, &redirect);
/// assert_eq!(orig_scopes, scopes);
/// assert_eq!(orig_force_login, force_login);
2022-11-27 14:44:43 +00:00
/// ```
pub fn into_parts(self) -> (String, String, String, String, Scopes, bool) {
(
self.base,
self.client_id,
self.client_secret,
self.redirect,
self.scopes,
self.force_login,
)
}
/// Returns the full url needed for authorization. This needs to be opened
2022-11-27 14:44:43 +00:00
/// in a browser.
pub fn authorize_url(&self) -> Result<String> {
let scopes = format!("{}", self.scopes);
let scopes: String = utf8_percent_encode(&scopes, DEFAULT_ENCODE_SET).collect();
let url = if self.force_login {
format!(
"{}/oauth/authorize?client_id={}&redirect_uri={}&scope={}&force_login=true&\
response_type=code",
self.base, self.client_id, self.redirect, scopes,
)
} else {
format!(
"{}/oauth/authorize?client_id={}&redirect_uri={}&scope={}&response_type=code",
self.base, self.client_id, self.redirect, scopes,
)
};
Ok(url)
}
/// Construct authentication data once token is known
fn registered(&self, token: String) -> Data {
Data {
base: self.base.clone().into(),
client_id: self.client_id.clone().into(),
client_secret: self.client_secret.clone().into(),
redirect: self.redirect.clone().into(),
token: token.into(),
}
}
2022-11-27 14:44:43 +00:00
/// Create an access token from the client id, client secret, and code
/// provided by the authorization url.
pub async fn complete(&self, code: &str) -> Result<Mastodon> {
2022-11-27 14:44:43 +00:00
let url = format!(
"{}/oauth/token?client_id={}&client_secret={}&code={}&grant_type=authorization_code&\
redirect_uri={}",
self.base, self.client_id, self.client_secret, code, self.redirect
);
let token: AccessToken = self
.client
.post(&url)
.send()
.await?
.error_for_status()?
.json()
.await?;
2022-11-27 14:44:43 +00:00
let data = self.registered(token.access_token);
2022-11-27 14:44:43 +00:00
Ok(Mastodon::new(self.client.clone(), data))
2022-11-27 14:44:43 +00:00
}
}
/// Represents the state of the auth flow when the app has been registered but
/// the user is not authenticated
#[derive(Debug, Clone)]
pub struct Registered {
2022-11-27 14:44:43 +00:00
base: String,
client: Client,
client_id: String,
client_secret: String,
redirect: String,
scopes: Scopes,
force_login: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_registration_new() {
let r = Registration::new("https://example.com");
assert_eq!(r.base, "https://example.com".to_string());
assert_eq!(r.app_builder, AppBuilder::new());
}
#[test]
fn test_registration_with_sender() {
let r = Registration::with_sender("https://example.com");
2022-11-27 14:44:43 +00:00
assert_eq!(r.base, "https://example.com".to_string());
assert_eq!(r.app_builder, AppBuilder::new());
}
#[test]
fn test_set_client_name() {
let mut r = Registration::new("https://example.com");
r.client_name("foo-test");
assert_eq!(r.base, "https://example.com".to_string());
assert_eq!(
&mut r.app_builder,
AppBuilder::new().client_name("foo-test")
);
}
#[test]
fn test_set_redirect_uris() {
let mut r = Registration::new("https://example.com");
r.redirect_uris("https://foo.com");
assert_eq!(r.base, "https://example.com".to_string());
assert_eq!(
&mut r.app_builder,
AppBuilder::new().redirect_uris("https://foo.com")
);
}
#[test]
fn test_set_scopes() {
let mut r = Registration::new("https://example.com");
r.scopes(Scopes::all());
assert_eq!(r.base, "https://example.com".to_string());
assert_eq!(&mut r.app_builder, AppBuilder::new().scopes(Scopes::all()));
}
#[test]
fn test_set_website() {
let mut r = Registration::new("https://example.com");
r.website("https://website.example.com");
assert_eq!(r.base, "https://example.com".to_string());
assert_eq!(
&mut r.app_builder,
AppBuilder::new().website("https://website.example.com")
);
}
#[test]
fn test_default_redirect_uri() {
assert_eq!(&default_redirect_uri()[..], DEFAULT_REDIRECT_URI);
}
}