diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..bff29e6 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +rustflags = ["--cfg", "tokio_unstable"] diff --git a/Cargo.lock b/Cargo.lock index bbdee01..d2bf50f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,6 +160,28 @@ dependencies = [ "syn 2.0.28", ] +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + [[package]] name = "async-trait" version = "0.1.68" @@ -435,6 +457,43 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "console-api" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd326812b3fd01da5bb1af7d340d0d555fd3d4b641e7f1dfcf5962a902952787" +dependencies = [ + "futures-core", + "prost", + "prost-types", + "tonic", + "tracing-core", +] + +[[package]] +name = "console-subscriber" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7481d4c57092cd1c19dd541b92bdce883de840df30aa5d03fd48a3935c01842e" +dependencies = [ + "console-api", + "crossbeam-channel", + "crossbeam-utils", + "futures-task", + "hdrhistogram", + "humantime", + "prost-types", + "serde", + "serde_json", + "thread_local", + "tokio", + "tokio-stream", + "tonic", + "tracing", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -475,6 +534,25 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.8" @@ -726,6 +804,16 @@ dependencies = [ "instant", ] +[[package]] +name = "flate2" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "flume" version = "0.10.14" @@ -932,6 +1020,19 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "hdrhistogram" +version = "7.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f19b9f54f7c7f55e31401bb647626ce0cf0f67b0004982ce815b3ee72a02aa8" +dependencies = [ + "base64 0.13.1", + "byteorder", + "flate2", + "nom", + "num-traits", +] + [[package]] name = "heck" version = "0.4.1" @@ -1005,6 +1106,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.27" @@ -1029,6 +1136,18 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -1256,6 +1375,15 @@ dependencies = [ "libc", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "matchit" version = "0.7.0" @@ -1675,6 +1803,38 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fdd22f3b9c31b53c060df4a0613a1c7f062d4115a2b984dd15b1858f7e340d" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "265baba7fabd416cf5078179f7d2cbeca4ce7a9041111900675ea7c4cb8a4c32" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "prost-types" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e081b29f63d83a4bc75cfc9f3fe424f9156cf92d8a4f0c9407cce9a1b67327cf" +dependencies = [ + "prost", +] + [[package]] name = "quote" version = "1.0.29" @@ -1746,8 +1906,17 @@ checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.3.8", + "regex-syntax 0.7.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -1758,9 +1927,15 @@ checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.7.5", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.7.5" @@ -1973,6 +2148,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b21f559e07218024e7e9f90f96f601825397de0e25420135f7f952453fed0b" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -2150,6 +2334,25 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + +[[package]] +name = "strum_macros" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.28", +] + [[package]] name = "syn" version = "1.0.109" @@ -2202,24 +2405,34 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", "syn 2.0.28", ] +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.3.20" @@ -2288,9 +2501,20 @@ dependencies = [ "signal-hook-registry", "socket2 0.5.3", "tokio-macros", + "tracing", "windows-sys 0.48.0", ] +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-macros" version = "2.1.0" @@ -2346,6 +2570,33 @@ dependencies = [ "serde", ] +[[package]] +name = "tonic" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64 0.21.4", + "bytes", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.4.13" @@ -2354,9 +2605,13 @@ checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", + "indexmap", "pin-project", "pin-project-lite", + "rand", + "slab", "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -2405,6 +2660,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "matchers", + "once_cell", + "regex", + "sharded-slab", + "thread_local", + "tracing", + "tracing-core", ] [[package]] @@ -2585,12 +2856,24 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vec1" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bda7c41ca331fe9a1c278a9e7ee055f4be7f5eb1c2b72f079b4ff8b5fce9d5c" + [[package]] name = "version_check" version = "0.9.4" @@ -2923,6 +3206,7 @@ dependencies = [ "chrono", "chrono-tz", "config", + "console-subscriber", "fake", "hyper", "lazy_static", @@ -2933,6 +3217,9 @@ dependencies = [ "serde", "serde_json", "sqlx", + "strum", + "strum_macros", + "thiserror", "timeago", "tokio", "tower", @@ -2941,6 +3228,7 @@ dependencies = [ "twitch_types", "unicode-segmentation", "url", + "vec1", "webbrowser", ] diff --git a/Cargo.toml b/Cargo.toml index fc66a89..1191454 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,5 +22,7 @@ sqlx = { version = "0.6.3", features = [ "offline", "migrate", ] } -tokio = { version = "1.32.0", features = ["full"] } +thiserror = { version = "1.0.48" } +tokio = { version = "1.32.0", features = ["full", "tracing"] } url = { version = "2.4.1", features = ["serde"] } +vec1 = { version = "1.10.1" } diff --git a/src/dankcontent b/src/dankcontent index 3dfb2e0..1635556 160000 --- a/src/dankcontent +++ b/src/dankcontent @@ -1 +1 @@ -Subproject commit 3dfb2e078ad885c09b6a1762d0336de6b72bb2b7 +Subproject commit 16355562a3453c0a62b0794dee8a7275370ba3a2 diff --git a/src/xddmod/Cargo.toml b/src/xddmod/Cargo.toml index 6d06570..307fd3c 100644 --- a/src/xddmod/Cargo.toml +++ b/src/xddmod/Cargo.toml @@ -15,6 +15,7 @@ axum = "0.6.20" chrono = { workspace = true } chrono-tz = "0.8.3" config = "0.13.3" +console-subscriber = { version = "0.2.0" } fake = { workspace = true } hyper = { version = "0.14.27", features = ["full"] } lazy_static = { workspace = true } @@ -25,7 +26,10 @@ reqwest = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } sqlx = { workspace = true } +strum = { version = "0.25" } +strum_macros = { version = "0.25" } timeago = "0.4.2" +thiserror = { workspace = true } tokio = { workspace = true } tower = "0.4.13" twitch_api = { version = "0.7.0-rc.7", features = [ @@ -41,4 +45,5 @@ twitch-irc = { git = "https://github.com/Retoon/twitch-irc-rs", "branch" = "repl ] } unicode-segmentation = "1.9.0" url = { workspace = true } +vec1 = { workspace = true } webbrowser = "0.8.11" diff --git a/src/xddmod/src/handlers.rs b/src/xddmod/src/handlers.rs index 753c7d9..0d3aa81 100644 --- a/src/xddmod/src/handlers.rs +++ b/src/xddmod/src/handlers.rs @@ -2,6 +2,11 @@ pub mod gamba_time; pub mod gg; pub mod npc; pub mod persistence; +pub mod rendering; pub mod rip_bozo; pub mod sniffa; pub mod the_grind; + +pub trait TwitchApiClient: twitch_api::HttpClient + twitch_api::twitch_oauth2::client::Client {} + +impl TwitchApiClient for T {} diff --git a/src/xddmod/src/handlers/gamba_time/core.rs b/src/xddmod/src/handlers/gamba_time/core.rs index 08ef6c5..ad7d00e 100644 --- a/src/xddmod/src/handlers/gamba_time/core.rs +++ b/src/xddmod/src/handlers/gamba_time/core.rs @@ -19,76 +19,63 @@ use twitch_api::types::PredictionOutcome; use twitch_api::types::PredictionStatus; use twitch_api::types::UserId; use twitch_api::HelixClient; +use twitch_irc::login::LoginCredentials; use twitch_irc::message::PrivmsgMessage; use twitch_irc::message::ServerMessage; +use twitch_irc::transport::Transport; +use twitch_irc::TwitchIRCClient; -use crate::auth::IRCClient; use crate::handlers::persistence::Handler; use crate::handlers::persistence::Reply; +use crate::handlers::TwitchApiClient; -pub struct GambaTime<'a> { +pub struct GambaTime<'a, C: TwitchApiClient, T: Transport, L: LoginCredentials> { pub token: UserToken, pub broadcaster_id: UserId, - pub helix_client: HelixClient<'a, reqwest::Client>, - pub irc_client: IRCClient, + pub helix_client: HelixClient<'a, C>, + pub irc_client: TwitchIRCClient, pub db_pool: SqlitePool, pub templates_env: Environment<'a>, } -impl<'a> GambaTime<'a> { +impl<'a, C: TwitchApiClient, T: Transport, L: LoginCredentials> GambaTime<'a, C, T, L> { pub fn handler(&self) -> Handler { Handler::Gamba } } -impl<'a> GambaTime<'a> { - pub async fn handle(&self, server_message: &ServerMessage) { +impl<'a, C: TwitchApiClient, T: Transport, L: LoginCredentials> GambaTime<'a, C, T, L> { + pub async fn handle(&self, server_message: &ServerMessage) -> anyhow::Result<()> { if let ServerMessage::Privmsg(message @ PrivmsgMessage { is_action: false, .. }) = server_message { - match Reply::matching(self.handler(), message, &self.db_pool).await.as_slice() { - [reply] => { - let prediction_request = GetPredictionsRequest::builder() - .broadcaster_id(self.broadcaster_id.clone()) - .first(Some(1)) - .build(); - - let predictions: Vec = self - .helix_client - .req_get(prediction_request.clone(), &self.token) - .await - .unwrap() - .data; - - match predictions.first() { - Some(prediction) => match Gamba::try_from(prediction.clone()) { - Ok(gamba) => { - match reply.render_template( - &self.templates_env, - Some(&minijinja::value::Value::from_serializable(&gamba)), - ) { - Ok(rendered_reply) if rendered_reply.is_empty() => { - eprintln!("Rendered reply template empty: {:?}.", reply) - } - Ok(rendered_reply) => { - self.irc_client.say_in_reply_to(message, rendered_reply).await.unwrap() - } - Err(e) => eprintln!("Error rendering reply template, error: {:?}, {:?}.", reply, e), - } - } - Err(e) => eprintln!( - "Error building GambaData for Prediction {:?}, error: {:?}.", - prediction, e - ), - }, - None => eprintln!("No Predictions found for request {:?}.", prediction_request), - } - } - [] => {} - multiple_matching_replies => eprintln!( - "Multiple matching replies for message: {:?}, {:?}.", - multiple_matching_replies, server_message - ), - } + let Some(first_matching) = Reply::first_matching(self.handler(), message, &self.db_pool).await? else { + return Ok(()); + }; + + let prediction_request = GetPredictionsRequest::builder() + .broadcaster_id(self.broadcaster_id.clone()) + .first(Some(1)) + .build(); + + let predictions: Vec = self + .helix_client + .req_get(prediction_request.clone(), &self.token) + .await? + .data; + + let prediction = predictions + .first() + .ok_or_else(|| anyhow!("no prediction found for request {:?}", prediction_request))?; + + let gamba = Gamba::try_from(prediction.clone())?; + + let rendered_reply = first_matching.reply.render_template( + &self.templates_env, + Some(&minijinja::value::Value::from_serializable(&gamba)), + )?; + + self.irc_client.say_in_reply_to(message, rendered_reply).await?; } + Ok(()) } } diff --git a/src/xddmod/src/handlers/gg/core.rs b/src/xddmod/src/handlers/gg/core.rs index ab5750f..54e801e 100644 --- a/src/xddmod/src/handlers/gg/core.rs +++ b/src/xddmod/src/handlers/gg/core.rs @@ -4,109 +4,81 @@ use minijinja::Environment; use serde::Deserialize; use serde::Serialize; use sqlx::SqlitePool; +use twitch_irc::login::LoginCredentials; use twitch_irc::message::PrivmsgMessage; use twitch_irc::message::ServerMessage; +use twitch_irc::transport::Transport; +use twitch_irc::TwitchIRCClient; use crate::apis::ddragon::champion::Champion; use crate::apis::op_gg; use crate::apis::op_gg::games::Game; use crate::apis::op_gg::Region; -use crate::auth::IRCClient; use crate::handlers::persistence::Handler; +use crate::handlers::persistence::PersistenceError; use crate::handlers::persistence::Reply; use crate::poor_man_throttling; -pub struct Gg<'a> { - pub irc_client: IRCClient, +pub struct Gg<'a, T: Transport, L: LoginCredentials> { + pub irc_client: TwitchIRCClient, pub db_pool: SqlitePool, pub templates_env: Environment<'a>, } -impl<'a> Gg<'a> { +impl<'a, T: Transport, L: LoginCredentials> Gg<'a, T, L> { pub fn handler(&self) -> Handler { Handler::Gg } } -impl<'a> Gg<'a> { - pub async fn handle(&self, server_message: &ServerMessage) { +impl<'a, T: Transport, L: LoginCredentials> Gg<'a, T, L> { + pub async fn handle(&self, server_message: &ServerMessage) -> anyhow::Result<()> { if let ServerMessage::Privmsg(message @ PrivmsgMessage { is_action: false, .. }) = server_message { - match Reply::matching(self.handler(), message, &self.db_pool).await.as_slice() { - [reply @ Reply { - additional_inputs: Some(additional_inputs), - .. - }] => { - match poor_man_throttling::should_throttle(message, reply) { - Ok(false) => (), - Ok(true) => { - eprintln!( - "Skip reply: message {:?}, sender {:?}, reply {:?}", - message.message_text, message.sender, reply.template - ); - return; - } - Err(error) => { - eprintln!("Error throttling, error: {:?}", error); - } - } + let Some(first_matching) = Reply::first_matching(self.handler(), message, &self.db_pool).await? else { + return Ok(()); + }; - match serde_json::from_value::(additional_inputs.0.clone()) { - Ok(additional_inputs) => { - let summoner = op_gg::summoners::get_summoner( - additional_inputs.region, - &additional_inputs.summoner_name, - ) - .await - .unwrap(); + let Some(additional_inputs) = first_matching.reply.additional_inputs.as_ref() else { + return Err(PersistenceError::MissingAdditionalInputs { + reply: first_matching.reply.clone(), + } + .into()); + }; + + // FIXME: poor man throttling + if poor_man_throttling::should_throttle(message, &first_matching.reply)? { + return Ok(()); + } - if let Some(game) = - op_gg::games::get_last_game(additional_inputs.region, &summoner.common.summoner_id) - .await - .unwrap() - { - let template_inputs = TemplateInputs { - champion: Champion::by_key(game.my_data.champion_key.into(), &self.db_pool) - .await - .unwrap(), - game, - }; + let additional_inputs = serde_json::from_value::(additional_inputs.0.clone())?; - match reply.render_template( - &self.templates_env, - Some(&Value::from_serializable(&template_inputs)), - ) { - Ok(rendered_reply) if rendered_reply.is_empty() => { - eprintln!("Rendered reply template empty: {:?}.", reply) - } - Ok(rendered_reply) => { - self.irc_client.say_in_reply_to(message, rendered_reply).await.unwrap() - } - Err(e) => eprintln!("Error rendering reply template, error: {:?}, {:?}.", reply, e), - } - } else { - eprintln!("No games returned for reply: {:?}.", reply) - } - } - Err(error) => eprintln!( - "Error deserializing AdditionalInputs from Reply for ServerMessage: {:?}, {:?}, {:?}.", - error, server_message, reply - ), - } - } - [reply @ Reply { - additional_inputs: None, - .. - }] => eprintln!( - "Reply for ServerMessage with missing AdditionalInputs: {:?}, {:?}.", - server_message, reply - ), - [] => {} - multiple_matching_replies => eprintln!( - "Multiple matching replies for message: {:?}, {:?}.", - multiple_matching_replies, server_message - ), + let summoner = + op_gg::summoners::get_summoner(additional_inputs.region, &additional_inputs.summoner_name).await?; + + if let Some(game) = + op_gg::games::get_last_game(additional_inputs.region, &summoner.common.summoner_id).await? + { + let template_inputs = TemplateInputs { + champion: Champion::by_key(game.my_data.champion_key.into(), &self.db_pool) + .await + .unwrap(), + game, + }; + + let rendered_reply = first_matching + .reply + .render_template::(&self.templates_env, Some(&Value::from_serializable(&template_inputs)))?; + + self.irc_client.say_in_reply_to(message, rendered_reply).await?; + + if let Ok(error) = PersistenceError::try_from(first_matching) { + return Err(error.into()); + }; + } else { + eprintln!("No games returned for reply: {:?}.", first_matching.reply) } } + Ok(()) } } diff --git a/src/xddmod/src/handlers/npc/core.rs b/src/xddmod/src/handlers/npc/core.rs index b0f5750..32bf447 100644 --- a/src/xddmod/src/handlers/npc/core.rs +++ b/src/xddmod/src/handlers/npc/core.rs @@ -1,60 +1,51 @@ use minijinja::value::Value; use minijinja::Environment; use sqlx::SqlitePool; +use twitch_irc::login::LoginCredentials; use twitch_irc::message::PrivmsgMessage; use twitch_irc::message::ServerMessage; +use twitch_irc::transport::Transport; +use twitch_irc::TwitchIRCClient; -use crate::auth::IRCClient; use crate::handlers::persistence::Handler; +use crate::handlers::persistence::PersistenceError; use crate::handlers::persistence::Reply; use crate::poor_man_throttling; -pub struct Npc<'a> { - pub irc_client: IRCClient, +pub struct Npc<'a, T: Transport, L: LoginCredentials> { + pub irc_client: TwitchIRCClient, pub db_pool: SqlitePool, pub templates_env: Environment<'a>, } -impl<'a> Npc<'a> { +impl<'a, T: Transport, L: LoginCredentials> Npc<'a, T, L> { pub fn handler(&self) -> Handler { Handler::Npc } } -impl<'a> Npc<'a> { - pub async fn handle(&self, server_message: &ServerMessage) { +impl<'a, T: Transport, L: LoginCredentials> Npc<'a, T, L> { + pub async fn handle(&self, server_message: &ServerMessage) -> anyhow::Result<()> { if let ServerMessage::Privmsg(message @ PrivmsgMessage { is_action: false, .. }) = server_message { - match Reply::matching(self.handler(), message, &self.db_pool).await.as_slice() { - [reply] => { - // FIXME: poor man throttling - match poor_man_throttling::should_throttle(message, reply) { - Ok(false) => (), - Ok(true) => { - eprintln!( - "Skip reply: message {:?}, sender {:?}, reply {:?}", - message.message_text, message.sender, reply.template - ); - return; - } - Err(error) => { - eprintln!("Error throttling, error: {:?}", error); - } - } - - match reply.render_template::(&self.templates_env, None) { - Ok(rendered_reply) if rendered_reply.is_empty() => { - eprintln!("Rendered reply template empty: {:?}", reply) - } - Ok(rendered_reply) => self.irc_client.say_in_reply_to(message, rendered_reply).await.unwrap(), - Err(e) => eprintln!("Error rendering reply template, error: {:?}, {:?}.", reply, e), - } - } - [] => {} - multiple_matching_replies => eprintln!( - "Multiple matching replies for message: {:?}, {:?}.", - multiple_matching_replies, server_message - ), + let Some(first_matching) = Reply::first_matching(self.handler(), message, &self.db_pool).await? else { + return Ok(()); + }; + + // FIXME: poor man throttling + if poor_man_throttling::should_throttle(message, &first_matching.reply)? { + return Ok(()); } + + let rendered_reply = first_matching + .reply + .render_template::(&self.templates_env, None)?; + + self.irc_client.say_in_reply_to(message, rendered_reply).await?; + + if let Ok(error) = PersistenceError::try_from(first_matching) { + return Err(error.into()); + }; } + Ok(()) } } diff --git a/src/xddmod/src/handlers/persistence.rs b/src/xddmod/src/handlers/persistence.rs index 205cddb..c1c7958 100644 --- a/src/xddmod/src/handlers/persistence.rs +++ b/src/xddmod/src/handlers/persistence.rs @@ -1,5 +1,3 @@ -use minijinja::context; -use minijinja::Environment; use regex::RegexBuilder; use serde::Deserialize; use serde::Serialize; @@ -8,20 +6,31 @@ use sqlx::types::chrono::DateTime; use sqlx::types::chrono::Utc; use sqlx::types::Json; use twitch_irc::message::PrivmsgMessage; +use vec1::Vec1; -#[derive(Debug, Clone)] -pub struct Reply { - pub id: i64, - pub handler: Option, - pub pattern: String, - pub case_insensitive: bool, - pub template: String, - pub channel: Option, - pub enabled: bool, - pub additional_inputs: Option>, - pub created_by: String, - pub created_at: DateTime, - pub updated_at: DateTime, +#[derive(thiserror::Error, Debug)] +pub enum PersistenceError { + #[error("single reply {reply:?} matching message {message:?}, regex errors {regex_errors:?}")] + SingleReplyAndErrors { + reply: Reply, + message: String, + regex_errors: Vec1, + }, + #[error("no reply matching message {message:?}, regex errors {regex_errors:?}")] + NoReplyAndErrors { + message: String, + regex_errors: Vec1, + }, + #[error("multiple replies {replies:?} matching message {message:?}, regex errors {regex_errors:?}")] + MultipleReplies { + replies: Vec, + message: String, + regex_errors: Vec, + }, + #[error("missing additional inputs in {reply:?}")] + MissingAdditionalInputs { reply: Reply }, + #[error(transparent)] + Db(#[from] sqlx::Error), } pub trait MatchableMessage { @@ -46,7 +55,76 @@ impl MatchableMessage for PrivmsgMessage { } } +#[derive(Debug, Clone)] +pub struct Reply { + pub id: i64, + pub handler: Option, + pub pattern: String, + pub case_insensitive: bool, + pub template: String, + pub channel: Option, + pub enabled: bool, + pub additional_inputs: Option>, + pub created_by: String, + pub created_at: DateTime, + pub updated_at: DateTime, +} + impl Reply { + pub async fn first_matching<'a>( + handler: Handler, + matchable_message: &impl MatchableMessage, + executor: impl SqliteExecutor<'a>, + ) -> Result, PersistenceError> { + let (replies, regex_errors) = Self::matching_2(handler, matchable_message, executor).await?; + + let message = matchable_message.text(); + + match (replies.as_slice(), regex_errors.as_slice()) { + ([], []) => Ok(None), + ([reply], regex_errors) => Ok(Some(FirstMatching { + reply: reply.to_owned(), + message: message.to_owned(), + regex_errors: regex_errors.to_owned(), + })), + ([], regex_errors) => Err(PersistenceError::NoReplyAndErrors { + message: message.into(), + regex_errors: regex_errors.try_into().unwrap(), + }), + (replies, regex_errors) => Err(PersistenceError::MultipleReplies { + replies: replies.to_owned(), + message: message.into(), + regex_errors: regex_errors.try_into().unwrap(), + }), + } + } + + pub async fn matching_2<'a>( + handler: Handler, + matchable_message: &impl MatchableMessage, + executor: impl SqliteExecutor<'a>, + ) -> Result<(Vec, Vec), sqlx::Error> { + let matchable_message_text = matchable_message.text(); + + let (matching_replies, re_errors) = Self::all(handler, matchable_message.channel(), executor) + .await? + .into_iter() + .fold((vec![], vec![]), |(mut matching_replies, mut re_errors), reply| { + match RegexBuilder::new(&reply.pattern) + .case_insensitive(reply.case_insensitive) + .build() + { + Ok(re) if re.is_match(matchable_message_text) => matching_replies.push(reply), + Ok(_) => (), + Err(re_error) => re_errors.push(re_error), + }; + + (matching_replies, re_errors) + }); + + Ok((matching_replies, re_errors)) + } + pub async fn matching<'a>( handler: Handler, matchable_message: &impl MatchableMessage, @@ -73,18 +151,6 @@ impl Reply { .collect() } - pub fn render_template( - &self, - template_env: &Environment, - ctx: Option<&S>, - ) -> Result { - let ctx = match ctx { - Some(ctx) => minijinja::value::Value::from_serializable(ctx), - None => context!(), - }; - template_env.render_str(&self.template, ctx).map(|s| s.trim().into()) - } - async fn all<'a>( handler: Handler, channel: &str, @@ -117,7 +183,25 @@ impl Reply { } } -#[derive(Debug, Clone, Copy, Serialize, Deserialize, sqlx::Type)] +pub struct FirstMatching { + pub reply: Reply, + message: String, + regex_errors: Vec, +} + +impl TryFrom for PersistenceError { + type Error = vec1::Size0Error; + + fn try_from(value: FirstMatching) -> Result { + Ok(Self::SingleReplyAndErrors { + reply: value.reply, + message: value.message, + regex_errors: value.regex_errors.try_into()?, + }) + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, sqlx::Type, strum_macros::Display)] #[sqlx(type_name = "TEXT")] pub enum Handler { Gamba, diff --git a/src/xddmod/src/handlers/rendering.rs b/src/xddmod/src/handlers/rendering.rs new file mode 100644 index 0000000..8250f05 --- /dev/null +++ b/src/xddmod/src/handlers/rendering.rs @@ -0,0 +1,33 @@ +use minijinja::context; +use minijinja::Environment; +use serde::Serialize; + +use crate::handlers::persistence::Reply; + +#[derive(thiserror::Error, Debug)] +pub enum RenderingError { + #[error("empty rendered reply {reply:?} with template env {template_env:?}")] + EmptyRenderedReply { reply: Reply, template_env: String }, + #[error(transparent)] + Templating(#[from] minijinja::Error), +} + +impl Reply { + pub fn render_template( + &self, + template_env: &Environment, + ctx: Option<&S>, + ) -> Result { + let ctx = ctx.map_or_else(|| context!(), |ctx| minijinja::value::Value::from_serializable(ctx)); + let rendered_reply: String = template_env.render_str(&self.template, ctx).map(|s| s.trim().into())?; + + if rendered_reply.is_empty() { + return Err(RenderingError::EmptyRenderedReply { + reply: self.clone(), + template_env: format!("{:?}", template_env), + }); + } + + Ok(rendered_reply) + } +} diff --git a/src/xddmod/src/handlers/rip_bozo/core.rs b/src/xddmod/src/handlers/rip_bozo/core.rs index f15129a..a160d1c 100644 --- a/src/xddmod/src/handlers/rip_bozo/core.rs +++ b/src/xddmod/src/handlers/rip_bozo/core.rs @@ -13,26 +13,27 @@ use unicode_segmentation::UnicodeSegmentation; use crate::apis::twitch; use crate::handlers::persistence::Handler; +use crate::handlers::TwitchApiClient; lazy_static! { static ref EMOJI_REGEX: Regex = Regex::new(r"\p{Emoji}").unwrap(); static ref MENTION_REGEX: Regex = Regex::new(r"(@(\w+))").unwrap(); } -pub struct RipBozo<'a> { +pub struct RipBozo<'a, C: TwitchApiClient> { pub broadcaster_id: UserId, pub token: UserToken, - pub helix_client: HelixClient<'a, reqwest::Client>, + pub helix_client: HelixClient<'a, C>, pub db_pool: SqlitePool, } -impl<'a> RipBozo<'a> { +impl<'a, C: TwitchApiClient> RipBozo<'a, C> { pub fn handler(&self) -> Handler { Handler::RipBozo } } -impl<'a> RipBozo<'a> { +impl<'a, C: TwitchApiClient> RipBozo<'a, C> { pub async fn handle(&mut self, server_message: &ServerMessage) -> anyhow::Result { if let ServerMessage::Privmsg(message @ PrivmsgMessage { is_action: false, .. }) = server_message { if twitch::helpers::is_from_streamer_or_mod(message) { diff --git a/src/xddmod/src/handlers/sniffa/core.rs b/src/xddmod/src/handlers/sniffa/core.rs index d4fa209..2e15469 100644 --- a/src/xddmod/src/handlers/sniffa/core.rs +++ b/src/xddmod/src/handlers/sniffa/core.rs @@ -4,105 +4,76 @@ use minijinja::Environment; use serde::Deserialize; use serde::Serialize; use sqlx::SqlitePool; +use twitch_irc::login::LoginCredentials; use twitch_irc::message::PrivmsgMessage; use twitch_irc::message::ServerMessage; +use twitch_irc::transport::Transport; +use twitch_irc::TwitchIRCClient; use crate::apis::op_gg; use crate::apis::op_gg::spectate::get_spectate_status; use crate::apis::op_gg::spectate::SpectateStatus; use crate::apis::op_gg::summoners::Summoner; use crate::apis::op_gg::Region; -use crate::auth::IRCClient; use crate::handlers::persistence::Handler; +use crate::handlers::persistence::PersistenceError; use crate::handlers::persistence::Reply; use crate::poor_man_throttling; -pub struct Sniffa<'a> { - pub irc_client: IRCClient, +pub struct Sniffa<'a, T: Transport, L: LoginCredentials> { + pub irc_client: TwitchIRCClient, pub db_pool: SqlitePool, pub templates_env: Environment<'a>, } -impl<'a> Sniffa<'a> { +impl<'a, T: Transport, L: LoginCredentials> Sniffa<'a, T, L> { pub fn handler(&self) -> Handler { Handler::Sniffa } } -impl<'a> Sniffa<'a> { - pub async fn handle(&self, server_message: &ServerMessage) { +impl<'a, T: Transport, L: LoginCredentials> Sniffa<'a, T, L> { + pub async fn handle(&self, server_message: &ServerMessage) -> anyhow::Result<()> { if let ServerMessage::Privmsg(message @ PrivmsgMessage { is_action: false, .. }) = server_message { - match Reply::matching(self.handler(), message, &self.db_pool).await.as_slice() { - [reply @ Reply { - additional_inputs: Some(additional_inputs), - .. - }] => { - match poor_man_throttling::should_throttle(message, reply) { - Ok(false) => (), - Ok(true) => { - eprintln!( - "Skip reply: message {:?}, sender {:?}, reply {:?}", - message.message_text, message.sender, reply.template - ); - return; - } - Err(error) => { - eprintln!("Error throttling, error: {:?}", error); - } - } + let Some(first_matching) = Reply::first_matching(self.handler(), message, &self.db_pool).await? else { + return Ok(()); + }; - match serde_json::from_value::(additional_inputs.0.clone()) { - Ok(additional_inputs) => { - let summoner = op_gg::summoners::get_summoner( - additional_inputs.region, - &additional_inputs.summoner_name, - ) - .await - .unwrap(); + let Some(additional_inputs) = first_matching.reply.additional_inputs.as_ref() else { + return Err(PersistenceError::MissingAdditionalInputs { + reply: first_matching.reply.clone(), + } + .into()); + }; - let spectate_status = - get_spectate_status(additional_inputs.region, &summoner.common.summoner_id) - .await - .unwrap(); + // FIXME: poor man throttling + if poor_man_throttling::should_throttle(message, &first_matching.reply)? { + return Ok(()); + } - let template_inputs = TemplateInputs { - summoner, - spectate_status, - }; + let additional_inputs = serde_json::from_value::(additional_inputs.0.clone())?; - match reply - .render_template(&self.templates_env, Some(&Value::from_serializable(&template_inputs))) - { - Ok(rendered_reply) if rendered_reply.is_empty() => { - eprintln!("Rendered reply template empty: {:?}.", reply) - } - Ok(rendered_reply) => { - self.irc_client.say_in_reply_to(message, rendered_reply).await.unwrap() - } - Err(e) => eprintln!("Error rendering reply template, error: {:?}, {:?}.", reply, e), - } - } - Err(error) => eprintln!( - "Error deserializing AdditionalInputs from Reply for ServerMessage: {:?}, {:?}, {:?}.", - error, server_message, reply - ), - } - } + let summoner = + op_gg::summoners::get_summoner(additional_inputs.region, &additional_inputs.summoner_name).await?; - [reply @ Reply { - additional_inputs: None, - .. - }] => eprintln!( - "Reply for ServerMessage with missing AdditionalInputs: {:?}, {:?}.", - server_message, reply - ), - [] => {} - multiple_matching_replies => eprintln!( - "Multiple matching replies for message: {:?}, {:?}.", - multiple_matching_replies, server_message - ), - } + let spectate_status = get_spectate_status(additional_inputs.region, &summoner.common.summoner_id).await?; + + let template_inputs = TemplateInputs { + summoner, + spectate_status, + }; + + let rendered_reply = first_matching + .reply + .render_template::(&self.templates_env, Some(&Value::from_serializable(&template_inputs)))?; + + self.irc_client.say_in_reply_to(message, rendered_reply).await?; + + if let Ok(error) = PersistenceError::try_from(first_matching) { + return Err(error.into()); + }; } + Ok(()) } } diff --git a/src/xddmod/src/handlers/the_grind/core.rs b/src/xddmod/src/handlers/the_grind/core.rs index 4e5226a..532bcfe 100644 --- a/src/xddmod/src/handlers/the_grind/core.rs +++ b/src/xddmod/src/handlers/the_grind/core.rs @@ -4,95 +4,70 @@ use minijinja::Environment; use serde::Deserialize; use serde::Serialize; use sqlx::SqlitePool; +use twitch_irc::login::LoginCredentials; use twitch_irc::message::PrivmsgMessage; use twitch_irc::message::ServerMessage; +use twitch_irc::transport::Transport; +use twitch_irc::TwitchIRCClient; use crate::apis::op_gg; use crate::apis::op_gg::summoners::LpHistory; use crate::apis::op_gg::summoners::SummonerJson; use crate::apis::op_gg::Region; -use crate::auth::IRCClient; use crate::handlers::persistence::Handler; +use crate::handlers::persistence::PersistenceError; use crate::handlers::persistence::Reply; use crate::poor_man_throttling; -pub struct TheGrind<'a> { - pub irc_client: IRCClient, +pub struct TheGrind<'a, T: Transport, L: LoginCredentials> { + pub irc_client: TwitchIRCClient, pub db_pool: SqlitePool, pub templates_env: Environment<'a>, } -impl<'a> TheGrind<'a> { +impl<'a, T: Transport, L: LoginCredentials> TheGrind<'a, T, L> { pub fn handler(&self) -> Handler { Handler::TheGrind } } -impl<'a> TheGrind<'a> { - pub async fn handle(&self, server_message: &ServerMessage) { +impl<'a, T: Transport, L: LoginCredentials> TheGrind<'a, T, L> { + pub async fn handle(&self, server_message: &ServerMessage) -> anyhow::Result<()> { if let ServerMessage::Privmsg(message @ PrivmsgMessage { is_action: false, .. }) = server_message { - match Reply::matching(self.handler(), message, &self.db_pool).await.as_slice() { - [reply @ Reply { - additional_inputs: Some(additional_inputs), - .. - }] => { - match poor_man_throttling::should_throttle(message, reply) { - Ok(false) => (), - Ok(true) => { - eprintln!( - "Skip reply: message {:?}, sender {:?}, reply {:?}", - message.message_text, message.sender, reply.template - ); - return; - } - Err(error) => { - eprintln!("Error throttling, error: {:?}", error); - } - } - - match serde_json::from_value::(additional_inputs.0.clone()) { - Ok(additional_inputs) => { - let summoner_json = op_gg::summoners::get_summoner_json( - additional_inputs.region, - &additional_inputs.summoner_name, - ) - .await - .unwrap(); - - let template_inputs = TemplateInputs::from(summoner_json); - - match reply - .render_template(&self.templates_env, Some(&Value::from_serializable(&template_inputs))) - { - Ok(rendered_reply) if rendered_reply.is_empty() => { - eprintln!("Rendered reply template empty: {:?}.", reply) - } - Ok(rendered_reply) => { - self.irc_client.say_in_reply_to(message, rendered_reply).await.unwrap() - } - Err(e) => eprintln!("Error rendering reply template, error: {:?}, {:?}.", reply, e), - } - } - Err(error) => eprintln!( - "Error deserializing AdditionalInputs from Reply for ServerMessage: {:?}, {:?}, {:?}.", - error, server_message, reply - ), - } + let Some(first_matching) = Reply::first_matching(self.handler(), message, &self.db_pool).await? else { + return Ok(()); + }; + + let Some(additional_inputs) = first_matching.reply.additional_inputs.as_ref() else { + return Err(PersistenceError::MissingAdditionalInputs { + reply: first_matching.reply.clone(), } - [reply @ Reply { - additional_inputs: None, - .. - }] => eprintln!( - "Reply for ServerMessage with missing AdditionalInputs: {:?}, {:?}.", - server_message, reply - ), - [] => {} - multiple_matching_replies => eprintln!( - "Multiple matching replies for message: {:?}, {:?}.", - multiple_matching_replies, server_message - ), + .into()); + }; + + // FIXME: poor man throttling + if poor_man_throttling::should_throttle(message, &first_matching.reply)? { + return Ok(()); } + + let additional_inputs = serde_json::from_value::(additional_inputs.0.clone())?; + + let summoner_json = + op_gg::summoners::get_summoner_json(additional_inputs.region, &additional_inputs.summoner_name).await?; + + let template_inputs = TemplateInputs::from(summoner_json); + + let rendered_reply = first_matching + .reply + .render_template::(&self.templates_env, Some(&Value::from_serializable(&template_inputs)))?; + + self.irc_client.say_in_reply_to(message, rendered_reply).await?; + + if let Ok(error) = PersistenceError::try_from(first_matching) { + return Err(error.into()); + }; } + Ok(()) } } diff --git a/src/xddmod/src/main.rs b/src/xddmod/src/main.rs index 693e828..71868fb 100644 --- a/src/xddmod/src/main.rs +++ b/src/xddmod/src/main.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use anyhow::anyhow; use sqlx::SqlitePool; use tokio::sync::Mutex; use twitch_api::HelixClient; @@ -12,11 +13,15 @@ use xddmod::handlers::sniffa::core::Sniffa; use xddmod::handlers::the_grind::core::TheGrind; #[tokio::main] -async fn main() { +async fn main() -> anyhow::Result<()> { + console_subscriber::init(); + let app_config = AppConfig::init(); - let channel = std::env::args().nth(1).unwrap(); + let channel = std::env::args() + .nth(1) + .ok_or_else(|| anyhow!("missing 1st CLI arg, `channel`"))?; - let db_pool = SqlitePool::connect(app_config.db_url.as_ref()).await.unwrap(); + let db_pool = SqlitePool::connect(app_config.db_url.as_ref()).await?; let (mut incoming_messages, irc_client, user_token) = auth::authenticate(app_config.clone()).await; @@ -24,11 +29,16 @@ async fn main() { let broadcaster = helix_client .get_user_from_login(&channel, &user_token) - .await - .unwrap() - .unwrap(); + .await? + .ok_or_else(|| { + anyhow!( + "no broacaster found for `channel` {} with `user_token` {:?}", + channel, + user_token + ) + })?; - irc_client.join(channel).unwrap(); + irc_client.join(channel)?; let templates_env = xddmod::templates_env::build_global_templates_env(); @@ -69,17 +79,26 @@ async fn main() { let the_grind = the_grind.clone(); tokio::spawn(async move { - let mut rip_bozo_g = rip_bozo.lock().await; - if let Ok(true) = rip_bozo_g.handle(&server_message).await { + let mut rip_bozo_guard = rip_bozo.lock().await; + if let Ok(true) = rip_bozo_guard.handle(&server_message).await { return; } - npc.handle(&server_message).await; - gg.handle(&server_message).await; - sniffa.handle(&server_message).await; - the_grind.handle(&server_message).await; + if let Err(e) = npc.handle(&server_message).await { + eprintln!("{} error {:?}", npc.handler(), e); + }; + if let Err(e) = gg.handle(&server_message).await { + eprintln!("{} error {:?}", gg.handler(), e); + }; + if let Err(e) = sniffa.handle(&server_message).await { + eprintln!("{} error {:?}", sniffa.handler(), e); + }; + if let Err(e) = the_grind.handle(&server_message).await { + eprintln!("{} error {:?}", the_grind.handler(), e); + }; }); } }) - .await - .unwrap(); + .await?; + + Ok(()) } diff --git a/src/xddmod/src/poor_man_throttling.rs b/src/xddmod/src/poor_man_throttling.rs index 6aed27e..731450f 100644 --- a/src/xddmod/src/poor_man_throttling.rs +++ b/src/xddmod/src/poor_man_throttling.rs @@ -1,3 +1,5 @@ +use anyhow::anyhow; +use sqlx::types::chrono::Utc; use twitch_irc::message::PrivmsgMessage; use crate::apis::twitch; @@ -17,18 +19,18 @@ pub fn should_throttle(message: &PrivmsgMessage, reply: &Reply) -> anyhow::Resul let mut throttle = THROTTLE .lock() - .map_err(|error| anyhow::anyhow!("Cannot get THROTTLE Lock, error: {:?}", error))?; + .map_err(|error| anyhow!("Cannot get THROTTLE Lock, error: {:?}", error))?; let throttling = throttle .get(&reply.id) .map(|last_reply_date_time| { - let time_passed = sqlx::types::chrono::Utc::now() - *last_reply_date_time; + let time_passed = Utc::now() - *last_reply_date_time; time_passed < chrono::Duration::seconds(20) }) .unwrap_or_default(); if !throttling { - throttle.insert(reply.id, sqlx::types::chrono::Utc::now()); + throttle.insert(reply.id, Utc::now()); } Ok(throttling) diff --git a/src/xtask/src/import_ddragon_champion.rs b/src/xtask/src/import_ddragon_champion.rs index fdf8c01..fd1a239 100644 --- a/src/xtask/src/import_ddragon_champion.rs +++ b/src/xtask/src/import_ddragon_champion.rs @@ -19,19 +19,19 @@ pub struct ImportDdragonChampion { impl ImportDdragonChampion { pub async fn run(self) -> anyhow::Result<()> { - let db_pool = SqlitePool::connect(self.db_url.as_ref()).await.unwrap(); + let db_pool = SqlitePool::connect(self.db_url.as_ref()).await?; let api_response: ApiResponse = reqwest::get(format!("{}/champion.json", self.ddragon_api_base_url)) .await? .json() .await?; - let mut tx = db_pool.begin().await.unwrap(); - Champion::truncate(&mut tx).await.unwrap(); + let mut tx = db_pool.begin().await?; + Champion::truncate(&mut tx).await?; for champion in api_response.data.into_values() { - champion.insert(&mut tx).await.unwrap(); + champion.insert(&mut tx).await?; } - tx.commit().await.unwrap(); + tx.commit().await?; Ok(()) }