use std::fmt::{Debug, Formatter};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::auth::{TokenResponseData, AUTH_CONTENT_TYPE};
use crate::{Authenticator, Authorized, utils};
use async_trait::async_trait;
use log::warn;
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE, USER_AGENT};
use reqwest::{Body, Client};
use crate::error::http_error::IntoResult;
use crate::error::internal_error::InternalError;
use crate::error::Error;
#[derive(Clone)]
pub struct CodeAuthenticator {
pub token: Option<String>,
pub expiration_time: Option<u128>,
pub refresh_token: Option<String>,
pub(crate) client_id: String,
pub(crate) client_secret: String,
authorization_code: String,
redirect_uri: String,
}
impl Debug for CodeAuthenticator {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"[CodeAuthenticator] Token Defined: {} Expires At {} And Refrsh Token Defined: {}",
self.token.is_some(),
self.expiration_time.unwrap_or(0),
self.refresh_token.is_some()
)
}
}
impl CodeAuthenticator {
#[allow(clippy::new_ret_no_self)]
pub fn new<S: Into<String>>(
client_id: S,
client_secret: S,
authorization_code: S,
redirect_uri: S,
) -> CodeAuthenticator {
CodeAuthenticator {
token: None,
expiration_time: None,
refresh_token: None,
client_id: client_id.into(),
client_secret: client_secret.into(),
authorization_code: authorization_code.into(),
redirect_uri: redirect_uri.into(),
}
}
pub fn generate_authorization_url(
client_id: impl AsRef<str>,
redirect_uri: impl AsRef<str>,
state: impl AsRef<str>,
duration: impl AsRef<str>,
scope: Vec<&str>,
) -> String {
format!(
"https://www.reddit.com/api/v1/authorize?client_id={}&response_type=code&state={}&redirect_uri={}&duration={}&scope={}",
client_id.as_ref(),
state.as_ref(),
redirect_uri.as_ref(),
duration.as_ref(),
scope.join(",")
)
}
}
#[async_trait(?Send)]
impl Authenticator for CodeAuthenticator {
async fn login(&mut self, client: &Client, user_agent: &str) -> Result<bool, Error> {
let url = "https://www.reddit.com/api/v1/access_token";
let body = format!(
"grant_type=authorization_code&code={}&redirect_uri={}",
&self.authorization_code.trim_end_matches("#_"),
&self.redirect_uri
);
let mut header = HeaderMap::new();
header.insert(
AUTHORIZATION,
HeaderValue::from_str(&*format!(
"Basic {}",
utils::basic_header(&self.client_id, &self.client_secret)
))
.unwrap(),
);
header.insert(USER_AGENT, HeaderValue::from_str(user_agent).unwrap());
header.insert(
CONTENT_TYPE,
HeaderValue::from_str("application/x-www-form-urlencoded").unwrap(),
);
let response = client
.post(url)
.body(Body::from(body))
.headers(header)
.send()
.await
.map_err(InternalError::from)?;
response.status().into_result()?;
let token: TokenResponseData = response.json().await?;
self.token = Some(token.access_token);
let x = token.expires_in * 1000;
let x1 = (x as u128)
+ SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis();
self.expiration_time = Some(x1);
if !token.refresh_token.is_empty() {
self.refresh_token = Some(token.refresh_token);
}
return Ok(true);
}
async fn logout(&mut self, client: &Client, user_agent: &str) -> Result<(), Error> {
let url = "https://www.reddit.com/api/v1/revoke_token";
let body = if let Some(refresh_token) = &self.refresh_token {
format!("token={}&token_type_hint=refresh_token", refresh_token)
} else if let Some(token) = self.token.as_ref() {
format!("token={}&token_type_hint=access_token", token)
} else {
return Ok(());
};
let mut header = HeaderMap::new();
header.insert(USER_AGENT, HeaderValue::from_str(user_agent).unwrap());
header.insert(CONTENT_TYPE, AUTH_CONTENT_TYPE.clone());
let response = client
.post(url)
.body(Body::from(body))
.headers(header)
.send()
.await?;
response.status().into_result()?;
self.token = None;
self.expiration_time = None;
self.refresh_token = None;
Ok(())
}
async fn token_refresh(&mut self, client: &Client, user_agent: &str) -> Result<bool, Error> {
let url = "https://www.reddit.com/api/v1/access_token";
let body = format!(
"grant_type=refresh_token&refresh_token={}",
&self.refresh_token.to_owned().unwrap()
);
let mut header = HeaderMap::new();
header.insert(
AUTHORIZATION,
HeaderValue::from_str(&*format!(
"Basic {}",
utils::basic_header(&self.client_id, &self.client_secret)
))
.unwrap(),
);
header.insert(USER_AGENT, HeaderValue::from_str(user_agent).unwrap());
header.insert(
CONTENT_TYPE,
HeaderValue::from_str("application/x-www-form-urlencoded").unwrap(),
);
let response = client
.post(url)
.body(Body::from(body))
.headers(header)
.send()
.await
.map_err(InternalError::from)?;
response.status().into_result()?;
let token: TokenResponseData = response.json().await?;
self.token = Some(token.access_token);
let x = token.expires_in * 1000;
let x1 = (x as u128)
+ SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis();
self.expiration_time = Some(x1);
return Ok(true);
}
fn headers(&self, headers: &mut HeaderMap) {
if let Some(token) = self.token.as_ref() {
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {}", token)).unwrap(),
);
} else {
warn!("No token found");
}
}
fn oauth(&self) -> bool {
true
}
fn needs_token_refresh(&self) -> bool {
let i = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis();
if self.refresh_token.is_some() {
if self.expiration_time.is_none() {
true
} else {
i >= self.expiration_time.unwrap()
}
} else {
false
}
}
fn get_refresh_token(&self) -> Option<String> {
self.refresh_token.to_owned()
}
}
impl Authorized for CodeAuthenticator {}