1use clap::Subcommand;
2
3use crate::api::HtbClient;
4use crate::output::{self, OutputFormat};
5
6#[derive(Subcommand)]
7#[command(
8 after_help = "Examples:\n htb machines list --os linux Linux machines only\n htb machines list --difficulty easy Easy machines only\n htb machines info Bedside Machine details\n htb machines start Bedside Spawn a machine\n htb machines submit Bedside 'HTB{f}' Submit a flag\n htb machines active Current machine\n htb machines todo Your todo list"
9)]
10pub enum MachineCommand {
11 List {
13 #[arg(long, help = "Include retired machines")]
14 retired: bool,
15 #[arg(long, help = "Filter by OS (linux, windows)")]
16 os: Option<String>,
17 #[arg(long, help = "Filter by difficulty (easy, medium, hard, insane)")]
18 difficulty: Option<String>,
19 #[arg(long, help = "Page number")]
20 page: Option<u32>,
21 #[arg(long, help = "Fetch all pages")]
22 all: bool,
23 },
24 Info {
26 name_or_id: String,
28 },
29 Start {
31 name_or_id: String,
33 },
34 Stop {
36 name_or_id: Option<String>,
38 },
39 Reset {
41 name_or_id: String,
43 },
44 Submit {
46 name_or_id: String,
48 flag: String,
50 },
51 Extend {
53 name_or_id: Option<String>,
55 },
56 Active,
58 Todo {
60 #[command(subcommand)]
61 command: Option<TodoCommand>,
62 },
63}
64
65#[derive(Subcommand)]
66pub enum TodoCommand {
67 Add {
69 name_or_id: String,
71 },
72 Remove {
74 name_or_id: String,
76 },
77}
78
79pub async fn handle(
80 client: &HtbClient,
81 cmd: MachineCommand,
82 format: OutputFormat,
83) -> anyhow::Result<()> {
84 match cmd {
85 MachineCommand::List {
86 retired,
87 os,
88 difficulty,
89 page,
90 all,
91 } => {
92 if retired {
93 tracing::warn!(
94 "--retired flag is not yet supported by the v5 API; showing all machines"
95 );
96 }
97 let per_page = 100;
98 let start_page = page.unwrap_or(1);
99
100 let result = client.machines().list(start_page, per_page).await?;
101 let mut machines = result.data;
102
103 if all {
104 let mut next = result.links.next;
105 let mut current = start_page;
106 while next.is_some() {
107 current += 1;
108 let page_result = client.machines().list(current, per_page).await?;
109 next = page_result.links.next;
110 machines.extend(page_result.data);
111 }
112 }
113
114 if let Some(ref os_filter) = os {
115 machines.retain(|m| m.os.eq_ignore_ascii_case(os_filter));
116 }
117 if let Some(ref diff_filter) = difficulty {
118 machines.retain(|m| {
119 m.difficulty_text
120 .as_ref()
121 .is_some_and(|d| d.eq_ignore_ascii_case(diff_filter))
122 });
123 }
124
125 output::print_list(&machines, format);
126 if !all {
127 output::print_pagination(
128 result.meta.current_page,
129 result.meta.last_page,
130 result.meta.total,
131 format,
132 );
133 }
134 }
135
136 MachineCommand::Info { name_or_id } => {
137 let machine = client.machines().profile(&name_or_id).await?;
138 let fields = vec![
139 ("ID", machine.id.to_string()),
140 ("Name", machine.name.clone()),
141 ("OS", machine.os.clone()),
142 (
143 "Difficulty",
144 machine.difficulty_text.clone().unwrap_or_default(),
145 ),
146 (
147 "Rating",
148 machine
149 .rating
150 .map(|r| format!("{r:.1}"))
151 .unwrap_or_default(),
152 ),
153 ("Points", machine.points.to_string()),
154 ("State", machine.state.clone().unwrap_or_default()),
155 ("User Owns", machine.user_owns_count.to_string()),
156 ("Root Owns", machine.root_owns_count.to_string()),
157 ("IP", machine.ip.clone().unwrap_or_else(|| "-".into())),
158 (
159 "Creator",
160 machine
161 .first_creator
162 .as_ref()
163 .map(|c| c.name.clone())
164 .unwrap_or_default(),
165 ),
166 ];
167 output::print_detail(&machine, format, &fields);
168 }
169
170 MachineCommand::Start { name_or_id } => {
171 let machine = client.machines().profile(&name_or_id).await?;
172 let resp = client.machines().start(machine.id).await?;
173 output::print_message(&resp.message);
174 }
175
176 MachineCommand::Stop { name_or_id } => {
177 let machine_id = match name_or_id {
178 Some(name) => client.machines().profile(&name).await?.id,
179 None => {
180 client
181 .machines()
182 .active()
183 .await?
184 .ok_or_else(|| anyhow::anyhow!("No active machine. Specify a name or ID."))?
185 .id
186 }
187 };
188 let resp = client.machines().stop(machine_id).await?;
189 output::print_message(&resp.message);
190 }
191
192 MachineCommand::Reset { name_or_id } => {
193 let machine = client.machines().profile(&name_or_id).await?;
194 eprint!("Reset {}? [y/N] ", machine.name);
195 let mut input = String::new();
196 std::io::stdin().read_line(&mut input)?;
197 if input.trim().eq_ignore_ascii_case("y") {
198 let resp = client.machines().reset(machine.id).await?;
199 output::print_message(&resp.message);
200 } else {
201 output::print_message("Cancelled.");
202 }
203 }
204
205 MachineCommand::Submit { name_or_id, flag } => {
206 let machine = client.machines().profile(&name_or_id).await?;
207 let difficulty = machine.difficulty.unwrap_or(50);
209 let resp = client
210 .machines()
211 .submit_flag(machine.id, &flag, difficulty)
212 .await?;
213 output::print_message(&resp.message);
214 }
215
216 MachineCommand::Extend { name_or_id } => {
217 let machine_id = match name_or_id {
218 Some(name) => client.machines().profile(&name).await?.id,
219 None => {
220 client
221 .machines()
222 .active()
223 .await?
224 .ok_or_else(|| anyhow::anyhow!("No active machine. Specify a name or ID."))?
225 .id
226 }
227 };
228 let resp = client.machines().extend(machine_id).await?;
229 output::print_message(&resp.message);
230 }
231
232 MachineCommand::Active => {
233 let active = client.machines().active().await?;
234 match active {
235 Some(vm) => {
236 let fields = vec![
237 ("ID", vm.id.to_string()),
238 ("Name", vm.name.clone()),
239 ("Type", vm.vm_type.clone().unwrap_or_default()),
240 (
241 "Expires",
242 vm.expires_at.clone().unwrap_or_else(|| "-".into()),
243 ),
244 ];
245 output::print_detail(&vm, format, &fields);
246 }
247 None => output::print_message("No active machine."),
248 }
249 }
250
251 MachineCommand::Todo { command } => match command {
252 None => {
253 let todos = client.machines().todo_list().await?;
254 if todos.is_empty() {
255 output::print_message("No machines in todo list.");
256 } else {
257 output::print_list(&todos, format);
258 }
259 }
260 Some(TodoCommand::Add { name_or_id }) => {
261 let machine = client.machines().profile(&name_or_id).await?;
262 let todos = client.machines().todo_list().await?;
263 if todos.iter().any(|m| m.id == machine.id) {
264 output::print_message(&format!(
265 "{} is already in your todo list.",
266 machine.name
267 ));
268 } else {
269 let resp = client.machines().todo_toggle(machine.id).await?;
270 output::print_message(&resp.message);
271 }
272 }
273 Some(TodoCommand::Remove { name_or_id }) => {
274 let machine = client.machines().profile(&name_or_id).await?;
275 let todos = client.machines().todo_list().await?;
276 if todos.iter().any(|m| m.id == machine.id) {
277 let resp = client.machines().todo_toggle(machine.id).await?;
278 output::print_message(&resp.message);
279 } else {
280 output::print_message(&format!("{} is not in your todo list.", machine.name));
281 }
282 }
283 },
284 }
285 Ok(())
286}