use super::{LoginError, LoginResult};
use crate::modules::bakalari::Client;
use std::{
sync::Arc,
time::{Duration, Instant},
};
use tokio::sync::{mpsc, oneshot};
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct TempToken {
token: String,
expiration: Instant,
}
const TOKEN_LIFETIME: u64 = 60 * 5;
impl TempToken {
fn new(token: String) -> Self {
Self {
token,
expiration: Instant::now() + Duration::from_secs(TOKEN_LIFETIME),
}
}
fn expired(&self) -> bool {
Instant::now() > self.expiration
}
fn get(&self) -> Option<&str> {
if self.expired() {
return None;
}
Some(&self.token)
}
}
type TokenRequest = (Arc<Client>, oneshot::Sender<LoginResult<String>>);
#[derive(Debug, Clone)]
pub struct Credentials {
sender: mpsc::Sender<TokenRequest>,
}
impl Credentials {
pub async fn new((username, password): (String, String), client: &Client) -> LoginResult<Self> {
let token = TempToken::new(Self::login((&username, &password), client).await?);
let (sender, mut receiver) = mpsc::channel::<TokenRequest>(10);
tokio::spawn(async move {
let mut store = token;
while let Some((client, sender)) = receiver.recv().await {
let token = if let Some(token) = store.get() {
Ok(token.to_owned())
} else {
let token = Self::login((&username, &password), &client).await;
token.map(|token| {
store = TempToken::new(token);
store.token.clone()
})
};
sender.send(token).unwrap();
}
});
Ok(Self { sender })
}
pub async fn get_token(&self, client: Arc<Client>) -> LoginResult<String> {
let (tx, rx) = oneshot::channel();
self.sender
.send((client, tx))
.await
.expect("failed to send token request");
rx.await.unwrap()
}
pub async fn login((username, password): (&str, &str), client: &Client) -> LoginResult<String> {
let res = client
.reqwest_client()
.post(client.url().join("Login").unwrap())
.body(format!("username={username}&password={password}"))
.header("Content-Type", "application/x-www-form-urlencoded")
.send()
.await?;
if res.status().as_u16() != 302 {
return Err(LoginError::Login(res));
}
let v = res
.headers()
.get_all("Set-Cookie")
.iter()
.filter_map(|h| h.to_str().ok())
.filter_map(|h| h.split_once(';'))
.map(|h| h.0)
.filter_map(|h| h.split_once('='))
.find(|h| h.0 == "BakaAuth")
.map(|h| h.1)
.ok_or(LoginError::CookieParse)?;
Ok(v.to_owned())
}
}