use clap::Subcommand;
use crate::api::HtbClient;
use crate::output::{self, OutputFormat};
#[derive(Subcommand)]
#[command(
after_help = "Examples:\n htb challenges list --category Web Web challenges\n htb challenges categories All categories\n htb challenges info Poly Challenge details\n htb challenges start Poly Start container instance\n htb challenges submit 112 'HTB{f}' Submit a flag\n htb challenges download Poly Download challenge files"
)]
pub enum ChallengeCommand {
List {
#[arg(long, help = "Filter by category")]
category: Option<String>,
#[arg(long, help = "Page number")]
page: Option<u32>,
},
Categories,
Info {
slug: String,
},
Download {
slug: String,
},
Start {
slug: String,
},
Stop {
slug: String,
},
Submit {
id: u64,
flag: String,
},
}
pub async fn handle(
client: &HtbClient,
cmd: ChallengeCommand,
format: OutputFormat,
) -> anyhow::Result<()> {
match cmd {
ChallengeCommand::List { category, page } => {
let result = client.challenges().list(page.unwrap_or(1), 100).await?;
let mut challenges = result.data;
if let Some(ref cat_filter) = category {
challenges.retain(|c| {
c.category_name
.as_ref()
.is_some_and(|cn| cn.eq_ignore_ascii_case(cat_filter))
});
}
output::print_list(&challenges, format);
output::print_pagination(
result.meta.current_page,
result.meta.last_page,
result.meta.total,
);
}
ChallengeCommand::Categories => {
let categories = client.challenges().categories().await?;
output::print_list(&categories, format);
}
ChallengeCommand::Info { slug } => {
let detail = client.challenges().info(&slug).await?;
let fields = vec![
("ID", detail.id.to_string()),
("Name", detail.name.clone()),
("Difficulty", detail.difficulty.clone().unwrap_or_default()),
("Category", detail.category_name.clone().unwrap_or_default()),
("Points", detail.points.clone().unwrap_or_default()),
(
"Rating",
detail.stars.map(|s| format!("{s:.1}")).unwrap_or_default(),
),
("Solves", detail.solves.to_string()),
(
"First Blood",
detail
.first_blood_user
.clone()
.unwrap_or_else(|| "-".into()),
),
(
"Blood Time",
detail
.first_blood_time
.clone()
.unwrap_or_else(|| "-".into()),
),
("Creator", detail.creator_name.clone().unwrap_or_default()),
(
"Description",
detail.description.clone().unwrap_or_default(),
),
("Play Methods", detail.play_methods.join(", ")),
(
"Solved",
if detail.auth_user_solve { "Yes" } else { "No" }.into(),
),
];
output::print_detail(&detail, format, &fields);
}
ChallengeCommand::Download { slug } => {
let detail = client.challenges().info(&slug).await?;
let url = client.challenges().download_link(detail.id).await?;
let bytes = client.challenges().download_file(&url).await?;
let raw_name = detail.file_name.unwrap_or_else(|| format!("{slug}.zip"));
let filename = crate::sanitize_filename(&raw_name, &format!("{slug}.zip"));
std::fs::write(&filename, &bytes)?;
output::print_message(&format!("Downloaded {} ({} bytes)", filename, bytes.len()));
}
ChallengeCommand::Start { slug } => {
let detail = client.challenges().info(&slug).await?;
let resp = client.challenges().start(detail.id).await?;
output::print_message(&resp.message);
}
ChallengeCommand::Stop { slug } => {
let detail = client.challenges().info(&slug).await?;
let resp = client.challenges().stop(detail.id).await?;
output::print_message(&resp.message);
}
ChallengeCommand::Submit { id, flag } => {
let resp = client.challenges().submit_flag(id, &flag).await?;
output::print_message(&resp.message);
}
}
Ok(())
}