|
| 1 | +/* |
| 2 | +amFOSS Daemon: A discord bot for the amFOSS Discord server. |
| 3 | +Copyright (C) 2024 amFOSS |
| 4 | +
|
| 5 | +This program is free software: you can redistribute it and/or modify |
| 6 | +it under the terms of the GNU General Public License as published by |
| 7 | +the Free Software Foundation, either version 3 of the License, or |
| 8 | +(at your option) any later version. |
| 9 | +
|
| 10 | +This program is distributed in the hope that it will be useful, |
| 11 | +but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 12 | +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 13 | +GNU General Public License for more details. |
| 14 | +
|
| 15 | +You should have received a copy of the GNU General Public License |
| 16 | +along with this program. If not, see <https://www.gnu.org/licenses/>. |
| 17 | +*/ |
| 18 | +use super::Task; |
| 19 | +use anyhow::Context as _; |
| 20 | +use chrono::{DateTime, Datelike, Local, NaiveTime, ParseError, TimeZone, Timelike, Utc}; |
| 21 | +use serenity::all::{ |
| 22 | + ChannelId, Colour, Context as SerenityContext, CreateEmbed, CreateEmbedAuthor, CreateMessage, |
| 23 | +}; |
| 24 | +use serenity::async_trait; |
| 25 | +use std::collections::HashMap; |
| 26 | +use tracing::{debug, trace}; |
| 27 | + |
| 28 | +use crate::{ |
| 29 | + graphql::{models::AttendanceRecord, queries::fetch_attendance}, |
| 30 | + ids::THE_LAB_CHANNEL_ID, |
| 31 | + utils::time::{get_five_forty_five_pm_timestamp, time_until}, |
| 32 | +}; |
| 33 | + |
| 34 | +const TITLE_URL: &str = "https://www.amfoss.in/"; |
| 35 | +const AUTHOR_URL: &str = "https://github.com/amfoss/amd"; |
| 36 | + |
| 37 | +pub struct PresenseReport; |
| 38 | + |
| 39 | +#[async_trait] |
| 40 | +impl Task for PresenseReport { |
| 41 | + fn name(&self) -> &str { |
| 42 | + "Lab Attendance Check" |
| 43 | + } |
| 44 | + |
| 45 | + fn run_in(&self) -> tokio::time::Duration { |
| 46 | + time_until(18, 00) |
| 47 | + } |
| 48 | + |
| 49 | + async fn run(&self, ctx: SerenityContext) -> anyhow::Result<()> { |
| 50 | + check_lab_attendance(ctx).await |
| 51 | + } |
| 52 | +} |
| 53 | + |
| 54 | +pub async fn check_lab_attendance(ctx: SerenityContext) -> anyhow::Result<()> { |
| 55 | + trace!("Starting lab attendance check"); |
| 56 | + let attendance = fetch_attendance() |
| 57 | + .await |
| 58 | + .context("Failed to fetch attendance from Root")?; |
| 59 | + |
| 60 | + let time = Local::now().with_timezone(&chrono_tz::Asia::Kolkata); |
| 61 | + let threshold_time = get_five_forty_five_pm_timestamp(time); |
| 62 | + |
| 63 | + let mut absent_list = Vec::new(); |
| 64 | + let mut late_list = Vec::new(); |
| 65 | + |
| 66 | + for record in &attendance { |
| 67 | + debug!("Checking attendance for member: {}", record.name); |
| 68 | + if !record.is_present || record.time_in.is_none() { |
| 69 | + absent_list.push(record.clone()); |
| 70 | + debug!("Member {} marked as absent", record.name); |
| 71 | + } else if let Some(time_str) = &record.time_in { |
| 72 | + if let Ok(time) = parse_time(time_str) { |
| 73 | + if time > threshold_time { |
| 74 | + late_list.push(record.clone()); |
| 75 | + debug!("Member {} marked as late", record.name); |
| 76 | + } |
| 77 | + } |
| 78 | + } |
| 79 | + } |
| 80 | + |
| 81 | + if absent_list.len() == attendance.len() { |
| 82 | + send_lab_closed_message(ctx).await?; |
| 83 | + } else { |
| 84 | + send_attendance_report(ctx, absent_list, late_list, attendance.len()).await?; |
| 85 | + } |
| 86 | + |
| 87 | + trace!("Completed lab attendance check"); |
| 88 | + Ok(()) |
| 89 | +} |
| 90 | + |
| 91 | +async fn send_lab_closed_message(ctx: SerenityContext) -> anyhow::Result<()> { |
| 92 | + let today_date = Utc::now().format("%B %d, %Y").to_string(); |
| 93 | + |
| 94 | + let bot_user = ctx.http.get_current_user().await?; |
| 95 | + let bot_avatar_url = bot_user |
| 96 | + .avatar_url() |
| 97 | + .unwrap_or_else(|| bot_user.default_avatar_url()); |
| 98 | + |
| 99 | + let embed = CreateEmbed::new() |
| 100 | + .title(format!("Presense Report - {}", today_date)) |
| 101 | + .url(TITLE_URL) |
| 102 | + .author( |
| 103 | + CreateEmbedAuthor::new("amD") |
| 104 | + .url(AUTHOR_URL) |
| 105 | + .icon_url(bot_avatar_url), |
| 106 | + ) |
| 107 | + .color(Colour::RED) |
| 108 | + .description("Uh-oh, seems like the lab is closed today! 🏖️ Everyone is absent!") |
| 109 | + .timestamp(Utc::now()); |
| 110 | + |
| 111 | + ChannelId::new(THE_LAB_CHANNEL_ID) |
| 112 | + .send_message(&ctx.http, CreateMessage::new().embed(embed)) |
| 113 | + .await |
| 114 | + .context("Failed to send lab closed message")?; |
| 115 | + |
| 116 | + Ok(()) |
| 117 | +} |
| 118 | + |
| 119 | +async fn send_attendance_report( |
| 120 | + ctx: SerenityContext, |
| 121 | + absent_list: Vec<AttendanceRecord>, |
| 122 | + late_list: Vec<AttendanceRecord>, |
| 123 | + total_count: usize, |
| 124 | +) -> anyhow::Result<()> { |
| 125 | + let today_date = Utc::now().format("%B %d, %Y").to_string(); |
| 126 | + |
| 127 | + let present = total_count - absent_list.len(); |
| 128 | + let attendance_percentage = if total_count > 0 { |
| 129 | + (present as f32 / total_count as f32) * 100.0 |
| 130 | + } else { |
| 131 | + 0.0 |
| 132 | + }; |
| 133 | + |
| 134 | + let bot_user = ctx.http.get_current_user().await?; |
| 135 | + let bot_avatar_url = bot_user |
| 136 | + .avatar_url() |
| 137 | + .unwrap_or_else(|| bot_user.default_avatar_url()); |
| 138 | + |
| 139 | + let embed_color = if attendance_percentage > 75.0 { |
| 140 | + Colour::DARK_GREEN |
| 141 | + } else if attendance_percentage > 50.0 { |
| 142 | + Colour::GOLD |
| 143 | + } else { |
| 144 | + Colour::RED |
| 145 | + }; |
| 146 | + |
| 147 | + let mut description = format!( |
| 148 | + "# Stats\n- Present: {} ({}%)\n- Absent: {}\n- Late: {}\n\n", |
| 149 | + present, |
| 150 | + attendance_percentage.round() as i32, |
| 151 | + absent_list.len(), |
| 152 | + late_list.len() |
| 153 | + ); |
| 154 | + |
| 155 | + description.push_str(&format_attendance_list("Absent", &absent_list)); |
| 156 | + description.push_str(&format_attendance_list("Late", &late_list)); |
| 157 | + |
| 158 | + let embed = CreateEmbed::new() |
| 159 | + .title(format!("Presense Report - {}", today_date)) |
| 160 | + .url(TITLE_URL) |
| 161 | + .author( |
| 162 | + CreateEmbedAuthor::new("amD") |
| 163 | + .url(AUTHOR_URL) |
| 164 | + .icon_url(bot_avatar_url), |
| 165 | + ) |
| 166 | + .color(embed_color) |
| 167 | + .description(description) |
| 168 | + .timestamp(Utc::now()); |
| 169 | + |
| 170 | + ChannelId::new(THE_LAB_CHANNEL_ID) |
| 171 | + .send_message(&ctx.http, CreateMessage::new().embed(embed)) |
| 172 | + .await |
| 173 | + .context("Failed to send attendance report")?; |
| 174 | + |
| 175 | + Ok(()) |
| 176 | +} |
| 177 | + |
| 178 | +fn format_attendance_list(title: &str, list: &[AttendanceRecord]) -> String { |
| 179 | + if list.is_empty() { |
| 180 | + return format!( |
| 181 | + "**{}**\nNo one is {} today! 🎉\n\n", |
| 182 | + title, |
| 183 | + title.to_lowercase() |
| 184 | + ); |
| 185 | + } |
| 186 | + |
| 187 | + let mut by_year: HashMap<i32, Vec<&str>> = HashMap::new(); |
| 188 | + for record in list { |
| 189 | + if record.year >= 1 && record.year <= 3 { |
| 190 | + by_year.entry(record.year).or_default().push(&record.name); |
| 191 | + } |
| 192 | + } |
| 193 | + |
| 194 | + let mut result = format!("# {}\n", title); |
| 195 | + |
| 196 | + for year in 1..=3 { |
| 197 | + if let Some(names) = by_year.get(&year) { |
| 198 | + if !names.is_empty() { |
| 199 | + result.push_str(&format!("### Year {}\n", year)); |
| 200 | + |
| 201 | + for name in names { |
| 202 | + result.push_str(&format!("- {}\n", name)); |
| 203 | + } |
| 204 | + result.push('\n'); |
| 205 | + } |
| 206 | + } |
| 207 | + } |
| 208 | + |
| 209 | + result |
| 210 | +} |
| 211 | + |
| 212 | +fn parse_time(time_str: &str) -> Result<DateTime<Local>, ParseError> { |
| 213 | + let time_only = time_str.split('.').next().unwrap(); |
| 214 | + let naive_time = NaiveTime::parse_from_str(time_only, "%H:%M:%S")?; |
| 215 | + let now = Local::now(); |
| 216 | + |
| 217 | + let result = Local |
| 218 | + .with_ymd_and_hms( |
| 219 | + now.year(), |
| 220 | + now.month(), |
| 221 | + now.day(), |
| 222 | + naive_time.hour(), |
| 223 | + naive_time.minute(), |
| 224 | + naive_time.second(), |
| 225 | + ) |
| 226 | + .single() |
| 227 | + .expect("Valid datetime must be created"); |
| 228 | + |
| 229 | + Ok(result) |
| 230 | +} |
0 commit comments