use std::borrow::Cow; use serde::Deserialize; use std::convert::TryInto; use crate::apps::{AppBuilder, App}; use crate::scopes::Scopes; use crate::{Result, Error, FediClient, ClientBuilder}; use crate::data::AppData; const DEFAULT_REDIRECT_URI: &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> { base: String, http_client: reqwest::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> { /// Construct a new registration process to the instance of the `base` url (e.g. "https://mastodon.social") pub fn new>(base: I) -> Self { Registration { base: base.into(), http_client: reqwest::Client::new(), app_builder: AppBuilder::new(), force_login: false, } } } impl<'a> Registration<'a> { /// 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>>(&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>>(&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>>(&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 } async fn send(&self, req: reqwest::RequestBuilder) -> Result { let req = req.build()?; Ok(self.http_client.execute(req).await?) } /// Register the given application pub async fn register>(&mut self, app: I) -> Result where Error: From<>::Error>, { let app = app.try_into()?; let oauth = self.send_app(&app).await?; Ok(Registered { base: self.base.clone(), http_client: self.http_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. pub async fn build(&mut self) -> Result { let app: App = self.app_builder.clone().build()?; let oauth = self.send_app(&app).await?; Ok(Registered { base: self.base.clone(), http_client: self.http_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 { let url = format!("{}/api/v1/apps", self.base); Ok(self.send(self.http_client.post(url).json(&app)).await? .json().await?) } } impl Registered { /// Skip having to retrieve the client id and secret from the server by /// creating a `Registered` struct directly pub fn from_parts( base: &str, client_id: &str, client_secret: &str, redirect: &str, scopes: Scopes, force_login: bool, ) -> Registered { Registered { base: base.to_string(), http_client: reqwest::Client::new(), client_id: client_id.to_string(), client_secret: client_secret.to_string(), redirect: redirect.to_string(), scopes, force_login, } } } impl Registered { async fn send(&self, req: reqwest::RequestBuilder) -> Result { let req = req.build()?; Ok(self.http_client.execute(req).await?) } /// Returns the parts of the `Registered` struct that can be used to /// recreate another `Registered` struct 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 authorisation. This needs to be opened /// in a browser. pub fn authorize_url(&self) -> Result { let mut url = url::Url::parse(&self.base)?.join("/oauth/authorize")?; url.query_pairs_mut() .append_pair("client_id", &self.client_id) .append_pair("redirect_uri", &self.redirect) .append_pair("scope", &self.scopes.to_string()) .append_pair("response_type", "code") .append_pair("force_login", &self.force_login.to_string()); Ok(url.into()) } /// Create an access token from the client id, client secret, and code /// provided by the authorisation url. pub async fn complete(&self, code: &str) -> Result { 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.send(self.http_client.post(&url)).await?.json().await?; let data = AppData { 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.access_token.into(), }; let mut builder = ClientBuilder::new(); builder.http_client(self.http_client.clone()).data(data); builder.build() } } /// 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 { base: String, http_client: reqwest::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::new("https://example.com"); 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); } }