use crate::{DDG_DOMAIN, DuckAddress, get_client, get_token_file};
use std::fs;
use std::io::{self, Write};
use reqwest::{StatusCode, Url};
use serde::Deserialize;
const DDG_API_LOGIN_LINK: &str = "https://quack.duckduckgo.com/api/auth/loginlink";
const DDG_API_LOGIN_OTP_VERIFY: &str = "https://quack.duckduckgo.com/api/auth/login";
const DDG_API_DASHBOARD: &str = "https://quack.duckduckgo.com/api/email/dashboard";
#[derive(Debug)]
pub enum SignInErr {
WrongUserEmail,
OtpVerificationFailed,
DashboardFailed,
TokenSaveFailure,
}
#[allow(unused)]
#[derive(Deserialize)]
struct SignInResponse {
status: String,
token: String,
user: String,
}
#[allow(unused)]
#[derive(Deserialize)]
struct Stats {
addresses_generated: u8,
}
#[allow(unused)]
#[derive(Deserialize)]
struct User {
access_token: String,
cohort: String,
email: String,
username: String,
}
#[allow(unused)]
#[derive(Deserialize)]
struct SignInDashboardResponse {
invites: Vec<u8>,
stats: Stats,
user: User,
}
async fn send_otp_to_user(user: &str) -> Result<(), SignInErr> {
let client = get_client();
let url = Url::parse_with_params(DDG_API_LOGIN_LINK, &[("user", user)]).unwrap();
let res = client
.get(url)
.send()
.await
.expect("Failed to send login request");
if res.status() != StatusCode::OK {
return Err(SignInErr::WrongUserEmail);
}
Ok(())
}
fn get_otp_from_user() -> String {
print!("one-time passphrase: ");
io::stdout().flush().unwrap();
let mut line = String::new();
io::stdin().read_line(&mut line).unwrap();
line.trim().replace(" ", "+")
}
async fn verify_otp(otp: &str, user: &str) -> Result<String, SignInErr> {
let client = get_client();
let url =
Url::parse_with_params(DDG_API_LOGIN_OTP_VERIFY, &[("otp", otp), ("user", user)]).unwrap();
let res = client.get(url).send().await.expect("Failed to verify OTP");
if res.status() != StatusCode::OK {
return Err(SignInErr::OtpVerificationFailed);
}
let res = res
.json::<SignInResponse>()
.await
.expect("Failed to convert sign-in response to struct");
Ok(res.token)
}
async fn get_user(token: &str) -> Result<User, SignInErr> {
let client = get_client();
let res = client
.get(DDG_API_DASHBOARD)
.header("Authorization", format!("Bearer {}", token))
.send()
.await
.expect("Failed to get dashboard");
if res.status() != StatusCode::OK {
return Err(SignInErr::DashboardFailed);
}
let res = res
.json::<SignInDashboardResponse>()
.await
.expect("Failed to convert dashboard response to struct");
Ok(res.user)
}
fn save_token(token: &str) -> io::Result<()> {
let file = get_token_file()?;
let mut file = fs::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(file)?;
write!(file, "{token}")?;
Ok(())
}
pub async fn sign_in(address: &DuckAddress) -> Result<String, SignInErr> {
let alias = address.get_alias();
send_otp_to_user(alias).await?;
let otp = get_otp_from_user();
let token = verify_otp(&otp, alias).await?;
let user = get_user(&token).await?;
if save_token(&user.access_token).is_err() {
return Err(SignInErr::TokenSaveFailure);
}
Ok(format!(
" Forwarding Address: <{}>\n Duck Address: <{}@{}>",
user.email, user.username, DDG_DOMAIN
))
}