client.rs
1 use std::{ 2 collections::{BTreeMap, HashMap}, 3 ops::Bound, 4 time::Duration, 5 }; 6 7 use chrono::{DateTime, Utc}; 8 use tokio::sync::RwLock; 9 use tracing::trace; 10 11 use super::{ 12 api::AocApiClient, 13 models::{AocWhoami, PrivateLeaderboard}, 14 }; 15 use crate::{ 16 aoc::day::AocDay, 17 utils::{datetime::now, store::Store}, 18 }; 19 20 const LEADERBOARD_CACHE_STORE_KEY: &[u8] = b"aoc_leaderboard"; 21 22 pub type LeaderboardCache = HashMap<i32, (PrivateLeaderboard, DateTime<Utc>)>; 23 24 pub struct AocClient { 25 api: AocApiClient, 26 whoami: AocWhoami, 27 default_cache_ttl: Duration, 28 cache_ttl_rules: BTreeMap<i64, Duration>, 29 leaderboard_cache: RwLock<LeaderboardCache>, 30 store: Store, 31 } 32 33 impl AocClient { 34 pub async fn new( 35 session: &str, 36 default_cache_ttl: Duration, 37 cache_ttl_rules: BTreeMap<i64, Duration>, 38 store: Store, 39 ) -> anyhow::Result<Self> { 40 let api = AocApiClient::new(session)?; 41 42 let whoami = api.whoami().await?; 43 44 let leaderboard_cache = store 45 .get::<LeaderboardCache>(LEADERBOARD_CACHE_STORE_KEY) 46 .await? 47 .unwrap_or_default(); 48 49 Ok(Self { 50 api, 51 whoami, 52 default_cache_ttl, 53 cache_ttl_rules, 54 leaderboard_cache: leaderboard_cache.into(), 55 store, 56 }) 57 } 58 59 pub fn whoami(&self) -> &AocWhoami { 60 &self.whoami 61 } 62 63 pub async fn clear_leaderboard_cache(&self) -> anyhow::Result<()> { 64 let mut guard = self.leaderboard_cache.write().await; 65 guard.clear(); 66 self.store 67 .set::<LeaderboardCache>(LEADERBOARD_CACHE_STORE_KEY, &guard) 68 .await?; 69 Ok(()) 70 } 71 72 pub async fn get_private_leaderboard_cached( 73 &self, 74 year: i32, 75 ) -> Option<(PrivateLeaderboard, DateTime<Utc>)> { 76 self.leaderboard_cache.read().await.get(&year).cloned() 77 } 78 79 pub async fn get_private_leaderboard( 80 &self, 81 year: i32, 82 ) -> anyhow::Result<(PrivateLeaderboard, DateTime<Utc>)> { 83 let now = now(); 84 let ttl = match AocDay::current() { 85 Some(day) => { 86 let minutes_since_unlock = (now - day.unlock_datetime()).num_minutes(); 87 self.cache_ttl_rules 88 .range((Bound::Excluded(minutes_since_unlock), Bound::Unbounded)) 89 .next() 90 .map(|(_, &ttl)| ttl) 91 .unwrap_or(self.default_cache_ttl) 92 } 93 None => self.default_cache_ttl, 94 }; 95 96 let guard = self.leaderboard_cache.read().await; 97 if let Some(cached) = guard.get(&year).filter(|(_, ts)| now < *ts + ttl) { 98 trace!( 99 year, 100 ttl_secs = (cached.1 + ttl - now).num_seconds(), 101 "leaderboard cached" 102 ); 103 return Ok(cached.clone()); 104 } 105 drop(guard); 106 107 let mut guard = self.leaderboard_cache.write().await; 108 if let Some(cached) = guard.get(&year).filter(|(_, ts)| now < *ts + ttl) { 109 trace!( 110 year, 111 ttl_secs = (cached.1 + ttl - now).num_seconds(), 112 "leaderboard cached" 113 ); 114 return Ok(cached.clone()); 115 } 116 117 trace!(year, "fetching leaderboard"); 118 let leaderboard = self 119 .api 120 .get_private_leaderboard(year, self.whoami.user_id) 121 .await?; 122 123 let entry = (leaderboard, now); 124 guard.insert(year, entry.clone()); 125 self.store 126 .set::<LeaderboardCache>(LEADERBOARD_CACHE_STORE_KEY, &guard) 127 .await?; 128 Ok(entry) 129 } 130 131 pub async fn get_daily_private_leaderboard( 132 &self, 133 year: i32, 134 day: u32, 135 parts: Parts, 136 ) -> anyhow::Result<(PrivateLeaderboard, DateTime<Utc>)> { 137 let (mut leaderboard, last_update) = self.get_private_leaderboard(year).await?; 138 139 let mut members_by_p1 = leaderboard 140 .members 141 .iter() 142 .filter_map(|(&id, m)| { 143 m.completion_day_level 144 .get(&day) 145 .map(|c| (id, m, c.fst.get_star_ts)) 146 }) 147 .collect::<Vec<_>>(); 148 members_by_p1.sort_unstable_by_key(|&(_, m, ts)| (ts, m)); 149 let member_ids_and_completion_ts_by_p1 = members_by_p1 150 .into_iter() 151 .map(|(id, _, c)| (id, c)) 152 .collect::<Vec<_>>(); 153 154 let mut members_by_p2 = leaderboard 155 .members 156 .iter() 157 .filter_map(|(&id, m)| { 158 m.completion_day_level 159 .get(&day) 160 .and_then(|c| c.snd.as_ref()) 161 .map(|c| (id, m, c.get_star_ts)) 162 }) 163 .collect::<Vec<_>>(); 164 members_by_p2.sort_unstable_by_key(|&(_, m, ts)| (ts, m)); 165 let member_ids_and_completion_ts_by_p2 = members_by_p2 166 .into_iter() 167 .map(|(id, _, c)| (id, c)) 168 .collect::<Vec<_>>(); 169 170 for m in leaderboard.members.values_mut() { 171 m.global_score = None; 172 m.local_score = 0; 173 m.stars = 0; 174 m.last_star_ts = Default::default(); 175 } 176 177 if matches!(parts, Parts::P1 | Parts::Both) { 178 for (i, (id, ts)) in member_ids_and_completion_ts_by_p1.into_iter().enumerate() { 179 let score = leaderboard.members.len() - i; 180 let member = leaderboard.members.get_mut(&id).unwrap(); 181 member.local_score += score as u32; 182 member.stars += 1; 183 member.last_star_ts = member.last_star_ts.max(ts); 184 } 185 } 186 187 if matches!(parts, Parts::P2 | Parts::Both) { 188 for (i, (id, ts)) in member_ids_and_completion_ts_by_p2.into_iter().enumerate() { 189 let score = leaderboard.members.len() - i; 190 let member = leaderboard.members.get_mut(&id).unwrap(); 191 member.local_score += score as u32; 192 member.stars += 1; 193 member.last_star_ts = member.last_star_ts.max(ts); 194 } 195 } 196 197 Ok((leaderboard, last_update)) 198 } 199 } 200 201 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 202 pub enum Parts { 203 P1, 204 P2, 205 Both, 206 }