diff --git a/aoc-client/src/lib.rs b/aoc-client/src/lib.rs index ee5a032..4aff2f2 100644 --- a/aoc-client/src/lib.rs +++ b/aoc-client/src/lib.rs @@ -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), @@ -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() @@ -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) }) @@ -421,6 +427,84 @@ impl AocClient { Ok(()) } + fn get_personal_stats_html(&self) -> AocResult { + 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)
(?P
.*)
") + .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"(?-+Part \d+-+)([ ]+)(?-+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, diff --git a/src/args.rs b/src/args.rs index fba86ac..51b98a3 100644 --- a/src/args.rs +++ b/src/args.rs @@ -108,4 +108,8 @@ pub enum Command { /// Private leaderboard ID leaderboard_id: LeaderboardId, }, + + /// Show personal stats + #[command(visible_alias = "pe")] + PersonalStats, } diff --git a/src/main.rs b/src/main.rs index ab9e7f0..3ffa9c3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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, @@ -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) }