From 32f100c4cf64999a64a3b7f9d4b7de52fe10c9e0 Mon Sep 17 00:00:00 2001 From: Joey Hines Date: Thu, 4 Mar 2021 19:07:31 -0600 Subject: [PATCH] Moved to sled for the database + code cleanup + Moved to the sled embedded rust database + Refactored code during migration + fmt+clippy --- Cargo.lock | 214 ++++++---- Cargo.toml | 4 +- diesel.toml | 5 - migrations/.gitkeep | 0 .../2020-04-22-222856_create_events/down.sql | 2 - .../2020-04-22-222856_create_events/up.sql | 12 - .../down.sql | 2 - .../up.sql | 2 - src/database/mod.rs | 148 +++---- src/database/models.rs | 70 ++-- src/database/schema.rs | 13 - src/discord/events.rs | 32 +- src/discord/handler.rs | 68 +++ src/discord/mod.rs | 394 ++---------------- src/discord/tasks.rs | 71 ++++ src/discord/utility.rs | 247 +++++++++++ src/hypebot_config.rs | 3 +- src/main.rs | 24 +- src/reminder/mod.rs | 32 +- 19 files changed, 687 insertions(+), 656 deletions(-) delete mode 100644 diesel.toml delete mode 100644 migrations/.gitkeep delete mode 100644 migrations/2020-04-22-222856_create_events/down.sql delete mode 100644 migrations/2020-04-22-222856_create_events/up.sql delete mode 100644 migrations/2020-07-10-032139_bigger_desc_field/down.sql delete mode 100644 migrations/2020-07-10-032139_bigger_desc_field/up.sql delete mode 100644 src/database/schema.rs create mode 100644 src/discord/handler.rs create mode 100644 src/discord/tasks.rs create mode 100644 src/discord/utility.rs diff --git a/Cargo.lock b/Cargo.lock index f653980..c643a25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,9 +118,9 @@ checksum = "12ae9db68ad7fac5fe51304d20f016c911539251075a214f8e663babefa35187" [[package]] name = "byteorder" -version = "1.3.4" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" +checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b" [[package]] name = "bytes" @@ -235,46 +235,35 @@ checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" [[package]] name = "crc32fast" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" +checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", ] [[package]] -name = "diesel" -version = "1.4.4" +name = "crossbeam-epoch" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d7ca63eb2efea87a7f56a283acc49e2ce4b2bd54adf7465dc1d81fef13d8fc" +checksum = "2584f639eb95fea8c798496315b297cf81b9b58b6d30ab066a75455333cf4b12" dependencies = [ - "byteorder", - "chrono", - "diesel_derives", - "mysqlclient-sys", - "percent-encoding", - "url", + "cfg-if 1.0.0", + "crossbeam-utils", + "lazy_static 1.4.0", + "memoffset", + "scopeguard", ] [[package]] -name = "diesel_derives" -version = "1.4.1" +name = "crossbeam-utils" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3" +checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49" dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "diesel_migrations" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf3cde8413353dc7f5d72fa8ce0b99a560a359d2c5ef1e5817ca731cd9008f4c" -dependencies = [ - "migrations_internals", - "migrations_macros", + "autocfg", + "cfg-if 1.0.0", + "lazy_static 1.4.0", ] [[package]] @@ -329,6 +318,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "futures" version = "0.3.4" @@ -408,6 +407,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "generic-array" version = "0.14.4" @@ -508,14 +516,14 @@ dependencies = [ "chrono-tz", "clap", "config", - "diesel", - "diesel_migrations", "log", "log4rs", "percent-encoding", "serde 1.0.106", "serde_derive", + "serde_json", "serenity", + "sled", "strfmt", "tokio", "url", @@ -589,6 +597,15 @@ dependencies = [ "bytes 0.5.4", ] +[[package]] +name = "instant" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +dependencies = [ + "cfg-if 1.0.0", +] + [[package]] name = "ipnet" version = "2.3.0" @@ -624,9 +641,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.69" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005" +checksum = "b7282d924be3275cec7f6756ff4121987bc6481325397dde6ba3e7802b1a8b1c" [[package]] name = "linked-hash-map" @@ -654,12 +671,21 @@ dependencies = [ ] [[package]] -name = "log" -version = "0.4.8" +name = "lock_api" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312" dependencies = [ - "cfg-if 0.1.10", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if 1.0.0", "serde 1.0.106", ] @@ -683,7 +709,7 @@ dependencies = [ "libc", "log", "log-mdc", - "parking_lot", + "parking_lot 0.10.2", "serde 1.0.106", "serde-value", "serde_derive", @@ -707,24 +733,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" [[package]] -name = "migrations_internals" -version = "1.4.1" +name = "memoffset" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b4fc84e4af020b837029e017966f86a1c2d5e83e64b589963d5047525995860" +checksum = "157b4208e3059a8f9e78d559edc658e13df41410cb3ae03979c83130067fdd87" dependencies = [ - "diesel", -] - -[[package]] -name = "migrations_macros" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9753f12909fd8d923f75ae5c3258cae1ed3c8ec052e1b38c93c21a6d157f789c" -dependencies = [ - "migrations_internals", - "proc-macro2", - "quote", - "syn", + "autocfg", ] [[package]] @@ -775,16 +789,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "mysqlclient-sys" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e9637d93448044078aaafea7419aed69d301b4a12bcc4aa0ae856eb169bef85" -dependencies = [ - "pkg-config", - "vcpkg", -] - [[package]] name = "nom" version = "4.2.3" @@ -863,8 +867,19 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" dependencies = [ - "lock_api", - "parking_lot_core", + "lock_api 0.3.4", + "parking_lot_core 0.7.2", +] + +[[package]] +name = "parking_lot" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +dependencies = [ + "instant", + "lock_api 0.4.2", + "parking_lot_core 0.8.3", ] [[package]] @@ -876,7 +891,21 @@ dependencies = [ "cfg-if 0.1.10", "cloudabi", "libc", - "redox_syscall", + "redox_syscall 0.1.56", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" +dependencies = [ + "cfg-if 1.0.0", + "instant", + "libc", + "redox_syscall 0.2.5", "smallvec", "winapi", ] @@ -954,12 +983,6 @@ version = "0.1.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5894c618ce612a3fa23881b152b608bafb8c56cfc22f434a3ba3120b40f7b587" -[[package]] -name = "pkg-config" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" - [[package]] name = "ppv-lite86" version = "0.2.6" @@ -1049,6 +1072,15 @@ version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" +[[package]] +name = "redox_syscall" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "0.2.11" @@ -1233,9 +1265,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.51" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da07b57ee2623368351e9a0488bb0b261322a15a6e0ae53e243cbdc0f4208da9" +checksum = "43535db9747a4ba938c0ce0a98cc631a46ebf943c9e1d604e091df6007620bf6" dependencies = [ "itoa", "ryu", @@ -1323,10 +1355,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" [[package]] -name = "smallvec" -version = "1.3.0" +name = "sled" +version = "0.34.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05720e22615919e4734f6a99ceae50d00226c3c5aca406e102ebc33298214e0a" +checksum = "1d0132f3e393bcb7390c60bb45769498cf4550bcb7a21d7f95c02b69f6362cdc" +dependencies = [ + "crc32fast", + "crossbeam-epoch", + "crossbeam-utils", + "fs2", + "fxhash", + "libc", + "log", + "parking_lot 0.11.1", +] + +[[package]] +name = "smallvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" [[package]] name = "socket2" @@ -1390,7 +1438,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7fbf4c9d56b320106cd64fd024dadfa0be7cb4706725fc44a7d7ce952d820c1" dependencies = [ "libc", - "redox_syscall", + "redox_syscall 0.1.56", "winapi", ] @@ -1685,12 +1733,6 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4bf03e0ca70d626ecc4ba6b0763b934b6f2976e8c744088bb3c1d646fbb1ad0" -[[package]] -name = "vcpkg" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168" - [[package]] name = "vec_map" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index b7d7980..04615cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,17 +10,17 @@ edition = "2018" clap = "2.33.0" serde = "1.0.106" serde_derive = "1.0.104" +serde_json = "1.0.63" config = "0.9" chrono = { version = "0.4.19", default-features = false, features = ["clock", "std"] } chrono-tz = "0.4" -diesel = { version = "1.4.4", features = ["mysql", "chrono"] } -diesel_migrations = "1.4.0" log = "0.4.8" log4rs = "0.11.0" strfmt = "0.1.6" url = "2.1.1" tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } percent-encoding = "2.1.0" +sled = "0.34.6" [dependencies.serenity] version = "0.10.2" diff --git a/diesel.toml b/diesel.toml deleted file mode 100644 index bfb01bc..0000000 --- a/diesel.toml +++ /dev/null @@ -1,5 +0,0 @@ -# For documentation on how to configure this file, -# see diesel.rs/guides/configuring-diesel-cli - -[print_schema] -file = "src/database/schema.rs" diff --git a/migrations/.gitkeep b/migrations/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/migrations/2020-04-22-222856_create_events/down.sql b/migrations/2020-04-22-222856_create_events/down.sql deleted file mode 100644 index cb693a2..0000000 --- a/migrations/2020-04-22-222856_create_events/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- This file should undo anything in `up.sql` -DROP TABLE events; \ No newline at end of file diff --git a/migrations/2020-04-22-222856_create_events/up.sql b/migrations/2020-04-22-222856_create_events/up.sql deleted file mode 100644 index 8b1d411..0000000 --- a/migrations/2020-04-22-222856_create_events/up.sql +++ /dev/null @@ -1,12 +0,0 @@ --- Your SQL goes here -CREATE TABLE events ( - id INTEGER AUTO_INCREMENT PRIMARY KEY, - event_name VARCHAR(255) NOT NULL, - event_desc VARCHAR(255) NOT NULL, - event_loc VARCHAR(255) NOT NULL, - organizer VARCHAR(255) NOT NULL, - event_time DATETIME NOT NULL, - message_id VARCHAR(255) NOT NULL, - thumbnail_link VARCHAR(255) NOT NULL, - reminder_sent INTEGER NOT NULL -) \ No newline at end of file diff --git a/migrations/2020-07-10-032139_bigger_desc_field/down.sql b/migrations/2020-07-10-032139_bigger_desc_field/down.sql deleted file mode 100644 index a0b6650..0000000 --- a/migrations/2020-07-10-032139_bigger_desc_field/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE events MODIFY event_desc VARCHAR(255) ; \ No newline at end of file diff --git a/migrations/2020-07-10-032139_bigger_desc_field/up.sql b/migrations/2020-07-10-032139_bigger_desc_field/up.sql deleted file mode 100644 index 3fee40b..0000000 --- a/migrations/2020-07-10-032139_bigger_desc_field/up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Your SQL goes here -ALTER TABLE events MODIFY event_desc VARCHAR(2050); \ No newline at end of file diff --git a/src/database/mod.rs b/src/database/mod.rs index 61a047e..5933dc3 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,84 +1,84 @@ pub mod models; -pub mod schema; -use diesel::prelude::*; -use diesel::result::Error; -use models::{Event, NewEvent}; -use std::vec::Vec; -use chrono::{Utc, DateTime}; +use crate::models::Event; +use serenity::prelude::TypeMapKey; +use sled::{open, Db, IVec, Result}; +use std::path::Path; +use std::sync::Arc; -/// Establish a connection to the database -pub fn establish_connection(database_url: String) -> MysqlConnection { - MysqlConnection::establish(&database_url) - .unwrap_or_else(|_| panic!("Error connecting to {}", database_url)) -} -/// Insert an event into the database -pub fn insert_event(database_url: String, new_event: &NewEvent) -> Result { - use schema::events::dsl::{events, id}; - - let connection = establish_connection(database_url); - - diesel::insert_into(events) - .values(new_event) - .execute(&connection) - .expect("Error saving event"); - - events.order(id).first(&connection) +pub struct Database { + db: Db, } -/// Remove event -pub fn remove_event(database_url: String, event_id: i32) -> Result { - use schema::events::dsl::{events, id}; +impl Database { + /// Open the database + pub fn new(db_path: &Path) -> Result { + let db = open(db_path)?; - let connection = establish_connection(database_url); + Ok(Self { db }) + } - diesel::delete(events.filter(id.eq(event_id))).execute(&connection) + /// DB iterator + fn db_iter(&self) -> sled::Iter { + self.db.iter() + } + + /// Get events by a filter + pub fn filter_events(&self, filter: F) -> Vec + where + F: Fn(&String, &Event) -> bool, + { + self.db_iter() + .filter_map(|pair| { + if let Ok((message_id, event)) = pair { + let event: Event = event.into(); + let message_id: String = String::from_utf8(message_id.to_vec()).unwrap(); + if filter(&message_id, &event) { + Some(event) + } else { + None + } + } else { + None + } + }) + .collect() + } + + /// Insert an event into the database + pub fn insert_event(&self, event: &Event) -> Result<()> { + let msg_id = &event.message_id; + let event: IVec = event.into(); + self.db.insert(msg_id, event)?; + + Ok(()) + } + + /// Remove event from database + pub fn remove_event(&self, event: &Event) -> Result<()> { + self.db.remove(event.message_id.clone())?; + + Ok(()) + } + + /// Get event by name + pub fn get_event_by_name(&self, event_name: &str) -> Option { + self.filter_events(|_, e| e.event_name.as_str() == event_name) + .pop() + } + + /// Get all events + pub fn get_all_events(&self) -> Vec { + self.filter_events(|_, _| true) + } + + /// Get event my discord msg id + pub fn get_event_by_msg_id(&self, target_msg_id: &str) -> Option { + self.filter_events(|msg_id, _| msg_id.as_str() == target_msg_id) + .pop() + } } -/// Get an event by name -pub fn get_event_by_name(database_url: String, name: String) -> Result { - use schema::events::dsl::{event_name, events}; - - let connection = establish_connection(database_url); - - events - .filter(event_name.eq(&name)) - .get_result::(&connection) -} - -/// Get event by its message id -pub fn get_event_by_msg_id(database_url: String, msg_id: String) -> Result { - use schema::events::dsl::{events, message_id}; - - let connection = establish_connection(database_url); - - events - .filter(message_id.eq(&msg_id)) - .get_result::(&connection) -} - -/// Get all events -pub fn get_all_events(database_url: String) -> Result, Error> { - use schema::events::dsl::{events}; - - let connection = establish_connection(database_url); - - events.load(&connection) -} - -/// Get event with id -pub fn get_event_by_id(database_url: String, event_id: i32) -> Result { - use schema::events::dsl::{events, id}; - - let connection = establish_connection(database_url); - - events.filter(id.eq(event_id)).first::(&connection) -} - -pub fn get_event_older_than(database_url: String, time: DateTime) -> Result, Error> { - use schema::events::dsl::{events, event_time}; - - let connection = establish_connection(database_url); - - events.filter(event_time.lt(time.naive_utc())).load(&connection) +impl TypeMapKey for Database { + type Value = Arc; } diff --git a/src/database/models.rs b/src/database/models.rs index c368b51..1523c6e 100644 --- a/src/database/models.rs +++ b/src/database/models.rs @@ -1,10 +1,9 @@ -use super::schema::events; use chrono::{NaiveDateTime, Utc}; +use serde::{Deserialize, Serialize}; +use sled::IVec; -#[derive(Queryable, Clone, Debug, Hash)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct Event { - /// Event ID - pub id: i32, /// Event name pub event_name: String, /// Event long description @@ -19,47 +18,9 @@ pub struct Event { pub message_id: String, /// Event message thumbnail link pub thumbnail_link: String, - /// Reminder sent tracker - pub reminder_sent: i32, } -impl Into for Event { - fn into(self) -> NewEvent { - NewEvent { - event_name: self.event_name.clone(), - event_desc: self.event_desc.clone(), - organizer: self.organizer.clone(), - event_loc: self.event_loc.clone(), - event_time: self.event_time, - message_id: self.message_id.clone(), - thumbnail_link: self.message_id.clone(), - reminder_sent: self.reminder_sent, - } - } -} - -#[derive(Insertable, Clone, Debug)] -#[table_name = "events"] -pub struct NewEvent { - /// Event name - pub event_name: String, - /// Event long description - pub event_desc: String, - /// Event location - pub event_loc: String, - /// Event organizer - pub organizer: String, - /// Event datetime - pub event_time: NaiveDateTime, - /// Event discord message id - pub message_id: String, - /// Event message thumbnail link - pub thumbnail_link: String, - /// Reminder sent tracker - pub reminder_sent: i32, -} - -impl Default for NewEvent { +impl Default for Event { fn default() -> Self { Self { message_id: String::default(), @@ -69,7 +30,28 @@ impl Default for NewEvent { event_desc: String::default(), event_loc: String::default(), thumbnail_link: String::default(), - reminder_sent: i32::default(), } } } + +impl Into for &Event { + fn into(self) -> IVec { + IVec::from(serde_json::to_string(self).unwrap().as_str()) + } +} + +impl From<&IVec> for Event { + fn from(v: &IVec) -> Self { + let s = String::from_utf8(v.to_vec()).unwrap(); + + let e: Event = serde_json::from_str(s.as_str()).unwrap(); + + e + } +} + +impl From for Event { + fn from(v: IVec) -> Self { + Self::from(&v) + } +} diff --git a/src/database/schema.rs b/src/database/schema.rs deleted file mode 100644 index 6aae4d0..0000000 --- a/src/database/schema.rs +++ /dev/null @@ -1,13 +0,0 @@ -table! { - events (id) { - id -> Integer, - event_name -> Varchar, - event_desc -> Varchar, - event_loc -> Varchar, - organizer -> Varchar, - event_time -> Datetime, - message_id -> Varchar, - thumbnail_link -> Varchar, - reminder_sent -> Integer, - } -} diff --git a/src/discord/events.rs b/src/discord/events.rs index ae3c512..96a8653 100644 --- a/src/discord/events.rs +++ b/src/discord/events.rs @@ -1,8 +1,3 @@ -use super::{get_config, send_event_msg}; -use crate::database::{get_event_by_name, insert_event}; -use crate::discord::{ - delete_event, get_draft_event, schedule_event, send_draft_event, update_draft_event, -}; use chrono::offset::TimeZone; use chrono::{Datelike, NaiveDateTime, Timelike, Utc}; use chrono_tz::Tz; @@ -12,6 +7,11 @@ use serenity::prelude::Context; use serenity::utils::{content_safe, ContentSafeOptions}; use url::Url; +use crate::discord::utility::{ + delete_event, get_config, schedule_event, send_draft_event, send_event_msg, update_draft_event, +}; +use crate::discord::{get_db, get_draft_event}; + #[command] /// Posts a previewed event /// @@ -22,6 +22,7 @@ use url::Url; async fn confirm(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { let config = get_config(&ctx.data.read().await).await?; let draft_event = get_draft_event(&ctx.data.read().await).await?; + let db = get_db(&ctx.data.read().await).await?; let mut new_event = draft_event.event.clone(); // Check to to see if message author is the owner of the pending event @@ -34,9 +35,9 @@ async fn confirm(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { new_event.message_id = event_msg.id.0.to_string(); - let event = insert_event(config.db_url.clone(), &new_event)?; + db.insert_event(&new_event)?; - schedule_event(&ctx, &event).await; + schedule_event(&ctx, &new_event).await; } else { msg.reply(&ctx, "You do not have a pending event!".to_string()) .await?; @@ -172,17 +173,18 @@ async fn create(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { /// /// `~cancel "event name"` async fn cancel(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - let config = get_config(&ctx.data.read().await).await?; + let db = get_db(&ctx.data.read().await).await?; // Parse args let event_name = args.single::()?.replace("\"", ""); - let event = get_event_by_name(config.db_url.clone(), event_name)?; - - delete_event(ctx, &event).await?; - - msg.reply(&ctx, format!("{} has been cancelled", event.event_name)) - .await?; - + if let Some(event) = db.get_event_by_name(&event_name) { + delete_event(ctx, &event).await?; + msg.reply(&ctx, format!("{} has been cancelled", event.event_name)) + .await?; + } else { + msg.reply(&ctx, format!("{} now found!", event_name)) + .await?; + } Ok(()) } diff --git a/src/discord/handler.rs b/src/discord/handler.rs new file mode 100644 index 0000000..10f3972 --- /dev/null +++ b/src/discord/handler.rs @@ -0,0 +1,68 @@ +use serenity::async_trait; +use serenity::model::channel::Reaction; +use serenity::model::gateway::Ready; +use serenity::prelude::{Context, EventHandler}; + +use crate::discord::utility::schedule_all_events; +use crate::discord::{tasks, utility}; +use crate::INTERESTED_EMOJI; + +/// Handler for Discord events +pub struct Handler; + +#[async_trait] +impl EventHandler for Handler { + /// On reaction add + async fn reaction_add(&self, ctx: Context, reaction: Reaction) { + let config = utility::get_config(&ctx.data.read().await) + .await + .expect("Unable to get config"); + if reaction.channel_id.0 == config.event_channel + && reaction.emoji.as_data().chars().next().unwrap() == INTERESTED_EMOJI + { + utility::send_message_to_reaction_users( + &ctx, + &reaction, + "Hello, you are now receiving reminders for **{event}**", + ) + .await; + } + } + + /// On reaction remove + async fn reaction_remove(&self, ctx: Context, reaction: Reaction) { + let config = utility::get_config(&ctx.data.read().await) + .await + .expect("Unable to get config"); + + if reaction.channel_id.0 == config.event_channel + && reaction.emoji.as_data().chars().next().unwrap() == INTERESTED_EMOJI + { + utility::send_message_to_reaction_users( + &ctx, + &reaction, + "Hello, you are no longer receiving reminders for **{event}**", + ) + .await; + } + } + + /// On bot ready + async fn ready(&self, ctx: Context, ready: Ready) { + info!("Connected to Discord as {}", ready.user.name); + + schedule_all_events(&ctx).await; + + let cleanup_ctx = ctx.clone(); + + tokio::spawn(async move { + tasks::send_reminders_task(&ctx).await; + }); + + tokio::spawn(async move { + tasks::cleanup_task(&cleanup_ctx).await; + }); + + info!("Setup complete.") + } +} diff --git a/src/discord/mod.rs b/src/discord/mod.rs index 48554e5..933a2cc 100644 --- a/src/discord/mod.rs +++ b/src/discord/mod.rs @@ -1,36 +1,29 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use std::sync::Arc; -use chrono::{DateTime, NaiveDateTime, Utc, Duration}; -use serenity::async_trait; use serenity::framework::standard::macros::help; use serenity::framework::standard::macros::hook; use serenity::framework::standard::{ help_commands, Args, CommandError, CommandGroup, CommandResult, HelpOptions, }; -use serenity::model::gateway::Ready; use serenity::model::id::UserId; -use serenity::model::prelude::{ChannelId, Message, Reaction, User}; -use serenity::prelude::{Context, EventHandler}; +use serenity::model::prelude::Message; +use serenity::prelude::Context; use serenity::prelude::{TypeMap, TypeMapKey}; -use serenity::utils::Colour; -use serenity::Result; -use strfmt::strfmt; use tokio::sync::RwLockReadGuard; -use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode}; -use crate::database::models::{Event, NewEvent}; -use crate::database::{get_all_events, get_event_by_id, get_event_by_msg_id, remove_event, get_event_older_than}; -use crate::hypebot_config::HypeBotConfig; -use crate::reminder::Reminders; -use crate::{INTERESTED_EMOJI, UNINTERESTED_EMOJI}; +use crate::database::models::Event; +use crate::database::Database; pub mod events; +pub mod handler; +pub mod tasks; +pub mod utility; /// Struct for storing drafted events #[derive(Debug, Clone, Default)] pub struct DraftEvent { - pub event: NewEvent, + pub event: Event, pub creator_id: u64, } @@ -38,178 +31,6 @@ impl TypeMapKey for DraftEvent { type Value = Arc; } -/// Send a message to a reaction user -pub async fn send_message_to_reaction_users(ctx: &Context, reaction: &Reaction, msg_text: &str) { - if let Ok(config) = get_config(&ctx.data.read().await).await { - let db_link = config.db_url.clone(); - let message_id = reaction.message_id.0.to_string(); - - let event = match get_event_by_msg_id(db_link, message_id) { - Ok(event) => event, - Err(e) => { - if !matches!(e, diesel::result::Error::NotFound) { - error!("Error getting event from reaction {}", e); - } - return; - } - }; - - let event_utc_time = DateTime::::from_utc(event.event_time, Utc); - let current_utc_time = chrono::offset::Utc::now(); - - let msg; - - if event_utc_time > current_utc_time { - // Format message - let mut fmt = HashMap::new(); - fmt.insert("event".to_string(), event.event_name); - msg = strfmt(msg_text, &fmt).unwrap(); - } else { - msg = format!("**{}** has already started!", &event.event_name) - } - - if let Ok(user) = reaction.user(&ctx).await { - send_dm_message(&ctx, user, &msg).await; - } - } -} - -/// Send a DM message to a user -pub async fn send_dm_message(ctx: &Context, user: User, message: &str) { - if let Ok(dm_channel) = user.create_dm_channel(ctx).await { - dm_channel - .send_message(ctx, |m| m.content(message)) - .await - .ok(); - } -} - -const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); - -/// Create a countdown link for the event -pub fn get_countdown_link(event_name: &str, utc: &DateTime) -> String { - let msg = utf8_percent_encode(event_name, FRAGMENT); - let time = utc.format("%G%m%dT%H%M").to_string(); - - format!( - "https://www.timeanddate.com/countdown/generic?iso={}&p0=&msg={}&font=sanserif&csz=1", - time, msg - ) -} - -/// Sends the event message to the event channel -pub async fn send_event_msg( - ctx: &Context, - config: &HypeBotConfig, - channel_id: u64, - event: &NewEvent, - react: bool, -) -> Result { - let channel = ctx.http.get_channel(channel_id).await?; - - let utc_time = DateTime::::from_utc(event.event_time, Utc); - - let native_time = utc_time.with_timezone(&config.event_timezone); - - let ping_roles = config - .ping_roles - .clone() - .into_iter() - .map(|role| format!("<@&{}>", role)) - .collect::>() - .join(" "); - - // Send message - let msg = channel.id().send_message(&ctx, |m| { - m.embed(|e| { - e.title(event.event_name.clone()) - .color(Colour::PURPLE) - .description(format!( - "**{}**\n{}\n\n[Click Here For A Countdown]({})\n\nReact with {} below to receive event reminders!", - native_time.format("%A, %B %d @ %I:%M %P %t %Z"), - event.event_desc, - get_countdown_link(&event.event_name, &utc_time), - INTERESTED_EMOJI - )) - .thumbnail(event.thumbnail_link.clone()) - .footer(|f| f.text("Local Event Time")) - .timestamp(utc_time.to_rfc3339()) - .field("Location", &event.event_loc, true) - .field("Organizer", &event.organizer, true) - }).content(ping_roles) - }).await?; - - if react { - // Add reacts - msg.react(ctx, INTERESTED_EMOJI).await?; - msg.react(ctx, UNINTERESTED_EMOJI).await?; - } - - Ok(msg) -} - -/// Updates the draft event stored in the context data -#[allow(clippy::too_many_arguments)] -pub async fn update_draft_event( - ctx: &Context, - event_name: String, - event_desc: String, - organizer: String, - location: String, - thumbnail: String, - event_time: NaiveDateTime, - creator_id: u64, -) -> CommandResult { - let mut data = ctx.data.write().await; - let draft_event = data - .get_mut::() - .ok_or_else(|| CommandError::from("Unable get draft event!".to_string()))?; - - let new_draft_event = DraftEvent { - event: NewEvent { - event_name, - event_desc, - event_loc: location, - organizer, - event_time, - message_id: "".to_string(), - thumbnail_link: thumbnail, - reminder_sent: 0, - }, - creator_id, - }; - - *draft_event = Arc::new(new_draft_event); - - Ok(()) -} - -/// Sends the draft event stored in the context data -pub async fn send_draft_event(ctx: &Context, channel: ChannelId) -> CommandResult { - let data = ctx.data.read().await; - let config = get_config(&data).await?; - let draft_event = get_draft_event(&data).await?; - - channel - .send_message(&ctx, |m| { - m.content("Draft message, use the `confirm` command to post it.".to_string()) - }) - .await?; - send_event_msg(ctx, &config, channel.0, &draft_event.event, false).await?; - Ok(()) -} - -/// Gets the config from context data -pub async fn get_config( - data: &RwLockReadGuard<'_, TypeMap>, -) -> std::result::Result, CommandError> { - let config = data - .get::() - .ok_or_else(|| CommandError::from("Unable to get config".to_string()))?; - - Ok(config.clone()) -} - /// Gets the draft event from context data pub async fn get_draft_event( data: &RwLockReadGuard<'_, TypeMap>, @@ -221,6 +42,17 @@ pub async fn get_draft_event( Ok(draft_event.clone()) } +/// Gets the database from context data +pub async fn get_db( + data: &RwLockReadGuard<'_, TypeMap>, +) -> std::result::Result, CommandError> { + let db = data + .get::() + .ok_or_else(|| CommandError::from("Unable to get db".to_string()))?; + + Ok(db.clone()) +} + /// Logs command errors to the logger #[hook] pub async fn log_error( @@ -239,14 +71,15 @@ pub async fn log_error( #[hook] pub async fn permission_check(ctx: &Context, msg: &Message, _command_name: &str) -> bool { if let Some(guild_id) = msg.guild_id { - if let Ok(config) = get_config(&ctx.data.read().await).await { + if let Ok(config) = utility::get_config(&ctx.data.read().await).await { if let Ok(roles) = ctx.http.get_guild_roles(guild_id.0).await { for role in roles { if config.event_roles.contains(&role.id.0) { - return match msg.author.has_role(&ctx, guild_id, role).await { - Ok(has_role) => has_role, - Err(_) => false, - }; + return msg + .author + .has_role(&ctx, guild_id, role) + .await + .unwrap_or(false) } } } @@ -256,117 +89,6 @@ pub async fn permission_check(ctx: &Context, msg: &Message, _command_name: &str) false } -/// Schedule event reminders -pub async fn schedule_event(ctx: &Context, event: &Event) { - let config = get_config(&ctx.data.read().await) - .await - .expect("Unable to get config"); - - let mut data = ctx.data.write().await; - - let reminders = data.get_mut::().unwrap(); - - reminders.add_reminders(event, &*config); -} - -/// Send reminders -pub async fn send_reminders_task(ctx: &Context) { - let duration = tokio::time::Duration::from_secs(1); - loop { - tokio::time::sleep(duration).await; - let config = get_config(&ctx.data.read().await) - .await - .expect("Unable to get config"); - let event_channel_id = config.event_channel; - - let reminder_msg: Vec<&String> = match &config.reminders { - None => return, - Some(reminder_msg) => reminder_msg.iter().map(|r| &r.msg).collect(), - }; - - let mut data = ctx.data.write().await; - let reminders = data - .get_mut::() - .unwrap(); - - let current_reminders = reminders.get_reminders(); - - for reminder in current_reminders { - if let Ok(event) = get_event_by_id(config.db_url.clone(), reminder.event_id) { - let message_id = event.message_id.parse::().unwrap_or_default(); - // Get message id - if let Ok(message) = ctx.http.get_message(event_channel_id, message_id).await { - if let Ok(reaction_users) = message - .reaction_users(ctx, INTERESTED_EMOJI, None, None) - .await - { - // Build reminder message - let msg: String = reminder_msg[reminder.reminder_id] - .replace("{EVENT_NAME}", event.event_name.as_str()); - - // Send reminder to each reacted user - for user in reaction_users { - send_dm_message(ctx, user, &msg).await; - } - } - } - } - } - } -} - -/// Delete old events -pub async fn cleanup_task(ctx: &Context) { - let duration = tokio::time::Duration::from_secs(60*60); - loop { - tokio::time::sleep(duration).await; - let config = get_config(&ctx.data.read().await) - .await - .expect("Unable to get config"); - let one_week_ago = Utc::now() - Duration::from_std(std::time::Duration::from_secs(60*60*24*7)).unwrap(); - - for event in get_event_older_than(config.db_url.clone(), one_week_ago).unwrap() { - delete_event(ctx, &event).await.ok(); - } - } -} - -/// Delete event -pub async fn delete_event(ctx: &Context, event: &Event) -> CommandResult { - let config = get_config(&ctx.data.read().await) - .await - .expect("Unable to get config"); - - let message_id = event.message_id.parse::()?; - let message = ctx - .http - .get_message(config.event_channel, message_id) - .await?; - let cancel_msg = format!("**{}** has been canceled!", event.event_name.clone()); - - // Only send a cancel message if the even has not already happened - if event.event_time > Utc::now().naive_utc() { - if let Ok(reaction_users) = message - .reaction_users(&ctx.http, INTERESTED_EMOJI, None, None) - .await - { - for user in reaction_users { - send_dm_message(ctx, user, &cancel_msg).await; - } - } - } - - remove_event(config.db_url.clone(), event.id)?; - - message.delete(ctx).await?; - - if let Some(reminders) = ctx.data.write().await.get_mut::() { - reminders.remove_reminders(&event); - } - - Ok(()) -} - #[help] #[command_not_found_text = "Could not find: `{}`."] #[strikethrough_commands_tip_in_guild("")] @@ -381,67 +103,3 @@ async fn bot_help( help_commands::with_embeds(context, msg, args, help_options, groups, owners).await; Ok(()) } - -/// Handler for Discord events -pub struct Handler; - -#[async_trait] -impl EventHandler for Handler { - /// On reaction add - async fn reaction_add(&self, ctx: Context, reaction: Reaction) { - let config = get_config(&ctx.data.read().await) - .await - .expect("Unable to get config"); - if reaction.channel_id.0 == config.event_channel - && reaction.emoji.as_data().chars().next().unwrap() == INTERESTED_EMOJI - { - send_message_to_reaction_users( - &ctx, - &reaction, - "Hello, you are now receiving reminders for **{event}**", - ) - .await; - } - } - - /// On reaction remove - async fn reaction_remove(&self, ctx: Context, reaction: Reaction) { - let config = get_config(&ctx.data.read().await) - .await - .expect("Unable to get config"); - - if reaction.channel_id.0 == config.event_channel - && reaction.emoji.as_data().chars().next().unwrap() == INTERESTED_EMOJI - { - send_message_to_reaction_users( - &ctx, - &reaction, - "Hello, you are no longer receiving reminders for **{event}**", - ) - .await; - } - } - - /// On bot ready - async fn ready(&self, ctx: Context, ready: Ready) { - info!("Connected to Discord as {}", ready.user.name); - - // Schedule current events - let config = get_config(&ctx.data.read().await) - .await - .expect("Unable to get config"); - for event in get_all_events(config.db_url.clone()).unwrap() { - schedule_event(&ctx, &event).await; - } - - let cleanup_ctx = ctx.clone(); - - tokio::spawn(async move { - send_reminders_task(&ctx).await; - }); - - tokio::spawn(async move { - cleanup_task(&cleanup_ctx).await; - }); - } -} diff --git a/src/discord/tasks.rs b/src/discord/tasks.rs new file mode 100644 index 0000000..1b35d91 --- /dev/null +++ b/src/discord/tasks.rs @@ -0,0 +1,71 @@ +use chrono::{Duration, Utc}; +use serenity::prelude::Context; + +use crate::discord::{get_db, utility}; +use crate::reminder::Reminders; +use crate::INTERESTED_EMOJI; + +/// Send reminders +pub async fn send_reminders_task(ctx: &Context) { + let duration = tokio::time::Duration::from_secs(1); + loop { + tokio::time::sleep(duration).await; + let config = utility::get_config(&ctx.data.read().await) + .await + .expect("Unable to get config"); + let event_channel_id = config.event_channel; + + let reminder_msg: Vec<&String> = match &config.reminders { + None => return, + Some(reminder_msg) => reminder_msg.iter().map(|r| &r.msg).collect(), + }; + + let db = get_db(&ctx.data.read().await) + .await + .expect("Unable to get DB"); + + let mut data = ctx.data.write().await; + let reminders = data + .get_mut::() + .expect("Unable to get reminders"); + + let current_reminders = reminders.get_reminders(); + + for reminder in current_reminders { + if let Some(event) = db.get_event_by_msg_id(&reminder.event_id) { + let message_id = event.message_id.parse::().unwrap_or_default(); + // Get message id + if let Ok(message) = ctx.http.get_message(event_channel_id, message_id).await { + if let Ok(reaction_users) = message + .reaction_users(ctx, INTERESTED_EMOJI, None, None) + .await + { + // Build reminder message + let msg: String = reminder_msg[reminder.reminder_id] + .replace("{EVENT_NAME}", event.event_name.as_str()); + + // Send reminder to each reacted user + for user in reaction_users { + utility::send_dm_message(ctx, user, &msg).await; + } + } + } + } + } + } +} + +/// Delete old events +pub async fn cleanup_task(ctx: &Context) { + let duration = tokio::time::Duration::from_secs(60 * 60); + loop { + tokio::time::sleep(duration).await; + let db = get_db(&ctx.data.read().await).await.unwrap(); + let one_week_ago = Utc::now() + - Duration::from_std(std::time::Duration::from_secs(60 * 60 * 24 * 7)).unwrap(); + + for event in db.filter_events(|_, event| event.event_time < one_week_ago.naive_utc()) { + utility::delete_event(ctx, &event).await.ok(); + } + } +} diff --git a/src/discord/utility.rs b/src/discord/utility.rs new file mode 100644 index 0000000..00c9dd2 --- /dev/null +++ b/src/discord/utility.rs @@ -0,0 +1,247 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use chrono::{DateTime, NaiveDateTime, Utc}; +use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; +use serenity::framework::standard::{CommandError, CommandResult}; +use serenity::model::channel::{Message, Reaction}; +use serenity::model::id::ChannelId; +use serenity::model::user::User; +use serenity::prelude::{Context, TypeMap}; +use serenity::utils::Colour; +use tokio::sync::RwLockReadGuard; + +use crate::database::models::Event; +use crate::discord::{get_db, DraftEvent}; +use crate::hypebot_config::HypeBotConfig; +use crate::reminder::Reminders; +use crate::{discord, INTERESTED_EMOJI, UNINTERESTED_EMOJI}; + +/// Send a message to a reaction user +pub async fn send_message_to_reaction_users(ctx: &Context, reaction: &Reaction, msg_text: &str) { + if let Ok(db) = get_db(&ctx.data.read().await).await { + let message_id = reaction.message_id.0.to_string(); + + let event = match db.get_event_by_msg_id(&message_id) { + Some(event) => event, + None => { + return; + } + }; + + let event_utc_time = DateTime::::from_utc(event.event_time, Utc); + let current_utc_time = chrono::offset::Utc::now(); + + let msg; + + if event_utc_time > current_utc_time { + // Format message + let mut fmt = HashMap::new(); + fmt.insert("event".to_string(), event.event_name); + msg = strfmt::strfmt(msg_text, &fmt).unwrap(); + + if let Ok(user) = reaction.user(&ctx).await { + send_dm_message(&ctx, user, &msg).await; + } + } + } +} + +/// Send a DM message to a user +pub async fn send_dm_message(ctx: &Context, user: User, message: &str) { + if let Ok(dm_channel) = user.create_dm_channel(ctx).await { + dm_channel + .send_message(ctx, |m| m.content(message)) + .await + .ok(); + } +} + +pub const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); + +/// Sends the event message to the event channel +pub async fn send_event_msg( + ctx: &Context, + config: &HypeBotConfig, + channel_id: u64, + event: &Event, + react: bool, +) -> serenity::Result { + let channel = ctx.http.get_channel(channel_id).await?; + + let utc_time = DateTime::::from_utc(event.event_time, Utc); + + let native_time = utc_time.with_timezone(&config.event_timezone); + + let ping_roles = config + .ping_roles + .clone() + .into_iter() + .map(|role| format!("<@&{}>", role)) + .collect::>() + .join(" "); + + // Send message + let msg = channel.id().send_message(&ctx, |m| { + m.embed(|e| { + e.title(event.event_name.clone()) + .color(Colour::PURPLE) + .description(format!( + "**{}**\n{}\n\n[Click Here For A Countdown]({})\n\nReact with {} below to receive event reminders!", + native_time.format("%A, %B %d @ %I:%M %P %t %Z"), + event.event_desc, + get_countdown_link(&event.event_name, &utc_time), + INTERESTED_EMOJI + )) + .thumbnail(event.thumbnail_link.clone()) + .footer(|f| f.text("Local Event Time")) + .timestamp(utc_time.to_rfc3339()) + .field("Location", &event.event_loc, true) + .field("Organizer", &event.organizer, true) + }).content(ping_roles) + }).await?; + + if react { + // Add reacts + msg.react(ctx, INTERESTED_EMOJI).await?; + msg.react(ctx, UNINTERESTED_EMOJI).await?; + } + + Ok(msg) +} + +/// Updates the draft event stored in the context data +#[allow(clippy::too_many_arguments)] +pub async fn update_draft_event( + ctx: &Context, + event_name: String, + event_desc: String, + organizer: String, + location: String, + thumbnail: String, + event_time: NaiveDateTime, + creator_id: u64, +) -> CommandResult { + let mut data = ctx.data.write().await; + let draft_event = data + .get_mut::() + .ok_or_else(|| CommandError::from("Unable get draft event!".to_string()))?; + + let new_draft_event = DraftEvent { + event: Event { + event_name, + event_desc, + event_loc: location, + organizer, + event_time, + message_id: "".to_string(), + thumbnail_link: thumbnail, + }, + creator_id, + }; + + *draft_event = Arc::new(new_draft_event); + + Ok(()) +} + +/// Sends the draft event stored in the context data +pub async fn send_draft_event(ctx: &Context, channel: ChannelId) -> CommandResult { + let data = ctx.data.read().await; + let config = get_config(&data).await?; + let draft_event = discord::get_draft_event(&data).await?; + + channel + .send_message(&ctx, |m| { + m.content("Draft message, use the `confirm` command to post it.".to_string()) + }) + .await?; + send_event_msg(ctx, &config, channel.0, &draft_event.event, false).await?; + Ok(()) +} + +/// Gets the config from context data +pub async fn get_config( + data: &RwLockReadGuard<'_, TypeMap>, +) -> std::result::Result, CommandError> { + let config = data + .get::() + .ok_or_else(|| CommandError::from("Unable to get config".to_string()))?; + + Ok(config.clone()) +} + +/// Schedule event reminders +pub async fn schedule_event(ctx: &Context, event: &Event) { + let config = get_config(&ctx.data.read().await) + .await + .expect("Unable to get config"); + + let mut data = ctx.data.write().await; + + let reminders = data.get_mut::().unwrap(); + + reminders.add_reminders(event, &*config); +} + +/// Schedule all events +pub async fn schedule_all_events(ctx: &Context) { + let db = get_db(&ctx.data.read().await) + .await + .expect("Could not get database"); + + for event in db.get_all_events() { + schedule_event(&ctx, &event).await; + } +} + +/// Delete event +pub async fn delete_event(ctx: &Context, event: &Event) -> CommandResult { + let config = get_config(&ctx.data.read().await) + .await + .expect("Unable to get config"); + + let db = get_db(&ctx.data.read().await) + .await + .expect("Unable to get db"); + + let message_id = event.message_id.parse::()?; + let message = ctx + .http + .get_message(config.event_channel, message_id) + .await?; + let cancel_msg = format!("**{}** has been canceled!", event.event_name.clone()); + + // Only send a cancel message if the even has not already happened + if event.event_time > Utc::now().naive_utc() { + if let Ok(reaction_users) = message + .reaction_users(&ctx.http, INTERESTED_EMOJI, None, None) + .await + { + for user in reaction_users { + send_dm_message(ctx, user, &cancel_msg).await; + } + } + } + + db.remove_event(event)?; + + message.delete(ctx).await?; + + if let Some(reminders) = ctx.data.write().await.get_mut::() { + reminders.remove_reminders(&event); + } + + Ok(()) +} + +/// Create a countdown link for the event +pub fn get_countdown_link(event_name: &str, utc: &DateTime) -> String { + let msg = utf8_percent_encode(event_name, FRAGMENT); + let time = utc.format("%G%m%dT%H%M").to_string(); + + format!( + "https://www.timeanddate.com/countdown/generic?iso={}&p0=&msg={}&font=sanserif&csz=1", + time, msg + ) +} diff --git a/src/hypebot_config.rs b/src/hypebot_config.rs index ab4f76f..0b7989d 100644 --- a/src/hypebot_config.rs +++ b/src/hypebot_config.rs @@ -4,6 +4,7 @@ use serde::de::{self, Error, Visitor}; use serde::{Deserialize, Deserializer}; use serenity::prelude::TypeMapKey; use std::fmt; +use std::path::PathBuf; use std::sync::Arc; #[derive(Debug, Deserialize, Clone)] @@ -14,7 +15,7 @@ pub struct EventReminder { #[derive(Debug, Deserialize, Clone)] pub struct HypeBotConfig { - pub db_url: String, + pub db_location: PathBuf, pub default_thumbnail_link: String, pub discord_key: String, pub prefix: String, diff --git a/src/main.rs b/src/main.rs index a03cf0f..6551570 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,8 @@ #[macro_use] -extern crate diesel; -#[macro_use] -extern crate diesel_migrations; -#[macro_use] extern crate log; extern crate log4rs; extern crate serde; -mod database; -mod discord; -mod hypebot_config; -mod reminder; - use std::path::Path; use std::process::exit; use std::sync::Arc; @@ -33,11 +24,17 @@ use serenity::framework::standard::StandardFramework; use database::*; use discord::events::{CANCEL_COMMAND, CONFIRM_COMMAND, CREATE_COMMAND}; +use discord::handler::Handler; use discord::BOT_HELP; -use discord::{log_error, permission_check, DraftEvent, Handler}; +use discord::{log_error, permission_check, DraftEvent}; use hypebot_config::HypeBotConfig; use reminder::Reminders; +mod database; +mod discord; +mod hypebot_config; +mod reminder; + const INTERESTED_EMOJI: char = '\u{2705}'; const UNINTERESTED_EMOJI: char = '\u{274C}'; @@ -108,7 +105,6 @@ fn setup_logging(config: &HypeBotConfig) -> HypeBotResult<()> { Ok(()) } -embed_migrations!("migrations/"); #[tokio::main] async fn main() -> HypeBotResult<()> { // Initialize arg parser @@ -140,10 +136,6 @@ async fn main() -> HypeBotResult<()> { // Setup logging setup_logging(&cfg)?; - // Run migrations - let connection = establish_connection(cfg.db_url.clone()); - embedded_migrations::run(&connection)?; - // New client let mut client = Client::builder(cfg.discord_key.clone()) .event_handler(Handler) @@ -164,10 +156,12 @@ async fn main() -> HypeBotResult<()> { // Copy config data to client data and setup scheduler { + let db = database::Database::new(&cfg.db_location)?; let mut data = client.data.write().await; data.insert::(Arc::new(cfg)); data.insert::(Arc::new(DraftEvent::default())); data.insert::(Reminders::default()); + data.insert::(Arc::new(db)); } // Start bot diff --git a/src/reminder/mod.rs b/src/reminder/mod.rs index 4555bcd..0b81061 100644 --- a/src/reminder/mod.rs +++ b/src/reminder/mod.rs @@ -4,9 +4,9 @@ use chrono::{Duration, NaiveDateTime, Utc}; use serenity::prelude::TypeMapKey; /// Event Reminder -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct Reminder { - pub event_id: i32, + pub event_id: String, pub time: NaiveDateTime, pub reminder_id: usize, } @@ -31,7 +31,7 @@ impl Reminders { if reminder_time > Utc::now().naive_utc() { self.reminders.push(Reminder { - event_id: event.id, + event_id: event.message_id.clone(), time: reminder_time, reminder_id, }); @@ -49,7 +49,7 @@ impl Reminders { let time_diff = r.time - Utc::now().naive_utc(); time_diff > Duration::seconds(-5) && time_diff < Duration::seconds(5) }) - .copied() + .cloned() .collect(); self.reminders.retain(|r| !reminders.contains(r)); @@ -59,7 +59,7 @@ impl Reminders { /// Removes reminders for an event pub fn remove_reminders(&mut self, event: &Event) { - self.reminders.retain(|e| e.event_id != event.id) + self.reminders.retain(|e| e.event_id != event.message_id) } /// Update reminders for an event @@ -81,7 +81,7 @@ mod tests { let r = Reminders::default(); let c = HypeBotConfig { - db_url: "".to_string(), + db_location: "".to_string(), default_thumbnail_link: "".to_string(), discord_key: "".to_string(), prefix: "".to_string(), @@ -91,14 +91,16 @@ mod tests { event_timezone: chrono_tz::UTC, log_path: "".to_string(), reminders: Some( - [EventReminder { - msg: "".to_string(), - reminder_time: 5, - }, - EventReminder { - msg: "".to_string(), - reminder_time: 1, - }] + [ + EventReminder { + msg: "".to_string(), + reminder_time: 5, + }, + EventReminder { + msg: "".to_string(), + reminder_time: 1, + }, + ] .to_vec(), ), }; @@ -132,7 +134,7 @@ mod tests { r.add_reminders(&e, &c); assert_eq!(r.get_reminders().len(), 1); - assert_eq!(r.reminders.len(), c.reminders.unwrap().len()-1); + assert_eq!(r.reminders.len(), c.reminders.unwrap().len() - 1); } #[test]