join_leave_notifications.rs
1 use std::{sync::Arc, time::Duration}; 2 3 use matrix_sdk::{Room, RoomState}; 4 use tracing::{error, info, trace, warn}; 5 6 use crate::{ 7 Context, 8 aoc::{ 9 day::AocDay, 10 models::{PrivateLeaderboard, PrivateLeaderboardMembers}, 11 }, 12 matrix::utils::notice, 13 }; 14 15 pub async fn start(context: Arc<Context>) -> ! { 16 let year = AocDay::most_recent().year; 17 let mut leaderboard = context 18 .aoc_client 19 .get_private_leaderboard_cached(year) 20 .await 21 .map(|(lb, _)| lb); 22 23 loop { 24 if let Err(err) = trigger(&context, &mut leaderboard).await { 25 error!("Failed to check for member join/leave events: {err}"); 26 } 27 tokio::time::sleep(Duration::from_secs(10)).await; 28 } 29 } 30 31 async fn trigger( 32 context: &Context, 33 leaderboard: &mut Option<PrivateLeaderboard>, 34 ) -> anyhow::Result<()> { 35 let room = &context.room; 36 if room.state() != RoomState::Joined { 37 warn!("not a member of target room {}", room.room_id()); 38 room.join().await?; 39 } 40 41 let year = AocDay::most_recent().year; 42 43 trace!("checking for member leave/join events"); 44 45 let new_leaderboard = context.aoc_client.get_private_leaderboard(year).await?.0; 46 47 send_notifications( 48 room, 49 context, 50 leaderboard 51 .as_ref() 52 .map(|l| &l.members) 53 .unwrap_or(&Default::default()), 54 &new_leaderboard.members, 55 ) 56 .await?; 57 58 *leaderboard = Some(new_leaderboard); 59 60 Ok(()) 61 } 62 63 async fn send_notifications( 64 room: &Room, 65 context: &Context, 66 old_leaderboard: &PrivateLeaderboardMembers, 67 new_leaderboard: &PrivateLeaderboardMembers, 68 ) -> anyhow::Result<()> { 69 if old_leaderboard.is_empty() { 70 info!( 71 members = new_leaderboard.len(), 72 "ignoring first leaderboard membership update" 73 ); 74 return Ok(()); 75 } 76 77 let mut notifications = Vec::new(); 78 79 for (id, member) in new_leaderboard { 80 if !old_leaderboard.contains_key(id) { 81 notifications.push((member, true)); 82 } 83 } 84 85 for (id, member) in old_leaderboard { 86 if !new_leaderboard.contains_key(id) { 87 notifications.push((member, false)); 88 } 89 } 90 91 trace!( 92 ?notifications, 93 "sending leaderboard join/leave notifications" 94 ); 95 96 for (member, joined) in notifications { 97 let matrix = context 98 .users 99 .by_aoc 100 .get(&member.id) 101 .and_then(|m| m.matrix.as_deref()); 102 103 let name = member.matrix_mention_or_display_name(matrix); 104 let action = if joined { "joined" } else { "left" }; 105 106 room.send(notice(format!( 107 "{name} has {action} the private leaderboard" 108 ))) 109 .await?; 110 } 111 112 Ok(()) 113 }