diff --git a/Cargo.lock b/Cargo.lock index e24c588..ae60465 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -283,14 +283,17 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.23" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" dependencies = [ "iana-time-zone", + "js-sys", "num-integer", "num-traits 0.2.15", "serde", + "time 0.1.45", + "wasm-bindgen", "winapi", ] @@ -301,7 +304,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29c39203181991a7dd4343b8005bd804e7a9a37afb8ac070e43771e8c820bbde" dependencies = [ "chrono", - "chrono-tz-build", + "chrono-tz-build 0.0.3", + "phf", +] + +[[package]] +name = "chrono-tz" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9cc2b23599e6d7479755f3594285efb3f74a1bdca7a7374948bc831e23a552" +dependencies = [ + "chrono", + "chrono-tz-build 0.1.0", "phf", ] @@ -316,6 +330,17 @@ dependencies = [ "phf_codegen", ] +[[package]] +name = "chrono-tz-build" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9998fb9f7e9b2111641485bf8beb32f92945f97f92a3d061f744cfef335f751" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + [[package]] name = "cipher" version = "0.3.0" @@ -659,6 +684,8 @@ version = "0.1.0" dependencies = [ "axum", "base64 0.21.0", + "chrono", + "chrono-tz 0.8.2", "config", "j_db", "json", @@ -817,7 +844,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -1297,7 +1324,7 @@ checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys", ] @@ -2155,7 +2182,7 @@ dependencies = [ "serde-value", "serde_json", "static_assertions", - "time", + "time 0.3.17", "tokio", "tracing", "typemap_rev", @@ -2451,7 +2478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3df578c295f9ec044ff1c829daf31bb7581d5b3c2a7a3d87419afe1f2531438c" dependencies = [ "chrono", - "chrono-tz", + "chrono-tz 0.6.3", "globwalk", "humansize", "lazy_static", @@ -2513,6 +2540,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + [[package]] name = "time" version = "0.3.17" @@ -3023,6 +3061,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 7f15eb2..6fc54b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,8 @@ axum = "0.6.3" sha3 = "0.10.6" base64 = "0.21.0" j_db = { version = "0.1.0", registry = "jojo-dev" } +chrono = { version = "0.4.24", features = ["serde"] } +chrono-tz = "0.8.2" [dependencies.serenity] version = "0.11.5" diff --git a/src/config.rs b/src/config.rs index 028aba1..aea82b4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,6 +6,7 @@ use config::{Config, File}; use j_db::database::Database; use rand::prelude::SliceRandom; use serde::{Deserialize, Serialize}; +use serenity::model::id::ChannelId; use serenity::model::prelude::{GuildId, UserId}; use serenity::prelude::TypeMapKey; use std::collections::HashMap; @@ -33,6 +34,7 @@ pub struct BotConfig { pub admins: Vec, pub guild_id: GuildId, pub api_addr: SocketAddr, + pub announcement_channel: ChannelId, } impl BotConfig { diff --git a/src/discord/birthday.rs b/src/discord/birthday.rs new file mode 100644 index 0000000..c30aeab --- /dev/null +++ b/src/discord/birthday.rs @@ -0,0 +1,96 @@ +use crate::models::birthday::BirthdayEntry; +use crate::{command, group, GlobalData}; +use rand::{thread_rng, Rng}; +use serenity::client::Context; +use serenity::framework::standard::{Args, CommandResult}; +use serenity::model::channel::Message; +use serenity::model::id::UserId; +use serenity::utils::MessageBuilder; + +#[group] +#[commands(add_birthday, list_birthdays)] +pub struct Birthday; + +#[command] +#[description("Add your birthday to the bot. Please use American dates or i will cut u.")] +#[example("add_birthday 04/21/1997")] +pub async fn add_birthday(ctx: &Context, msg: &Message, args: Args) -> CommandResult { + if args.is_empty() { + msg.reply( + &ctx.http, + "Look just give me a day, doesn't matter. I'm not the IRS", + ) + .await?; + return Ok(()); + } + + let date_split: Vec<&str> = args.rest().split('/').collect(); + + if date_split.len() < 3 { + msg.reply(&ctx.http, "Try again with a real date").await?; + return Ok(()); + } + + let month: u32 = date_split[0].parse().unwrap_or(0); + let day: u32 = date_split[1].parse().unwrap_or(0); + let year: i32 = date_split[2].parse().unwrap_or(0); + + if month == 0 || day == 0 || year == 0 { + msg.reply(&ctx.http, "Try again with a real date").await?; + return Ok(()); + } + + if let Some(date) = chrono::NaiveDate::from_ymd_opt(year, month, day) { + let data = ctx.data.read().await; + let global_data = data.get::().unwrap(); + + BirthdayEntry::add_birthday(&global_data.db, msg.author.id.0, date)?; + + msg.reply( + &ctx.http, + format!( + "Thank you subject #{}, I am now {}% closer to making a full AI replica of you.", + msg.author.id.0, + thread_rng().gen_range(0.0..100.0) + ), + ) + .await?; + } else { + msg.reply(&ctx.http, "Try again with a real date").await?; + return Ok(()); + } + + Ok(()) +} + +#[command] +#[description("Add your birthday to the bot. Please use American dates or i will cut u.")] +pub async fn list_birthdays(ctx: &Context, msg: &Message) -> CommandResult { + let data = ctx.data.read().await; + let global_data = data.get::().unwrap(); + + let birthdays: Vec = global_data + .db + .filter(|_, _: &BirthdayEntry| true)? + .collect(); + + let mut msg_builder = MessageBuilder::new(); + + msg_builder.push_bold_line("All the birthdays I know:"); + for birthday in birthdays { + let user = msg + .guild(&ctx.cache) + .unwrap() + .member(&ctx.http, UserId::from(birthday.discord_id)) + .await?; + msg_builder.push_line(format!( + "* {} {}", + user.display_name(), + birthday.birthday.format("%m-%d-%Y") + )); + } + + msg.reply(&ctx.http, msg_builder.build()).await?; + + Ok(()) +} diff --git a/src/discord/mod.rs b/src/discord/mod.rs index 319849c..1b93dd0 100644 --- a/src/discord/mod.rs +++ b/src/discord/mod.rs @@ -1,5 +1,6 @@ pub mod admin; pub mod album; +pub mod birthday; pub mod celeryman; pub mod color; pub mod emoji_race; @@ -14,7 +15,10 @@ use crate::api::web_server; use crate::discord::fren_coin::give_coin; use crate::discord::joke::random; use crate::discord::shop::restock_shop; +use crate::models::birthday::BirthdayEntry; +use crate::models::insult_compliment::{RandomResponseTemplate, ResponseType}; use crate::{help, hook, GlobalData}; +use chrono::{Days, TimeZone, Timelike, Utc}; use rand::prelude::IteratorRandom; use rand::thread_rng; use serenity::async_trait; @@ -26,7 +30,7 @@ use serenity::model::channel::{Message, ReactionType}; use serenity::model::gateway::Activity; use serenity::model::id::UserId; use serenity::model::prelude::{GuildId, OnlineStatus, Ready}; -use serenity::prelude::EventHandler; +use serenity::prelude::{EventHandler, Mentionable}; use std::collections::HashSet; use std::time::Duration; @@ -39,12 +43,63 @@ static ERROR_MSG: &str = impl EventHandler for Handler { async fn cache_ready(&self, ctx: Context, _guilds: Vec) { tokio::spawn(async move { + let mut next_check = chrono_tz::America::Chicago + .from_utc_datetime(&Utc::now().naive_utc()) + .checked_sub_days(Days::new(1)) + .unwrap(); loop { { println!("Restocking shop..."); restock_shop(&ctx).await.unwrap(); } + { + let now = + chrono_tz::America::Chicago.from_utc_datetime(&Utc::now().naive_utc()); + if now >= next_check { + let data = ctx.data.read().await; + let global_data = data.get::().unwrap(); + let todays_birthdays = + BirthdayEntry::todays_birthdays(&global_data.db, now.date_naive()) + .unwrap(); + + for birth in todays_birthdays { + if let Ok(user) = global_data + .cfg + .guild_id + .member(&ctx.http, birth.discord_id) + .await + { + global_data + .cfg + .announcement_channel + .say(&ctx.http, format!("Happy birthday {}!", user.mention())) + .await + .unwrap(); + + let compliment = RandomResponseTemplate::get_random_response( + &global_data.db, + ResponseType::Compliment, + user.display_name().as_str(), + ) + .unwrap() + .unwrap(); + global_data + .cfg + .announcement_channel + .say(&ctx.http, compliment) + .await + .unwrap(); + } + } + + next_check = next_check + .with_hour(7) + .unwrap() + .checked_add_days(Days::new(1)) + .unwrap(); + } + } tokio::time::sleep(Duration::from_secs(60 * 60)).await; { { diff --git a/src/main.rs b/src/main.rs index 3a541aa..2c592aa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,6 +59,7 @@ async fn main() { .group(&discord::motivate::MOTIVATE_GROUP) .group(&discord::voices::VOICES_GROUP) .group(&discord::shop::SHOP_GROUP) + .group(&discord::birthday::BIRTHDAY_GROUP) .unrecognised_command(unrecognised_command_hook) .bucket("bad_apple", |b| b.delay(60 * 10)) .await diff --git a/src/models/birthday.rs b/src/models/birthday.rs new file mode 100644 index 0000000..0927049 --- /dev/null +++ b/src/models/birthday.rs @@ -0,0 +1,62 @@ +use crate::error::Error; +use chrono::Datelike; +use j_db::database::Database; +use j_db::model::JdbModel; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct BirthdayEntry { + id: Option, + pub discord_id: u64, + pub birthday: chrono::NaiveDate, +} + +impl BirthdayEntry { + pub fn add_birthday( + db: &Database, + discord_id: u64, + birthday: chrono::NaiveDate, + ) -> Result<(), Error> { + let mut entry = db + .filter(|_, entry: &BirthdayEntry| entry.discord_id == discord_id)? + .next() + .unwrap_or(BirthdayEntry { + id: None, + discord_id, + birthday, + }); + + entry.birthday = birthday; + + db.insert(entry)?; + + Ok(()) + } + + pub fn todays_birthdays( + db: &Database, + date: chrono::NaiveDate, + ) -> Result, Error> { + let birthdays: Vec = db + .filter(|_, entry: &BirthdayEntry| { + entry.birthday.month() == date.month() && entry.birthday.day() == date.day() + })? + .collect(); + + Ok(birthdays) + } +} + +impl JdbModel for BirthdayEntry { + fn id(&self) -> Option { + self.id + } + + fn set_id(&mut self, id: u64) { + self.id = Some(id) + } + + fn tree() -> String { + "Birthday".to_string() + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs index aa208c1..4d8f094 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,5 +1,6 @@ pub mod album; pub mod api_key; +pub mod birthday; pub mod insult_compliment; pub mod motivation; pub mod random;