use crate::error::{Result, WaitHumanError};
use crate::types::*;
use reqwest::Client;
use std::time::Instant;
use tokio::time::{sleep, Duration};
const DEFAULT_ENDPOINT: &str = "https://api.waithuman.com";
const POLL_INTERVAL_MS: u64 = 3000;
#[derive(Debug, Clone)]
pub struct WaitHuman {
api_key: String,
endpoint: String,
client: Client,
}
impl WaitHuman {
pub fn new_from_key<S: Into<String>>(api_key: S) -> Result<Self> {
Self::new(WaitHumanConfig::new(api_key))
}
pub fn new(config: WaitHumanConfig) -> Result<Self> {
if config.api_key.is_empty() {
return Err(WaitHumanError::InvalidResponse(
"api_key is mandatory".to_string(),
));
}
let mut endpoint = config
.endpoint
.unwrap_or_else(|| DEFAULT_ENDPOINT.to_string());
if endpoint.ends_with('/') {
endpoint.pop();
}
Ok(Self {
api_key: config.api_key,
endpoint,
client: Client::new(),
})
}
pub async fn ask(
&self,
question: ConfirmationQuestion,
options: Option<AskOptions>,
) -> Result<ConfirmationAnswerWithDate> {
let confirmation_id = self.create_confirmation(question).await?;
let timeout_seconds = options.and_then(|o| o.timeout_seconds);
self.poll_for_answer(confirmation_id, timeout_seconds).await
}
pub async fn ask_free_text<S, B>(
&self,
subject: S,
body: Option<B>,
options: Option<AskOptions>,
) -> Result<String>
where
S: Into<String>,
B: Into<String>,
{
let question = ConfirmationQuestion {
method: QuestionMethod::Push,
subject: subject.into(),
body: body.map(|b| b.into()),
answer_format: AnswerFormat::FreeText,
};
let answer = self.ask(question, options).await?;
match answer.answer.answer_content {
AnswerContent::FreeText { text } => Ok(text),
other => Err(WaitHumanError::UnexpectedAnswerType {
expected: "free_text".to_string(),
actual: format!("{:?}", other),
}),
}
}
pub async fn ask_multiple_choice<S, B, C>(
&self,
subject: S,
choices: C,
body: Option<B>,
options: Option<AskOptions>,
) -> Result<String>
where
S: Into<String>,
B: Into<String>,
C: IntoIterator,
C::Item: Into<String>,
{
let choices_vec: Vec<String> = choices.into_iter().map(|c| c.into()).collect();
let question = ConfirmationQuestion {
method: QuestionMethod::Push,
subject: subject.into(),
body: body.map(|b| b.into()),
answer_format: AnswerFormat::Options {
options: choices_vec.clone(),
multiple: false,
},
};
let answer = self.ask(question, options).await?;
match answer.answer.answer_content {
AnswerContent::Options { selected_indexes } => {
let index = selected_indexes.first().ok_or_else(|| {
WaitHumanError::InvalidResponse("No selection received".to_string())
})?;
let index_usize = *index as usize;
choices_vec
.get(index_usize)
.cloned()
.ok_or_else(|| WaitHumanError::InvalidSelectedIndex { index: *index })
}
other => Err(WaitHumanError::UnexpectedAnswerType {
expected: "options".to_string(),
actual: format!("{:?}", other),
}),
}
}
async fn create_confirmation(&self, question: ConfirmationQuestion) -> Result<String> {
let url = format!("{}/confirmations/create", self.endpoint);
let request_body = CreateConfirmationRequest { question };
let response = self
.client
.post(&url)
.header("Authorization", &self.api_key)
.json(&request_body)
.send()
.await?;
if !response.status().is_success() {
return Err(WaitHumanError::CreateFailed {
status_text: response.status().to_string(),
});
}
let data: CreateConfirmationResponse = response.json().await?;
Ok(data.confirmation_request_id)
}
async fn poll_for_answer(
&self,
confirmation_id: String,
timeout_seconds: Option<u64>,
) -> Result<ConfirmationAnswerWithDate> {
let start = Instant::now();
loop {
let elapsed_seconds = start.elapsed().as_secs_f64();
if let Some(timeout) = timeout_seconds {
if elapsed_seconds > timeout as f64 {
return Err(WaitHumanError::Timeout { elapsed_seconds });
}
}
let url = format!(
"{}/confirmations/get/{}?long_poll=false",
self.endpoint, confirmation_id
);
let response = self
.client
.get(&url)
.header("Authorization", &self.api_key)
.send()
.await?;
if !response.status().is_success() {
return Err(WaitHumanError::PollFailed {
status_text: response.status().to_string(),
});
}
let data: GetConfirmationResponse = response.json().await?;
if let Some(answer) = data.maybe_answer {
return Ok(answer);
}
sleep(Duration::from_millis(POLL_INTERVAL_MS)).await;
}
}
}