use std::sync::Arc;
use log::debug;
use rand::{distributions::Alphanumeric, Rng};
use reqwest::{IntoUrl, Method, Url};
#[cfg(feature = "async")]
use super::private::AsyncClient;
#[cfg(feature = "sync")]
use super::private::SyncClient;
use super::{
private::{self, HttpClient},
SpotifyClientRef, ACCOUNTS_AUTHORIZE_ENDPOINT, RANDOM_STATE_LENGTH,
};
use crate::{
error::{Error, Result},
scope::ToScopesString,
};
#[cfg(feature = "async")]
pub type AsyncImplicitGrantUserClient = ImplicitGrantUserClient<AsyncClient>;
#[cfg(feature = "sync")]
pub type SyncImplicitGrantUserClient = ImplicitGrantUserClient<SyncClient>;
#[cfg(feature = "async")]
pub type AsyncIncompleteImplicitGrantUserClient = IncompleteImplicitGrantUserClient<AsyncClient>;
#[cfg(feature = "sync")]
pub type SyncIncompleteImplicitGrantUserClient = IncompleteImplicitGrantUserClient<SyncClient>;
#[cfg(feature = "async")]
pub type AsyncImplicitGrantUserClientBuilder = ImplicitGrantUserClientBuilder<AsyncClient>;
#[cfg(feature = "sync")]
pub type SyncImplicitGrantUserClientBuilder = ImplicitGrantUserClientBuilder<SyncClient>;
#[derive(Debug, Clone)]
pub struct ImplicitGrantUserClient<C>
where
C: HttpClient + Clone,
{
inner: Arc<ImplicitGrantUserClientRef>,
http_client: C,
}
#[derive(Debug)]
struct ImplicitGrantUserClientRef {
access_token: String,
}
#[derive(Debug, Clone)]
pub struct IncompleteImplicitGrantUserClient<C>
where
C: HttpClient + Clone,
{
redirect_uri: String,
state: String,
scopes: Option<String>,
show_dialog: bool,
spotify_client_ref: Arc<SpotifyClientRef>,
http_client: C,
}
pub struct ImplicitGrantUserClientBuilder<C>
where
C: HttpClient + Clone,
{
redirect_uri: String,
scopes: Option<String>,
show_dialog: bool,
spotify_client_ref: Arc<SpotifyClientRef>,
http_client: C,
}
impl<C> IncompleteImplicitGrantUserClient<C>
where
C: HttpClient + Clone,
{
pub fn get_authorize_url(&self) -> String {
let mut query_params = vec![
("response_type", "token"),
("redirect_uri", self.redirect_uri.as_str()),
("client_id", self.spotify_client_ref.client_id.as_str()),
("state", self.state.as_str()),
];
if let Some(scopes) = &self.scopes {
query_params.push(("scope", scopes.as_str()));
}
if self.show_dialog {
query_params.push(("show_dialog", "true"));
}
let authorize_url = Url::parse_with_params(ACCOUNTS_AUTHORIZE_ENDPOINT, &query_params)
.expect("failed to build authorize URL: invalid base URL (this is likely a bug)");
authorize_url.into()
}
pub fn finalize<S>(self, access_token: S, state: &str) -> Result<ImplicitGrantUserClient<C>>
where
S: Into<String>,
{
let access_token = access_token.into();
debug!(
"Attempting to finalize implicit grant flow user client with access_token: {} and state: {}",
access_token, state
);
if state != self.state {
return Err(Error::AuthorizationCodeStateMismatch);
}
Ok(ImplicitGrantUserClient {
inner: Arc::new(ImplicitGrantUserClientRef { access_token }),
http_client: self.http_client,
})
}
}
impl<C> ImplicitGrantUserClientBuilder<C>
where
C: HttpClient + Clone,
{
pub(super) fn new(redirect_uri: String, spotify_client_ref: Arc<SpotifyClientRef>, http_client: C) -> Self {
Self {
redirect_uri,
scopes: None,
show_dialog: false,
spotify_client_ref,
http_client,
}
}
pub fn scopes<T>(self, scopes: T) -> Self
where
T: ToScopesString,
{
Self {
scopes: Some(scopes.to_scopes_string()),
..self
}
}
pub fn show_dialog(self, show_dialog: bool) -> Self {
Self { show_dialog, ..self }
}
pub fn build(self) -> IncompleteImplicitGrantUserClient<C> {
let state = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(RANDOM_STATE_LENGTH)
.map(char::from)
.collect();
IncompleteImplicitGrantUserClient {
redirect_uri: self.redirect_uri,
state,
scopes: self.scopes,
show_dialog: self.show_dialog,
spotify_client_ref: self.spotify_client_ref,
http_client: self.http_client,
}
}
}
impl<C> crate::private::Sealed for ImplicitGrantUserClient<C> where C: HttpClient + Clone {}
#[cfg(feature = "async")]
impl private::BuildHttpRequestAsync for AsyncImplicitGrantUserClient {
fn build_http_request<U>(&self, method: Method, url: U) -> reqwest::RequestBuilder
where
U: IntoUrl,
{
self.http_client
.request(method, url)
.bearer_auth(self.inner.access_token.as_str())
}
}
#[cfg(feature = "sync")]
impl private::BuildHttpRequestSync for SyncImplicitGrantUserClient {
fn build_http_request<U>(&self, method: Method, url: U) -> reqwest::blocking::RequestBuilder
where
U: IntoUrl,
{
self.http_client
.request(method, url)
.bearer_auth(self.inner.access_token.as_str())
}
}
#[cfg(feature = "async")]
impl super::ScopedClient for AsyncImplicitGrantUserClient {}
#[cfg(feature = "sync")]
impl super::ScopedClient for SyncImplicitGrantUserClient {}
#[cfg(feature = "async")]
impl super::UnscopedClient for AsyncImplicitGrantUserClient {}
#[cfg(feature = "sync")]
impl super::UnscopedClient for SyncImplicitGrantUserClient {}
#[cfg(feature = "async")]
#[async_trait::async_trait]
impl private::AccessTokenExpiryAsync for AsyncImplicitGrantUserClient {
async fn handle_access_token_expired(&self) -> Result<private::AccessTokenExpiryResult> {
Ok(private::AccessTokenExpiryResult::Inapplicable)
}
}
#[cfg(feature = "sync")]
impl private::AccessTokenExpirySync for SyncImplicitGrantUserClient {
fn handle_access_token_expired(&self) -> Result<private::AccessTokenExpiryResult> {
Ok(private::AccessTokenExpiryResult::Inapplicable)
}
}