/ src / aoc / client.rs
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  }