use chrono::Duration;
use regex::Regex;
use crate::{data::CheckResult, Answer, Day, Part, Year};
use super::{ClientError, Config};
pub trait AdventOfCodeProtocol: std::fmt::Debug {
fn get_input(&self, day: Day, year: Year) -> Result<String, ClientError>;
fn submit_answer(
&self,
answer: &Answer,
part: Part,
day: Day,
year: Year,
) -> Result<(CheckResult, Option<Duration>), ClientError>;
}
#[derive(Debug)]
pub struct AdventOfCodeHttpProtocol {
session_id: String,
http_client: reqwest::blocking::Client,
}
impl AdventOfCodeHttpProtocol {
const ADVENT_OF_CODE_DOMAIN: &'static str = "adventofcode.com";
const ADVENT_OF_CODE_URL: &'static str = "https://adventofcode.com";
pub fn new(config: &Config) -> Self {
let cookies: reqwest::cookie::Jar = Default::default();
let cookie_data = format!(
"session={}; Domain={}",
config.session_id,
Self::ADVENT_OF_CODE_DOMAIN
);
tracing::debug!(
"adding session id to cookie jar with value `{}`",
cookie_data
);
cookies.add_cookie_str(
&cookie_data,
&Self::ADVENT_OF_CODE_URL.parse::<reqwest::Url>().unwrap(),
);
Self {
session_id: config.session_id.clone(),
http_client: reqwest::blocking::ClientBuilder::new()
.cookie_provider(cookies.into())
.user_agent("github.com/smacdo/advent-of-code-rust [email: dev@smacdo.com]")
.build()
.expect("unexpected error when constructing reqwest http client"),
}
}
}
impl AdventOfCodeProtocol for AdventOfCodeHttpProtocol {
fn get_input(&self, day: Day, year: Year) -> Result<String, ClientError> {
let url = format!("{}/{}/day/{}/input", Self::ADVENT_OF_CODE_URL, year, day);
tracing::debug!(
"url to get puzzle input for day {} year {} is `{}`",
day,
year,
url
);
let response = self.http_client.get(url).send().unwrap();
tracing::debug!("server responed with HTTP {}", response.status());
match response.status() {
reqwest::StatusCode::OK => Ok(response.text().unwrap()),
reqwest::StatusCode::BAD_REQUEST => {
Err(ClientError::BadSessionId(self.session_id.to_string()))
}
reqwest::StatusCode::NOT_FOUND => {
Err(ClientError::PuzzleNotFound(day, year))
}
_ => Err(ClientError::Http(response.status())),
}
}
fn submit_answer(
&self,
answer: &Answer,
part: Part,
day: Day,
year: Year,
) -> Result<(CheckResult, Option<Duration>), ClientError> {
let url = format!("{}/{}/day/{}/answer", Self::ADVENT_OF_CODE_URL, year, day);
tracing::debug!(
"creating url to post puzzle answer for part {:?} day {} year {} answer `{}` with url = `{}`",
part,
day,
year,
answer,
url
);
let response = self
.http_client
.post(url)
.form(&[
(
"level",
if part == Part::One {
"1".to_string()
} else {
"2".to_string()
},
),
("answer", answer.to_string()),
])
.send()
.unwrap();
tracing::debug!("server responed with HTTP {}", response.status());
if response.status().is_client_error() || response.status().is_server_error() {
return match response.status() {
reqwest::StatusCode::BAD_REQUEST => {
Err(ClientError::BadSessionId(self.session_id.to_string()))
}
reqwest::StatusCode::NOT_FOUND => {
Err(ClientError::PuzzleNotFound(day, year))
}
_ => Err(ClientError::Http(response.status())),
};
}
let response_text = response.text().unwrap();
tracing::debug!("got advent of code server response for answer: {answer}");
let extract_wait_time_funcs = &[
Self::extract_error_time_to_wait,
Self::extract_one_minute_time_to_wait,
Self::extract_wrong_answer_time_to_wait,
];
let time_to_wait = extract_wait_time_funcs
.iter()
.filter_map(|f| f(&response_text))
.next();
if response_text.contains("gave an answer too recently") {
return Err(ClientError::SubmitTimeOut(time_to_wait.unwrap()));
}
if response_text.contains("you already complete it") {
return Err(ClientError::AlreadySubmittedAnswer(day, year));
}
let responses_texts = &[
("not the right answer", CheckResult::Wrong),
("the right answer", CheckResult::Correct),
("answer is too low", CheckResult::TooLow),
("answer is too high", CheckResult::TooHigh),
];
let check_result = responses_texts
.iter()
.find(|x| response_text.contains(x.0))
.map(|x| x.1.clone())
.unwrap_or_else(|| panic!("expected server response text to map to predetermined response in LUT. Response:\n```\n{response_text}\n```\n"));
Ok((check_result, time_to_wait))
}
}
impl AdventOfCodeHttpProtocol {
fn extract_one_minute_time_to_wait(response: &str) -> Option<Duration> {
match response.contains("Please wait one minute before trying again") {
true => Some(Duration::minutes(5)),
false => None,
}
}
fn extract_wrong_answer_time_to_wait(response: &str) -> Option<Duration> {
let regex = Regex::new(r"please wait (\d) minutes?").unwrap();
regex
.captures(response)
.map(|c| Duration::minutes(c[1].parse::<i64>().unwrap()))
}
fn extract_error_time_to_wait(response: &str) -> Option<Duration> {
let regex = Regex::new(r"You have (\d+)m( (\d+)s)? left to wait").unwrap();
regex.captures(response).map(|c| {
let mut time_to_wait = Duration::minutes(c[1].parse::<i64>().unwrap());
if let Some(secs) = c.get(3) {
time_to_wait += Duration::seconds(secs.as_str().parse::<i64>().unwrap());
}
time_to_wait
})
}
}