1use std::borrow::Cow;
2
3use log::{as_debug, as_serde, debug, error, trace};
4use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
5use reqwest::Client;
6use uuid::Uuid;
7
8use crate::{
9 apps::{App, AppBuilder},
10 helpers::read_response::read_response,
11 log_serde,
12 scopes::Scopes,
13 Data, Error, Mastodon, Result,
14};
15
16const DEFAULT_REDIRECT_URI: &str = "urn:ietf:wg:oauth:2.0:oob";
17
18#[derive(Debug, Clone)]
21pub struct Registration<'a> {
22 base: String,
23 client: Client,
24 app_builder: AppBuilder<'a>,
25 force_login: bool,
26}
27
28#[derive(Serialize, Deserialize)]
29struct OAuth {
30 client_id: String,
31 client_secret: String,
32 #[serde(default = "default_redirect_uri")]
33 redirect_uri: String,
34}
35
36fn default_redirect_uri() -> String {
37 DEFAULT_REDIRECT_URI.to_string()
38}
39
40#[derive(Serialize, Deserialize)]
41struct AccessToken {
42 access_token: String,
43}
44
45impl<'a> Registration<'a> {
46 pub fn new<I: Into<String>>(base: I) -> Self {
53 Registration::new_with_client(base, Client::new())
54 }
55
56 pub fn new_with_client<I: Into<String>>(base: I, client: Client) -> Self {
65 Registration {
66 base: base.into(),
67 client,
68 app_builder: AppBuilder::new(),
69 force_login: false,
70 }
71 }
72}
73
74impl<'a> Registration<'a> {
75 #[allow(dead_code)]
76 pub(crate) fn with_sender<I: Into<String>>(base: I) -> Self {
77 Registration {
78 base: base.into(),
79 client: Client::new(),
80 app_builder: AppBuilder::new(),
81 force_login: false,
82 }
83 }
84
85 pub fn client_name<I: Into<Cow<'a, str>>>(&mut self, name: I) -> &mut Self {
90 self.app_builder.client_name(name.into());
91 self
92 }
93
94 pub fn redirect_uris<I: Into<Cow<'a, str>>>(&mut self, uris: I) -> &mut Self {
96 self.app_builder.redirect_uris(uris);
97 self
98 }
99
100 pub fn scopes(&mut self, scopes: Scopes) -> &mut Self {
104 self.app_builder.scopes(scopes);
105 self
106 }
107
108 pub fn website<I: Into<Cow<'a, str>>>(&mut self, website: I) -> &mut Self {
110 self.app_builder.website(website);
111 self
112 }
113
114 pub fn force_login(&mut self, force_login: bool) -> &mut Self {
117 self.force_login = force_login;
118 self
119 }
120
121 pub async fn register<I: TryInto<App>>(&mut self, app: I) -> Result<Registered>
144 where
145 Error: From<<I as TryInto<App>>::Error>,
146 {
147 let app = app.try_into()?;
148 let oauth = self.send_app(&app).await?;
149
150 Ok(Registered {
151 base: self.base.clone(),
152 client: self.client.clone(),
153 client_id: oauth.client_id,
154 client_secret: oauth.client_secret,
155 redirect: oauth.redirect_uri,
156 scopes: app.scopes().clone(),
157 force_login: self.force_login,
158 })
159 }
160
161 pub async fn build(&mut self) -> Result<Registered> {
182 let app: App = self.app_builder.clone().build()?;
183 let oauth = self.send_app(&app).await?;
184
185 Ok(Registered {
186 base: self.base.clone(),
187 client: self.client.clone(),
188 client_id: oauth.client_id,
189 client_secret: oauth.client_secret,
190 redirect: oauth.redirect_uri,
191 scopes: app.scopes().clone(),
192 force_login: self.force_login,
193 })
194 }
195
196 async fn send_app(&self, app: &App) -> Result<OAuth> {
197 let url = format!("{}/api/v1/apps", self.base);
198 let call_id = Uuid::new_v4();
199 debug!(url = url, app = as_serde!(app), call_id = as_debug!(call_id); "registering app");
200 let response = self.client.post(&url).json(&app).send().await?;
201
202 match response.error_for_status() {
203 Ok(response) => {
204 let response = read_response(response).await?;
205 debug!(
206 response = as_serde!(response), app = as_serde!(app),
207 url = url, method = stringify!($method),
208 call_id = as_debug!(call_id);
209 "received API response"
210 );
211 Ok(response)
212 }
213 Err(err) => {
214 error!(
215 err = as_debug!(err), url = url, method = stringify!($method),
216 call_id = as_debug!(call_id);
217 "error making API request"
218 );
219 Err(err.into())
220 }
221 }
222 }
223}
224
225impl Registered {
226 pub fn from_parts(
253 base: &str,
254 client_id: &str,
255 client_secret: &str,
256 redirect: &str,
257 scopes: Scopes,
258 force_login: bool,
259 ) -> Registered {
260 Registered {
261 base: base.to_string(),
262 client: Client::new(),
263 client_id: client_id.to_string(),
264 client_secret: client_secret.to_string(),
265 redirect: redirect.to_string(),
266 scopes,
267 force_login,
268 }
269 }
270}
271
272impl Registered {
273 pub fn into_parts(self) -> (String, String, String, String, Scopes, bool) {
307 (
308 self.base,
309 self.client_id,
310 self.client_secret,
311 self.redirect,
312 self.scopes,
313 self.force_login,
314 )
315 }
316
317 pub fn authorize_url(&self) -> Result<String> {
320 let scopes = format!("{}", self.scopes);
321 let scopes: String = utf8_percent_encode(&scopes, NON_ALPHANUMERIC).collect();
322 let url = if self.force_login {
323 format!(
324 "{}/oauth/authorize?client_id={}&redirect_uri={}&scope={}&force_login=true&\
325 response_type=code",
326 self.base, self.client_id, self.redirect, scopes,
327 )
328 } else {
329 format!(
330 "{}/oauth/authorize?client_id={}&redirect_uri={}&scope={}&response_type=code",
331 self.base, self.client_id, self.redirect, scopes,
332 )
333 };
334
335 Ok(url)
336 }
337
338 fn registered(&self, token: String) -> Data {
340 Data {
341 base: self.base.clone().into(),
342 client_id: self.client_id.clone().into(),
343 client_secret: self.client_secret.clone().into(),
344 redirect: self.redirect.clone().into(),
345 token: token.into(),
346 }
347 }
348
349 pub async fn complete<C>(&self, code: C) -> Result<Mastodon>
352 where
353 C: AsRef<str>,
354 {
355 let url =
356 format!(
357 "{}/oauth/token?client_id={}&client_secret={}&code={}&grant_type=authorization_code&\
358 redirect_uri={}",
359 self.base, self.client_id, self.client_secret, code.as_ref(), self.redirect
360 );
361 debug!(url = url; "completing registration");
362 let response = self.client.post(&url).send().await?;
363 debug!(
364 status = log_serde!(response Status), url = url,
365 headers = log_serde!(response Headers);
366 "received API response"
367 );
368 let token: AccessToken = read_response(response).await?;
369 debug!(url = url, body = as_serde!(token); "parsed response body");
370 let data = self.registered(token.access_token);
371 trace!(auth_data = as_serde!(data); "registered");
372
373 Ok(Mastodon::new(self.client.clone(), data))
374 }
375}
376
377#[derive(Debug, Clone)]
380pub struct Registered {
381 base: String,
382 client: Client,
383 client_id: String,
384 client_secret: String,
385 redirect: String,
386 scopes: Scopes,
387 force_login: bool,
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393
394 #[test]
395 fn test_registration_new() {
396 let r = Registration::new("https://example.com");
397 assert_eq!(r.base, "https://example.com".to_string());
398 assert_eq!(r.app_builder, AppBuilder::new());
399 }
400
401 #[test]
402 fn test_registration_with_sender() {
403 let r = Registration::with_sender("https://example.com");
404 assert_eq!(r.base, "https://example.com".to_string());
405 assert_eq!(r.app_builder, AppBuilder::new());
406 }
407
408 #[test]
409 fn test_set_client_name() {
410 let mut r = Registration::new("https://example.com");
411 r.client_name("foo-test");
412
413 assert_eq!(r.base, "https://example.com".to_string());
414 assert_eq!(
415 &mut r.app_builder,
416 AppBuilder::new().client_name("foo-test")
417 );
418 }
419
420 #[test]
421 fn test_set_redirect_uris() {
422 let mut r = Registration::new("https://example.com");
423 r.redirect_uris("https://foo.com");
424
425 assert_eq!(r.base, "https://example.com".to_string());
426 assert_eq!(
427 &mut r.app_builder,
428 AppBuilder::new().redirect_uris("https://foo.com")
429 );
430 }
431
432 #[test]
433 fn test_set_scopes() {
434 let mut r = Registration::new("https://example.com");
435 r.scopes(Scopes::all());
436
437 assert_eq!(r.base, "https://example.com".to_string());
438 assert_eq!(&mut r.app_builder, AppBuilder::new().scopes(Scopes::all()));
439 }
440
441 #[test]
442 fn test_set_website() {
443 let mut r = Registration::new("https://example.com");
444 r.website("https://website.example.com");
445
446 assert_eq!(r.base, "https://example.com".to_string());
447 assert_eq!(
448 &mut r.app_builder,
449 AppBuilder::new().website("https://website.example.com")
450 );
451 }
452
453 #[test]
454 fn test_default_redirect_uri() {
455 assert_eq!(&default_redirect_uri()[..], DEFAULT_REDIRECT_URI);
456 }
457}