1use crate::error::{Result, WaitHumanError};
2use crate::types::*;
3use reqwest::Client;
4use std::time::Instant;
5use tokio::time::{sleep, Duration};
6
7const DEFAULT_ENDPOINT: &str = "https://api.waithuman.com";
8const POLL_INTERVAL_MS: u64 = 3000;
9
10#[derive(Debug, Clone)]
12pub struct WaitHuman {
13 api_key: String,
14 endpoint: String,
15 client: Client,
16}
17
18impl WaitHuman {
19 pub fn new_from_key<S: Into<String>>(api_key: S) -> Result<Self> {
40 Self::new(WaitHumanConfig::new(api_key))
41 }
42
43 pub fn new(config: WaitHumanConfig) -> Result<Self> {
53 if config.api_key.is_empty() {
54 return Err(WaitHumanError::InvalidResponse(
55 "api_key is mandatory".to_string(),
56 ));
57 }
58
59 let mut endpoint = config
60 .endpoint
61 .unwrap_or_else(|| DEFAULT_ENDPOINT.to_string());
62
63 if endpoint.ends_with('/') {
65 endpoint.pop();
66 }
67
68 Ok(Self {
69 api_key: config.api_key,
70 endpoint,
71 client: Client::new(),
72 })
73 }
74
75 pub async fn ask(
90 &self,
91 question: ConfirmationQuestion,
92 options: Option<AskOptions>,
93 ) -> Result<ConfirmationAnswerWithDate> {
94 let confirmation_id = self.create_confirmation(question).await?;
95 let timeout_seconds = options.and_then(|o| o.timeout_seconds);
96 self.poll_for_answer(confirmation_id, timeout_seconds).await
97 }
98
99 pub async fn ask_free_text<S, B>(
113 &self,
114 subject: S,
115 body: Option<B>,
116 options: Option<AskOptions>,
117 ) -> Result<String>
118 where
119 S: Into<String>,
120 B: Into<String>,
121 {
122 let question = ConfirmationQuestion {
123 method: QuestionMethod::Push,
124 subject: subject.into(),
125 body: body.map(|b| b.into()),
126 answer_format: AnswerFormat::FreeText,
127 };
128
129 let answer = self.ask(question, options).await?;
130
131 match answer.answer.answer_content {
132 AnswerContent::FreeText { text } => Ok(text),
133 other => Err(WaitHumanError::UnexpectedAnswerType {
134 expected: "free_text".to_string(),
135 actual: format!("{:?}", other),
136 }),
137 }
138 }
139
140 pub async fn ask_multiple_choice<S, B, C>(
156 &self,
157 subject: S,
158 choices: C,
159 body: Option<B>,
160 options: Option<AskOptions>,
161 ) -> Result<String>
162 where
163 S: Into<String>,
164 B: Into<String>,
165 C: IntoIterator,
166 C::Item: Into<String>,
167 {
168 let choices_vec: Vec<String> = choices.into_iter().map(|c| c.into()).collect();
169
170 let question = ConfirmationQuestion {
171 method: QuestionMethod::Push,
172 subject: subject.into(),
173 body: body.map(|b| b.into()),
174 answer_format: AnswerFormat::Options {
175 options: choices_vec.clone(),
176 multiple: false,
177 },
178 };
179
180 let answer = self.ask(question, options).await?;
181
182 match answer.answer.answer_content {
183 AnswerContent::Options { selected_indexes } => {
184 let index = selected_indexes.first().ok_or_else(|| {
185 WaitHumanError::InvalidResponse("No selection received".to_string())
186 })?;
187
188 let index_usize = *index as usize;
189
190 choices_vec
191 .get(index_usize)
192 .cloned()
193 .ok_or_else(|| WaitHumanError::InvalidSelectedIndex { index: *index })
194 }
195 other => Err(WaitHumanError::UnexpectedAnswerType {
196 expected: "options".to_string(),
197 actual: format!("{:?}", other),
198 }),
199 }
200 }
201
202 async fn create_confirmation(&self, question: ConfirmationQuestion) -> Result<String> {
205 let url = format!("{}/confirmations/create", self.endpoint);
206 let request_body = CreateConfirmationRequest { question };
207
208 let response = self
209 .client
210 .post(&url)
211 .header("Authorization", &self.api_key)
212 .json(&request_body)
213 .send()
214 .await?;
215
216 if !response.status().is_success() {
217 return Err(WaitHumanError::CreateFailed {
218 status_text: response.status().to_string(),
219 });
220 }
221
222 let data: CreateConfirmationResponse = response.json().await?;
223 Ok(data.confirmation_request_id)
224 }
225
226 async fn poll_for_answer(
227 &self,
228 confirmation_id: String,
229 timeout_seconds: Option<u64>,
230 ) -> Result<ConfirmationAnswerWithDate> {
231 let start = Instant::now();
232
233 loop {
234 let elapsed_seconds = start.elapsed().as_secs_f64();
235
236 if let Some(timeout) = timeout_seconds {
237 if elapsed_seconds > timeout as f64 {
238 return Err(WaitHumanError::Timeout { elapsed_seconds });
239 }
240 }
241
242 let url = format!(
243 "{}/confirmations/get/{}?long_poll=false",
244 self.endpoint, confirmation_id
245 );
246
247 let response = self
248 .client
249 .get(&url)
250 .header("Authorization", &self.api_key)
251 .send()
252 .await?;
253
254 if !response.status().is_success() {
255 return Err(WaitHumanError::PollFailed {
256 status_text: response.status().to_string(),
257 });
258 }
259
260 let data: GetConfirmationResponse = response.json().await?;
261
262 if let Some(answer) = data.maybe_answer {
263 return Ok(answer);
264 }
265
266 sleep(Duration::from_millis(POLL_INTERVAL_MS)).await;
268 }
269 }
270}