1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
//! Tools used to fetch input contents from adventofcode.com.

use std::error::Error;
use std::fs::{create_dir_all, read_to_string, File};
use std::io::Write;
use std::io::{stdin, stdout};
use std::path::{Path, PathBuf};
use std::time::Instant;

use attohttpc::header::{COOKIE, USER_AGENT};

use crate::utils::Line;

const BASE_URL: &str = "https://adventofcode.com";
const USER_AGENT_VALUE: &str = "github.com/remi-dupre/aoc by remi@dupre.io";

fn input_path(year: u16, day: u8) -> PathBuf {
    format!("input/{}/day{}.txt", year, day).into()
}

fn token_path() -> PathBuf {
    dirs::config_dir()
        .map(|mut cfg| {
            cfg.push("aoc/token.txt");
            cfg
        })
        .unwrap_or_else(|| ".token".into())
}

pub fn get_input(year: u16, day: u8) -> Result<String, Box<dyn Error>> {
    let mut result = get_from_path_or_else(&input_path(year, day), || {
        let start = Instant::now();
        let url = format!("{}/{}/day/{}/input", BASE_URL, year, day);
        let session_cookie = format!("session={}", get_conn_token()?);

        let resp = attohttpc::get(&url)
            .header(COOKIE, session_cookie)
            .header(USER_AGENT, USER_AGENT_VALUE)
            .send()?;

        let elapsed = start.elapsed();

        println!(
            "  - {}",
            Line::new("downloaded input file").with_duration(elapsed)
        );

        resp.text()
    })?;

    if result.ends_with('\n') {
        result.pop();
    }

    Ok(result)
}

fn get_conn_token() -> Result<String, std::io::Error> {
    get_from_path_or_else(&token_path(), || {
        let mut stdout = stdout();
        write!(&mut stdout, "Write your connection token: ")?;
        stdout.flush()?;

        let mut output = String::new();
        stdin().read_line(&mut output)?;
        Ok(output.trim().to_string())
    })
}

fn get_from_path_or_else<E: Error>(
    path: &Path,
    fallback: impl FnOnce() -> Result<String, E>,
) -> Result<String, E> {
    let from_path = read_to_string(path);

    if let Ok(res) = from_path {
        Ok(res)
    } else {
        let res = fallback()?;
        create_dir_all(path.parent().expect("no parent directory"))
            .and_then(|_| File::create(path))
            .and_then(|mut file| file.write_all(res.as_bytes()))
            .unwrap_or_else(|err| eprintln!("could not write {}: {}", path.display(), err));
        Ok(res)
    }
}