ddgm 0.1.0

ddgm: DuckDuckGo eMail
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)
}

// TODO: encrypt file with pgp for storage. (or some other "standard" way)
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
    ))
}