Skip to content

Commit

Permalink
Send a DM to TA/EC for multiple controller no-show
Browse files Browse the repository at this point in the history
  • Loading branch information
Celeo committed Nov 19, 2024
1 parent 4a6c00a commit 9c68ab8
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 12 deletions.
77 changes: 65 additions & 12 deletions vzdv-bot/src/tasks/no_shows.rs
Original file line number Diff line number Diff line change
@@ -1,33 +1,46 @@
use anyhow::Result;
use log::{debug, error, info};
use log::{debug, error, info, warn};
use sqlx::{Pool, Sqlite};
use std::{collections::HashMap, sync::Arc, time::Duration};
use tokio::time::sleep;
use twilight_http::Client;
use twilight_model::id::Id;
use vzdv::{
config::Config,
get_controller_cids_and_names,
get_controller_cids_and_names, get_staff_member_by_role,
sql::{self, Controller, NoShow},
};

/// Create the message to send to the Discord user.
fn create_message(
no_show: &NoShow,
cid_name_map: &HashMap<u32, (String, String)>,
count: usize,
config: &Arc<Config>,
) -> String {
format!(
"## Warning\n\nYou have been added to the **{} no-show list** by {} {}.\n\nFor more information, reach out to the {} at `{}@{}`.\n\n\nResponses to this DM are not monitored.",
"## Warning\n\nYou have been added to the **{} no-show list** by {} {}.\nYou have been on this list {} time(s).\n\nFor more information, reach out to the {} at `{}@{}`.\n\n\nResponses to this DM are not monitored.",
no_show.entry_type,
cid_name_map.get(&no_show.reported_by).unwrap().0,
cid_name_map.get(&no_show.reported_by).unwrap().1,
count,
if no_show.entry_type == "training" { "TA" } else { "EC" },
if no_show.entry_type == "training" { "ta" } else { "ec" },
config.staff.email_domain
)
}

/// Open a DM with the Discord user and send a message.
async fn send_dm(http: &Arc<Client>, user_id: &str, message: &str) -> Result<()> {
let channel = http
.create_private_channel(Id::new(user_id.parse()?))
.await?
.model()
.await?;
http.create_message(channel.id).content(message)?.await?;
Ok(())
}

/// Single loop execution.
async fn tick(config: &Arc<Config>, db: &Pool<Sqlite>, http: &Arc<Client>) -> Result<()> {
debug!("Checking for new no-show entries");
Expand All @@ -37,26 +50,66 @@ async fn tick(config: &Arc<Config>, db: &Pool<Sqlite>, http: &Arc<Client>) -> Re
return Ok(());
}
let cid_name_map = get_controller_cids_and_names(db).await?;
for entry in entries {
for entry in &entries {
if !entry.notified {
debug!("Need to notify {} of no-show", entry.cid);
let controller: Controller = sqlx::query_as(sql::GET_CONTROLLER_BY_CID)
.bind(entry.cid)
.fetch_one(db)
.await?;
if let Some(ref discord_user_id) = controller.discord_id {
let channel = http
.create_private_channel(Id::new(discord_user_id.parse()?))
.await?
.model()
.await?;
http.create_message(channel.id)
.content(&create_message(&entry, &cid_name_map, config))?
.await?;
let occurrence_count = entries
.iter()
.filter(|e| e.cid == entry.cid && e.entry_type == entry.entry_type)
.count();
// notify the controller
send_dm(
http,
discord_user_id,
&create_message(&entry, &cid_name_map, occurrence_count, config),
)
.await?;
sqlx::query(sql::UPDATE_NO_SHOW_NOTIFIED)
.bind(entry.id)
.execute(db)
.await?;
// if this isn't the first instance, notify the appropriate staff member
if occurrence_count > 1 {
let staff_role = if entry.entry_type == "training" {
"TA"
} else {
"EC"
};
let staff_member = get_staff_member_by_role(db, &staff_role).await?;
if let Some(staff_member) = staff_member.first() {
if let Some(ref discord_user_id) = staff_member.discord_id {
send_dm(
http,
discord_user_id,
&format!(
"Controller {} has been added to the {} no-show list {} times.\n\nSee the no-show list [here](https://{}/admin/no_show_list).",
entry.cid, entry.entry_type, occurrence_count, config.staff.email_domain
),
)
.await?;
info!(
"Notified {} {} ({}) of {} no-show occurrences for {}",
staff_member.first_name,
staff_member.last_name,
staff_member.cid,
occurrence_count,
entry.cid
);
} else {
warn!(
"Staff member {} does not have their Discord linked",
staff_member.cid
);
}
} else {
warn!("Could not find staff member with role {staff_role}");
}
}
info!("Notified {} of their new no-show entry", entry.cid);
} else {
debug!(
Expand Down
11 changes: 11 additions & 0 deletions vzdv-site/templates/changelog.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@

<hr>

<div class="card shadow mb-3">
<div class="card-body">
<h5 class="card-title">2024-11-18</h5>
<div class="card-text">
<ul>
<li>Modifications to the Discord no-show integration</li>
</ul>
</div>
</div>
</div>

<div class="card shadow mb-3">
<div class="card-body">
<h5 class="card-title">2024-11-16</h5>
Expand Down
13 changes: 13 additions & 0 deletions vzdv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use fern::{
colors::{Color, ColoredLevelConfig},
Dispatch,
};
use itertools::Itertools;
use log::{debug, error};
use reqwest::ClientBuilder;
use sql::Controller;
Expand Down Expand Up @@ -512,6 +513,18 @@ pub fn generate_operating_initials_for(
bail!("Apparently there are no OIs available")
}

/// Get controllers from the DB with that role.
pub async fn get_staff_member_by_role(db: &Pool<Sqlite>, role: &str) -> Result<Vec<Controller>> {
let with_roles: Vec<Controller> = sqlx::query_as(sql::GET_CONTROLLERS_WITH_ROLES)
.fetch_all(db)
.await?;
Ok(with_roles
.iter()
.filter(|c| c.roles.split_terminator(',').contains(&role))
.cloned()
.collect())
}

#[cfg(test)]
pub mod tests {
use super::{
Expand Down
2 changes: 2 additions & 0 deletions vzdv/src/sql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,8 @@ pub const SET_CONTROLLER_DISCORD_ID: &str = "UPDATE controller SET discord_id=$2
pub const UNSET_CONTROLLER_DISCORD_ID: &str = "UPDATE controller SET discord_id=NULL WHERE cid=$1";
pub const SET_CONTROLLER_ROLES: &str = "UPDATE controller SET roles=$2 WHERE cid=$1";
pub const SET_CONTROLLER_ON_ROSTER: &str = "UPDATE controller SET is_on_roster=$2 WHERE cid=$1";
pub const GET_CONTROLLERS_WITH_ROLES: &str =
"SELECT * FROM controller WHERE roles IS NOT NULL AND roles <> ''";

pub const GET_ALL_CERTIFICATIONS: &str = "SELECT * FROM certification";
pub const GET_ALL_CERTIFICATIONS_FOR: &str = "SELECT * FROM certification WHERE cid=$1";
Expand Down

0 comments on commit 9c68ab8

Please sign in to comment.