From 1825d36b2e6bc15009c60f2b106c64bacf43318e Mon Sep 17 00:00:00 2001 From: Alessia Date: Sat, 28 Oct 2023 21:10:13 +1100 Subject: [PATCH 01/13] Add a small amount of docs --- src/commands.rs | 1 + src/database.rs | 25 +++++++++++++++++++++++-- src/event_handler.rs | 2 ++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index b6c0efc..67cf38b 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -3,6 +3,7 @@ use poise::serenity_prelude::Timestamp; use crate::{Context, Error}; +/// Test connection speed. #[command(slash_command)] pub async fn ping(ctx: Context<'_>) -> Result<(), Error> { let before_timestamp = Timestamp::now().timestamp_millis(); diff --git a/src/database.rs b/src/database.rs index 76c20be..09c9718 100644 --- a/src/database.rs +++ b/src/database.rs @@ -4,11 +4,18 @@ use uuid::Uuid; use crate::{Error, log::Log}; +/// Core data within bot, containing a +/// connection to its corresponding database pub struct App { pool: Pool, } impl App { + /// Create a postgres connection, creating database if nessessary. + /// + /// # Errors + /// This function requires that the environment variable `DATABASE_URL` + /// is set to a url to a postgres database. #[allow(clippy::cognitive_complexity)] pub async fn new() -> Result { let db_url = std::env::var("DATABASE_URL")?; @@ -31,9 +38,11 @@ impl App { Ok(Self { pool }) } + /// Creates a new log for a channel at the given timstamp + /// + /// # Assumptions + /// This function assumes that `timestamp` is a UNIX timestamp pub async fn new_log(&self, timestamp: i64, channel_id: i64) -> Result<(), Error> { - - query!(r#" INSERT INTO logs (id, channel_id, count, time) VALUES ($1, $2 , 1, $3) @@ -48,6 +57,10 @@ impl App { Ok(()) } + /// Updates a given log with a new count. + /// + /// # Errors + /// This functions assumes that there is an entry with `log_id` pub async fn update_log(&self, log_id: Uuid, new_count: i32) -> Result<(), Error> { query!(r#" UPDATE logs @@ -63,6 +76,13 @@ impl App { Ok(()) } + /// Fetches a log for a channel at `timestamp`. + /// + /// # Assumptions + /// This function assumes that `timestamp` is a factor of `RESOLUTION`. + /// If it is not, this function will never return Some. + /// + /// `timestamp` is expected to be a UNIX timestamp pub async fn fetch_log(&self, channel_id: i64, timestamp: i64) -> Option { let log = query_as! (Log, r#"SELECT * @@ -78,6 +98,7 @@ impl App { log } + /// Fetch all of the logs between two UNIX timestamps. pub async fn fetch_logs(&self, channel_id: i64, lower_time_bound: i64, upper_time_bound: i64) -> Result, Error> { #[allow(clippy::cast_possible_wrap)] let logs: Vec = query_as! (Log, diff --git a/src/event_handler.rs b/src/event_handler.rs index 1daf100..a2e704b 100644 --- a/src/event_handler.rs +++ b/src/event_handler.rs @@ -7,6 +7,7 @@ use tracing::info; use crate::RESOLUTION; use crate::{database::App, Error}; +/// Runs for each event recieved in the discord server. pub async fn event_handler( _ctx: &serenity::Context, event: &Event<'_>, @@ -30,6 +31,7 @@ pub async fn event_handler( Ok(()) } +/// Logs the message recieved for a message event in `event_handler`. async fn log(data: &App, message: &Message) { let timestamp = message.timestamp.timestamp(); #[allow(clippy::cast_possible_wrap)] From abc97026ace45f4b5e7391822743d0c37c6293d7 Mon Sep 17 00:00:00 2001 From: Alessia Date: Sun, 29 Oct 2023 18:32:40 +1100 Subject: [PATCH 02/13] Implement log command log command generates a log graph for the channel it is called in --- Cargo.lock | 456 +++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + build.rs | 6 +- src/commands.rs | 32 ++- src/database.rs | 64 +++--- src/event_handler.rs | 68 ++++--- src/main.rs | 14 +- src/plotting.rs | 100 ++++++++++ 8 files changed, 675 insertions(+), 67 deletions(-) create mode 100644 src/plotting.rs diff --git a/Cargo.lock b/Cargo.lock index c0b81c0..ad7d1fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -164,6 +164,12 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +[[package]] +name = "bytemuck" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" + [[package]] name = "byteorder" version = "1.5.0" @@ -206,6 +212,27 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "const-cstr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3d0b5ff30645a68f35ece8cea4556ca14ef8a1651455f789a099a0513532a6" + [[package]] name = "const-oid" version = "0.9.5" @@ -228,6 +255,42 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bb142d41022986c1d8ff29103a1411c8a3dfad3552f87a4f8dc50d61d4f4e33" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "core-text" +version = "19.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d74ada66e07c1cefa18f8abfba765b486f250de2e4a999e5727fc0dd4b4a25" +dependencies = [ + "core-foundation", + "core-graphics", + "foreign-types", + "libc", +] + [[package]] name = "cpufeatures" version = "0.2.10" @@ -383,12 +446,54 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + [[package]] name = "dotenvy" version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "dwrote" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439a1c2ba5611ad3ed731280541d36d2e9c4ac5e7fb818a27b604bdc5a6aa65b" +dependencies = [ + "lazy_static", + "libc", + "winapi", + "wio", +] + [[package]] name = "either" version = "1.9.0" @@ -440,12 +545,30 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "fallible_collections" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88c69768c0a15262df21899142bc6df9b9b823546d4b4b9a7bc2d6c448ec6fd" +dependencies = [ + "hashbrown 0.13.2", +] + [[package]] name = "fastrand" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "fdeflate" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +dependencies = [ + "simd-adler32", +] + [[package]] name = "finl_unicode" version = "1.2.0" @@ -462,6 +585,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float-ord" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bad48618fdb549078c333a7a8528acb57af271d0433bdecd523eb620628364e" + [[package]] name = "flume" version = "0.11.0" @@ -479,6 +608,46 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "font-kit" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21fe28504d371085fae9ac7a3450f0b289ab71e07c8e57baa3fb68b9e57d6ce5" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "core-foundation", + "core-graphics", + "core-text", + "dirs-next", + "dwrote", + "float-ord", + "freetype", + "lazy_static", + "libc", + "log", + "pathfinder_geometry", + "pathfinder_simd", + "walkdir", + "winapi", + "yeslogic-fontconfig-sys", +] + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.0" @@ -488,6 +657,27 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "freetype" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee38378a9e3db1cc693b4f88d166ae375338a0ff75cb8263e1c601d51f35dc6" +dependencies = [ + "freetype-sys", + "libc", +] + +[[package]] +name = "freetype-sys" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a37d4011c0cc628dfa766fcc195454f4b068d7afdc2adfd28861191d866e731a" +dependencies = [ + "cmake", + "libc", + "pkg-config", +] + [[package]] name = "futures" version = "0.3.28" @@ -608,6 +798,16 @@ dependencies = [ "wasi", ] +[[package]] +name = "gif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.28.0" @@ -639,6 +839,15 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.14.2" @@ -817,6 +1026,21 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "image" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "jpeg-decoder", + "num-rational", + "num-traits", + "png", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -858,6 +1082,12 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" + [[package]] name = "js-sys" version = "0.3.64" @@ -882,6 +1112,16 @@ version = "0.2.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" +[[package]] +name = "libloading" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" +dependencies = [ + "cfg-if", + "windows-sys", +] + [[package]] name = "libm" version = "0.2.8" @@ -915,6 +1155,19 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "lodepng" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3cdccd0cf57a5d456f0656ebcff72c2e19503287e1afbf3b84382812adc0606" +dependencies = [ + "crc32fast", + "fallible_collections", + "flate2", + "libc", + "rgb", +] + [[package]] name = "log" version = "0.4.20" @@ -966,6 +1219,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", + "simd-adler32", ] [[package]] @@ -1037,6 +1291,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.17" @@ -1116,6 +1381,25 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "pathfinder_geometry" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3" +dependencies = [ + "log", + "pathfinder_simd", +] + +[[package]] +name = "pathfinder_simd" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0444332826c70dc47be74a7c6a5fc44e23a7905ad6858d4162b658320455ef93" +dependencies = [ + "rustc_version", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1170,6 +1454,65 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "plotters" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +dependencies = [ + "chrono", + "font-kit", + "image", + "lazy_static", + "num-traits", + "pathfinder_geometry", + "plotters-backend", + "plotters-bitmap", + "plotters-svg", + "ttf-parser", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" + +[[package]] +name = "plotters-bitmap" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cebbe1f70205299abc69e8b295035bb52a6a70ee35474ad10011f0a4efb8543" +dependencies = [ + "gif", + "image", + "plotters-backend", +] + +[[package]] +name = "plotters-svg" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "png" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "poise" version = "0.5.6" @@ -1228,6 +1571,8 @@ version = "0.1.0" dependencies = [ "chrono", "dotenvy", + "lodepng", + "plotters", "poise", "sqlx", "tokio", @@ -1275,6 +1620,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.3.5" @@ -1293,6 +1647,17 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall 0.2.16", + "thiserror", +] + [[package]] name = "regex" version = "1.10.2" @@ -1365,6 +1730,15 @@ dependencies = [ "winreg", ] +[[package]] +name = "rgb" +version = "0.8.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ec2d3e3fc7a92ced357df9cebd5a10b6fb2aa1ee797bf7e9ce2f17dffc8f59" +dependencies = [ + "bytemuck", +] + [[package]] name = "ring" version = "0.16.20" @@ -1422,6 +1796,15 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.20" @@ -1490,6 +1873,15 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1506,6 +1898,12 @@ dependencies = [ "untrusted 0.7.1", ] +[[package]] +name = "semver" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" + [[package]] name = "serde" version = "1.0.189" @@ -1643,6 +2041,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slab" version = "0.4.9" @@ -2213,6 +2617,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "ttf-parser" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375812fa44dab6df41c195cd2f7fecb488f6c09fbaafb62807488cefab642bff" + [[package]] name = "tungstenite" version = "0.17.3" @@ -2345,6 +2755,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -2483,6 +2903,12 @@ version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + [[package]] name = "whoami" version = "1.4.1" @@ -2505,6 +2931,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -2596,6 +3031,27 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "wio" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" +dependencies = [ + "winapi", +] + +[[package]] +name = "yeslogic-fontconfig-sys" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2bbd69036d397ebbff671b1b8e4d918610c181c5a16073b96f984a38d08c386" +dependencies = [ + "const-cstr", + "dlib", + "once_cell", + "pkg-config", +] + [[package]] name = "zeroize" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index a7a762d..353c38a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,5 @@ dotenvy = "0.15.7" sqlx = { version = "0.7.2", features = [ "runtime-tokio", "tls-rustls", "postgres", "uuid" ] } chrono = "0.4.31" uuid = { version = "1.5.0", features = ["v4"] } +plotters = "0.3.5" +lodepng = "3.9.1" diff --git a/build.rs b/build.rs index 7609593..4a943a5 100644 --- a/build.rs +++ b/build.rs @@ -1,5 +1,5 @@ // generated by `sqlx migrate build-script` fn main() { - // trigger recompilation when a new migration is added - println!("cargo:rerun-if-changed=migrations"); -} \ No newline at end of file + // trigger recompilation when a new migration is added + println!("cargo:rerun-if-changed=migrations"); +} diff --git a/src/commands.rs b/src/commands.rs index 67cf38b..81fe812 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,6 +1,7 @@ -use poise::command; +use poise::{command, serenity_prelude::AttachmentType}; use poise::serenity_prelude::Timestamp; +use crate::plotting::create_log_graph; use crate::{Context, Error}; /// Test connection speed. @@ -24,3 +25,32 @@ pub async fn ping(ctx: Context<'_>) -> Result<(), Error> { Ok(()) } + +/// Fetch the logs for the current channel. +#[command(slash_command)] +pub async fn logs(ctx: Context<'_>) -> Result<(), Error> { + let channel_id = ctx.channel_id().0 as i64; + + let logs = ctx + .data() + .fetch_logs(channel_id, i64::MIN, i64::MAX) + .await?; + + if logs.is_empty() { + ctx.say("No logs found for this period").await?; + return Ok(()); + } + + let channel_name = ctx.channel_id().name(ctx.cache()).await.unwrap_or_else(|| "Current Channel".to_string()); + + let oldest_log_timestamp = logs.first().expect("Unreachable").time; + let now = Timestamp::now().timestamp(); + + let graph = create_log_graph(logs, &channel_name, oldest_log_timestamp, now)?; + + ctx.send(|reply| { + reply.attachment(AttachmentType::Bytes { data: graph.into(), filename: "graph.png".to_string() }) + }).await?; + + Ok(()) +} diff --git a/src/database.rs b/src/database.rs index 09c9718..369f97a 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,8 +1,11 @@ -use sqlx::{postgres::PgPoolOptions, Pool, Postgres, migrate::MigrateDatabase, query_as, query}; +use sqlx::migrate::MigrateDatabase; +use sqlx::postgres::PgPoolOptions; +use sqlx::{query, query_as, Pool, Postgres}; use tracing::info; use uuid::Uuid; -use crate::{Error, log::Log}; +use crate::log::Log; +use crate::Error; /// Core data within bot, containing a /// connection to its corresponding database @@ -26,14 +29,14 @@ impl App { info!("Database created"); } - info!("Attempting to connect to database"); + info!("Attempting to connect to database"); let pool = PgPoolOptions::new() .max_connections(5) .connect(&db_url) .await?; - info!("Connected to database"); + info!("Connected to database"); Ok(Self { pool }) } @@ -43,14 +46,17 @@ impl App { /// # Assumptions /// This function assumes that `timestamp` is a UNIX timestamp pub async fn new_log(&self, timestamp: i64, channel_id: i64) -> Result<(), Error> { - query!(r#" + query!( + r#" INSERT INTO logs (id, channel_id, count, time) VALUES ($1, $2 , 1, $3) "#, Uuid::new_v4(), channel_id, timestamp - ).execute(&self.pool).await?; + ) + .execute(&self.pool) + .await?; info!("New log created"); @@ -62,14 +68,17 @@ impl App { /// # Errors /// This functions assumes that there is an entry with `log_id` pub async fn update_log(&self, log_id: Uuid, new_count: i32) -> Result<(), Error> { - query!(r#" + query!( + r#" UPDATE logs SET count = $1 WHERE id = $2; "#, - new_count, - log_id - ).execute(&self.pool).await?; + new_count, + log_id + ) + .execute(&self.pool) + .await?; info!("Log {log_id} updated"); @@ -84,32 +93,43 @@ impl App { /// /// `timestamp` is expected to be a UNIX timestamp pub async fn fetch_log(&self, channel_id: i64, timestamp: i64) -> Option { - let log = query_as! (Log, + let log = query_as!( + Log, r#"SELECT * FROM logs WHERE channel_id = $1 AND time = $2"#, - channel_id, - timestamp - ).fetch_one(&self.pool).await.ok(); + channel_id, + timestamp + ) + .fetch_one(&self.pool) + .await + .ok(); info!("Recent log fetched"); log } - /// Fetch all of the logs between two UNIX timestamps. - pub async fn fetch_logs(&self, channel_id: i64, lower_time_bound: i64, upper_time_bound: i64) -> Result, Error> { - #[allow(clippy::cast_possible_wrap)] - let logs: Vec = query_as! (Log, + /// Fetch all of the logs from a channel between two UNIX timestamps. + pub async fn fetch_logs( + &self, + channel_id: i64, + lower_time_bound: i64, + upper_time_bound: i64, + ) -> Result, Error> { + let logs: Vec = query_as!( + Log, r#"SELECT * FROM logs WHERE channel_id = $1 AND time BETWEEN $2 AND $3"#, - channel_id, - lower_time_bound, - upper_time_bound - ).fetch_all(&self.pool).await?; + channel_id, + lower_time_bound, + upper_time_bound + ) + .fetch_all(&self.pool) + .await?; info!("Logs between `{lower_time_bound}` and `{upper_time_bound}` fetched"); diff --git a/src/event_handler.rs b/src/event_handler.rs index a2e704b..fba57b2 100644 --- a/src/event_handler.rs +++ b/src/event_handler.rs @@ -1,55 +1,53 @@ -use poise::Event; -use poise::serenity_prelude as serenity; use poise::serenity_prelude::Message; -use tracing::error; -use tracing::info; +use poise::{serenity_prelude as serenity, Event}; +use tracing::{error, info}; -use crate::RESOLUTION; -use crate::{database::App, Error}; +use crate::database::App; +use crate::{Error, RESOLUTION}; /// Runs for each event recieved in the discord server. pub async fn event_handler( - _ctx: &serenity::Context, - event: &Event<'_>, - _framework: poise::FrameworkContext<'_, App, Error>, - data: &App, + _ctx: &serenity::Context, + event: &Event<'_>, + _framework: poise::FrameworkContext<'_, App, Error>, + data: &App, ) -> Result<(), Error> { match event { - Event::Ready { data_about_bot } => { - info!("Logged in as {}", data_about_bot.user.name); - } - Event::Message { new_message } => { + Event::Ready { .. } => { + info!("Event handler active"); + } + Event::Message { new_message } => { // Doesnt detect itself if new_message.author.id == 848_902_037_957_115_916 { - return Ok(()) + return Ok(()); } - log(data, new_message).await; - } - _ => {} - } - Ok(()) + log(data, new_message).await; + } + _ => {} + } + Ok(()) } /// Logs the message recieved for a message event in `event_handler`. async fn log(data: &App, message: &Message) { - let timestamp = message.timestamp.timestamp(); - #[allow(clippy::cast_possible_wrap)] - let channel_id = *message.channel_id.as_u64() as i64; + let timestamp = message.timestamp.timestamp(); + #[allow(clippy::cast_possible_wrap)] + let channel_id = *message.channel_id.as_u64() as i64; - let normalized_timestamp = timestamp - timestamp % RESOLUTION; + let normalized_timestamp = timestamp - timestamp % RESOLUTION; - info!("Logging message"); + info!("Logging message"); - let result = if let Some(log) = data.fetch_log(channel_id, normalized_timestamp).await { - info!("Recent log found, updating"); - data.update_log(log.id, log.count + 1).await - } else { - info!("Could not find recent log, creating new"); - data.new_log(normalized_timestamp, channel_id).await - }; + let result = if let Some(log) = data.fetch_log(channel_id, normalized_timestamp).await { + info!("Recent log found, updating"); + data.update_log(log.id, log.count + 1).await + } else { + info!("Could not find recent log, creating new"); + data.new_log(normalized_timestamp, channel_id).await + }; - if let Err(error) = result { - error!("Log failed: {error}"); - } + if let Err(error) = result { + error!("Log failed: {error}"); + } } diff --git a/src/main.rs b/src/main.rs index 664e4b9..ca02a8d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,13 @@ #![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::unwrap_used)] +#![allow(clippy::cast_possible_wrap)] mod commands; mod database; -mod log; mod event_handler; +mod log; +mod plotting; -use commands::ping; +use commands::{logs, ping}; use dotenvy::dotenv; use poise::serenity_prelude::GatewayIntents; use tracing::{error, info}; @@ -28,13 +30,13 @@ async fn main() -> Result<(), poise::serenity_prelude::Error> { let app = App::new().await.expect("Failed to initialise app state"); let options = poise::FrameworkOptions { - commands: vec![ping()], + commands: vec![ping(), logs()], command_check: Some(|ctx| { Box::pin(async move { Ok(ctx.author().id.0 == 366_491_742_679_072_768) }) }), event_handler: |ctx, event, framework, data| { - Box::pin(event_handler::event_handler(ctx, event, framework, data)) - }, + Box::pin(event_handler::event_handler(ctx, event, framework, data)) + }, pre_command: |ctx| { Box::pin(async move { info!("Executing command {}...", ctx.command().qualified_name); @@ -47,7 +49,7 @@ async fn main() -> Result<(), poise::serenity_prelude::Error> { }, on_error: |err| { Box::pin(async move { - error!("Error while executing command: {}", err); + error!("Error while executing command: {}", err.to_string()); }) }, ..Default::default() diff --git a/src/plotting.rs b/src/plotting.rs new file mode 100644 index 0000000..00aa25a --- /dev/null +++ b/src/plotting.rs @@ -0,0 +1,100 @@ +use lodepng::encode24; +use plotters::backend::{PixelFormat, RGBPixel}; +use plotters::prelude::*; +use plotters::style::full_palette::{GREY_900, GREY_A100}; + +use crate::log::Log; +use crate::{Error, RESOLUTION}; + +const HEIGHT: u32 = 1000; +const WIDTH: u32 = 600; + +/// Generates a graph counting the amount of logs between two points in time. +pub fn create_log_graph( + mut logs: Vec, + channel_name: &str, + oldest_log_timestamp: i64, + newest_log_timestamp: i64, +) -> Result, Error> { + if logs.is_empty() { + return Err("Logs vector empty, cannot generate graph".into()); + } + + logs.sort_by(|lhs, rhs| lhs.time.cmp(&rhs.time)); + + // Calculates the highest count of messages. + let max = logs.iter().fold(0, |max, current| { + if current.count > max { + current.count + } else { + max + } + }); + + // Generate graph data. + let buffer = generate_graph( + &logs, + channel_name, + max.into(), + oldest_log_timestamp, + newest_log_timestamp, + )?; + + // Encode data to png. + let image = encode24(&buffer, HEIGHT as usize, WIDTH as usize)?; + + Ok(image) +} + +/// Core logic for generating the graph for `create_log_graph` +fn generate_graph( + logs: &[Log], + channel_name: &str, + max: i64, + oldest: i64, + newest: i64, +) -> Result, Error> { + let mut buffer = vec![0; WIDTH as usize * HEIGHT as usize * RGBPixel::PIXEL_SIZE]; + + let root = BitMapBackend::with_buffer(&mut buffer, (HEIGHT, WIDTH)).into_drawing_area(); + root.fill(&BLACK.mix(0.2))?; + + // Draw background and title, + let mut chart = ChartBuilder::on(&root) + .caption( + format!("Logs for {channel_name}"), + ("sans-serif", 45).into_font().with_color(GREY_A100), + ) + .margin(5) + .x_label_area_size(65) + .y_label_area_size(65) + .margin_right(30) + .build_cartesian_2d(oldest - RESOLUTION..newest + RESOLUTION, 0..max)?; + + // Draw axis labels. + chart + .configure_mesh() + .bold_line_style(GREY_900) + .x_desc("Time") + .y_desc("Messages") + .x_label_style(("sans-serif", 16).into_font().color(&GREY_A100)) + .y_label_style(("sans-serif", 20).into_font().color(&GREY_A100)) + .axis_desc_style(("sans-serif", 25).with_color(GREY_A100)) + .draw()?; + + // Draw data. + chart.draw_series( + Histogram::vertical(&chart) + .style(GREEN.mix(0.9).filled()) + .data(logs.iter().map(|log| (log.time, log.count.into()))), + )?; + + // Save data to buffer/ + root.present()?; + + // Drop chart and root to allow return of buffer + drop(chart); + drop(root); + + Ok(buffer) +} From 84115f8cbd196c23fae0505ef25b39c6a07a40c6 Mon Sep 17 00:00:00 2001 From: Alessia Date: Sun, 29 Oct 2023 18:35:48 +1100 Subject: [PATCH 03/13] Implement log command log command generates a log graph for the channel it is called in --- Dockerfile | 49 ++++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 8 ++++++++ 2 files changed, 57 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..853e00e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,49 @@ +#################################################################################################### +## Builder +#################################################################################################### +FROM rust:latest AS builder + +RUN update-ca-certificates + +# Create appuser +ENV USER=queery +ENV UID=10001 + +RUN adduser \ + --disabled-password \ + --gecos "" \ + --home "/nonexistent" \ + --shell "/sbin/nologin" \ + --no-create-home \ + --uid "${UID}" \ + "${USER}" + + +WORKDIR /queery + +COPY ./ . + +RUN cargo install sqlx-cli + +RUN cargo sqlx migrate run + +RUN cargo build --release + +#################################################################################################### +## Final image +#################################################################################################### +FROM debian:bookworm-slim + +# Import from builder. +COPY --from=builder /etc/passwd /etc/passwd +COPY --from=builder /etc/group /etc/group + +WORKDIR /queery + +# Copy our build +COPY --from=builder /queery/target/release/queery ./ + +# Use an unprivileged user. +USER queery:queery + +CMD ["/queery/queery"] diff --git a/docker-compose.yml b/docker-compose.yml index 6b3f573..c30945b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,13 @@ version: '3.8' services: + queery: + image: queery:latest + env_file: .env + command: ["queery"] + build: + dockerfile: Dockerfile + depends_on: + - db db: image: postgres:16 restart: always From eb9bcca55d45f41f3190cae0a851a6add8a427ee Mon Sep 17 00:00:00 2001 From: Alessia Date: Sun, 29 Oct 2023 20:25:10 +1100 Subject: [PATCH 04/13] Add parameter to log command to select specific time period --- src/commands.rs | 17 +++++++++-------- src/main.rs | 1 + src/time_period.rs | 31 +++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 8 deletions(-) create mode 100644 src/time_period.rs diff --git a/src/commands.rs b/src/commands.rs index 81fe812..139d2d1 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,7 +1,8 @@ use poise::{command, serenity_prelude::AttachmentType}; -use poise::serenity_prelude::Timestamp; +use poise::serenity_prelude::{Timestamp, Channel}; use crate::plotting::create_log_graph; +use crate::time_period::TimePeriod; use crate::{Context, Error}; /// Test connection speed. @@ -28,12 +29,12 @@ pub async fn ping(ctx: Context<'_>) -> Result<(), Error> { /// Fetch the logs for the current channel. #[command(slash_command)] -pub async fn logs(ctx: Context<'_>) -> Result<(), Error> { - let channel_id = ctx.channel_id().0 as i64; +pub async fn logs(ctx: Context<'_>, time_period: TimePeriod, channel: Option) -> Result<(), Error> { + let channel_id = channel.map_or_else(|| ctx.channel_id(), |channel| channel.id()); let logs = ctx .data() - .fetch_logs(channel_id, i64::MIN, i64::MAX) + .fetch_logs(channel_id.0 as i64, i64::MIN, i64::MAX) .await?; if logs.is_empty() { @@ -41,12 +42,12 @@ pub async fn logs(ctx: Context<'_>) -> Result<(), Error> { return Ok(()); } - let channel_name = ctx.channel_id().name(ctx.cache()).await.unwrap_or_else(|| "Current Channel".to_string()); + let channel_name = channel_id.name(ctx.cache()).await.unwrap_or_else(|| "Current Channel".to_string()); - let oldest_log_timestamp = logs.first().expect("Unreachable").time; - let now = Timestamp::now().timestamp(); + let end_timestamp = Timestamp::now().timestamp(); + let start_timestamp = time_period.relative_timestamp_from(end_timestamp); - let graph = create_log_graph(logs, &channel_name, oldest_log_timestamp, now)?; + let graph = create_log_graph(logs, &channel_name, start_timestamp, end_timestamp)?; ctx.send(|reply| { reply.attachment(AttachmentType::Bytes { data: graph.into(), filename: "graph.png".to_string() }) diff --git a/src/main.rs b/src/main.rs index ca02a8d..395d43f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ mod database; mod event_handler; mod log; mod plotting; +mod time_period; use commands::{logs, ping}; use dotenvy::dotenv; diff --git a/src/time_period.rs b/src/time_period.rs new file mode 100644 index 0000000..ae6f46d --- /dev/null +++ b/src/time_period.rs @@ -0,0 +1,31 @@ +#[derive(Debug, Clone, Copy, poise::ChoiceParameter)] +pub enum TimePeriod { + Hour, + #[name = "Half Day"] + HalfDay, + Day, +} + +impl TimePeriod { + const HOUR: i64 = 3600; + const HALFDAY: i64 = Self::HOUR * 6; + const DAY: i64 = Self::HOUR * 12; + /// Gets the timestamp that is the cases before the `timestamp` + /// + /// # Example + /// + /// ``` + /// let now = Timestamp::now().timestamp(); // Eg 1698569658 + /// let period = TimePeriod::Hour; + /// let an_hour_ago = 1698566058 + /// + /// assert!(period.relative_timestamp_from(now), an_hour_ago) + /// ``` + pub const fn relative_timestamp_from(self, timestamp: i64) -> i64 { + match self { + Self::Hour => timestamp - Self::HOUR, + Self::HalfDay => timestamp - Self::HALFDAY, + Self::Day => timestamp - Self::DAY, + } + } +} From 407666f8298302a4a098f7c8669890e3d5f59c18 Mon Sep 17 00:00:00 2001 From: Alessia Date: Mon, 30 Oct 2023 11:12:30 +1100 Subject: [PATCH 05/13] Add some more docs and change some parameter names --- src/log.rs | 7 +++++++ src/plotting.rs | 45 +++++++++++++++++++++++++++------------------ src/time_period.rs | 3 ++- 3 files changed, 36 insertions(+), 19 deletions(-) diff --git a/src/log.rs b/src/log.rs index c870ab2..b0dc0ab 100644 --- a/src/log.rs +++ b/src/log.rs @@ -1,10 +1,17 @@ use sqlx::FromRow; use uuid::Uuid; +/// Represents the data accumulated from `RESOLUTION` seconds of time +/// within a channel. #[derive(Debug, Clone, Copy, FromRow)] pub struct Log { + /// ID used within database. pub id: Uuid, + /// Discord channel ID for log. pub channel_id: i64, + /// Amount of messages sent during pas `RESOLUTION` seconds pub count: i32, + /// Time of log period. + /// Must be a multiple of `RESOLUTION` pub time: i64, } diff --git a/src/plotting.rs b/src/plotting.rs index 00aa25a..c311ba2 100644 --- a/src/plotting.rs +++ b/src/plotting.rs @@ -10,11 +10,16 @@ const HEIGHT: u32 = 1000; const WIDTH: u32 = 600; /// Generates a graph counting the amount of logs between two points in time. +/// +/// # Parameters +/// `channel_name` The name of the channel, used for the title of the graph. +/// +/// `start_timestamp` `end_timestamp` The starting and ending UNIX timestamps to visualise. pub fn create_log_graph( mut logs: Vec, channel_name: &str, - oldest_log_timestamp: i64, - newest_log_timestamp: i64, + start_timestamp: i64, + end_timestamp: i64, ) -> Result, Error> { if logs.is_empty() { return Err("Logs vector empty, cannot generate graph".into()); @@ -23,21 +28,15 @@ pub fn create_log_graph( logs.sort_by(|lhs, rhs| lhs.time.cmp(&rhs.time)); // Calculates the highest count of messages. - let max = logs.iter().fold(0, |max, current| { - if current.count > max { - current.count - } else { - max - } - }); + let max = logs.iter().map(|log| log.count).max().unwrap_or(0); // Generate graph data. let buffer = generate_graph( &logs, channel_name, max.into(), - oldest_log_timestamp, - newest_log_timestamp, + start_timestamp, + end_timestamp, )?; // Encode data to png. @@ -46,20 +45,27 @@ pub fn create_log_graph( Ok(image) } -/// Core logic for generating the graph for `create_log_graph` +/// Core logic for generating the graph for `create_log_graph`. +/// +/// # Parameters +/// `channel_name` The name of the channel, used for the title of the graph. +/// +/// `max` The highest amount of messages to show on the graph. +/// +/// `start_timestamp` `end_timestamp` The starting and ending UNIX timestamps to visualise. fn generate_graph( logs: &[Log], channel_name: &str, max: i64, - oldest: i64, - newest: i64, + start_timestamp: i64, + end_timestamp: i64, ) -> Result, Error> { let mut buffer = vec![0; WIDTH as usize * HEIGHT as usize * RGBPixel::PIXEL_SIZE]; let root = BitMapBackend::with_buffer(&mut buffer, (HEIGHT, WIDTH)).into_drawing_area(); root.fill(&BLACK.mix(0.2))?; - // Draw background and title, + // Draw background and title. let mut chart = ChartBuilder::on(&root) .caption( format!("Logs for {channel_name}"), @@ -69,7 +75,10 @@ fn generate_graph( .x_label_area_size(65) .y_label_area_size(65) .margin_right(30) - .build_cartesian_2d(oldest - RESOLUTION..newest + RESOLUTION, 0..max)?; + .build_cartesian_2d( + start_timestamp - RESOLUTION..end_timestamp + RESOLUTION, + 0..max, + )?; // Draw axis labels. chart @@ -89,10 +98,10 @@ fn generate_graph( .data(logs.iter().map(|log| (log.time, log.count.into()))), )?; - // Save data to buffer/ + // Save data to buffer. root.present()?; - // Drop chart and root to allow return of buffer + // Drop chart and root to allow return of buffer. drop(chart); drop(root); diff --git a/src/time_period.rs b/src/time_period.rs index ae6f46d..b060957 100644 --- a/src/time_period.rs +++ b/src/time_period.rs @@ -1,3 +1,4 @@ +/// Represent a period of time. #[derive(Debug, Clone, Copy, poise::ChoiceParameter)] pub enum TimePeriod { Hour, @@ -17,7 +18,7 @@ impl TimePeriod { /// ``` /// let now = Timestamp::now().timestamp(); // Eg 1698569658 /// let period = TimePeriod::Hour; - /// let an_hour_ago = 1698566058 + /// let an_hour_ago = 1698566058 // 3600 less than now /// /// assert!(period.relative_timestamp_from(now), an_hour_ago) /// ``` From c44609e954d6145f7f8bcb709f2d07b26d0793f7 Mon Sep 17 00:00:00 2001 From: Alessia Date: Mon, 30 Oct 2023 14:31:23 +1100 Subject: [PATCH 06/13] Graphs now show accurate times Fix some crashes --- src/commands.rs | 36 ++++++++++++++++++++++++++---------- src/main.rs | 15 +++++++++++++-- src/plotting.rs | 15 +++++++++++++-- 3 files changed, 52 insertions(+), 14 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 139d2d1..04caf35 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,5 +1,5 @@ -use poise::{command, serenity_prelude::AttachmentType}; -use poise::serenity_prelude::{Timestamp, Channel}; +use poise::command; +use poise::serenity_prelude::{AttachmentType, Channel, Timestamp}; use crate::plotting::create_log_graph; use crate::time_period::TimePeriod; @@ -29,29 +29,45 @@ pub async fn ping(ctx: Context<'_>) -> Result<(), Error> { /// Fetch the logs for the current channel. #[command(slash_command)] -pub async fn logs(ctx: Context<'_>, time_period: TimePeriod, channel: Option) -> Result<(), Error> { +pub async fn logs( + ctx: Context<'_>, + // How far back to search. + time_period: TimePeriod, + // The channel to fetch the logs for, defaults to the current channel + channel: Option, +) -> Result<(), Error> { let channel_id = channel.map_or_else(|| ctx.channel_id(), |channel| channel.id()); - let logs = ctx + let end_timestamp = Timestamp::now().timestamp(); + let start_timestamp = time_period.relative_timestamp_from(end_timestamp); + + let mut logs = ctx .data() .fetch_logs(channel_id.0 as i64, i64::MIN, i64::MAX) .await?; + // Filter out the logs that are out of the time bounds. + logs.retain(|log| log.time < end_timestamp && log.time > start_timestamp); + if logs.is_empty() { ctx.say("No logs found for this period").await?; return Ok(()); } - let channel_name = channel_id.name(ctx.cache()).await.unwrap_or_else(|| "Current Channel".to_string()); - - let end_timestamp = Timestamp::now().timestamp(); - let start_timestamp = time_period.relative_timestamp_from(end_timestamp); + let channel_name = channel_id + .name(ctx.cache()) + .await + .unwrap_or_else(|| "Current Channel".to_string()); let graph = create_log_graph(logs, &channel_name, start_timestamp, end_timestamp)?; ctx.send(|reply| { - reply.attachment(AttachmentType::Bytes { data: graph.into(), filename: "graph.png".to_string() }) - }).await?; + reply.attachment(AttachmentType::Bytes { + data: graph.into(), + filename: "graph.png".to_string(), + }) + }) + .await?; Ok(()) } diff --git a/src/main.rs b/src/main.rs index 395d43f..dafada0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,7 @@ mod time_period; use commands::{logs, ping}; use dotenvy::dotenv; -use poise::serenity_prelude::GatewayIntents; +use poise::serenity_prelude::{GatewayIntents, GuildContainer}; use tracing::{error, info}; use crate::database::App; @@ -23,6 +23,8 @@ type Context<'a> = poise::Context<'a, App, Error>; /// Eg: a resolution of 30 would count the amount of messages in spans of 30 seconds. const RESOLUTION: i64 = 30; +const MODERATOR_ROLE_ID: u64 = 928_317_082_670_604_298; + #[tokio::main] async fn main() -> Result<(), poise::serenity_prelude::Error> { dotenv().ok(); @@ -33,7 +35,16 @@ async fn main() -> Result<(), poise::serenity_prelude::Error> { let options = poise::FrameworkOptions { commands: vec![ping(), logs()], command_check: Some(|ctx| { - Box::pin(async move { Ok(ctx.author().id.0 == 366_491_742_679_072_768) }) + // Only allows moderators to invoke commands. + Box::pin(async move { + let Some(guild) = ctx.partial_guild().await else { + return Err("Could not find guild".into()); + }; + Ok(ctx + .author() + .has_role(ctx.http(), GuildContainer::Guild(guild), MODERATOR_ROLE_ID) + .await?) + }) }), event_handler: |ctx, event, framework, data| { Box::pin(event_handler::event_handler(ctx, event, framework, data)) diff --git a/src/plotting.rs b/src/plotting.rs index c311ba2..d95d508 100644 --- a/src/plotting.rs +++ b/src/plotting.rs @@ -1,7 +1,9 @@ +use chrono::NaiveDateTime; use lodepng::encode24; use plotters::backend::{PixelFormat, RGBPixel}; use plotters::prelude::*; use plotters::style::full_palette::{GREY_900, GREY_A100}; +use tracing::info; use crate::log::Log; use crate::{Error, RESOLUTION}; @@ -21,12 +23,13 @@ pub fn create_log_graph( start_timestamp: i64, end_timestamp: i64, ) -> Result, Error> { + logs.sort_by(|lhs, rhs| lhs.time.cmp(&rhs.time)); + if logs.is_empty() { + info!("Logs vector empty, cannot generate graph"); return Err("Logs vector empty, cannot generate graph".into()); } - logs.sort_by(|lhs, rhs| lhs.time.cmp(&rhs.time)); - // Calculates the highest count of messages. let max = logs.iter().map(|log| log.count).max().unwrap_or(0); @@ -39,9 +42,13 @@ pub fn create_log_graph( end_timestamp, )?; + info!("Graph image generated"); + // Encode data to png. let image = encode24(&buffer, HEIGHT as usize, WIDTH as usize)?; + info!("Graph image encoded"); + Ok(image) } @@ -86,6 +93,10 @@ fn generate_graph( .bold_line_style(GREY_900) .x_desc("Time") .y_desc("Messages") + .x_label_formatter(&|x| { + let dt = NaiveDateTime::from_timestamp_opt(*x, 0).unwrap_or_default(); + dt.format("%H:%M").to_string() + }) .x_label_style(("sans-serif", 16).into_font().color(&GREY_A100)) .y_label_style(("sans-serif", 20).into_font().color(&GREY_A100)) .axis_desc_style(("sans-serif", 25).with_color(GREY_A100)) From b8b3706f71d7a4c3b21c30aa087332859a2ad5ae Mon Sep 17 00:00:00 2001 From: Alessia Date: Mon, 30 Oct 2023 19:32:00 +1100 Subject: [PATCH 07/13] Add week to time_period.rs --- src/event_handler.rs | 1 - src/time_period.rs | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/event_handler.rs b/src/event_handler.rs index fba57b2..70b6eaf 100644 --- a/src/event_handler.rs +++ b/src/event_handler.rs @@ -32,7 +32,6 @@ pub async fn event_handler( /// Logs the message recieved for a message event in `event_handler`. async fn log(data: &App, message: &Message) { let timestamp = message.timestamp.timestamp(); - #[allow(clippy::cast_possible_wrap)] let channel_id = *message.channel_id.as_u64() as i64; let normalized_timestamp = timestamp - timestamp % RESOLUTION; diff --git a/src/time_period.rs b/src/time_period.rs index b060957..f7d2570 100644 --- a/src/time_period.rs +++ b/src/time_period.rs @@ -5,12 +5,15 @@ pub enum TimePeriod { #[name = "Half Day"] HalfDay, Day, + Week, } impl TimePeriod { const HOUR: i64 = 3600; const HALFDAY: i64 = Self::HOUR * 6; const DAY: i64 = Self::HOUR * 12; + const WEEK: i64 = Self::DAY * 7; + /// Gets the timestamp that is the cases before the `timestamp` /// /// # Example @@ -27,6 +30,7 @@ impl TimePeriod { Self::Hour => timestamp - Self::HOUR, Self::HalfDay => timestamp - Self::HALFDAY, Self::Day => timestamp - Self::DAY, + Self::Week => timestamp - Self::WEEK, } } } From ffeed0390fea449b4b8b8dfdfdeb994ec98f58e4 Mon Sep 17 00:00:00 2001 From: Alessia Date: Mon, 30 Oct 2023 19:58:28 +1100 Subject: [PATCH 08/13] Fix inaccurate times in time_period.rs (oopsie) --- src/time_period.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/time_period.rs b/src/time_period.rs index f7d2570..9b28bdd 100644 --- a/src/time_period.rs +++ b/src/time_period.rs @@ -10,8 +10,8 @@ pub enum TimePeriod { impl TimePeriod { const HOUR: i64 = 3600; - const HALFDAY: i64 = Self::HOUR * 6; - const DAY: i64 = Self::HOUR * 12; + const HALFDAY: i64 = Self::HOUR * 12; + const DAY: i64 = Self::HOUR * 24; const WEEK: i64 = Self::DAY * 7; /// Gets the timestamp that is the cases before the `timestamp` From 50587e0e9d05977334e1775053aa3a55d73b1728 Mon Sep 17 00:00:00 2001 From: Alessia Date: Sun, 12 Nov 2023 13:32:09 +1100 Subject: [PATCH 09/13] Time sampled on graph get changed based on time period --- src/commands.rs | 38 +++++++++++++++++++++++++++++--------- src/log.rs | 35 +++++++++++++++++++++++++++++++++++ src/main.rs | 16 +--------------- src/plotting.rs | 34 ++++++++++++++++++++++++++-------- 4 files changed, 91 insertions(+), 32 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 04caf35..b48c664 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,6 +1,8 @@ use poise::command; use poise::serenity_prelude::{AttachmentType, Channel, Timestamp}; +use tracing::{error, info}; +use crate::log::ChangeResolution; use crate::plotting::create_log_graph; use crate::time_period::TimePeriod; use crate::{Context, Error}; @@ -28,26 +30,32 @@ pub async fn ping(ctx: Context<'_>) -> Result<(), Error> { } /// Fetch the logs for the current channel. -#[command(slash_command)] +#[command(slash_command, required_permissions="MANAGE_CHANNELS")] pub async fn logs( ctx: Context<'_>, // How far back to search. time_period: TimePeriod, - // The channel to fetch the logs for, defaults to the current channel + // The channel to fetch the logs for, defaults to the current channel. channel: Option, ) -> Result<(), Error> { let channel_id = channel.map_or_else(|| ctx.channel_id(), |channel| channel.id()); - let end_timestamp = Timestamp::now().timestamp(); - let start_timestamp = time_period.relative_timestamp_from(end_timestamp); + let now_timestamp = Timestamp::now(); + let search_start_timestamp = time_period.relative_timestamp_from(now_timestamp.timestamp()); - let mut logs = ctx + let logs = ctx .data() - .fetch_logs(channel_id.0 as i64, i64::MIN, i64::MAX) + .fetch_logs(channel_id.0 as i64, search_start_timestamp, now_timestamp.timestamp()) .await?; - // Filter out the logs that are out of the time bounds. - logs.retain(|log| log.time < end_timestamp && log.time > start_timestamp); + // Combign logs together to make displaying on graph easier + // Todo: This has not been fully tested yet + let logs = match time_period { + TimePeriod::Hour => logs, + TimePeriod::HalfDay => logs.change_resolution(360), + TimePeriod::Day => logs.change_resolution(720), + TimePeriod::Week => logs.change_resolution(2520), + }; if logs.is_empty() { ctx.say("No logs found for this period").await?; @@ -59,7 +67,13 @@ pub async fn logs( .await .unwrap_or_else(|| "Current Channel".to_string()); - let graph = create_log_graph(logs, &channel_name, start_timestamp, end_timestamp)?; + let graph = match create_log_graph(logs, &channel_name, search_start_timestamp, now_timestamp.timestamp(), time_period) { + Ok(graph) => graph, + Err(err) => { + error!("Failed to generate log graph: {}", err); + return Err(err) + } + }; ctx.send(|reply| { reply.attachment(AttachmentType::Bytes { @@ -69,5 +83,11 @@ pub async fn logs( }) .await?; + + // Time it took for command to be run. + let finished_timestamp = Timestamp::now().timestamp_millis() - now_timestamp.timestamp_millis(); + + info!("Command took {}ms", finished_timestamp); + Ok(()) } diff --git a/src/log.rs b/src/log.rs index b0dc0ab..9c66625 100644 --- a/src/log.rs +++ b/src/log.rs @@ -15,3 +15,38 @@ pub struct Log { /// Must be a multiple of `RESOLUTION` pub time: i64, } + +pub trait ChangeResolution { + fn change_resolution(self, new_resolution: i64) -> Vec; +} + +impl ChangeResolution for Vec { + fn change_resolution(self, new_resolution: i64) -> Vec { + if self.is_empty() { + return self; + } + + let mut updated_logs = Self::new(); + + let mut current_log = Log { + count: 0, + ..self[0] + }; + + let mut next_timestamp = current_log.time + new_resolution; + + for log in self { + if log.time < next_timestamp { + current_log.count += log.count; + } else { + updated_logs.push(current_log); + next_timestamp = log.time + new_resolution; + current_log = log; + } + } + + updated_logs.push(current_log); + + updated_logs + } +} diff --git a/src/main.rs b/src/main.rs index dafada0..8cc27d7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,7 @@ mod time_period; use commands::{logs, ping}; use dotenvy::dotenv; -use poise::serenity_prelude::{GatewayIntents, GuildContainer}; +use poise::serenity_prelude::GatewayIntents; use tracing::{error, info}; use crate::database::App; @@ -23,8 +23,6 @@ type Context<'a> = poise::Context<'a, App, Error>; /// Eg: a resolution of 30 would count the amount of messages in spans of 30 seconds. const RESOLUTION: i64 = 30; -const MODERATOR_ROLE_ID: u64 = 928_317_082_670_604_298; - #[tokio::main] async fn main() -> Result<(), poise::serenity_prelude::Error> { dotenv().ok(); @@ -34,18 +32,6 @@ async fn main() -> Result<(), poise::serenity_prelude::Error> { let options = poise::FrameworkOptions { commands: vec![ping(), logs()], - command_check: Some(|ctx| { - // Only allows moderators to invoke commands. - Box::pin(async move { - let Some(guild) = ctx.partial_guild().await else { - return Err("Could not find guild".into()); - }; - Ok(ctx - .author() - .has_role(ctx.http(), GuildContainer::Guild(guild), MODERATOR_ROLE_ID) - .await?) - }) - }), event_handler: |ctx, event, framework, data| { Box::pin(event_handler::event_handler(ctx, event, framework, data)) }, diff --git a/src/plotting.rs b/src/plotting.rs index d95d508..f577921 100644 --- a/src/plotting.rs +++ b/src/plotting.rs @@ -6,11 +6,13 @@ use plotters::style::full_palette::{GREY_900, GREY_A100}; use tracing::info; use crate::log::Log; +use crate::time_period::TimePeriod; use crate::{Error, RESOLUTION}; -const HEIGHT: u32 = 1000; -const WIDTH: u32 = 600; +const WIDTH: u32 = 1400; +const HEIGHT: u32 = 700; +#[allow(clippy::cognitive_complexity)] /// Generates a graph counting the amount of logs between two points in time. /// /// # Parameters @@ -22,7 +24,10 @@ pub fn create_log_graph( channel_name: &str, start_timestamp: i64, end_timestamp: i64, + time_period: TimePeriod, ) -> Result, Error> { + info!("Creating log graph"); + logs.sort_by(|lhs, rhs| lhs.time.cmp(&rhs.time)); if logs.is_empty() { @@ -33,6 +38,17 @@ pub fn create_log_graph( // Calculates the highest count of messages. let max = logs.iter().map(|log| log.count).max().unwrap_or(0); + // Text for X label + let time_interval_text = format!( + "Time (Per {})", + match time_period { + TimePeriod::Hour => "30 seconds", + TimePeriod::HalfDay => "6 minutes", + TimePeriod::Day => "12 minutes", + TimePeriod::Week => "1.4 hours", + } + ); + // Generate graph data. let buffer = generate_graph( &logs, @@ -40,12 +56,13 @@ pub fn create_log_graph( max.into(), start_timestamp, end_timestamp, + &time_interval_text, )?; info!("Graph image generated"); // Encode data to png. - let image = encode24(&buffer, HEIGHT as usize, WIDTH as usize)?; + let image = encode24(&buffer, WIDTH as usize, HEIGHT as usize)?; info!("Graph image encoded"); @@ -66,10 +83,11 @@ fn generate_graph( max: i64, start_timestamp: i64, end_timestamp: i64, + time_interval_text: &str, ) -> Result, Error> { let mut buffer = vec![0; WIDTH as usize * HEIGHT as usize * RGBPixel::PIXEL_SIZE]; - let root = BitMapBackend::with_buffer(&mut buffer, (HEIGHT, WIDTH)).into_drawing_area(); + let root = BitMapBackend::with_buffer(&mut buffer, (WIDTH, HEIGHT)).into_drawing_area(); root.fill(&BLACK.mix(0.2))?; // Draw background and title. @@ -91,15 +109,15 @@ fn generate_graph( chart .configure_mesh() .bold_line_style(GREY_900) - .x_desc("Time") + .x_desc(time_interval_text) .y_desc("Messages") .x_label_formatter(&|x| { let dt = NaiveDateTime::from_timestamp_opt(*x, 0).unwrap_or_default(); dt.format("%H:%M").to_string() }) - .x_label_style(("sans-serif", 16).into_font().color(&GREY_A100)) - .y_label_style(("sans-serif", 20).into_font().color(&GREY_A100)) - .axis_desc_style(("sans-serif", 25).with_color(GREY_A100)) + .x_label_style(("sans-serif", 20).into_font().color(&GREY_A100)) + .y_label_style(("sans-serif", 25).into_font().color(&GREY_A100)) + .axis_desc_style(("sans-serif", 30).with_color(GREY_A100)) .draw()?; // Draw data. From 87a851d1a95d1022141370849563d8937553124d Mon Sep 17 00:00:00 2001 From: Alessia Date: Mon, 13 Nov 2023 11:52:14 +1100 Subject: [PATCH 10/13] Get queery functioning in Docker --- Dockerfile | 14 ++++++++------ docker-compose.yml | 4 ++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 853e00e..0766013 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,10 +5,14 @@ FROM rust:latest AS builder RUN update-ca-certificates +RUN apt install pkg-config libfreetype6-dev libfontconfig1-dev + # Create appuser ENV USER=queery ENV UID=10001 +ARG SQLX_OFFLINE=true + RUN adduser \ --disabled-password \ --gecos "" \ @@ -23,10 +27,6 @@ WORKDIR /queery COPY ./ . -RUN cargo install sqlx-cli - -RUN cargo sqlx migrate run - RUN cargo build --release #################################################################################################### @@ -34,6 +34,8 @@ RUN cargo build --release #################################################################################################### FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y libfontconfig1 + # Import from builder. COPY --from=builder /etc/passwd /etc/passwd COPY --from=builder /etc/group /etc/group @@ -41,9 +43,9 @@ COPY --from=builder /etc/group /etc/group WORKDIR /queery # Copy our build -COPY --from=builder /queery/target/release/queery ./ +COPY --from=builder /queery/target/release/queery /usr/bin/queery # Use an unprivileged user. USER queery:queery -CMD ["/queery/queery"] +CMD ["queery"] diff --git a/docker-compose.yml b/docker-compose.yml index c30945b..377cdd6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,7 @@ version: '3.8' services: queery: + container_name: "queery" image: queery:latest env_file: .env command: ["queery"] @@ -8,7 +9,9 @@ services: dockerfile: Dockerfile depends_on: - db + restart: unless-stopped db: + container_name: "queery-db" image: postgres:16 restart: always env_file: @@ -17,5 +20,6 @@ services: - '5432:5432' volumes: - db-data:/var/lib/postgresql/data + restart: unless-stopped volumes: db-data: From 59f2ac7eaa7ef8c7c1e14820f99dfa2e4c006960 Mon Sep 17 00:00:00 2001 From: Alessia Date: Mon, 13 Nov 2023 11:59:38 +1100 Subject: [PATCH 11/13] Add param descriptions to log command --- src/commands.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index b48c664..cb2760d 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -33,9 +33,9 @@ pub async fn ping(ctx: Context<'_>) -> Result<(), Error> { #[command(slash_command, required_permissions="MANAGE_CHANNELS")] pub async fn logs( ctx: Context<'_>, - // How far back to search. + #[description="How far back to search."] time_period: TimePeriod, - // The channel to fetch the logs for, defaults to the current channel. + #[description="The channel to fetch the logs for, defaults to the current channel."] channel: Option, ) -> Result<(), Error> { let channel_id = channel.map_or_else(|| ctx.channel_id(), |channel| channel.id()); From 777157bbb68533b585209d28e863f9c42db44bbc Mon Sep 17 00:00:00 2001 From: Alessia Date: Mon, 13 Nov 2023 14:42:48 +1100 Subject: [PATCH 12/13] Rename fetch_logs to fetch_logs_between --- src/commands.rs | 2 +- src/database.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index cb2760d..24b0e51 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -45,7 +45,7 @@ pub async fn logs( let logs = ctx .data() - .fetch_logs(channel_id.0 as i64, search_start_timestamp, now_timestamp.timestamp()) + .fetch_logs_between(channel_id.0 as i64, search_start_timestamp, now_timestamp.timestamp()) .await?; // Combign logs together to make displaying on graph easier diff --git a/src/database.rs b/src/database.rs index 369f97a..30ca9b1 100644 --- a/src/database.rs +++ b/src/database.rs @@ -112,7 +112,7 @@ impl App { } /// Fetch all of the logs from a channel between two UNIX timestamps. - pub async fn fetch_logs( + pub async fn fetch_logs_between( &self, channel_id: i64, lower_time_bound: i64, From 9e7e7fdea3d8ecbce726f7cb00e7dedd30f84380 Mon Sep 17 00:00:00 2001 From: Skye Date: Mon, 13 Nov 2023 22:11:04 +1100 Subject: [PATCH 13/13] Remove unneeded restart statement from docker-compose.yml --- docker-compose.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 377cdd6..c7f93ff 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,6 @@ services: db: container_name: "queery-db" image: postgres:16 - restart: always env_file: - .env ports: