1mod protocol;
2
3use chrono::Datelike;
4use protocol::{AdventOfCodeHttpProtocol, AdventOfCodeProtocol};
5use thiserror::Error;
6
7use crate::{
8 cache::{CacheError, PuzzleCache, PuzzleFsCache, SessionCache, SessionFsCache},
9 config::{load_config, Config, ConfigError},
10 data::{Answers, CheckResult, Puzzle},
11 utils::get_puzzle_unlock_time,
12 Answer, Day, Part, Year,
13};
14
15#[derive(Debug, Error)]
17pub enum ClientError {
18 #[error("the answer was submitted too soon, please wait until {} trying again", .0)]
20 TooSoon(chrono::DateTime<chrono::Utc>),
21 #[error("the session id `{}` is invalid or has expired", .0)]
23 BadSessionId(String),
24 #[error("a puzzle could not be found for day {} year {}", .0, .1)]
26 PuzzleNotFound(Day, Year),
27 #[error("please wait {} before submitting another answer to the Advent of Code service", .0)]
29 SubmitTimeOut(chrono::Duration),
30 #[error("a correct answer has already been submitted for day {} year {}", .0, .1)]
32 AlreadySubmittedAnswer(Day, Year),
33 #[error("an unexpected HTTP {} error was returned by the Advent of Code service", .0)]
35 Http(reqwest::StatusCode),
36 #[error("an unexpected error {} error happened when reading cached data", .0)]
38 CacheError(#[from] CacheError),
39 #[error("an unexpected error {} happened when reading configuration values", .0)]
41 SettingsError(#[from] ConfigError),
42}
43
44pub trait Client {
73 fn years(&self) -> Vec<Year>;
76 fn days(&self, year: Year) -> Option<Vec<Day>>;
79 fn get_input(&self, day: Day, year: Year) -> Result<String, ClientError>;
82 fn submit_answer(
85 &mut self,
86 answer: Answer,
87 part: Part,
88 day: Day,
89 year: Year,
90 ) -> Result<CheckResult, ClientError>;
91 fn get_puzzle(&self, day: Day, year: Year) -> Result<Puzzle, ClientError>;
93}
94
95#[derive(Debug)]
117pub struct WebClient {
118 pub config: Config,
120 protocol: Box<dyn AdventOfCodeProtocol>,
121 pub puzzle_cache: Box<dyn PuzzleCache>,
123 pub session_cache: Box<dyn SessionCache>,
125}
126
127impl WebClient {
128 pub fn new() -> Result<Self, ClientError> {
130 Ok(Self::with_config(load_config()?.build()?))
131 }
132
133 pub fn with_config(config: Config) -> Self {
135 let advent_protocol = Box::new(AdventOfCodeHttpProtocol::new(&config));
136 Self::with_custom_impl(config, advent_protocol)
137 }
138
139 pub fn with_custom_impl(
144 config: Config,
145 advent_protocol: Box<dyn AdventOfCodeProtocol>,
146 ) -> Self {
147 let puzzle_dir = config.puzzle_dir.clone();
150 let sessions_dir = config.sessions_dir.clone();
151 let passphrase = config.passphrase.clone();
152
153 tracing::debug!("puzzle cache dir: {puzzle_dir:?}");
155 tracing::debug!("sessions dir: {sessions_dir:?}");
156 tracing::debug!("using encryption: {}", !passphrase.is_empty());
157
158 Self {
159 config,
160 protocol: advent_protocol,
161 puzzle_cache: Box::new(PuzzleFsCache::new(puzzle_dir, Some(passphrase))),
162 session_cache: Box::new(SessionFsCache::new(sessions_dir)),
163 }
164 }
165}
166
167impl Client for WebClient {
168 fn years(&self) -> Vec<Year> {
169 let start_time = self.config.start_time;
170 let unlock_time = get_puzzle_unlock_time(start_time.year().into());
171
172 let mut end_year = start_time.year();
173
174 if start_time < unlock_time {
175 end_year -= 1;
176 }
177
178 (2015..(end_year + 1)).map(|y| y.into()).collect()
179 }
180
181 fn days(&self, year: Year) -> Option<Vec<Day>> {
182 let start_time = self.config.start_time;
183 let eastern_start_time = start_time.with_timezone(&chrono_tz::US::Eastern);
184 let requested_year = year.0 as i32;
185
186 match (
187 eastern_start_time.year().cmp(&requested_year),
188 eastern_start_time.month() == 12,
189 ) {
190 (std::cmp::Ordering::Equal, true) => Some(
191 (1..(eastern_start_time.day() + 1))
192 .map(|d| d.into())
193 .collect(),
194 ),
195 (std::cmp::Ordering::Greater, _) => Some((0..25).map(|d| d.into()).collect()),
196 _ => None,
197 }
198 }
199
200 fn get_input(&self, day: Day, year: Year) -> Result<String, ClientError> {
201 tracing::trace!("get_input(day=`{day}`, year=`{year}`)",);
202
203 if let Some(input) = self
206 .puzzle_cache
207 .load_input(day, year)
208 .map_err(ClientError::CacheError)?
209 {
210 return Ok(input);
211 }
212
213 let input = self.protocol.get_input(day, year)?;
215
216 self.puzzle_cache.save_input(&input, day, year)?;
219 Ok(input)
220 }
221
222 fn submit_answer(
223 &mut self,
224 answer: Answer,
225 part: Part,
226 day: Day,
227 year: Year,
228 ) -> Result<CheckResult, ClientError> {
229 tracing::trace!(
230 "submit_answer(answer=`{:?}`, part=`{}`, day=`{}`, year=`{}`)",
231 answer,
232 part,
233 day,
234 year
235 );
236
237 let mut answers = match self.puzzle_cache.load_answers(part, day, year)? {
240 Some(cached_answers) => {
241 if let Some(check_result) = cached_answers.check(&answer) {
242 tracing::debug!("answer check result was found in the cache {check_result:?}");
243 return Ok(check_result);
244 }
245
246 cached_answers
247 }
248 _ => Answers::new(),
249 };
250
251 let mut session = self.session_cache.load(&self.config.session_id)?;
254
255 if let Some(submit_wait_until) = session.submit_wait_until {
256 if self.config.start_time <= submit_wait_until {
257 tracing::warn!("you cannot submit an answer until {submit_wait_until}");
258 return Err(ClientError::SubmitTimeOut(
259 submit_wait_until - self.config.start_time,
260 ));
261 } else {
262 tracing::debug!("the submission timeout has expired, ignoring");
264 }
265 }
266
267 let (check_result, time_to_wait) = self.protocol.submit_answer(&answer, part, day, year)?;
269
270 if let Some(time_to_wait) = time_to_wait {
273 let wait_until = chrono::Utc::now() + time_to_wait;
274 tracing::debug!("setting time to wait ({time_to_wait}) to be {wait_until}");
275
276 session.submit_wait_until = Some(wait_until);
277 self.session_cache.save(&session)?;
278 }
279
280 match check_result {
283 CheckResult::Correct => {
284 tracing::debug!("Setting correct answer as {answer}");
285 answers.set_correct_answer(answer);
286 }
287 CheckResult::Wrong => {
288 tracing::debug!("Setting wrong answer {answer}");
289 answers.add_wrong_answer(answer);
290 }
291 CheckResult::TooLow => {
292 tracing::debug!("Setting low bounds wrong answer {answer}");
293 answers.set_low_bounds(answer);
294 }
295 CheckResult::TooHigh => {
296 tracing::debug!("Setting high bounds wrong answer {answer}");
297 answers.set_high_bounds(answer);
298 }
299 };
300
301 tracing::debug!("Saving answers database to puzzle cache");
302 self.puzzle_cache.save_answers(&answers, part, day, year)?;
303
304 Ok(check_result)
305 }
306
307 fn get_puzzle(&self, day: Day, year: Year) -> Result<Puzzle, ClientError> {
308 Ok(Puzzle {
309 day,
310 year,
311 input: self.get_input(day, year)?,
312 part_one_answers: self
313 .puzzle_cache
314 .load_answers(Part::One, day, year)?
315 .unwrap_or_default(),
316 part_two_answers: self
317 .puzzle_cache
318 .load_answers(Part::Two, day, year)?
319 .unwrap_or_default(),
320 })
321 }
322
323 }
327
328#[cfg(test)]
329mod tests {
330 use chrono::{NaiveDate, NaiveTime, TimeZone};
331 use chrono_tz::US::Eastern;
332
333 use crate::config::ConfigBuilder;
334
335 use super::*;
336
337 fn web_client_with_time(
338 year: i32,
339 month: u32,
340 day: u32,
341 hour: u32,
342 min: u32,
343 sec: u32,
344 ) -> WebClient {
345 WebClient::with_config(
346 ConfigBuilder::new()
347 .with_session_id("UNIT_TEST_SESSION_ID")
348 .with_passphrase("UNIT_TEST_PASSWORD")
349 .with_puzzle_dir("DO_NOT_USE")
350 .with_fake_time(
351 Eastern
352 .from_local_datetime(
353 &NaiveDate::from_ymd_opt(year, month, day)
354 .unwrap()
355 .and_time(NaiveTime::from_hms_opt(hour, min, sec).unwrap()),
356 )
357 .unwrap()
358 .with_timezone(&chrono::Utc),
359 )
360 .build()
361 .unwrap(),
362 )
363 }
364
365 #[test]
366 fn list_years_when_date_is_after_start() {
367 let client = web_client_with_time(2018, 12, 1, 0, 0, 0);
368 assert_eq!(
369 client.years(),
370 vec![Year(2015), Year(2016), Year(2017), Year(2018)]
371 );
372 }
373
374 #[test]
375 fn list_years_when_date_is_before_start() {
376 let client = web_client_with_time(2018, 11, 30, 23, 59, 59);
377 assert_eq!(client.years(), vec![Year(2015), Year(2016), Year(2017)]);
378 }
379
380 #[test]
381 fn list_years_when_date_aoc_start() {
382 let client = web_client_with_time(2015, 3, 10, 11, 15, 7);
383 assert_eq!(client.years(), vec![]);
384 }
385
386 #[test]
387 fn list_days_before_start() {
388 let client = web_client_with_time(2020, 11, 30, 23, 59, 59);
389 assert_eq!(client.days(Year(2020)), None);
390 }
391
392 #[test]
393 fn list_days_at_start() {
394 let client = web_client_with_time(2020, 12, 1, 0, 0, 0);
395 assert_eq!(client.days(Year(2020)), Some(vec![Day(1)]));
396 }
397
398 #[test]
399 fn list_days_in_middle() {
400 let client = web_client_with_time(2020, 12, 6, 0, 0, 0);
401 assert_eq!(
402 client.days(Year(2020)),
403 Some(vec![Day(1), Day(2), Day(3), Day(4), Day(5), Day(6)])
404 );
405 }
406}