aoc_bud/
lib.rs

1use std::{
2    env, fs,
3    io::{Read, Write},
4    net::TcpStream,
5    path::Path,
6};
7
8use dotenv::dotenv;
9use env::var;
10use native_tls::{TlsConnector, TlsStream};
11
12pub use regex::Regex;
13
14const HOST: &str = "adventofcode.com";
15const PORT: u16 = 443;
16const INPUT_FOLDER: &str = "aoc_inputs";
17
18#[cfg(feature = "time")]
19use time::{macros::offset, OffsetDateTime};
20
21struct AocInput {
22    path: String,
23}
24
25impl AocInput {
26    fn new(day: u8, year: i32) -> Result<Self, String> {
27        if !Path::new(INPUT_FOLDER).exists() {
28            fs::create_dir(INPUT_FOLDER).map_err(|e| {
29                format!("Failed to create directory 'aoc_inputs'.\nFs create_dir error: {e}")
30            })?;
31        }
32
33        return Ok(AocInput {
34            path: format!("./{INPUT_FOLDER}/{day}_{year}.input"),
35        });
36    }
37
38    fn new_test(day: u8, year: i32) -> Result<Self, String> {
39        if !Path::new(INPUT_FOLDER).exists() {
40            fs::create_dir(INPUT_FOLDER).map_err(|e| {
41                format!("Failed to create directory 'aoc_inputs'.\nFs create_dir error: {e}")
42            })?;
43        }
44
45        return Ok(AocInput {
46            path: format!("./{INPUT_FOLDER}/test_{day}_{year}.input"),
47        });
48    }
49
50    fn read(&self) -> Option<String> {
51        if Path::new(&self.path).exists() {
52            return Some(
53                fs::read_to_string(&self.path).expect("The file exits but it can't be read"),
54            );
55        }
56        None
57    }
58
59    fn write(&self, contents: &str) -> Result<(), String> {
60        fs::write(&self.path, contents)
61            .map_err(|e| format!("Failed to write content problem input.\nFs write error: {e}"))
62    }
63}
64
65struct Client {
66    session: String,
67    day: u8,
68    year: i32,
69    path: String,
70    sol_re: Regex,
71    input: AocInput,
72    test_input: AocInput,
73}
74
75impl Client {
76    fn new(day: u8, year: i32) -> Result<Self, String> {
77        dotenv().ok();
78        let cookie = var("AOC_SESSION").expect("AOC_SESSION must be set in the .env file");
79
80        let sol_re = Regex::new(r"(?P<wrong_answer>That's\snot\sthe\sright\sanswer)|(?P<wrong_level>You\sdon't\sseem\sto\sbe\ssolving\sthe\sright\slevel)|(?P<timeout>You\shave\s.*\sleft\sto\swait)").unwrap();
81
82        Ok(Self {
83            session: format!("session={}", cookie),
84            day,
85            year,
86            path: format!("/{year}/day/{day}"),
87            sol_re,
88            input: AocInput::new(day, year)?,
89            test_input: AocInput::new_test(day, year)?,
90        })
91    }
92
93    fn build_client(&self) -> Result<TlsStream<TcpStream>, String> {
94        let tcp = TcpStream::connect(format!("{HOST}:{PORT}"))
95            .map_err(|e| format!("Unable to connect to adventofcode, maybe your internet is down?\nTcpStream Error: {}", e))?;
96
97        let connector = TlsConnector::new().expect("Unable to create a TlsConnector");
98
99        connector.connect(HOST, tcp)
100            .map_err(|e| format!("Unable to connect to adventofcode, maybe your internet is down?\nTlsStream Error: {}", e))
101    }
102
103    fn get(&self, path: &str) -> Result<String, String> {
104        let get_request = format!(
105            "GET {0}{path} HTTP/1.1\r\nHost: {HOST}\r\nUser-Aget: AocBud-RustHttp\r\nAccept: */*\r\nCookie: {1}\r\nConnection: close\r\n\r\n",
106            self.path,
107            self.session
108        );
109
110        let mut stream = self.build_client()?;
111
112        stream.write_all(get_request.as_bytes())
113            .map_err(|e| format!("Couldn't perform the GET request for the endpoint {path}.\nTlsStream write_all error: {e}"))?;
114
115        let mut buf: Vec<u8> = Vec::new();
116
117        stream.read_to_end(&mut buf)
118            .map_err(|e| format!("Couldn't read the response for the endpoint {path}.\nTlsStream read_to_end error {e}"))?;
119
120        let binding = String::from_utf8_lossy(&buf);
121        let (_, resp_body) = binding
122            .split_once("\r\n\r\n")
123            .expect("Response has no body");
124
125        self.check_exists(resp_body)?;
126        self.check_404(resp_body)?;
127
128        Ok(resp_body.to_string())
129    }
130
131    fn post(&self, path: &str, body: String) -> Result<String, String> {
132        let post_request = format!(
133            "POST {0}{path} HTTP/1.1\r\nHost: {HOST}\r\nUser-Aget: AocBud-RustHttp\r\nAccept: */*\r\nCookie: {1}\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: {2}\r\nConnection: close\r\n\r\n{body}",
134            self.path,
135            self.session,
136            body.len(),
137        );
138
139        let mut stream = self.build_client()?;
140
141        stream.write_all(post_request.as_bytes())
142            .map_err(|e| format!("Couldn't perform the POST request for the endpoint {path}.\nTlsStream write_all error: {e}"))?;
143
144        let mut buf: Vec<u8> = Vec::new();
145
146        stream.read_to_end(&mut buf)
147            .map_err(|e| format!("Couldn't read the response for the endpoint {path}.\nTlsStream read_to_end error {e}"))?;
148
149        let binding = String::from_utf8_lossy(&buf);
150        let (_, resp_body) = binding
151            .split_once("\r\n\r\n")
152            .expect("Response has no body");
153
154        self.check_exists(resp_body)?;
155        self.check_404(resp_body)?;
156
157        Ok(resp_body.to_string())
158    }
159
160    fn check_exists(&self, contents: &str) -> Result<(), String> {
161        if let Some(_) =
162            contents.split_once("The calendar countdown is synchronized with the server time;")
163        {
164            return Err(format!(
165                "The puzzle for day {0} of {1} has not been unlocked yet!",
166                self.day, self.year
167            ));
168        }
169        Ok(())
170    }
171
172    fn check_404(&self, contents: &str) -> Result<(), String> {
173        if let Some(_) = contents.split_once("404 Not Found") {
174            return Err(format!(
175                "The puzzle for day {0} of {1} does not exist!",
176                self.day, self.year
177            ));
178        }
179
180        Ok(())
181    }
182
183    fn get_input(&self) -> Result<String, String> {
184        if let Some(input) = self.input.read() {
185            return Ok(input);
186        }
187        let contents: &str = &self.get("/input")?;
188        self.input.write(contents)?;
189        Ok(contents.to_string())
190    }
191
192    fn post_solution(&self, level: u8, answer: String) -> Result<(), String> {
193        let html = self.post("/answer", format!("level={level}&answer={answer}"))?;
194        if let Some(captures) = self.sol_re.captures(&html) {
195            if let Some(_) = captures.name("wrong_answer") {
196                return Err("Incorrect solution, try again".to_string());
197            } else if let Some(_) = captures.name("wrong_level") {
198                return Err("Incorrect level, did you already solve it? Or are you trying to access a level that is not unlocked?".to_string());
199            } else if let Some(timeout) = captures.name("timeout") {
200                return Err(format!(
201                    "You gave an answer too recently. {0}.",
202                    timeout.as_str()
203                ));
204            }
205        }
206        Ok(())
207    }
208
209    fn get_test_input(&self) -> Result<String, String> {
210        if let Some(input) = self.test_input.read() {
211            return Ok(input);
212        }
213        let re = Regex::new(r"(?s)<pre><code>(.*)</code></pre>").unwrap();
214        let html = self.get("")?;
215        if let Some(captures) = re.captures(&html) {
216            if let Some(test_input) = captures.get(1) {
217                let contents = test_input.as_str();
218                self.test_input.write(contents)?;
219                return Ok(contents.to_string());
220            }
221        }
222
223        Err(String::from("Couldn't find test input"))
224    }
225}
226
227pub struct Aoc {
228    client: Client,
229}
230
231impl Aoc {
232    /// Creates a new [`Aoc`].
233    ///
234    /// # Arguments
235    ///
236    /// - `day` -> day of aoc you want to solve
237    /// - `year` -> year of aoc you want to solve
238    ///
239    /// # Examples:
240    ///
241    /// ```
242    /// use aoc_bud::Aoc;
243    ///
244    /// // Create a new handler for the first aoc day of 2023
245    /// let aoc = Aoc::new(1, 2023);
246    /// ```
247    /// # Panics
248    ///
249    /// Panics if:
250    ///  - .env file is not present
251    ///  - AOC_SESSION is not present in .env
252    ///  - Current working directory is not writteable
253    pub fn new(day: u8, year: i32) -> Self {
254        match Client::new(day, year) {
255            Ok(client) => Self { client },
256            Err(e) => panic!("{e}"),
257        }
258    }
259
260    /// Returns the input of the puzzle of the corresponding day.
261    ///
262    /// # Examples:
263    ///
264    /// ```
265    /// use aoc_bud::Aoc;
266    ///
267    /// let aoc: Aoc = Aoc::new(1, 2023);
268    /// let input: String = aoc.input();
269    /// ```
270    ///
271    /// # Panics
272    ///
273    /// Panics if:
274    ///  - It can't perform a request
275    ///  - Current working directory is not writteable
276    ///  - The selected date and year don't have a puzzle yet or don't exist
277    pub fn input(&self) -> String {
278        match self.client.get_input() {
279            Ok(contents) => contents.to_string(),
280            Err(e) => panic!("{e}"),
281        }
282    }
283
284    /// Returns the test input of the puzzle of the corresponding day.
285    ///
286    /// # Examples:
287    ///
288    /// ```
289    /// use aoc_bud::Aoc;
290    ///
291    /// let aoc: Aoc = Aoc::new(1, 2023);
292    /// let test_input: String = aoc.test_input();
293    /// ```
294    ///
295    /// # Panics
296    ///
297    /// Panics if:
298    ///  - It can't perform a request
299    ///  - It can't find the test input in the webpage
300    ///  - Current working directory is not writteable
301    ///  - The selected date and year don't have a puzzle yet or don't exist
302    pub fn test_input(&self) -> String {
303        match self.client.get_test_input() {
304            Ok(contents) => contents,
305            Err(e) => panic!("{e}"),
306        }
307    }
308
309    #[cfg(feature = "time")]
310    /// Creates a new [`Aoc`] with the current date.
311    /// Only useful if the the aoc is ongoing.
312    ///
313    /// # Examples:
314    ///
315    /// ```
316    /// use aoc_bud::Aoc;
317    ///
318    /// // Create a new handler for the current day
319    /// let aoc = Aoc::today();
320    /// ```
321    /// # Panics
322    ///
323    /// Panics if:
324    ///  - .env file is not present
325    ///  - AOC_SESSION is not present in .env
326    ///  - Current working directory is not writteable
327    pub fn today() -> Self {
328        let date = OffsetDateTime::now_utc().to_offset(offset!(-5));
329
330        match Client::new(date.day(), date.year()) {
331            Ok(client) => Self { client },
332            Err(e) => panic!("{e}"),
333        }
334    }
335
336    /// Send the solution for the first puzzle, will return [Ok] if correct.
337    ///
338    /// # Argument
339    ///
340    /// - `solution` -> your solution
341    ///
342    /// # Example:
343    ///
344    /// ```
345    /// use aoc_bud::Aoc;
346    ///
347    /// let aoc: Aoc = Aoc::new(1, 2023);
348    /// let input: String = aoc.input();
349    ///
350    /// let solution = get_solution(input);
351    /// // Unwrap so we can get the result
352    /// aoc.solve1(solution).unwrap();
353    /// ```
354    ///
355    /// # Errors
356    ///
357    /// This function will return an error if:
358    ///  - It can't perform a request
359    ///  - The solution is incorrect
360    ///  - You sent the solution too quickly
361    ///  - You are trying to solve a puzzle already solved
362    ///  - The selected date and year don't have a puzzle yet or don't exist
363    pub fn solve1<T: ToString>(&self, solution: T) -> Result<(), String> {
364        self.client.post_solution(1, solution.to_string())
365    }
366
367    /// Send the solution for the second puzzle, will return [Ok] if correct.
368    ///
369    /// # Argument
370    ///
371    /// - `solution` -> your solution
372    ///
373    /// # Example:
374    ///
375    /// ```
376    /// use aoc_bud::Aoc;
377    ///
378    /// let aoc: Aoc = Aoc::new(1, 2023);
379    /// let input: String = aoc.input();
380    ///
381    /// let solution = get_solution(input);
382    /// // Unwrap so we can get the result
383    /// aoc.solve2(solution).unwrap();
384    /// ```
385    ///
386    /// # Errors
387    ///
388    /// This function will return an error if:
389    ///  - It can't perform a request
390    ///  - The solution is incorrect
391    ///  - You sent the solution too quickly
392    ///  - You are trying to solve a puzzle already solved
393    ///  - You are trying to solve the 2nd puzzle without having solved the 1st
394    ///  - The selected date and year don't have a puzzle yet or don't exist
395    pub fn solve2<T: ToString>(&self, solution: T) -> Result<(), String> {
396        self.client.post_solution(2, solution.to_string())
397    }
398}