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 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 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 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 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 pub fn solve1<T: ToString>(&self, solution: T) -> Result<(), String> {
364 self.client.post_solution(1, solution.to_string())
365 }
366
367 pub fn solve2<T: ToString>(&self, solution: T) -> Result<(), String> {
396 self.client.post_solution(2, solution.to_string())
397 }
398}