diff --git a/Cargo.lock b/Cargo.lock index 6270796..1bf9532 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,6 +36,7 @@ dependencies = [ "openssl", "regex", "reqwest", + "serde", "term_size", "thiserror", ] @@ -1360,6 +1361,20 @@ name = "serde" version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "256b9932320c590e707b94576e3cc1f7c9024d0ee6612dfbcf1cb106cbe8e055" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4eae9b04cbffdfd550eb462ed33bc6a1b68c935127d008b27444d08380f94e4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "serde_json" diff --git a/Cargo.toml b/Cargo.toml index 5692566..97836a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,8 @@ html2text = "0.4" http = "0.2" log = "0.4" regex = "1.7" -reqwest = { version = "0.11", features = ["blocking"] } +reqwest = { version = "0.11", features = ["blocking", "json"] } +serde = { version = "1.0", features=["derive"] } term_size = "0.3" thiserror = "1.0" diff --git a/src/aoc.rs b/src/aoc.rs index c2b3be1..75c4915 100644 --- a/src/aoc.rs +++ b/src/aoc.rs @@ -11,6 +11,9 @@ use reqwest::header::{ USER_AGENT, }; use reqwest::redirect::Policy; +use serde::Deserialize; +use std::cmp::Reverse; +use std::collections::HashMap; use std::env; use std::fs::{read_to_string, OpenOptions}; use std::io::Write; @@ -317,3 +320,103 @@ pub fn read( println!("\n{}", from_read(desc.as_bytes(), col_width)); Ok(()) } + +fn get_private_leaderboard_results( + args: &Args, + session: &str, + leaderboard: &str, + year: PuzzleYear, +) -> AocResult { + debug!("🦌 Fetching private leaderboard {}", leaderboard); + + let url = format!( + "https://adventofcode.com/{}/leaderboard/private/view/{}.json", + year, leaderboard + ); + + let leaderboard: PrivateLeaderboard = + build_client(session, "application/json")? + .get(&url) + .send() + .and_then(|response| response.error_for_status()) + .and_then(|response| response.json()) + .map_err(AocError::from)?; + Ok(leaderboard) +} + +pub fn show_private_leaderboard_results( + args: &Args, + session: &str, + leaderboard: &str, +) -> AocResult<()> { + let (year, day) = puzzle_year_day(args.year, args.day)?; + let leaderboard = + get_private_leaderboard_results(args, session, leaderboard, year)?; + + let mut members: Vec<_> = leaderboard.members.values().collect(); + members.sort_by_key(|m| Reverse(m.local_score)); + members.iter().enumerate().for_each(|(idx, m)| { + let display_name = m + .name + .clone() + .unwrap_or(format!("anonymous user #{}", m.id)); + + let stars: String = (1..=25) + .map(|d| { + if d > day { + ' ' + } else { + let stars = m.stars_per_day(d); + match stars { + 2 => '★', + 1 => '☆', + _ => '.', + } + } + }) + .collect(); + + let order = idx + 1; + println!("{}\t{}\t{}\t{}", order, m.local_score, stars, display_name); + }); + + Ok(()) +} + +#[derive(Deserialize)] +struct PrivateLeaderboard { + owner_id: usize, + event: String, + members: HashMap, +} + +#[derive(Deserialize)] +struct Member { + name: Option, + id: u64, + global_score: u64, + local_score: u64, + stars: u8, + completion_day_level: HashMap, +} + +impl Member { + fn stars_per_day(&self, day: u32) -> u8 { + self.completion_day_level + .get(&day) + .map(|d| d.stars.len() as u8) + .unwrap_or(0) + } +} + +#[derive(Deserialize)] +struct DayLevel { + #[serde(flatten)] + stars: HashMap, +} + +#[derive(Deserialize)] +struct Star { + get_star_ts: u64, + star_index: u64, +} diff --git a/src/args.rs b/src/args.rs index 5bd157a..4e5c530 100644 --- a/src/args.rs +++ b/src/args.rs @@ -95,6 +95,10 @@ pub enum Command { /// Puzzle answer answer: String, }, + + /// Get private leaderboard results + #[command(visible_alias = "pr")] + PrivateLeaderboard { leaderboard: String }, } fn convert_number(s: &str) -> Result diff --git a/src/main.rs b/src/main.rs index cc113fb..c003004 100644 --- a/src/main.rs +++ b/src/main.rs @@ -77,6 +77,9 @@ fn run(args: &Args) -> AocResult<()> { Some(Command::Submit { part, answer }) => { submit(args, &session, width, part, answer) } + Some(Command::PrivateLeaderboard { leaderboard }) => { + show_private_leaderboard_results(args, &session, leaderboard) + } _ => read(args, &session, width), } }