trustblock-cli 0.9.1

human-friendly console utility that helps to interact with Trustblock from command line.
Documentation
use std::{
    future::Future,
    path::PathBuf,
    sync::{Arc, Mutex},
};

use eyre::{eyre, ContextCompat};
use reqwest::{
    header::{self, HeaderValue},
    Client,
};
use serde::ser::{Serialize, Serializer};
use serde_json::Value;
use tempfile::NamedTempFile;
use w3s::helper;

use crate::{
    constants::{
        CRATES_API_RELEASE_ENDPOINT, GITHUB_LATEST_RELEASE, PDF_GENERATE_ENDPOINT,
        TRUSTBLOCK_API_KEY_HEADER, WEB3_STORAGE_API_ENDPOINT, WEB3_STORAGE_ENDPOINT,
    },
    types::{Issue, IssueCount, Severity, Status},
    utils::{apply_dotenv, validate_pdf},
};

pub trait Cmd: clap::Parser + Sized {
    fn run(self) -> eyre::Result<()>;
}

pub fn block_on<F: Future>(future: F) -> F::Output {
    let rt = tokio::runtime::Runtime::new().expect("could not start tokio rt");
    rt.block_on(future)
}

#[allow(clippy::future_not_send)]
pub async fn upload_ipfs(
    report_file_path: PathBuf,
    api_key: &str,
) -> eyre::Result<(String, String)> {
    apply_dotenv()?;

    let client = Client::new();

    let web3_storage_endpoint = std::env::var("WEB3_STORAGE_API_ENDPOINT")
        .unwrap_or_else(|_| WEB3_STORAGE_API_ENDPOINT.to_string());

    let response = client
        .post(web3_storage_endpoint)
        .header(TRUSTBLOCK_API_KEY_HEADER, api_key)
        .send()
        .await?;

    if !response.status().is_success() {
        return Err(eyre!("Could not upload to report. Try again"));
    }

    let api_response_data = response.json::<Value>().await?;

    let api_key = &api_response_data["apiKey"]
        .to_string()
        .trim()
        .replace('\"', "");

    if api_key.is_empty() {
        return Err(eyre!("Could not upload to report. Try again"));
    }

    let results = helper::upload(
        report_file_path.to_str().wrap_err("Invalid File Path")?,
        api_key,
        2,
        Some(Arc::new(Mutex::new(|name, _, pos, total| {
            if pos != 0 {
                if pos == total {
                    println!("[+] Uploading is done\n");
                } else {
                    let percentage = (pos * 100) / total;
                    println!("[+] Uploading {name}. Finished %{percentage:}");
                }
            }
        }))),
        None,
        None,
        None,
    )
    .await?;

    if report_file_path.starts_with(std::env::temp_dir()) {
        std::fs::remove_file(report_file_path)?;
    }

    let cid = results[0].to_string();

    let report_url = format!("https://{cid}{WEB3_STORAGE_ENDPOINT}");

    Ok((cid, report_url))
}

pub async fn generate_pdf_from_url(url: String, api_key: &str) -> eyre::Result<PathBuf> {
    apply_dotenv()?;

    let client = Client::new();

    let temp_pdf_file = NamedTempFile::new()?;

    let pdf_generate_endpoint = std::env::var("PDF_GENERATE_ENDPOINT")
        .unwrap_or_else(|_| PDF_GENERATE_ENDPOINT.to_string());

    let response = client
        .get(pdf_generate_endpoint)
        .header(TRUSTBLOCK_API_KEY_HEADER, api_key)
        .query(&[("url", url)])
        .send()
        .await?;

    if !response.status().is_success() {
        return Err(eyre!(
            "Could not fetch web based audit report: {:#?}",
            response.json::<Value>().await?
        ));
    }

    let response_bytes = response.bytes().await?;

    let temp_pdf_path = temp_pdf_file.path();

    std::fs::write(temp_pdf_path, response_bytes)?;

    validate_pdf(temp_pdf_path.to_str().expect("should not fail"))?;

    Ok(temp_pdf_file.into_temp_path().keep()?)
}

pub fn serialize_issues<S>(issues: &IssueCount, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    let transformed_issues = get_transformed_issues(issues);
    transformed_issues.serialize(serializer)
}

#[must_use]
pub fn get_transformed_issues(issues: &IssueCount) -> Vec<Issue> {
    let mut result: Vec<Issue> = Vec::new();

    for (status, severity_count) in [
        (Status::Fixed, issues.fixed),
        (Status::RiskAccepted, issues.risk_accepted),
    ] {
        for (severity, count) in [
            (Severity::Low, severity_count.low),
            (Severity::Medium, severity_count.medium),
            (Severity::High, severity_count.high),
            (Severity::Critical, severity_count.critical),
        ] {
            for _ in 0..count {
                result.push(Issue { status, severity });
            }
        }
    }

    result
}

pub async fn check_update() -> eyre::Result<()> {
    let client = Client::new();

    let local_version = clap::crate_version!();

    let response = client
        .get(CRATES_API_RELEASE_ENDPOINT)
        .header(
            header::USER_AGENT,
            HeaderValue::from_str(
                "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1",
            )?,
        )
        .send()
        .await?;

    if response.status().is_success() {
        let json_response = response.json::<Value>().await?;

        let version = &json_response["crate"]["newest_version"]
            .as_str()
            .unwrap_or_default();

        if version != &local_version {
            println!(
                "New version of Trustblock CLI is available: {version}.\nPlease download a new version from: {GITHUB_LATEST_RELEASE}\n\n"
            );
        }
    }

    Ok(())
}