1#![doc = include_str!("../README.md")]
2
3use anyhow::Result;
4use reqwest::{blocking::Client, cookie::Jar, header::CONTENT_TYPE};
5use std::{collections::BTreeMap, env, fs, sync::Arc};
6
7const AOC_ROOT: &str = "https://adventofcode.com";
8const PROGRESS_FILE: &str = "aoc_progress.json";
9
10type Progress = BTreeMap<u8, (bool, bool)>;
11
12pub trait Day {
14 const DAY: u8;
16
17 fn from_input(input: String) -> Self;
19
20 fn first_part(&mut self) -> String;
22 fn second_part(&mut self) -> String;
24}
25
26pub struct AocSession {
28 year: u16,
29 client: Client,
30 progress: Progress,
31}
32
33impl AocSession {
34 pub fn new(year: u16) -> Result<Self> {
36 let session = env::var("AOC_SESSION")?;
37
38 Self::new_from_session(year, session)
39 }
40
41 pub fn new_from_session(year: u16, session: String) -> Result<Self> {
43 let cookies = Jar::default();
44 cookies.add_cookie_str(format!("session={session}").as_str(), &AOC_ROOT.parse()?);
45 let client = Client::builder()
46 .cookie_provider(Arc::new(cookies))
47 .build()?;
48
49 let progress: Progress =
50 if let Ok(Ok(progress)) = fs::read_to_string(PROGRESS_FILE).map(|content | serde_json::from_str(&content)) {
51 progress
52 } else {
53 Default::default()
54 };
55
56 Ok(Self {
57 client,
58 year,
59 progress,
60 })
61 }
62
63 fn save_progress(&mut self, day: u8, second_part: bool) -> Result<()> {
64 if let Some(day_progress) = self.progress.get_mut(&day) {
65 if second_part {
66 day_progress.1 = true
67 } else {
68 day_progress.0 = true
69 }
70 } else {
71 self.progress.insert(
72 day,
73 if second_part {
74 (false, true)
75 } else {
76 (true, false)
77 },
78 );
79 }
80
81 Ok(fs::write(
82 PROGRESS_FILE,
83 serde_json::to_string(&self.progress)?,
84 )?)
85 }
86
87 fn submit_part(&mut self, day: u8, part: u8, answer: String) -> Result<bool> {
88 let submission_result = self
89 .client
90 .post(format!("{AOC_ROOT}/{}/day/{}/answer", self.year, day))
91 .body(format!("level={part}&answer={answer}"))
92 .header(CONTENT_TYPE, "application/x-www-form-urlencoded")
93 .send()?
94 .text()?;
95
96 if submission_result.contains("That's not the right answer") {
97 println!("Answer {} is wrong (day {} part {})", answer, day, part);
98
99 return Ok(false);
100 } else if !submission_result.contains("You don't seem to be solving the right level") {
101 println!("Challenge completed successfully #{} {}/2", day, part);
102 }
103
104 self.save_progress(day, part == 2)?;
105
106 Ok(true)
107 }
108
109 pub fn day<D: Day>(mut self) -> Result<Self> {
117 let day = D::DAY;
118
119 let curret_progress = self.progress.get(&day).map(ToOwned::to_owned);
120
121 if matches!(curret_progress, Some((true, true))) {
122 return Ok(self);
123 };
124
125 println!("Running day #{}", day);
126
127 let input = self
128 .client
129 .get(format!("{AOC_ROOT}/{}/day/{}/input", self.year, day))
130 .send()?
131 .text()?;
132
133 let mut parsed = D::from_input(input);
134
135 if matches!(curret_progress, None | Some((false, _))) {
136 self.submit_part(day, 1, parsed.first_part())?;
137 }
138
139 if matches!(curret_progress, None | Some((_, false))) {
140 self.submit_part(day, 2, parsed.second_part())?;
141 }
142
143 Ok(self)
144 }
145}