leaderboard.rs
1 use std::fmt::Write; 2 3 use chrono::TimeZone; 4 use matrix_sdk::{ 5 Room, 6 ruma::{api::client::error::ErrorKind, events::room::message::OriginalRoomMessageEvent}, 7 }; 8 9 use crate::{ 10 aoc::{day::AocDay, models::PrivateLeaderboardMember}, 11 context::Context, 12 matrix::{ 13 commands::{CommandResult, parser::ParsedCommand, parser_ext::ParserExt}, 14 utils::{RoomExt, error_message, html_message}, 15 }, 16 utils::{ 17 datetime::DateTimeExt, 18 fmt::{fmt_option, fmt_rank}, 19 }, 20 }; 21 22 pub async fn invoke( 23 event: &OriginalRoomMessageEvent, 24 room: &Room, 25 context: &Context, 26 mut cmd: ParsedCommand<'_>, 27 ) -> CommandResult<()> { 28 let most_recent_year = AocDay::most_recent().year; 29 30 let year = cmd.parse_year_default_most_recent(most_recent_year)?; 31 let rows = cmd 32 .parse_rows()? 33 .unwrap_or(context.config.aoc.leaderboard_rows); 34 let offset = cmd.parse_offset()?; 35 36 let (leaderboard, last_update) = match context.aoc_client.get_private_leaderboard(year).await { 37 Ok(resp) => resp, 38 Err(err) => match err.downcast::<reqwest::Error>() { 39 Ok(err) => { 40 if let Some(status) = err.status() { 41 room.reply_to( 42 event, 43 error_message(format!( 44 "Failed to fetch private leaderboard for {year} ({status})" 45 )), 46 ) 47 .await?; 48 return Ok(()); 49 } else { 50 return Err(err.into()); 51 } 52 } 53 Err(err) => return Err(err.into()), 54 }, 55 }; 56 let last_update = context 57 .config 58 .local_timezone 59 .from_utc_datetime(&last_update.naive_utc()) 60 .format_ymd_hms_z(); 61 62 let mut members = leaderboard.members.into_values().collect::<Vec<_>>(); 63 members.sort_unstable(); 64 65 let has_global_scores = members.iter().any(|m| m.global_score.is_some()); 66 let global_score_header = has_global_scores 67 .then_some("<th>Global Score</th>") 68 .unwrap_or_default(); 69 70 let mut leaderboard = format!( 71 r#" 72 <h3>Private Leaderboard (Advent of Code {year})</h3> 73 <table> 74 <tr> <th>Rank</th> <th>Local Score</th> {global_score_header} <th>Stars</th> <th>AoC Name</th> <th>Matrix User</th> <th>Repository</th> </tr> 75 "# 76 ); 77 78 let mut last_score = u32::MAX; 79 let mut rank = 0; 80 for (rank, member) in members 81 .into_iter() 82 .enumerate() 83 .map(|(i, member)| { 84 if member.local_score != last_score { 85 last_score = member.local_score; 86 rank = i + 1; 87 } 88 (rank, member) 89 }) 90 .skip(offset) 91 .take(rows) 92 { 93 let PrivateLeaderboardMember { 94 local_score, 95 global_score, 96 stars, 97 .. 98 } = member; 99 100 let name = member.display_name(); 101 102 let matrix_name = context 103 .users 104 .by_aoc 105 .get(&member.id) 106 .and_then(|u| u.matrix.as_ref()) 107 .map(|m| m.matrix_to_uri().to_string()) 108 .unwrap_or_default(); 109 110 let repo = context 111 .users 112 .by_aoc 113 .get(&member.id) 114 .and_then(|u| u.repo.as_deref()) 115 .unwrap_or_default(); 116 let repo_title = context 117 .config 118 .aoc 119 .repo_rules 120 .match_and_replace(repo) 121 .map(|m| m.replacement); 122 let repo_title = repo_title.as_deref().unwrap_or(repo); 123 124 let (m, m_) = if rank <= 3 { 125 ("<b>", "</b>") 126 } else { 127 Default::default() 128 }; 129 130 let rank = fmt_rank(rank); 131 132 let link_prefix = &context.config.matrix.link_prefix; 133 let global_score = has_global_scores 134 .then(|| format!("<td>{m}{}{m_}</td>", fmt_option(global_score.as_ref()))) 135 .unwrap_or_default(); 136 write!( 137 &mut leaderboard, 138 r#" 139 <tr> 140 <td>{m}{rank}{m_}</td> 141 <td>{m}{local_score}{m_}</td> 142 {global_score} 143 <td>{m}{stars}{m_}</td> 144 <td>{m}{name}{m_}</td> 145 <td>{matrix_name}</td> 146 <td>{m}<a href="{link_prefix}{repo}">{repo_title}</a>{m_}</td> 147 </tr> 148 "# 149 ) 150 .unwrap(); 151 } 152 153 write!( 154 &mut leaderboard, 155 r#" 156 </table> 157 <sup>Last update: {last_update}</sup> 158 "# 159 ) 160 .unwrap(); 161 162 if let Err(err) = room.reply_to(event, html_message(leaderboard)).await { 163 if err 164 .as_client_api_error() 165 .and_then(|err| err.error_kind()) 166 .is_some_and(|kind| matches!(kind, ErrorKind::TooLarge)) 167 { 168 room.reply_to( 169 event, 170 error_message( 171 "The requested leaderboard slice would be too large to fit in a matrix \ 172 message. Try to reduce the number of rows.", 173 ), 174 ) 175 .await?; 176 return Ok(()); 177 } else { 178 return Err(err.into()); 179 } 180 } 181 182 Ok(()) 183 }