Skip to content
98 changes: 91 additions & 7 deletions aoc-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ pub enum AocError {
#[error("Invalid session cookie")]
InvalidSessionCookie,

#[error("Not logged in")]
NotLoggedIn,

#[error("HTTP request error: {0}")]
HttpRequestError(#[from] reqwest::Error),

Expand Down Expand Up @@ -384,6 +387,8 @@ impl AocClient {
))
.unwrap();

let all_stars = main.contains("calendar calendar-perfect");

// Remove stars that have not been collected
let calendar = cleaned_up
.lines()
Expand All @@ -394,13 +399,14 @@ impl AocClient {
.map(|c| c.as_str())
.unwrap_or("");

let stars = if class.contains("calendar-verycomplete") {
"**"
} else if class.contains("calendar-complete") {
"*"
} else {
""
};
let stars =
if class.contains("calendar-verycomplete") || all_stars {
"**"
} else if class.contains("calendar-complete") {
"*"
} else {
""
};

star_regex.replace(line, stars)
})
Expand All @@ -421,6 +427,84 @@ impl AocClient {
Ok(())
}

fn get_personal_stats_html(&self) -> AocResult<String> {
debug!("🦌 Fetching {} personal stats", self.year);

let url =
format!("https://adventofcode.com/{}/leaderboard/self", self.year);
let response = http_client(&self.session_cookie, "text/html")?
.get(url)
.send()?;

if response.status() == StatusCode::NOT_FOUND {
// A 402 reponse means the calendar for
// the requested year is not yet available
return Err(AocError::InvalidEventYear(self.year));
} else if response.status() == StatusCode::FOUND {
// A 302 reponse is a redirect and it likely
// means we're not logged in
return Err(AocError::NotLoggedIn);
}

let contents = response.error_for_status()?.text()?;

let main = Regex::new(r"(?i)(?s)<main>(?P<main>.*)</main>")
.unwrap()
.captures(&contents)
.ok_or(AocError::AocResponseError)?
.name("main")
.unwrap()
.as_str()
.to_string();

Ok(main)
}

pub fn show_personal_stats(&self) -> AocResult<()> {
let stats_html = self.get_personal_stats_html()?;
let stats_text = self.html2text(&stats_html);

// print explanatory paragraph before recoloring the text
for line in stats_text.lines().take_while(|line| !line.is_empty()) {
println!("{}", line);
if line.eq("You haven't collected any stars... yet.") {
return Ok(());
}
}

let caps =
Regex::new(r"(?<Part1>-+Part \d+-+)([ ]+)(?<Part2>-+Part \d+-+)")
.unwrap()
.captures(&stats_text)
.ok_or(AocError::AocResponseError)?;

let part_1_str = caps
.name("Part1")
.ok_or(AocError::AocResponseError)?
.as_str();
let part_2_str = caps
.name("Part2")
.ok_or(AocError::AocResponseError)?
.as_str();

let stats_text = stats_text
.replace(part_1_str, &part_1_str.color(SILVER).to_string())
.replace(part_2_str, &part_2_str.color(GOLD).to_string())
.replace("Time", &"Time".color(GOLD).to_string())
.replace("Rank", &"Rank".color(GOLD).to_string())
.replace("Score", &"Score".color(GOLD).to_string())
.replacen("Time", &"Time".color(SILVER).to_string(), 1)
.replacen("Rank", &"Rank".color(SILVER).to_string(), 2)
.replacen("Score", &"Score".color(SILVER).to_string(), 2);

// just print out the day by day stats, expalanatory paragraph is recolored now
for line in stats_text.lines().skip_while(|line| !line.is_empty()) {
println!("{}", line);
}

Ok(())
}

fn get_private_leaderboard(
&self,
leaderboard_id: LeaderboardId,
Expand Down
4 changes: 4 additions & 0 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,8 @@ pub enum Command {
/// Private leaderboard ID
leaderboard_id: LeaderboardId,
},

/// Show personal stats
#[command(visible_alias = "pe")]
PersonalStats,
}
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ fn main() {
AocError::SessionFileNotFound => NO_INPUT,
AocError::SessionFileReadError { .. } => IO_ERROR,
AocError::InvalidSessionCookie { .. } => DATA_ERROR,
AocError::NotLoggedIn => DATA_ERROR,
AocError::HttpRequestError { .. } => FAILURE,
AocError::AocResponseError => FAILURE,
AocError::PrivateLeaderboardNotAvailable => FAILURE,
Expand Down Expand Up @@ -103,6 +104,7 @@ fn run(args: &Args, client: AocClient) -> AocResult<()> {
}
Ok(())
}
Some(Command::PersonalStats) => client.show_personal_stats(),
Some(Command::Submit { part, answer }) => {
client.submit_answer_and_show_outcome(part, answer)
}
Expand Down