/ src / tasks / join_leave_notifications.rs
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  }