From 1a7abfa8abe698202b81757fdd82d8c7c2a26195 Mon Sep 17 00:00:00 2001 From: hkx05 Date: Tue, 26 Nov 2024 21:43:09 +0530 Subject: [PATCH 1/9] amd support for streaks --- src/scheduler/tasks/status_update.rs | 32 ++++++++++++++++------ src/utils/graphql.rs | 41 +++++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/src/scheduler/tasks/status_update.rs b/src/scheduler/tasks/status_update.rs index 0fd7184..4eed39f 100644 --- a/src/scheduler/tasks/status_update.rs +++ b/src/scheduler/tasks/status_update.rs @@ -15,15 +15,19 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ -use serenity::all::{ChannelId, Context, Message}; +use serenity::all::{ChannelId, Context, Message}; use crate::{ ids::{ GROUP_FOUR_CHANNEL_ID, GROUP_ONE_CHANNEL_ID, GROUP_THREE_CHANNEL_ID, GROUP_TWO_CHANNEL_ID, STATUS_UPDATE_CHANNEL_ID, }, - utils::{graphql::fetch_members, time::get_five_am_timestamp}, + utils::{ + graphql::{fetch_members, send_streak_update}, + time::get_five_am_timestamp, + }, }; +use chrono::Duration; pub async fn check_status_updates(ctx: Context) { let members = fetch_members().await.expect("Root must be up."); @@ -37,7 +41,7 @@ pub async fn check_status_updates(ctx: Context) { let time = chrono::Local::now().with_timezone(&chrono_tz::Asia::Kolkata); let today_five_am = get_five_am_timestamp(time); - let yesterday_five_am = today_five_am - chrono::Duration::hours(24); + let yesterday_five_am = today_five_am - Duration::hours(24); let mut valid_updates: Vec = vec![]; @@ -68,12 +72,20 @@ pub async fn check_status_updates(ctx: Context) { let name_parts: Vec<&str> = member.split_whitespace().collect(); let first_name = name_parts.get(0).unwrap_or(&""); let last_name = name_parts.get(1).unwrap_or(&""); - let has_sent_update = valid_updates - .iter() - .any(|msg| msg.content.contains(first_name) || msg.content.contains(last_name)); + + let has_sent_update = valid_updates.iter().any(|msg| { + let msg_author = &msg.author.name.to_lowercase(); + msg_author.contains(first_name.to_lowercase().as_str()) + || msg_author.contains(last_name.to_lowercase().as_str()) + }); if !has_sent_update { - naughty_list.push(member.to_string()); + naughty_list.push(member.clone()); + } + + match send_streak_update("https://root.shuttleapp.rs/", member.parse().unwrap_or(0), has_sent_update).await { + Ok(_) => println!("Successfully updated streak for {}", member), + Err(e) => println!("Failed to update streak for {}: {:?}", member, e), } } @@ -82,7 +94,8 @@ pub async fn check_status_updates(ctx: Context) { if naughty_list.is_empty() { status_update_channel .say(ctx.http, "Everyone sent their update today!") - .await; + .await + .expect("Failed to send message"); } else { let formatted_list = naughty_list .iter() @@ -98,6 +111,7 @@ pub async fn check_status_updates(ctx: Context) { formatted_list ), ) - .await; + .await + .expect("Failed to send message"); } } diff --git a/src/utils/graphql.rs b/src/utils/graphql.rs index 4d4bcf7..02602dd 100644 --- a/src/utils/graphql.rs +++ b/src/utils/graphql.rs @@ -16,6 +16,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ use serde_json::Value; +use anyhow::{Result}; const REQUEST_URL: &str = "https://root.shuttleapp.rs/"; @@ -38,10 +39,48 @@ pub async fn fetch_members() -> Result, reqwest::Error> { let member_names: Vec = json["data"]["getMember"] .as_array() - .unwrap() + .unwrap_or(&vec![]) .iter() .map(|member| member["name"].as_str().unwrap().to_string()) .collect(); Ok(member_names) } + +pub async fn send_streak_update( + root_api_url: &str, + id: i32, + has_sent_update: bool, +) -> Result<(), Box> { + let client = reqwest::Client::new(); + let query = format!( + r#" + mutation {{ + updateStreak(id: {}, hasSentUpdate: {}) {{ + id + streak + max_streak + }} + }} + "#, + id, has_sent_update + ); + let response = client + .post(root_api_url) + .header("Content-Type", "application/json") + .body(query) + .send() + .await?; + + if response.status().is_success() { + println!("Successfully updated streak for ID {}", id); + Ok(()) + } else { + Err(anyhow::anyhow!( + "Failed to update streak for ID {}. HTTP status: {}", + id, + response.status() + ) + .into()) + } +} \ No newline at end of file From c6a4d2cbffe2d2e00d58cb6f7a7aa8804e9e5fcb Mon Sep 17 00:00:00 2001 From: hkx05 Date: Sat, 30 Nov 2024 03:47:53 +0530 Subject: [PATCH 2/9] required support for root for implementing streaks --- src/ids.rs | 2 +- src/scheduler/scheduler.rs | 3 +- src/scheduler/tasks/status_update.rs | 15 +++---- src/utils/graphql.rs | 62 +++++++++++++++++----------- 4 files changed, 49 insertions(+), 33 deletions(-) diff --git a/src/ids.rs b/src/ids.rs index 570394d..91f3db5 100644 --- a/src/ids.rs +++ b/src/ids.rs @@ -28,4 +28,4 @@ pub const GROUP_ONE_CHANNEL_ID: u64 = 1225098248293716008; pub const GROUP_TWO_CHANNEL_ID: u64 = 1225098298935738489; pub const GROUP_THREE_CHANNEL_ID: u64 = 1225098353378070710; pub const GROUP_FOUR_CHANNEL_ID: u64 = 1225098407216156712; -pub const STATUS_UPDATE_CHANNEL_ID: u64 = 764575524127244318; +pub const STATUS_UPDATE_CHANNEL_ID: u64 = 764575524127244318; \ No newline at end of file diff --git a/src/scheduler/scheduler.rs b/src/scheduler/scheduler.rs index 0426708..b1f7701 100644 --- a/src/scheduler/scheduler.rs +++ b/src/scheduler/scheduler.rs @@ -19,6 +19,8 @@ use crate::scheduler::tasks::{get_tasks, Task}; use serenity::client::Context as SerenityContext; use tokio::spawn; +use tokio::time::Duration; + pub async fn run_scheduler(ctx: SerenityContext) { let tasks = get_tasks(); @@ -32,7 +34,6 @@ async fn schedule_task(ctx: SerenityContext, task: Box) { loop { let next_run_in = task.run_in(); tokio::time::sleep(next_run_in).await; - task.run(ctx.clone()).await; } } diff --git a/src/scheduler/tasks/status_update.rs b/src/scheduler/tasks/status_update.rs index 4eed39f..beb7245 100644 --- a/src/scheduler/tasks/status_update.rs +++ b/src/scheduler/tasks/status_update.rs @@ -19,7 +19,8 @@ along with this program. If not, see . use serenity::all::{ChannelId, Context, Message}; use crate::{ ids::{ - GROUP_FOUR_CHANNEL_ID, GROUP_ONE_CHANNEL_ID, GROUP_THREE_CHANNEL_ID, GROUP_TWO_CHANNEL_ID, + GROUP_FOUR_CHANNEL_ID, GROUP_THREE_CHANNEL_ID, GROUP_TWO_CHANNEL_ID, + GROUP_ONE_CHANNEL_ID, STATUS_UPDATE_CHANNEL_ID, }, utils::{ @@ -68,7 +69,7 @@ pub async fn check_status_updates(ctx: Context) { let mut naughty_list: Vec = vec![]; - for member in &members { + for (member,id) in &members { let name_parts: Vec<&str> = member.split_whitespace().collect(); let first_name = name_parts.get(0).unwrap_or(&""); let last_name = name_parts.get(1).unwrap_or(&""); @@ -82,8 +83,8 @@ pub async fn check_status_updates(ctx: Context) { if !has_sent_update { naughty_list.push(member.clone()); } - - match send_streak_update("https://root.shuttleapp.rs/", member.parse().unwrap_or(0), has_sent_update).await { + + match send_streak_update("https://root.shuttleapp.rs/", *id, has_sent_update).await { Ok(_) => println!("Successfully updated streak for {}", member), Err(e) => println!("Failed to update streak for {}: {:?}", member, e), } @@ -93,7 +94,7 @@ pub async fn check_status_updates(ctx: Context) { if naughty_list.is_empty() { status_update_channel - .say(ctx.http, "Everyone sent their update today!") + .say(ctx.http, "Everyone sent their update in the last 1 minutes!") .await .expect("Failed to send message"); } else { @@ -107,11 +108,11 @@ pub async fn check_status_updates(ctx: Context) { .say( ctx.http, format!( - "These members did not send their updates:\n{}", + "These members did not send their updates in the last 3 minutes:\n{}", formatted_list ), ) .await .expect("Failed to send message"); } -} +} \ No newline at end of file diff --git a/src/utils/graphql.rs b/src/utils/graphql.rs index 02602dd..4bef9e8 100644 --- a/src/utils/graphql.rs +++ b/src/utils/graphql.rs @@ -20,12 +20,13 @@ use anyhow::{Result}; const REQUEST_URL: &str = "https://root.shuttleapp.rs/"; -pub async fn fetch_members() -> Result, reqwest::Error> { +pub async fn fetch_members() -> Result, reqwest::Error> { let client = reqwest::Client::new(); let query = r#" query { getMember { name + id } }"#; @@ -37,50 +38,63 @@ pub async fn fetch_members() -> Result, reqwest::Error> { let json: Value = response.json().await?; - let member_names: Vec = json["data"]["getMember"] + let member_names: Vec<(String, i32)> = json["data"]["getMember"] .as_array() .unwrap_or(&vec![]) .iter() - .map(|member| member["name"].as_str().unwrap().to_string()) + .map(|member| { + let id = member["id"].as_i64().unwrap_or(0) as i32; + let name = member["name"].as_str().unwrap_or("").to_string(); + (name, id) + }) .collect(); Ok(member_names) } pub async fn send_streak_update( - root_api_url: &str, - id: i32, + root_api_url: &str, + id: i32, has_sent_update: bool, -) -> Result<(), Box> { +) -> Result<(), Box> { let client = reqwest::Client::new(); - let query = format!( - r#" - mutation {{ - updateStreak(id: {}, hasSentUpdate: {}) {{ + let query = r#" + mutation updateStreak($id: Int!, $hasSentUpdate: Boolean!) { + updateStreak(id: $id, hasSentUpdate: $hasSentUpdate) { id streak - max_streak - }} - }} - "#, - id, has_sent_update - ); + maxStreak + } + } + "#; + + let variables = serde_json::json!({ + "id": id, + "hasSentUpdate": has_sent_update + }); + + let body = serde_json::json!({ + "query": query, + "variables": variables + }); + let response = client .post(root_api_url) .header("Content-Type", "application/json") - .body(query) + .json(&body) .send() .await?; - if response.status().is_success() { - println!("Successfully updated streak for ID {}", id); + let response_status = response.status(); + let response_body = response.text().await?; + + if response_status.is_success() { + println!("Successfully updated streak for ID {}: {}", id, response_body); Ok(()) } else { Err(anyhow::anyhow!( - "Failed to update streak for ID {}. HTTP status: {}", - id, - response.status() - ) - .into()) + "Failed to update streak for ID {}. HTTP status: {}, response: {}", + id, response_status, response_body + ).into()) } } \ No newline at end of file From 4c7e789e891e063a7990275cc4e25490a9c52def Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Wed, 4 Dec 2024 20:19:10 +0530 Subject: [PATCH 3/9] update workflow to point to correct repo and version --- .github/workflows/shuttle-run.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/shuttle-run.yml b/.github/workflows/shuttle-run.yml index bd27c99..3d833b5 100644 --- a/.github/workflows/shuttle-run.yml +++ b/.github/workflows/shuttle-run.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v4 - name: Run shuttle project locally. - uses: denytwice/shuttle-run@v1.0 + uses: ivinjabraham/shuttle-run@v1.1 with: secrets: | DISCORD_TOKEN = '${{ secrets.DISCORD_TOKEN }}' From 48a754e0198c87d80ba3c4085594a3b72d0376ae Mon Sep 17 00:00:00 2001 From: hkx05 Date: Thu, 5 Dec 2024 21:13:58 +0530 Subject: [PATCH 4/9] updated print message to 1 day instead of 1 minute --- src/scheduler/tasks/status_update.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scheduler/tasks/status_update.rs b/src/scheduler/tasks/status_update.rs index beb7245..032e58c 100644 --- a/src/scheduler/tasks/status_update.rs +++ b/src/scheduler/tasks/status_update.rs @@ -94,7 +94,7 @@ pub async fn check_status_updates(ctx: Context) { if naughty_list.is_empty() { status_update_channel - .say(ctx.http, "Everyone sent their update in the last 1 minutes!") + .say(ctx.http, "Everyone sent their update today!") .await .expect("Failed to send message"); } else { From 1312c3fbc607e74ed810e2ddc94f1969ad48fc18 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Fri, 20 Dec 2024 06:58:58 +0530 Subject: [PATCH 5/9] simplify codebase structure and add additional documentation --- docs/CONTRIBUTING.md | 2 +- src/commands.rs | 3 + src/{scheduler => graphql}/mod.rs | 6 +- .../tasks/mod.rs => graphql/models.rs} | 28 ++++- src/graphql/queries.rs | 51 ++++++++ src/ids.rs | 6 +- src/main.rs | 116 ++++++++++++++---- src/scheduler/scheduler.rs | 39 ------ .../tasks/tasks.rs => tasks/mod.rs} | 25 ++-- src/{scheduler => }/tasks/status_update.rs | 42 +++---- src/utils/graphql.rs | 100 --------------- src/utils/mod.rs | 1 - 12 files changed, 209 insertions(+), 210 deletions(-) rename src/{scheduler => graphql}/mod.rs (90%) rename src/{scheduler/tasks/mod.rs => graphql/models.rs} (60%) create mode 100644 src/graphql/queries.rs delete mode 100644 src/scheduler/scheduler.rs rename src/{scheduler/tasks/tasks.rs => tasks/mod.rs} (66%) rename src/{scheduler => }/tasks/status_update.rs (72%) delete mode 100644 src/utils/graphql.rs diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 8814ad7..ef2d900 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -122,7 +122,7 @@ The event handler takes care of the rest: ```rust // On the event of a reaction being added FullEvent::ReactionAdd { add_reaction } => { - let message_id = MessageId::new(ARCHIVE_MESSAGE_ID); + let message_id = MessageId::new(ROLES_MESSAGE_ID); // Check if the reaction was added to the message we want and if it is reacted with the // emoji we want if add_reaction.message_id == message_id && data.reaction_roles.contains_key(&add_reaction.emoji) { diff --git a/src/commands.rs b/src/commands.rs index 2e5df92..0b5a6b5 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -23,6 +23,9 @@ async fn amdctl(ctx: Context<'_>) -> Result<(), Error> { Ok(()) } +/// Every function that is defined *should* be added to the +/// returned vector in get_commands to ensure it is registered (available for the user) +/// when the bot goes online. pub fn get_commands() -> Vec> { vec![amdctl()] } diff --git a/src/scheduler/mod.rs b/src/graphql/mod.rs similarity index 90% rename from src/scheduler/mod.rs rename to src/graphql/mod.rs index 51fbf35..d9c1786 100644 --- a/src/scheduler/mod.rs +++ b/src/graphql/mod.rs @@ -15,7 +15,5 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ -pub mod scheduler; -pub mod tasks; - -pub use self::scheduler::run_scheduler; +pub mod models; +pub mod queries; diff --git a/src/scheduler/tasks/mod.rs b/src/graphql/models.rs similarity index 60% rename from src/scheduler/tasks/mod.rs rename to src/graphql/models.rs index 5d3d4ab..ce77481 100644 --- a/src/scheduler/tasks/mod.rs +++ b/src/graphql/models.rs @@ -15,7 +15,29 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ -pub mod status_update; -pub mod tasks; +use serde::Deserialize; +use std::borrow::Cow; -pub use self::tasks::{get_tasks, Task}; +#[derive(Deserialize)] +pub struct Member<'a> { + id: Option, + roll_num: Option>, + name: Option>, + hostel: &'a str, + email: &'a str, + sex: &'a str, + year: i32, + mac_addr: &'a str, + discord_id: &'a str, + group_id: i32, +} + +#[derive(Deserialize)] +struct Data<'a> { + getMember: Vec>, +} + +#[derive(Deserialize)] +struct Root<'a> { + data: Data<'a>, +} diff --git a/src/graphql/queries.rs b/src/graphql/queries.rs new file mode 100644 index 0000000..1e26aed --- /dev/null +++ b/src/graphql/queries.rs @@ -0,0 +1,51 @@ +/* +amFOSS Daemon: A discord bot for the amFOSS Discord server. +Copyright (C) 2024 amFOSS + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ +use serde_json::Value; + +use super::models::Member; + +const REQUEST_URL: &str = "https://root.shuttleapp.rs/"; + +pub async fn fetch_members() -> Result, reqwest::Error> { + let client = reqwest::Client::new(); + let query = r#" + query { + getMember { + name, + groupId, + discordId + } + }"#; + + let response = client + .post(REQUEST_URL) + .json(&serde_json::json!({"query": query})) + .send() + .await?; + + let json: Value = response.json().await?; + + let member_names: Vec = json["data"]["getMember"] + .as_array() + .unwrap() + .iter() + .map(Member) + .collect(); + + Ok(member_names) +} diff --git a/src/ids.rs b/src/ids.rs index 91f3db5..91ee247 100644 --- a/src/ids.rs +++ b/src/ids.rs @@ -15,7 +15,10 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ -pub const ARCHIVE_MESSAGE_ID: u64 = 1298636092886749294; +/// Points to the Embed in the #roles channel. +pub const ROLES_MESSAGE_ID: u64 = 1298636092886749294; + +// Role IDs pub const ARCHIVE_ROLE_ID: u64 = 1208457364274028574; pub const MOBILE_ROLE_ID: u64 = 1298553701094395936; pub const SYSTEMS_ROLE_ID: u64 = 1298553801191718944; @@ -24,6 +27,7 @@ pub const RESEARCH_ROLE_ID: u64 = 1298553855474270219; pub const DEVOPS_ROLE_ID: u64 = 1298553883169132554; pub const WEB_ROLE_ID: u64 = 1298553910167994428; +// Channel IDs pub const GROUP_ONE_CHANNEL_ID: u64 = 1225098248293716008; pub const GROUP_TWO_CHANNEL_ID: u64 = 1225098298935738489; pub const GROUP_THREE_CHANNEL_ID: u64 = 1225098353378070710; diff --git a/src/main.rs b/src/main.rs index 5439c40..9aa4394 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,15 +15,25 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ +/// Stores all the commands for the bot. mod commands; +/// Responsible for queries, models and mutation requests sent to and from +/// [root's](https://www.github.com/amfoss/root) graphql interace. +mod graphql; +/// Stores Discord IDs that are needed across the bot. mod ids; +/// This module is a simple cron equivalent. It spawns threads for the regular [`Task`]s that need to be completed. mod scheduler; +/// An interface to define a job that needs to be executed regularly, for example checking for status updates daily. +mod tasks; +/// Misc. helper functions that don't really have a place anywhere else. mod utils; -use crate::ids::{ - AI_ROLE_ID, ARCHIVE_MESSAGE_ID, ARCHIVE_ROLE_ID, DEVOPS_ROLE_ID, MOBILE_ROLE_ID, - RESEARCH_ROLE_ID, SYSTEMS_ROLE_ID, WEB_ROLE_ID, +use ids::{ + AI_ROLE_ID, ARCHIVE_ROLE_ID, DEVOPS_ROLE_ID, MOBILE_ROLE_ID, RESEARCH_ROLE_ID, + ROLES_MESSAGE_ID, SYSTEMS_ROLE_ID, WEB_ROLE_ID, }; + use anyhow::Context as _; use poise::{Context as PoiseContext, Framework, FrameworkOptions, PrefixFrameworkOptions}; use serenity::{ @@ -36,50 +46,90 @@ use std::collections::HashMap; pub type Error = Box; pub type Context<'a> = PoiseContext<'a, Data, Error>; +/// Runtime allocated storage for the bot. pub struct Data { pub reaction_roles: HashMap, } +/// This function is responsible for allocating the necessary fields +/// in [`Data`], before it is passed to the bot. +/// +/// Currently, it only needs to store the (emoji, [`RoleId`]) pair used +/// for assigning roles to users who react to a particular message. pub fn initialize_data() -> Data { let mut data = Data { reaction_roles: HashMap::new(), }; - let roles = [ - (ReactionType::Unicode("📁".to_string()), RoleId::new(ARCHIVE_ROLE_ID)), - (ReactionType::Unicode("📱".to_string()), RoleId::new(MOBILE_ROLE_ID)), - (ReactionType::Unicode("⚙️".to_string()), RoleId::new(SYSTEMS_ROLE_ID)), - (ReactionType::Unicode("🤖".to_string()), RoleId::new(AI_ROLE_ID)), - (ReactionType::Unicode("📜".to_string()), RoleId::new(RESEARCH_ROLE_ID)), - (ReactionType::Unicode("🚀".to_string()), RoleId::new(DEVOPS_ROLE_ID)), - (ReactionType::Unicode("🌐".to_string()), RoleId::new(WEB_ROLE_ID)), + // Define the emoji-role pairs + let roles = [ + ( + ReactionType::Unicode("📁".to_string()), + RoleId::new(ARCHIVE_ROLE_ID), + ), + ( + ReactionType::Unicode("📱".to_string()), + RoleId::new(MOBILE_ROLE_ID), + ), + ( + ReactionType::Unicode("⚙️".to_string()), + RoleId::new(SYSTEMS_ROLE_ID), + ), + ( + ReactionType::Unicode("🤖".to_string()), + RoleId::new(AI_ROLE_ID), + ), + ( + ReactionType::Unicode("📜".to_string()), + RoleId::new(RESEARCH_ROLE_ID), + ), + ( + ReactionType::Unicode("🚀".to_string()), + RoleId::new(DEVOPS_ROLE_ID), + ), + ( + ReactionType::Unicode("🌐".to_string()), + RoleId::new(WEB_ROLE_ID), + ), ]; + // Populate reaction_roles map. data.reaction_roles .extend::>(roles.into()); data } + +/// Sets up the bot using a [`poise::Framework`], which handles most of the +/// configuration including the command prefix, the event handler, the available commands, +/// managing [`Data`] and running the [`scheduler`]. #[shuttle_runtime::main] async fn main( #[shuttle_runtime::Secrets] secret_store: shuttle_runtime::SecretStore, ) -> shuttle_serenity::ShuttleSerenity { + // Uses Shuttle's environment variable storage solution SecretStore + // to access the token let discord_token = secret_store .get("DISCORD_TOKEN") .context("'DISCORD_TOKEN' was not found")?; let framework = Framework::builder() .options(FrameworkOptions { + // Load bot commands commands: commands::get_commands(), + // Pass the event handler function event_handler: |ctx, event, framework, data| { Box::pin(event_handler(ctx, event, framework, data)) }, + // General bot settings, set to default except for prefix prefix_options: PrefixFrameworkOptions { prefix: Some(String::from("$")), ..Default::default() }, ..Default::default() }) + // This function that's passed to setup() is called just as + // the bot is ready to start. .setup(|ctx, _ready, framework| { Box::pin(async move { poise::builtins::register_globally(ctx, &framework.options().commands).await?; @@ -102,7 +152,13 @@ async fn main( Ok(client.into()) } - +/// Handles various events from Discord, such as reactions. +/// +/// Current functionality includes: +/// - Adding roles to users based on reactions. +/// - Removing roles from users when their reactions are removed. +/// +/// TODO: Refactor for better readability and modularity. async fn event_handler( ctx: &SerenityContext, event: &FullEvent, @@ -110,13 +166,14 @@ async fn event_handler( data: &Data, ) -> Result<(), Error> { match event { + // Handle reactions being added. FullEvent::ReactionAdd { add_reaction } => { - let message_id = MessageId::new(ARCHIVE_MESSAGE_ID); - if add_reaction.message_id == message_id - && data.reaction_roles.contains_key(&add_reaction.emoji) - { + // Check if a role needs to be added i.e check if the reaction was added to [`ROLES_MESSAGE_ID`] + if is_relevant_reaction(add_reaction.message_id, &add_reaction.emoji, data) { + // This check for a guild_id isn't strictly necessary, since we're already checking + // if the reaction was added to the [`ROLES_MESSAGE_ID`] which *should* point to a + // message in the server. if let Some(guild_id) = add_reaction.guild_id { - // TODO: Use try_join to await concurrently? if let Ok(member) = guild_id.member(ctx, add_reaction.user_id.unwrap()).await { if let Err(e) = member .add_role( @@ -127,18 +184,21 @@ async fn event_handler( ) .await { - eprintln!("Error: {:?}", e); + // TODO: Replace with tracing + eprintln!("Error adding role: {:?}", e); } } } } } + // Handle reactions being removed. FullEvent::ReactionRemove { removed_reaction } => { - let message_id = MessageId::new(ARCHIVE_MESSAGE_ID); - if message_id == removed_reaction.message_id - && data.reaction_roles.contains_key(&removed_reaction.emoji) - { + // Check if a role needs to be added i.e check if the reaction was added to [`ROLES_MESSAGE_ID`] + if is_relevant_reaction(removed_reaction.message_id, &removed_reaction.emoji, data) { + // This check for a guild_id isn't strictly necessary, since we're already checking + // if the reaction was added to the [`ROLES_MESSAGE_ID`] which *should* point to a + // message in the server. if let Some(guild_id) = removed_reaction.guild_id { if let Ok(member) = guild_id .member(ctx, removed_reaction.user_id.unwrap()) @@ -150,18 +210,26 @@ async fn event_handler( *data .reaction_roles .get(&removed_reaction.emoji) - .expect("Hard coded value verified earlier"), + .expect("Hard coded value verified earlier."), ) .await { - eprintln!("Error: {:?}", e); + eprintln!("Error removing role: {:?}", e); } } } } } + + // Ignore all other events for now. _ => {} } Ok(()) } + +/// Helper function to check if a reaction was made to [`ROLES_MESSAGE_ID`] and if +/// [`Data::reaction_roles`] contains a relevant (emoji, role) pair. +fn is_relevant_reaction(message_id: MessageId, emoji: &ReactionType, data: &Data) -> bool { + message_id == MessageId::new(ROLES_MESSAGE_ID) && data.reaction_roles.contains_key(emoji) +} diff --git a/src/scheduler/scheduler.rs b/src/scheduler/scheduler.rs deleted file mode 100644 index b1f7701..0000000 --- a/src/scheduler/scheduler.rs +++ /dev/null @@ -1,39 +0,0 @@ -/* -amFOSS Daemon: A discord bot for the amFOSS Discord server. -Copyright (C) 2024 amFOSS - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ -use crate::scheduler::tasks::{get_tasks, Task}; -use serenity::client::Context as SerenityContext; - -use tokio::spawn; -use tokio::time::Duration; - - -pub async fn run_scheduler(ctx: SerenityContext) { - let tasks = get_tasks(); - - for task in tasks { - spawn(schedule_task(ctx.clone(), task)); - } -} - -async fn schedule_task(ctx: SerenityContext, task: Box) { - loop { - let next_run_in = task.run_in(); - tokio::time::sleep(next_run_in).await; - task.run(ctx.clone()).await; - } -} diff --git a/src/scheduler/tasks/tasks.rs b/src/tasks/mod.rs similarity index 66% rename from src/scheduler/tasks/tasks.rs rename to src/tasks/mod.rs index 4956c15..91a60f9 100644 --- a/src/scheduler/tasks/tasks.rs +++ b/src/tasks/mod.rs @@ -15,14 +15,18 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ -use crate::utils::time::time_until; +mod status_update; + +use crate::{tasks::status_update::check_status_updates, utils::time::time_until}; use async_trait::async_trait; use serenity::client::Context; use tokio::time::Duration; -use super::status_update; - +/// A [`Task`] is any job that needs to be executed on a regular basis. +/// A task has a function [`Task::run_in`] that returns the time till the +/// next ['Task::run`] is run. It also has a [`Task::name`] that can be used +/// in the future to display to the end user. #[async_trait] pub trait Task: Send + Sync { fn name(&self) -> &'static str; @@ -30,6 +34,13 @@ pub trait Task: Send + Sync { async fn run(&self, ctx: Context); } +/// Analogous to [`crate::commands::get_commands`], every task that is defined +/// must be included in the returned vector in order for it to be scheduled. +pub fn get_tasks() -> Vec> { + vec![Box::new(StatusUpdateCheck)] +} + +/// Checks for status updates daily at 9 AM. pub struct StatusUpdateCheck; #[async_trait] @@ -39,14 +50,10 @@ impl Task for StatusUpdateCheck { } fn run_in(&self) -> Duration { - time_until(5, 0) + time_until(9, 0) } async fn run(&self, ctx: Context) { - status_update::check_status_updates(ctx).await; + check_status_updates(ctx).await; } } - -pub fn get_tasks() -> Vec> { - vec![Box::new(StatusUpdateCheck)] -} diff --git a/src/scheduler/tasks/status_update.rs b/src/tasks/status_update.rs similarity index 72% rename from src/scheduler/tasks/status_update.rs rename to src/tasks/status_update.rs index 032e58c..2656499 100644 --- a/src/scheduler/tasks/status_update.rs +++ b/src/tasks/status_update.rs @@ -15,20 +15,16 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ - use serenity::all::{ChannelId, Context, Message}; + use crate::{ + graphql::queries::fetch_members, ids::{ - GROUP_FOUR_CHANNEL_ID, GROUP_THREE_CHANNEL_ID, GROUP_TWO_CHANNEL_ID, - GROUP_ONE_CHANNEL_ID, + GROUP_FOUR_CHANNEL_ID, GROUP_ONE_CHANNEL_ID, GROUP_THREE_CHANNEL_ID, GROUP_TWO_CHANNEL_ID, STATUS_UPDATE_CHANNEL_ID, }, - utils::{ - graphql::{fetch_members, send_streak_update}, - time::get_five_am_timestamp, - }, + utils::time::get_five_am_timestamp, }; -use chrono::Duration; pub async fn check_status_updates(ctx: Context) { let members = fetch_members().await.expect("Root must be up."); @@ -42,7 +38,7 @@ pub async fn check_status_updates(ctx: Context) { let time = chrono::Local::now().with_timezone(&chrono_tz::Asia::Kolkata); let today_five_am = get_five_am_timestamp(time); - let yesterday_five_am = today_five_am - Duration::hours(24); + let yesterday_five_am = today_five_am - chrono::Duration::hours(24); let mut valid_updates: Vec = vec![]; @@ -69,24 +65,16 @@ pub async fn check_status_updates(ctx: Context) { let mut naughty_list: Vec = vec![]; - for (member,id) in &members { + for member in &members { let name_parts: Vec<&str> = member.split_whitespace().collect(); let first_name = name_parts.get(0).unwrap_or(&""); let last_name = name_parts.get(1).unwrap_or(&""); - - let has_sent_update = valid_updates.iter().any(|msg| { - let msg_author = &msg.author.name.to_lowercase(); - msg_author.contains(first_name.to_lowercase().as_str()) - || msg_author.contains(last_name.to_lowercase().as_str()) - }); + let has_sent_update = valid_updates + .iter() + .any(|msg| msg.content.contains(first_name) || msg.content.contains(last_name)); if !has_sent_update { - naughty_list.push(member.clone()); - } - - match send_streak_update("https://root.shuttleapp.rs/", *id, has_sent_update).await { - Ok(_) => println!("Successfully updated streak for {}", member), - Err(e) => println!("Failed to update streak for {}: {:?}", member, e), + naughty_list.push(member.to_string()); } } @@ -95,8 +83,7 @@ pub async fn check_status_updates(ctx: Context) { if naughty_list.is_empty() { status_update_channel .say(ctx.http, "Everyone sent their update today!") - .await - .expect("Failed to send message"); + .await; } else { let formatted_list = naughty_list .iter() @@ -108,11 +95,10 @@ pub async fn check_status_updates(ctx: Context) { .say( ctx.http, format!( - "These members did not send their updates in the last 3 minutes:\n{}", + "These members did not send their updates:\n{}", formatted_list ), ) - .await - .expect("Failed to send message"); + .await; } -} \ No newline at end of file +} diff --git a/src/utils/graphql.rs b/src/utils/graphql.rs deleted file mode 100644 index 4bef9e8..0000000 --- a/src/utils/graphql.rs +++ /dev/null @@ -1,100 +0,0 @@ -/* -amFOSS Daemon: A discord bot for the amFOSS Discord server. -Copyright (C) 2024 amFOSS - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ -use serde_json::Value; -use anyhow::{Result}; - -const REQUEST_URL: &str = "https://root.shuttleapp.rs/"; - -pub async fn fetch_members() -> Result, reqwest::Error> { - let client = reqwest::Client::new(); - let query = r#" - query { - getMember { - name - id - } - }"#; - - let response = client - .post(REQUEST_URL) - .json(&serde_json::json!({"query": query})) - .send() - .await?; - - let json: Value = response.json().await?; - - let member_names: Vec<(String, i32)> = json["data"]["getMember"] - .as_array() - .unwrap_or(&vec![]) - .iter() - .map(|member| { - let id = member["id"].as_i64().unwrap_or(0) as i32; - let name = member["name"].as_str().unwrap_or("").to_string(); - (name, id) - }) - .collect(); - - Ok(member_names) -} - -pub async fn send_streak_update( - root_api_url: &str, - id: i32, - has_sent_update: bool, -) -> Result<(), Box> { - let client = reqwest::Client::new(); - let query = r#" - mutation updateStreak($id: Int!, $hasSentUpdate: Boolean!) { - updateStreak(id: $id, hasSentUpdate: $hasSentUpdate) { - id - streak - maxStreak - } - } - "#; - - let variables = serde_json::json!({ - "id": id, - "hasSentUpdate": has_sent_update - }); - - let body = serde_json::json!({ - "query": query, - "variables": variables - }); - - let response = client - .post(root_api_url) - .header("Content-Type", "application/json") - .json(&body) - .send() - .await?; - - let response_status = response.status(); - let response_body = response.text().await?; - - if response_status.is_success() { - println!("Successfully updated streak for ID {}: {}", id, response_body); - Ok(()) - } else { - Err(anyhow::anyhow!( - "Failed to update streak for ID {}. HTTP status: {}, response: {}", - id, response_status, response_body - ).into()) - } -} \ No newline at end of file diff --git a/src/utils/mod.rs b/src/utils/mod.rs index c0e3b11..b2c2aba 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -15,5 +15,4 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ -pub mod graphql; pub mod time; From 265868bd6a155156f82b8c12e923e6cd01b82fe4 Mon Sep 17 00:00:00 2001 From: hkx05 Date: Tue, 26 Nov 2024 21:43:09 +0530 Subject: [PATCH 6/9] amd support for streaks --- src/graphql/queries.rs | 41 +++++++++++++++++++++++++++++++++++++- src/tasks/status_update.rs | 32 ++++++++++++++++++++--------- 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/src/graphql/queries.rs b/src/graphql/queries.rs index 1e26aed..6eb0b02 100644 --- a/src/graphql/queries.rs +++ b/src/graphql/queries.rs @@ -16,6 +16,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ use serde_json::Value; +use anyhow::{Result}; use super::models::Member; @@ -42,10 +43,48 @@ pub async fn fetch_members() -> Result, reqwest::Error> { let member_names: Vec = json["data"]["getMember"] .as_array() - .unwrap() + .unwrap_or(&vec![]) .iter() .map(Member) .collect(); Ok(member_names) } + +pub async fn send_streak_update( + root_api_url: &str, + id: i32, + has_sent_update: bool, +) -> Result<(), Box> { + let client = reqwest::Client::new(); + let query = format!( + r#" + mutation {{ + updateStreak(id: {}, hasSentUpdate: {}) {{ + id + streak + max_streak + }} + }} + "#, + id, has_sent_update + ); + let response = client + .post(root_api_url) + .header("Content-Type", "application/json") + .body(query) + .send() + .await?; + + if response.status().is_success() { + println!("Successfully updated streak for ID {}", id); + Ok(()) + } else { + Err(anyhow::anyhow!( + "Failed to update streak for ID {}. HTTP status: {}", + id, + response.status() + ) + .into()) + } +} \ No newline at end of file diff --git a/src/tasks/status_update.rs b/src/tasks/status_update.rs index 2656499..d48d8d9 100644 --- a/src/tasks/status_update.rs +++ b/src/tasks/status_update.rs @@ -15,16 +15,20 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ -use serenity::all::{ChannelId, Context, Message}; +use serenity::all::{ChannelId, Context, Message}; use crate::{ graphql::queries::fetch_members, ids::{ GROUP_FOUR_CHANNEL_ID, GROUP_ONE_CHANNEL_ID, GROUP_THREE_CHANNEL_ID, GROUP_TWO_CHANNEL_ID, STATUS_UPDATE_CHANNEL_ID, }, - utils::time::get_five_am_timestamp, + utils::{ + graphql::{fetch_members, send_streak_update}, + time::get_five_am_timestamp, + }, }; +use chrono::Duration; pub async fn check_status_updates(ctx: Context) { let members = fetch_members().await.expect("Root must be up."); @@ -38,7 +42,7 @@ pub async fn check_status_updates(ctx: Context) { let time = chrono::Local::now().with_timezone(&chrono_tz::Asia::Kolkata); let today_five_am = get_five_am_timestamp(time); - let yesterday_five_am = today_five_am - chrono::Duration::hours(24); + let yesterday_five_am = today_five_am - Duration::hours(24); let mut valid_updates: Vec = vec![]; @@ -69,12 +73,20 @@ pub async fn check_status_updates(ctx: Context) { let name_parts: Vec<&str> = member.split_whitespace().collect(); let first_name = name_parts.get(0).unwrap_or(&""); let last_name = name_parts.get(1).unwrap_or(&""); - let has_sent_update = valid_updates - .iter() - .any(|msg| msg.content.contains(first_name) || msg.content.contains(last_name)); + + let has_sent_update = valid_updates.iter().any(|msg| { + let msg_author = &msg.author.name.to_lowercase(); + msg_author.contains(first_name.to_lowercase().as_str()) + || msg_author.contains(last_name.to_lowercase().as_str()) + }); if !has_sent_update { - naughty_list.push(member.to_string()); + naughty_list.push(member.clone()); + } + + match send_streak_update("https://root.shuttleapp.rs/", member.parse().unwrap_or(0), has_sent_update).await { + Ok(_) => println!("Successfully updated streak for {}", member), + Err(e) => println!("Failed to update streak for {}: {:?}", member, e), } } @@ -83,7 +95,8 @@ pub async fn check_status_updates(ctx: Context) { if naughty_list.is_empty() { status_update_channel .say(ctx.http, "Everyone sent their update today!") - .await; + .await + .expect("Failed to send message"); } else { let formatted_list = naughty_list .iter() @@ -99,6 +112,7 @@ pub async fn check_status_updates(ctx: Context) { formatted_list ), ) - .await; + .await + .expect("Failed to send message"); } } From 501db01f4a40b03199e5a72d1a9400b7e106c498 Mon Sep 17 00:00:00 2001 From: hkx05 Date: Fri, 20 Dec 2024 22:05:24 +0530 Subject: [PATCH 7/9] fix merge conflicts --- src/scheduler.rs | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/scheduler.rs diff --git a/src/scheduler.rs b/src/scheduler.rs new file mode 100644 index 0000000..a206615 --- /dev/null +++ b/src/scheduler.rs @@ -0,0 +1,45 @@ +/* +amFOSS Daemon: A discord bot for the amFOSS Discord server. +Copyright (C) 2024 amFOSS + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ +use crate::tasks::{get_tasks, Task}; +use serenity::client::Context as SerenityContext; + +use tokio::spawn; +use tokio::time::Duration; + + +/// Spawns a thread for each [`Task`]. +/// +/// [`SerenityContext`] is passed along with it so that they can +/// call any required Serenity functions without creating a new [`serenity::http`] +/// interface with a Discord token. +pub async fn run_scheduler(ctx: SerenityContext) { + let tasks = get_tasks(); + + for task in tasks { + spawn(schedule_task(ctx.clone(), task)); + } +} + +/// Runs the function [`Task::run`] and goes back to sleep until it's time to run again. +async fn schedule_task(ctx: SerenityContext, task: Box) { + loop { + let next_run_in = task.run_in(); + tokio::time::sleep(next_run_in).await; + task.run(ctx.clone()).await; + } +} From 21303922cf69bf29ecee31e69b1e2478f7d6126c Mon Sep 17 00:00:00 2001 From: hkx05 Date: Sat, 30 Nov 2024 03:47:53 +0530 Subject: [PATCH 8/9] required support for root for implementing streaks --- src/graphql/queries.rs | 63 +++++++++++++++++++++++--------------- src/tasks/status_update.rs | 15 ++++----- 2 files changed, 47 insertions(+), 31 deletions(-) diff --git a/src/graphql/queries.rs b/src/graphql/queries.rs index 6eb0b02..8bae2ee 100644 --- a/src/graphql/queries.rs +++ b/src/graphql/queries.rs @@ -22,7 +22,7 @@ use super::models::Member; const REQUEST_URL: &str = "https://root.shuttleapp.rs/"; -pub async fn fetch_members() -> Result, reqwest::Error> { +pub async fn fetch_members() -> Result, reqwest::Error> { let client = reqwest::Client::new(); let query = r#" query { @@ -30,6 +30,8 @@ pub async fn fetch_members() -> Result, reqwest::Error> { name, groupId, discordId + name + id } }"#; @@ -41,50 +43,63 @@ pub async fn fetch_members() -> Result, reqwest::Error> { let json: Value = response.json().await?; - let member_names: Vec = json["data"]["getMember"] + let member_names: Vec<(String, i32)> = json["data"]["getMember"] .as_array() .unwrap_or(&vec![]) .iter() - .map(Member) + .map(|member| { + let id = member["id"].as_i64().unwrap_or(0) as i32; + let name = member["name"].as_str().unwrap_or("").to_string(); + (name, id) + }) .collect(); Ok(member_names) } pub async fn send_streak_update( - root_api_url: &str, - id: i32, + root_api_url: &str, + id: i32, has_sent_update: bool, -) -> Result<(), Box> { +) -> Result<(), Box> { let client = reqwest::Client::new(); - let query = format!( - r#" - mutation {{ - updateStreak(id: {}, hasSentUpdate: {}) {{ + let query = r#" + mutation updateStreak($id: Int!, $hasSentUpdate: Boolean!) { + updateStreak(id: $id, hasSentUpdate: $hasSentUpdate) { id streak - max_streak - }} - }} - "#, - id, has_sent_update - ); + maxStreak + } + } + "#; + + let variables = serde_json::json!({ + "id": id, + "hasSentUpdate": has_sent_update + }); + + let body = serde_json::json!({ + "query": query, + "variables": variables + }); + let response = client .post(root_api_url) .header("Content-Type", "application/json") - .body(query) + .json(&body) .send() .await?; - if response.status().is_success() { - println!("Successfully updated streak for ID {}", id); + let response_status = response.status(); + let response_body = response.text().await?; + + if response_status.is_success() { + println!("Successfully updated streak for ID {}: {}", id, response_body); Ok(()) } else { Err(anyhow::anyhow!( - "Failed to update streak for ID {}. HTTP status: {}", - id, - response.status() - ) - .into()) + "Failed to update streak for ID {}. HTTP status: {}, response: {}", + id, response_status, response_body + ).into()) } } \ No newline at end of file diff --git a/src/tasks/status_update.rs b/src/tasks/status_update.rs index d48d8d9..484aa76 100644 --- a/src/tasks/status_update.rs +++ b/src/tasks/status_update.rs @@ -20,7 +20,8 @@ use serenity::all::{ChannelId, Context, Message}; use crate::{ graphql::queries::fetch_members, ids::{ - GROUP_FOUR_CHANNEL_ID, GROUP_ONE_CHANNEL_ID, GROUP_THREE_CHANNEL_ID, GROUP_TWO_CHANNEL_ID, + GROUP_FOUR_CHANNEL_ID, GROUP_THREE_CHANNEL_ID, GROUP_TWO_CHANNEL_ID, + GROUP_ONE_CHANNEL_ID, STATUS_UPDATE_CHANNEL_ID, }, utils::{ @@ -69,7 +70,7 @@ pub async fn check_status_updates(ctx: Context) { let mut naughty_list: Vec = vec![]; - for member in &members { + for (member,id) in &members { let name_parts: Vec<&str> = member.split_whitespace().collect(); let first_name = name_parts.get(0).unwrap_or(&""); let last_name = name_parts.get(1).unwrap_or(&""); @@ -83,8 +84,8 @@ pub async fn check_status_updates(ctx: Context) { if !has_sent_update { naughty_list.push(member.clone()); } - - match send_streak_update("https://root.shuttleapp.rs/", member.parse().unwrap_or(0), has_sent_update).await { + + match send_streak_update("https://root.shuttleapp.rs/", *id, has_sent_update).await { Ok(_) => println!("Successfully updated streak for {}", member), Err(e) => println!("Failed to update streak for {}: {:?}", member, e), } @@ -94,7 +95,7 @@ pub async fn check_status_updates(ctx: Context) { if naughty_list.is_empty() { status_update_channel - .say(ctx.http, "Everyone sent their update today!") + .say(ctx.http, "Everyone sent their update in the last 1 minutes!") .await .expect("Failed to send message"); } else { @@ -108,11 +109,11 @@ pub async fn check_status_updates(ctx: Context) { .say( ctx.http, format!( - "These members did not send their updates:\n{}", + "These members did not send their updates in the last 3 minutes:\n{}", formatted_list ), ) .await .expect("Failed to send message"); } -} +} \ No newline at end of file From f0c1508da95405ebda31c76c983b58bd6f227ff6 Mon Sep 17 00:00:00 2001 From: hkx05 Date: Thu, 5 Dec 2024 21:13:58 +0530 Subject: [PATCH 9/9] updated print message to 1 day instead of 1 minute --- src/tasks/status_update.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasks/status_update.rs b/src/tasks/status_update.rs index 484aa76..181e959 100644 --- a/src/tasks/status_update.rs +++ b/src/tasks/status_update.rs @@ -95,7 +95,7 @@ pub async fn check_status_updates(ctx: Context) { if naughty_list.is_empty() { status_update_channel - .say(ctx.http, "Everyone sent their update in the last 1 minutes!") + .say(ctx.http, "Everyone sent their update today!") .await .expect("Failed to send message"); } else {