aoc_main/
input.rs

1//! Tools used to fetch input contents from adventofcode.com.
2
3use std::error::Error;
4use std::fs::{create_dir_all, read_to_string, File};
5use std::io::Write;
6use std::io::{stdin, stdout};
7use std::path::{Path, PathBuf};
8use std::time::Instant;
9
10use attohttpc::header::{COOKIE, USER_AGENT};
11
12use crate::utils::Line;
13
14const BASE_URL: &str = "https://adventofcode.com";
15const USER_AGENT_VALUE: &str = "github.com/remi-dupre/aoc by remi@dupre.io";
16
17fn input_path(year: u16, day: u8) -> PathBuf {
18    format!("input/{}/day{}.txt", year, day).into()
19}
20
21fn token_path() -> PathBuf {
22    dirs::config_dir()
23        .map(|mut cfg| {
24            cfg.push("aoc/token.txt");
25            cfg
26        })
27        .unwrap_or_else(|| ".token".into())
28}
29
30pub fn get_input(year: u16, day: u8) -> Result<String, Box<dyn Error>> {
31    let mut result = get_from_path_or_else(&input_path(year, day), || {
32        let start = Instant::now();
33        let url = format!("{}/{}/day/{}/input", BASE_URL, year, day);
34        let session_cookie = format!("session={}", get_conn_token()?);
35
36        let resp = attohttpc::get(&url)
37            .header(COOKIE, session_cookie)
38            .header(USER_AGENT, USER_AGENT_VALUE)
39            .send()?;
40
41        let elapsed = start.elapsed();
42
43        println!(
44            "  - {}",
45            Line::new("downloaded input file").with_duration(elapsed)
46        );
47
48        resp.text()
49    })?;
50
51    if result.ends_with('\n') {
52        result.pop();
53    }
54
55    Ok(result)
56}
57
58fn get_conn_token() -> Result<String, std::io::Error> {
59    get_from_path_or_else(&token_path(), || {
60        let mut stdout = stdout();
61        write!(&mut stdout, "Write your connection token: ")?;
62        stdout.flush()?;
63
64        let mut output = String::new();
65        stdin().read_line(&mut output)?;
66        Ok(output.trim().to_string())
67    })
68}
69
70fn get_from_path_or_else<E: Error>(
71    path: &Path,
72    fallback: impl FnOnce() -> Result<String, E>,
73) -> Result<String, E> {
74    let from_path = read_to_string(path);
75
76    if let Ok(res) = from_path {
77        Ok(res)
78    } else {
79        let res = fallback()?;
80        create_dir_all(path.parent().expect("no parent directory"))
81            .and_then(|_| File::create(path))
82            .and_then(|mut file| file.write_all(res.as_bytes()))
83            .unwrap_or_else(|err| eprintln!("could not write {}: {}", path.display(), err));
84        Ok(res)
85    }
86}