diff --git a/Cargo.toml b/Cargo.toml
index 8fadf71..1f30f7d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,14 +5,18 @@ edition = "2021"
[dependencies]
anyhow = "1.0.66"
+async-trait = "0.1.83"
chrono = "0.4.38"
-chrono-tz = "0.9.0"
-poise = "0.6.1"
-reqwest = { version = "0.12.5", features = ["blocking", "json"] }
+chrono-tz = "0.10.0"
+poise = { git = "https://github.com/serenity-rs/poise", branch = "current" }
+reqwest = { version = "0.12.5", features = ["json"] }
serde = { version = "1.0.203", features = ["derive"] }
serde_json = "1.0.117"
-serenity = { version = "0.12.0", default-features = false, features = ["client", "gateway", "rustls_backend", "model"] }
-shuttle-runtime = "0.47.0"
-shuttle-serenity = "0.47.0"
+serenity = { git = "https://github.com/serenity-rs/serenity", branch = "current" }
+shuttle-runtime = "0.48.0"
+shuttle-serenity = "0.48.0"
tokio = "1.26.0"
tracing = "0.1.37"
+
+[patch.crates-io]
+serenity = { git = "https://github.com/serenity-rs/serenity", branch = "current" }
diff --git a/src/graphql.rs b/src/graphql.rs
new file mode 100644
index 0000000..4d4bcf7
--- /dev/null
+++ b/src/graphql.rs
@@ -0,0 +1,47 @@
+/*
+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;
+
+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
+ }
+ }"#;
+
+ 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| member["name"].as_str().unwrap().to_string())
+ .collect();
+
+ Ok(member_names)
+}
diff --git a/src/main.rs b/src/main.rs
index 28af8ed..5b40c5e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -16,6 +16,10 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
mod commands;
+mod graphql;
+mod scheduler;
+mod tasks;
+mod utils;
use anyhow::Context as _;
use std::collections::HashMap;
@@ -77,6 +81,8 @@ async fn main(
(ReactionType::Unicode("📁".to_string()), role_id),
);
+ crate::scheduler::run_scheduler(ctx.clone()).await;
+
Ok(data)
})
})
@@ -126,7 +132,8 @@ async fn event_handler(
if &removed_reaction.emoji == expected_reaction {
if let Some(guild_id) = removed_reaction.guild_id {
if let Ok(member) = guild_id
- .member(ctx, removed_reaction.user_id.unwrap()).await
+ .member(ctx, removed_reaction.user_id.unwrap())
+ .await
{
if let Err(e) = member.remove_role(&ctx.http, *role_id).await {
eprintln!("Error: {:?}", e);
diff --git a/src/scheduler.rs b/src/scheduler.rs
new file mode 100644
index 0000000..7f701d4
--- /dev/null
+++ b/src/scheduler.rs
@@ -0,0 +1,38 @@
+/*
+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;
+
+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.interval();
+ tokio::time::sleep(next_run_in).await;
+
+ task.run(ctx.clone()).await;
+ }
+}
diff --git a/src/tasks.rs b/src/tasks.rs
new file mode 100644
index 0000000..5a3811e
--- /dev/null
+++ b/src/tasks.rs
@@ -0,0 +1,134 @@
+/*
+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::{
+ graphql::fetch_members,
+ utils::{get_five_am_timestamp, time_until},
+};
+use async_trait::async_trait;
+use serenity::{
+ all::{ChannelId, Message},
+ client::Context,
+};
+
+use tokio::time::Duration;
+
+const GROUP_ONE_CHANNEL_ID: u64 = 1225098248293716008;
+const GROUP_TWO_CHANNEL_ID: u64 = 1225098298935738489;
+const GROUP_THREE_CHANNEL_ID: u64 = 1225098353378070710;
+const GROUP_FOUR_CHANNEL_ID: u64 = 1225098407216156712;
+const STATUS_UPDATE_CHANNEL_ID: u64 = 764575524127244318;
+
+#[async_trait]
+pub trait Task: Send + Sync {
+ fn name(&self) -> &'static str;
+ fn interval(&self) -> Duration;
+ async fn run(&self, ctx: Context);
+}
+
+pub struct StatusUpdateCheck;
+
+#[async_trait]
+impl Task for StatusUpdateCheck {
+ fn name(&self) -> &'static str {
+ "StatusUpdateCheck"
+ }
+
+ fn interval(&self) -> Duration {
+ time_until(5, 0)
+ }
+
+ async fn run(&self, ctx: Context) {
+ let members = fetch_members().await.expect("Root must be up.");
+
+ let channel_ids: Vec = vec![
+ ChannelId::new(GROUP_ONE_CHANNEL_ID),
+ ChannelId::new(GROUP_TWO_CHANNEL_ID),
+ ChannelId::new(GROUP_THREE_CHANNEL_ID),
+ ChannelId::new(GROUP_FOUR_CHANNEL_ID),
+ ];
+
+ 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 mut valid_updates: Vec = vec![];
+
+ for &channel_id in &channel_ids {
+ let builder = serenity::builder::GetMessages::new().limit(50);
+ match channel_id.messages(&ctx.http, builder).await {
+ Ok(messages) => {
+ let filtered_messages: Vec = messages
+ .into_iter()
+ .filter(|msg| {
+ msg.timestamp >= yesterday_five_am.into()
+ && msg.timestamp < today_five_am.into()
+ && msg.content.to_lowercase().contains("namah shivaya")
+ && msg.content.to_lowercase().contains("regards")
+ })
+ .collect();
+
+ valid_updates.extend(filtered_messages);
+ }
+ Err(e) => println!("ERROR: {:?}", e),
+ }
+ }
+
+ let mut naughty_list: Vec = vec![];
+
+ 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| msg.content.contains(first_name) || msg.content.contains(last_name));
+
+ if !has_sent_update {
+ naughty_list.push(member.to_string());
+ }
+ }
+
+ let status_update_channel = ChannelId::new(STATUS_UPDATE_CHANNEL_ID);
+
+ if naughty_list.is_empty() {
+ status_update_channel
+ .say(ctx.http, "Everyone sent their update today!")
+ .await;
+ } else {
+ let formatted_list = naughty_list
+ .iter()
+ .enumerate()
+ .map(|(i, member)| format!("{}. {:?}", i + 1, member))
+ .collect::>()
+ .join("\n");
+ status_update_channel
+ .say(
+ ctx.http,
+ format!(
+ "These members did not send their updates:\n{}",
+ formatted_list
+ ),
+ )
+ .await;
+ }
+ }
+}
+
+pub fn get_tasks() -> Vec> {
+ vec![Box::new(StatusUpdateCheck)]
+}
diff --git a/src/utils.rs b/src/utils.rs
new file mode 100644
index 0000000..7409f79
--- /dev/null
+++ b/src/utils.rs
@@ -0,0 +1,41 @@
+/*
+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 chrono::{DateTime, Datelike, Local, TimeZone};
+use chrono_tz::Tz;
+use tokio::time::Duration;
+
+pub fn time_until(hour: u32, minute: u32) -> Duration {
+ let now = chrono::Local::now().with_timezone(&chrono_tz::Asia::Kolkata);
+ let today_run = now.date().and_hms(hour, minute, 0);
+
+ let next_run = if now < today_run {
+ today_run
+ } else {
+ today_run + chrono::Duration::days(1)
+ };
+
+ let time_until = (next_run - now).to_std().unwrap();
+ Duration::from_secs(time_until.as_secs())
+}
+
+pub fn get_five_am_timestamp(now: DateTime) -> DateTime {
+ chrono::Local
+ .ymd(now.year(), now.month(), now.day())
+ .and_hms_opt(5, 0, 0)
+ .expect("Chrono must work.")
+}