1use serde_json::json;
2
3use crate::error::HtbError;
4use crate::models::ctf::{
5 CtfChallengeSolve, CtfEvent, CtfEventData, CtfEventDetail, CtfFlagResult, CtfMenu,
6 CtfScoreboard, CtfSolve, CtfTeamOverview, CtfUserProfile,
7};
8use crate::models::ActionResponse;
9
10#[derive(serde::Serialize)]
11struct ProgressBody {
12 status: Option<String>,
13}
14
15use super::HtbClient;
16
17pub struct CtfApi<'a>(pub(crate) &'a HtbClient);
18
19impl CtfApi<'_> {
20 pub async fn profile(&self) -> Result<CtfUserProfile, HtbError> {
21 self.0.get("/api/users/profile").await
22 }
23
24 pub async fn events(&self) -> Result<Vec<CtfEvent>, HtbError> {
25 self.0.get("/api/ctfs").await
26 }
27
28 pub async fn event_details(&self, slug: &str) -> Result<CtfEventDetail, HtbError> {
29 let encoded = super::encode_path(slug);
30 self.0.get(&format!("/api/ctfs/details/{encoded}")).await
31 }
32
33 pub async fn event_data(&self, event_id: u64) -> Result<CtfEventData, HtbError> {
34 self.0.get(&format!("/api/ctfs/{event_id}")).await
35 }
36
37 pub async fn menu(&self, event_id: u64) -> Result<CtfMenu, HtbError> {
38 self.0.get(&format!("/api/ctfs/{event_id}/menu")).await
39 }
40
41 pub async fn submit_flag(
42 &self,
43 challenge_id: u64,
44 flag: &str,
45 ) -> Result<CtfFlagResult, HtbError> {
46 self.0
47 .post(
48 "/api/flags/own",
49 &json!({"challenge_id": challenge_id, "flag": flag}),
50 )
51 .await
52 }
53
54 pub async fn download_file(&self, challenge_id: u64) -> Result<Vec<u8>, HtbError> {
55 self.0
56 .get_bytes(&format!("/api/challenges/{challenge_id}/download"))
57 .await
58 }
59
60 pub async fn container_start(&self, challenge_id: u64) -> Result<ActionResponse, HtbError> {
61 self.0
62 .post(
63 "/api/challenges/containers/start",
64 &json!({"id": challenge_id}),
65 )
66 .await
67 }
68
69 pub async fn container_stop(&self, challenge_id: u64) -> Result<ActionResponse, HtbError> {
70 self.0
71 .post(
72 "/api/challenges/containers/stop",
73 &json!({"id": challenge_id}),
74 )
75 .await
76 }
77
78 pub async fn scoreboard(&self, event_id: u64) -> Result<CtfScoreboard, HtbError> {
79 self.0.get(&format!("/api/ctfs/scores/{event_id}")).await
80 }
81
82 pub async fn solves(&self, event_id: u64) -> Result<Vec<CtfSolve>, HtbError> {
83 self.0.get(&format!("/api/ctfs/solves/{event_id}")).await
84 }
85
86 pub async fn challenge_solves(
87 &self,
88 challenge_id: u64,
89 ) -> Result<Vec<CtfChallengeSolve>, HtbError> {
90 self.0
91 .get(&format!("/api/challenges/{challenge_id}/solves"))
92 .await
93 }
94
95 pub async fn team_overview(&self, team_id: u64) -> Result<CtfTeamOverview, HtbError> {
96 self.0.get(&format!("/api/teams/{team_id}/overview")).await
97 }
98
99 pub async fn associate_challenge(
100 &self,
101 challenge_id: u64,
102 user_id: u64,
103 ) -> Result<ActionResponse, HtbError> {
104 self.0
105 .get(&format!(
106 "/api/challenges/{challenge_id}/associate/{user_id}"
107 ))
108 .await
109 }
110
111 pub async fn disassociate_challenge(
112 &self,
113 challenge_id: u64,
114 user_id: u64,
115 ) -> Result<ActionResponse, HtbError> {
116 self.0
117 .get(&format!(
118 "/api/challenges/{challenge_id}/disassociate/{user_id}"
119 ))
120 .await
121 }
122
123 pub async fn set_challenge_progress(
124 &self,
125 challenge_id: u64,
126 status: Option<&str>,
127 ) -> Result<(), HtbError> {
128 self.0
129 .post_no_content(
130 &format!("/api/challenges/{challenge_id}/progress"),
131 &ProgressBody {
132 status: status.map(|s| s.to_string()),
133 },
134 )
135 .await
136 }
137}