pagbank-cli 0.2.0

CLI para as APIs do PagBank
mod cli;
mod client;
mod commands;
mod config;
mod errors;
mod output;
mod pix;
mod validators;

use clap::CommandFactory;
use clap::Parser;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let cli = cli::Cli::parse();

    if cli.verbose {
        eprintln!("pb: modo verbose habilitado");
    }

    let env_override = cli.environment.as_deref();
    let output_fmt = &cli.output;

    let result = match cli.command {
        cli::Commands::Auth { action } => commands::auth::run(action).await,
        cli::Commands::Config { action } => commands::config_cmd::run(action).await,
        cli::Commands::Keys { action } => {
            commands::keys::run(action, env_override, output_fmt).await
        }
        cli::Commands::Connect { action } => {
            commands::connect::run(action, env_override, output_fmt).await
        }
        cli::Commands::Certs { action } => {
            commands::certs::run(action, env_override, output_fmt).await
        }
        cli::Commands::Accounts { action } => {
            commands::accounts::run(action, env_override, output_fmt).await
        }
        cli::Commands::Orders { action } => {
            commands::orders::run(action, env_override, output_fmt).await
        }
        cli::Commands::Checkouts { action } => {
            commands::checkouts::run(action, env_override, output_fmt).await
        }
        cli::Commands::Plans { action } => {
            commands::plans::run(action, env_override, output_fmt).await
        }
        cli::Commands::Subscribers { action } => {
            commands::subscribers::run(action, env_override, output_fmt).await
        }
        cli::Commands::Subscriptions { action } => {
            commands::subscriptions::run(action, env_override, output_fmt).await
        }
        cli::Commands::Coupons { action } => {
            commands::coupons::run(action, env_override, output_fmt).await
        }
        cli::Commands::Invoices { action } => {
            commands::invoices::run(action, env_override, output_fmt).await
        }
        cli::Commands::Chargebacks { action } => {
            commands::chargebacks::run(action, env_override, output_fmt).await
        }
        cli::Commands::Notifications { action } => {
            commands::notifications::run(action, env_override, output_fmt).await
        }
        cli::Commands::Transfers { action } => {
            commands::transfers::run(action, env_override, output_fmt).await
        }
        cli::Commands::Edi { action } => commands::edi::run(action, env_override, output_fmt).await,
        cli::Commands::Clubpag { action } => {
            commands::clubpag::run(action, env_override, output_fmt).await
        }
        cli::Commands::Preferences { action } => {
            commands::preferences::run(action, env_override, output_fmt).await
        }
        cli::Commands::Retries { action } => {
            commands::retries::run(action, env_override, output_fmt).await
        }
        cli::Commands::Webhooks { action } => match action {
            cli::WebhooksAction::Verify {
                token,
                signature,
                payload_file,
            } => {
                use std::io::Read;
                let payload = if let Some(path) = payload_file {
                    std::fs::read_to_string(&path)?
                } else {
                    let mut buf = String::new();
                    std::io::stdin().read_to_string(&mut buf)?;
                    buf
                };
                let mut hasher = sha2::Sha256::new();
                use sha2::Digest;
                hasher.update(token.as_bytes());
                hasher.update(b"-");
                hasher.update(payload.as_bytes());
                let computed = hasher
                    .finalize()
                    .iter()
                    .map(|b| format!("{:02x}", b))
                    .collect::<String>();
                if constant_time_eq(&computed, &signature) {
                    output::print_success("Assinatura válida!");
                } else {
                    output::print_error("Assinatura inválida");
                    eprintln!("  Esperada: {signature}");
                    eprintln!("  Computada:  {computed}");
                    std::process::exit(1);
                }
                Ok(())
            }
        },
        cli::Commands::Completion { shell } => {
            generate_completion(shell);
            Ok(())
        }
        cli::Commands::Doctor { connect } => commands::doctor::run(connect).await,
    };

    if let Err(e) = result {
        let msg = if let Some(pagbank_err) = e.downcast_ref::<pagbank_sdk::PagBankError>() {
            errors::translate(pagbank_err)
        } else {
            e.to_string()
        };
        output::print_error(&msg);
        std::process::exit(1);
    }

    Ok(())
}

fn constant_time_eq(a: &str, b: &str) -> bool {
    let a = a.as_bytes();
    let b = b.as_bytes();
    if a.len() != b.len() {
        return false;
    }
    let mut diff = 0u8;
    for (x, y) in a.iter().zip(b.iter()) {
        diff |= x ^ y;
    }
    diff == 0
}

fn generate_completion(shell: cli::Shell) {
    use clap_complete::{generate, Shell};
    let mut cmd = cli::Cli::command();
    let shell = match shell {
        cli::Shell::Bash => Shell::Bash,
        cli::Shell::Zsh => Shell::Zsh,
        cli::Shell::Fish => Shell::Fish,
        cli::Shell::Powershell => Shell::PowerShell,
        cli::Shell::Elvish => Shell::Elvish,
    };
    generate(shell, &mut cmd, "pb", &mut std::io::stdout());
}