diff --git a/Cargo.lock b/Cargo.lock index 60dbe4c..aa6c11e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -363,7 +363,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", + "futures-core", "memchr", + "pin-project-lite", + "tokio", + "tokio-util", ] [[package]] @@ -382,6 +386,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "crc16" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338089f42c427b86394a5ee60ff321da23a5c89c9d89514c829687b26359fcff" + [[package]] name = "crc32fast" version = "1.5.0" @@ -467,6 +477,36 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "deadpool" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ed5957ff93768adf7a65ab167a17835c3d2c3c50d084fe305174c112f468e2f" +dependencies = [ + "deadpool-runtime", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-redis" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0965b977f1244bc3783bb27cd79cfcff335a8341da18f79232d00504b18eb1a" +dependencies = [ + "deadpool", + "redis", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" +dependencies = [ + "tokio", +] + [[package]] name = "deranged" version = "0.4.0" @@ -476,6 +516,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -726,6 +777,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "http" version = "1.3.1" @@ -861,12 +918,119 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "include_dir" version = "0.7.4" @@ -1045,6 +1209,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + [[package]] name = "lock_api" version = "0.4.13" @@ -1146,12 +1316,31 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1161,6 +1350,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "num_threads" version = "0.1.7" @@ -1238,13 +1437,14 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "payphone" -version = "4.0.25" +version = "4.0.26" dependencies = [ "async-trait", "axum", "clap", "colored", "csv", + "deadpool-redis", "dotenvy", "include_dir", "itertools", @@ -1296,6 +1496,15 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1373,6 +1582,31 @@ dependencies = [ "rand_core", ] +[[package]] +name = "redis" +version = "0.32.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd3650deebc68526b304898b192fa4102a4ef0b9ada24da096559cb60e0eef8" +dependencies = [ + "bytes", + "cfg-if", + "combine", + "crc16", + "futures-sink", + "futures-util", + "itoa", + "log", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "rand", + "ryu", + "socket2 0.6.0", + "tokio", + "tokio-util", + "url", +] + [[package]] name = "redox_syscall" version = "0.5.17" @@ -1942,6 +2176,16 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tokio" version = "1.47.1" @@ -2256,6 +2500,23 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -2706,6 +2967,12 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + [[package]] name = "yoke" version = "0.8.0" @@ -2776,3 +3043,36 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 09a3d91..d6fa8fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "payphone" -version = "4.0.25" +version = "4.0.26" edition = "2024" default-run = "payphone" @@ -10,6 +10,7 @@ axum = "0.8.4" clap = { version = "4.5.42", features = ["derive", "env", "string"] } colored = "3.0.0" csv = "1.3.1" +deadpool-redis = { version = "0.22.0", features = ["cluster", "cluster-async"] } dotenvy = "0.15.7" include_dir = "0.7.4" itertools = "0.14.0" diff --git a/compose.yml b/compose.yml index 48826cb..d1add0c 100644 --- a/compose.yml +++ b/compose.yml @@ -31,6 +31,36 @@ services: scylla-net: ipv4_address: 172.20.0.4 + redis-node-1: + image: redis:latest + container_name: redis-node-1 + command: redis-server --port 7000 --cluster-enabled yes --cluster-config-file nodes.conf --appendonly yes + ports: + - "7000:7000" + networks: + redis-net: + ipv4_address: 172.20.1.2 + + redis-node-2: + image: redis:latest + container_name: redis-node-2 + command: redis-server --port 7001 --cluster-enabled yes --cluster-config-file nodes.conf --appendonly yes + ports: + - "7001:7001" + networks: + redis-net: + ipv4_address: 172.20.1.3 + + redis-node-3: + image: redis:latest + container_name: redis-node-3 + command: redis-server --port 7002 --cluster-enabled yes --cluster-config-file nodes.conf --appendonly yes + ports: + - "7002:7002" + networks: + redis-net: + ipv4_address: 172.20.1.4 + volumes: scylla-data-1: scylla-data-2: @@ -42,3 +72,9 @@ networks: ipam: config: - subnet: 172.20.0.0/24 + + redis-net: + driver: bridge + ipam: + config: + - subnet: 172.20.1.0/24 \ No newline at end of file diff --git a/src/cli.rs b/src/cli.rs index 3943831..6e64338 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -212,6 +212,15 @@ pub enum Commands { default_value_t = Shard::default()) ] shard: Shard, + + /// The Redis URLs to connect to. + #[arg( + long, + env = "PAYPHONE_REDIS_URLS", + value_name = "URLS", + default_values = &["redis://127.0.0.1:6379"], + )] + redis_urls: Vec, }, /// Starts a documentation server for the project which serves `cargo doc` and refreshes on diff --git a/src/database/models.rs b/src/database/models.rs index feaff9f..226f5d6 100644 --- a/src/database/models.rs +++ b/src/database/models.rs @@ -13,7 +13,9 @@ use thiserror::Error; use time::{Duration, OffsetDateTime, UtcDateTime, UtcOffset}; use twilight_http::Client; use twilight_model::{ - channel::Message as TwilightMessage, guild::Guild as TwilightGuild, id::Id, + channel::{Message as TwilightMessage, Webhook}, + guild::Guild as TwilightGuild, + id::Id, user::User as TwilightUser, }; @@ -478,6 +480,38 @@ impl<'a> Object<'a> for Channel { } } +async fn get_or_create_channel_webhook( + channel_id: u64, + http: &Client, +) -> Result { + let webhooks = http + .channel_webhooks(Id::new(channel_id)) + .await + .inspect_err(|e| log::error!("Failed to get webhooks from channel {channel_id}: {e:?}")) + .map_err(DatabaseOrDiscordError::Discord)? + .model() + .await + .inspect_err(|e| log::error!("Failed to get model for webhooks {channel_id}: {e:?}")) + .map_err(|_| DatabaseOrDiscordError::DiscordDeserializationFailed)?; + + let webhook = if let Some(item) = webhooks.into_iter().find(|item| item.token.is_some()) { + item + } else { + http.create_webhook(Id::new(channel_id), "Payphone Messages") + .await + .inspect_err(|e| { + log::error!("Failed to create webhook for channel {channel_id}: {e:?}") + }) + .map_err(DatabaseOrDiscordError::Discord)? + .model() + .await + .inspect_err(|e| log::error!("Failed to get model for webhook {channel_id}: {e:?}")) + .map_err(|_| DatabaseOrDiscordError::DiscordDeserializationFailed)? + }; + + Ok(webhook) +} + impl Channel { pub async fn get_by_id(id: u64, session: &Session) -> Result, DatabaseError> { let id = u64_to_i64(id); @@ -520,15 +554,7 @@ impl Channel { { Ok(channel) } else { - let webhook = http - .create_webhook(Id::new(id), "Payphone Messages") - .await - .inspect_err(|e| log::error!("Failed to create webhook for channel {id}: {e:?}")) - .map_err(DatabaseOrDiscordError::Discord)? - .model() - .await - .inspect_err(|e| log::error!("Failed to get model for webhook {id}: {e:?}")) - .map_err(|_| DatabaseOrDiscordError::DiscordDeserializationFailed)?; + let webhook = get_or_create_channel_webhook(id, http).await?; let response = http .channel(Id::new(id)) @@ -593,6 +619,21 @@ impl Channel { Ok(()) } + + pub async fn update_webhook( + &mut self, + session: &Session, + http: &Client, + ) -> Result<(), DatabaseOrDiscordError> { + let webhook = get_or_create_channel_webhook(self.id, http).await?; + + self.webhook_id = Some(webhook.id.get()); + self.webhook_token = webhook.token; + + self.upsert(session).await?; + + Ok(()) + } } #[derive(Debug, Clone)] diff --git a/src/groups/calls/events.rs b/src/groups/calls/events.rs index 2c89874..526a191 100644 --- a/src/groups/calls/events.rs +++ b/src/groups/calls/events.rs @@ -2,6 +2,7 @@ use std::{error::Error, sync::Arc}; use async_trait::async_trait; use itertools::Itertools; +use twilight_http::{api_error::ApiError, error::ErrorType}; use twilight_model::{ gateway::payload::incoming::{MessageCreate, MessageDelete, MessageUpdate}, id::Id, @@ -151,7 +152,7 @@ impl OnMessageHandler for CallMessageHandler { return Ok(()); } - let Some(opposite_channel) = Channel::get_by_id( + let Some(mut opposite_channel) = Channel::get_by_id( call.other_party_channel_id(event.channel_id.get()), &rt.state.db, ) @@ -167,7 +168,7 @@ impl OnMessageHandler for CallMessageHandler { let Some((webhook_id, webhook_token)) = opposite_channel .webhook_id - .zip(opposite_channel.webhook_token) + .zip(opposite_channel.webhook_token.clone()) else { // Handle DM calls in the future. return Ok(()); @@ -202,14 +203,14 @@ impl OnMessageHandler for CallMessageHandler { rt.http .create_message(event.channel_id) .content( - "❌ Your message has been blocked because it broke Payphone's code of conduct. Check your DMs for more information.", + "🚩 Your message has been blocked because it broke Payphone's code of conduct. Check your DMs for more information.", ) .await?; rt.http .create_message(Id::new(opposite_channel.id)) .content(&format!( - "❌ **{}** Sent a message that was blocked.\n-# If you see an inappropiate message, please report it using `Message > Apps > Report Message`.", + "🚩 **{}** Sent a message that was blocked.\n-# If you see an inappropiate message, please report it using `Message > Apps > Report Message`.", profile.name.sanitize(false) )) .await?; @@ -241,7 +242,7 @@ impl OnMessageHandler for CallMessageHandler { ); } - let message_sent = rt + let message_sent = match rt .http .execute_webhook(Id::new(webhook_id), &webhook_token) .allowed_mentions(None) @@ -257,9 +258,26 @@ impl OnMessageHandler for CallMessageHandler { call.id, opposite_channel.id ); - })? - .model() - .await?; + }) { + Ok(message) => message, + Err(e) => { + if let ErrorType::Response { + error: ApiError::General(error), + .. + } = e.kind() + { + if error.code == 10015 { + log::debug!("Webhook not found, updating..."); + log::trace!("Webhook ID before the update: {}", opposite_channel.webhook_id.unwrap_or(0)); + opposite_channel.update_webhook(&rt.state.db, &rt.http).await?; + log::trace!("Webhook ID after the update: {}", opposite_channel.webhook_id.unwrap_or(0)); + } + } + return Ok(()); + } + } + .model() + .await?; message = Message::build() .call_id(call.id) @@ -306,6 +324,13 @@ pub struct CallMessageEditHandler; #[async_trait] impl OnMessageEditHandler for CallMessageEditHandler { async fn on_message_edit(&self, event: &MessageUpdate, rt: Arc>) -> EventResult { + log::trace!( + "Edited message was '{}' sent by {} (bot? {:?})", + event.content, + event.author.id.get(), + event.author.bot + ); + let Some(channel) = Channel::get_by_id(event.channel_id.get(), &rt.state.db).await? else { return Ok(()); }; @@ -346,7 +371,7 @@ impl OnMessageEditHandler for CallMessageEditHandler { rt.http .create_message(event.channel_id) .content( - "❌ Your message's edit has been blocked because it broke Payphone's code of conduct. Check your DMs for more information.", + "🚩 Your message's edit has been blocked because it broke Payphone's code of conduct. Check your DMs for more information.", ) .reply(event.id) .await?; diff --git a/src/groups/calls/lists/explicit-sexual-language.txt b/src/groups/calls/lists/explicit-sexual-language.txt index 3db96cb..4d97380 100644 --- a/src/groups/calls/lists/explicit-sexual-language.txt +++ b/src/groups/calls/lists/explicit-sexual-language.txt @@ -1,5 +1,7 @@ sex +segs esex +esegs give head gave head give me head @@ -35,7 +37,11 @@ stick it in yr pussy cock dick dih -pussy +pusy +posay +vagina +vgina +vgna dck psy pu-c @@ -92,6 +98,7 @@ masturbate masturbating masterbating masterbaiting +masturbatigng horny hrny whore diff --git a/src/groups/calls/lists/hate-speech-and-slurs.txt b/src/groups/calls/lists/hate-speech-and-slurs.txt index a86ad49..f503843 100644 --- a/src/groups/calls/lists/hate-speech-and-slurs.txt +++ b/src/groups/calls/lists/hate-speech-and-slurs.txt @@ -8,6 +8,7 @@ qiga niqa nqa nlger +ngiga digga fag fagot @@ -45,4 +46,5 @@ get the fvck out get the fck out get tfo blackie -motherfucker \ No newline at end of file +motherfucker +chinese ape \ No newline at end of file diff --git a/src/groups/calls/lists/threats-and-self-harm.txt b/src/groups/calls/lists/threats-and-self-harm.txt index 40b8551..159d95a 100644 --- a/src/groups/calls/lists/threats-and-self-harm.txt +++ b/src/groups/calls/lists/threats-and-self-harm.txt @@ -12,6 +12,3 @@ rape u slit grape graped -throat slit -slit your throat -slit ur throat \ No newline at end of file diff --git a/src/groups/leveling/events.rs b/src/groups/leveling/events.rs new file mode 100644 index 0000000..f0b9e3b --- /dev/null +++ b/src/groups/leveling/events.rs @@ -0,0 +1,80 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use deadpool_redis::redis::{AsyncTypedCommands, IntegerReplyOrNoOp}; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha20Rng; +use time::{Date, Month, OffsetDateTime, Time}; +use twilight_model::gateway::payload::incoming::MessageCreate; + +use crate::{ + database::models::Channel, + framework::{ + events::traits::{EventResult, OnMessageHandler}, + runtime::Runtime, + }, + state::State, +}; + +const KEY_LEADERBOARD_MONTHLY: &str = "lvl:monthly"; + +fn seconds_until_end_of_month() -> i64 { + let now = OffsetDateTime::now_utc(); + let year = now.year(); + let month = now.month(); + + // figure out the first day of the next month + let (next_year, next_month) = if month == Month::December { + (year + 1, Month::January) + } else { + (year, month.next()) + }; + + let first_of_next_month = Date::from_calendar_date(next_year, next_month, 1) + .unwrap() + .with_time(Time::MIDNIGHT) + .assume_utc(); + + (first_of_next_month - now).whole_seconds() +} + +#[derive(Debug)] +pub struct MessageLevelingHandler; + +#[async_trait] +impl OnMessageHandler for MessageLevelingHandler { + async fn on_message(&self, event: &MessageCreate, rt: Arc>) -> EventResult { + let Some(channel) = Channel::get_by_id(event.channel_id.get(), &rt.state.db).await? else { + return Ok(()); + }; + + if channel.call_id.is_none() { + return Ok(()); + } + + let mut redis = rt.state.redis.get().await?; + + let key = format!("lvl:{}", event.author.id); + + let lock = redis.exists(&key).await?; + + if lock { + return Ok(()); + } + + let xp = ChaCha20Rng::from_os_rng().random_range(10..=25); + + redis.set_ex(&key, true, 60).await?; + redis + .zincr(KEY_LEADERBOARD_MONTHLY, event.author.id.to_string(), xp) + .await?; + + let leaderboard_exat = redis.ttl(KEY_LEADERBOARD_MONTHLY).await?; + + if leaderboard_exat == IntegerReplyOrNoOp::ExistsButNotRelevant { + redis.expire(key, seconds_until_end_of_month()).await?; + } + + Ok(()) + } +} diff --git a/src/groups/leveling/mod.rs b/src/groups/leveling/mod.rs new file mode 100644 index 0000000..a9970c2 --- /dev/null +++ b/src/groups/leveling/mod.rs @@ -0,0 +1 @@ +pub mod events; diff --git a/src/groups/mod.rs b/src/groups/mod.rs index 128bfcc..1d8149d 100644 --- a/src/groups/mod.rs +++ b/src/groups/mod.rs @@ -17,5 +17,6 @@ pub mod about; pub mod calls; pub mod gateway; -pub mod servers; +pub mod leveling; pub mod moderation; +pub mod servers; diff --git a/src/main.rs b/src/main.rs index 449a3f7..7512022 100644 --- a/src/main.rs +++ b/src/main.rs @@ -125,16 +125,13 @@ use crate::{ runtime::Runtime, }, groups::{ - about::{commands::HelpCommand, components::HelpGoBackButton}, - calls::{ + about::{commands::HelpCommand, components::HelpGoBackButton}, calls::{ commands::{CallCommand, FriendRequestCommand, HangupCommand, SkipCommand}, events::{CallMessageDeleteHandler, CallMessageEditHandler, CallMessageHandler}, - }, - gateway::events::{ReadyHandler, ReconnectHandler}, - servers::{ + }, gateway::events::{ReadyHandler, ReconnectHandler}, leveling::events::MessageLevelingHandler, servers::{ commands::PrefixCommand, events::{ServerCreateHandler, ServerDeleteHandler, ServerUpdateHandler}, - }, + } }, state::State, }; @@ -146,6 +143,7 @@ mod discord; mod framework; mod groups; mod logging; +mod redis; mod state; mod utils; @@ -169,6 +167,7 @@ pub async fn main() { database_username, database_password, shard, + redis_urls, } => { start( token, @@ -176,6 +175,7 @@ pub async fn main() { database_username, database_password, shard.into(), + redis_urls, ) .await; } @@ -217,6 +217,7 @@ async fn start( database_username: Option, database_password: Option, shard: ShardId, + redis_urls: Vec, ) { log::info!("Welcome to Payphone, Nyeki!"); log::info!( @@ -230,7 +231,14 @@ async fn start( log::info!("Database connection established successfully."); - let state = State { db }; + let redis_pool = redis::init(redis_urls).expect("Could not connect to Redis!"); + + log::info!("Redis connection established successfully."); + + let state = State { + db, + redis: redis_pool, + }; let bot = Bot::new(state, ServerPrefixOrPayphoneMention) .shard(shard) @@ -252,6 +260,7 @@ async fn start( .on_ready(ReadyHandler) .on_reconnect(ReconnectHandler) .on_message(CallMessageHandler) + .on_message(MessageLevelingHandler) .on_message_edit(CallMessageEditHandler) .on_message_delete(CallMessageDeleteHandler) .on_server_create(ServerCreateHandler) diff --git a/src/redis.rs b/src/redis.rs new file mode 100644 index 0000000..e028e21 --- /dev/null +++ b/src/redis.rs @@ -0,0 +1,8 @@ +use deadpool_redis::{ + Runtime, + cluster::{Config, CreatePoolError, Pool}, +}; + +pub fn init(urls: Vec) -> Result { + Config::from_urls(urls).create_pool(Some(Runtime::Tokio1)) +} diff --git a/src/state.rs b/src/state.rs index c7f44d1..9edf21f 100644 --- a/src/state.rs +++ b/src/state.rs @@ -18,8 +18,10 @@ //! } //! ``` +use deadpool_redis::cluster::Pool; use scylla::client::session::Session; pub struct State { pub db: Session, + pub redis: Pool, }