diff options
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | Cargo.lock | 120 | ||||
| -rw-r--r-- | Cargo.toml | 3 | ||||
| -rw-r--r-- | flake.lock | 34 | ||||
| -rw-r--r-- | flake.nix | 5 | ||||
| -rw-r--r-- | parts/dev.nix | 5 | ||||
| -rw-r--r-- | parts/module.nix | 15 | ||||
| -rw-r--r-- | src/handlers/error.rs | 6 | ||||
| -rw-r--r-- | src/handlers/event/message.rs | 16 | ||||
| -rw-r--r-- | src/handlers/event/mod.rs | 10 | ||||
| -rw-r--r-- | src/handlers/event/pinboard.rs | 31 | ||||
| -rw-r--r-- | src/handlers/event/reactboard.rs | 143 | ||||
| -rw-r--r-- | src/main.rs | 8 | ||||
| -rw-r--r-- | src/settings.rs | 10 | ||||
| -rw-r--r-- | src/utils.rs | 2 |
15 files changed, 356 insertions, 54 deletions
@@ -29,3 +29,5 @@ result .vs/ .vscode/ +# redis +dump.rdb @@ -249,6 +249,20 @@ dependencies = [ ] [[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] name = "core-foundation" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -901,6 +915,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] name = "ordered-float" version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1137,6 +1157,53 @@ dependencies = [ ] [[package]] +name = "redis" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f49cdc0bb3f412bf8e7d1bd90fe1d9eb10bc5c399ba90973c14662a27b3f8ba" +dependencies = [ + "async-trait", + "bytes", + "combine", + "futures-util", + "itoa", + "percent-encoding", + "pin-project-lite", + "rustls 0.21.8", + "rustls-native-certs", + "ryu", + "sha1_smol", + "socket2 0.4.10", + "tokio", + "tokio-rustls 0.24.1", + "tokio-util", + "url", +] + +[[package]] +name = "redis-macros" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60eb39e2b44d4c0f9c84e7c5fc4fc3adc8dd26ec48f1ac3a160033f7c03b18fd" +dependencies = [ + "redis", + "redis-macros-derive", + "serde", + "serde_json", +] + +[[package]] +name = "redis-macros-derive" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39550b9e94ce430a349c5490ca4efcae90ab8189603320f88c1d69f0326f169e" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] name = "redox_syscall" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1290,6 +1357,18 @@ dependencies = [ ] [[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] name = "rustls-pemfile" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1321,6 +1400,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys", +] + +[[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1337,6 +1425,29 @@ dependencies = [ ] [[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] name = "serde" version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1433,6 +1544,12 @@ dependencies = [ ] [[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[package]] name = "sharded-slab" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1571,8 +1688,11 @@ dependencies = [ "once_cell", "poise", "rand 0.8.5", + "redis", + "redis-macros", "reqwest", "serde", + "serde_json", "tokio", "url", ] @@ -18,11 +18,14 @@ log = "0.4.20" poise = "0.5.7" once_cell = "1.18.0" rand = "0.8.5" +redis = { version = "0.23.3", features = ["tokio-comp", "tokio-rustls-comp"] } +redis-macros = "0.2.1" reqwest = { version = "0.11.22", default-features = false, features = [ "rustls-tls", "json", ] } serde = "1.0.193" +serde_json = "1.0.108" tokio = { version = "1.33.0", features = [ "macros", "rt-multi-thread", @@ -37,6 +37,21 @@ "type": "github" } }, + "flake-root": { + "locked": { + "lastModified": 1692742795, + "narHash": "sha256-f+Y0YhVCIJ06LemO+3Xx00lIcqQxSKJHXT/yk1RTKxw=", + "owner": "srid", + "repo": "flake-root", + "rev": "d9a70d9c7a5fd7f3258ccf48da9335e9b47c3937", + "type": "github" + }, + "original": { + "owner": "srid", + "repo": "flake-root", + "type": "github" + } + }, "flake-utils": { "inputs": { "systems": "systems" @@ -158,13 +173,30 @@ "type": "github" } }, + "proc-flake": { + "locked": { + "lastModified": 1692742849, + "narHash": "sha256-Nv8SOX+O6twFfPnA9BfubbPLZpqc+UeK6JvIWnWkdb0=", + "owner": "srid", + "repo": "proc-flake", + "rev": "25291b6e3074ad5dd573c1cb7d96110a9591e10f", + "type": "github" + }, + "original": { + "owner": "srid", + "repo": "proc-flake", + "type": "github" + } + }, "root": { "inputs": { "fenix": "fenix", + "flake-root": "flake-root", "naersk": "naersk", "nixpkgs": "nixpkgs", "parts": "parts", - "pre-commit": "pre-commit" + "pre-commit": "pre-commit", + "proc-flake": "proc-flake" } }, "rust-analyzer-src": { @@ -28,6 +28,9 @@ inputs.nixpkgs.follows = "nixpkgs"; }; + proc-flake.url = "github:srid/proc-flake"; + flake-root.url = "github:srid/flake-root"; + pre-commit = { url = "github:cachix/pre-commit-hooks.nix"; inputs.nixpkgs.follows = "nixpkgs"; @@ -39,6 +42,8 @@ parts.lib.mkFlake {inherit inputs;} { imports = [ inputs.pre-commit.flakeModule + inputs.proc-flake.flakeModule + inputs.flake-root.flakeModule ./parts/deployment.nix ./parts/dev.nix diff --git a/parts/dev.nix b/parts/dev.nix index 4cd51d6..5628e33 100644 --- a/parts/dev.nix +++ b/parts/dev.nix @@ -49,12 +49,17 @@ pkgs.linkFarm "ci-gate" paths; }; + proc.groups.daemons.processes = { + redis.command = lib.getExe' pkgs.redis "redis-server"; + }; + devShells = { default = pkgs.mkShell { packages = with pkgs; [ # general actionlint nodePackages_latest.prettier + config.proc.groups.daemons.package # rust cargo diff --git a/parts/module.nix b/parts/module.nix index 5c1af12..5048c13 100644 --- a/parts/module.nix +++ b/parts/module.nix @@ -22,6 +22,18 @@ in { options.services.teawiebot = { enable = mkEnableOption "teawiebot"; package = mkPackageOption self.packages.${pkgs.stdenv.hostPlatform.system} "teawiebot" {}; + + redisUrl = mkOption { + description = mdDoc '' + Redis URL for teawieBot + ''; + type = types.str; + default = "unix:${config.services.redis.servers.teawiebot.unixSocket}"; + example = literalExpression '' + "redis://localhost/" + ''; + }; + environmentFile = mkOption { description = mdDoc '' Environment file as defined in {manpage}`systemd.exec(5)` @@ -35,6 +47,8 @@ in { }; config = mkIf cfg.enable { + services.redis.servers.teawiebot.enable = true; + systemd.services."teawiebot" = { enable = true; wantedBy = mkDefault ["multi-user.target"]; @@ -48,6 +62,7 @@ in { Restart = "always"; EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; + Environment = ["REDIS_URL=${cfg.redisUrl}"]; # hardening DynamicUser = true; diff --git a/src/handlers/error.rs b/src/handlers/error.rs index b4e1361..e256bcf 100644 --- a/src/handlers/error.rs +++ b/src/handlers/error.rs @@ -11,7 +11,7 @@ pub async fn handle(error: poise::FrameworkError<'_, Data, Report>) { FrameworkError::Setup { error, .. } => error!("error setting up client! {error:#?}"), FrameworkError::Command { error, ctx } => { - error!("error in command {}:\n{error:?}", ctx.command().name); + error!("Error in command {}:\n{error:?}", ctx.command().name); ctx.send(|c| { c.embed(|e| { e.title("Something went wrong!") @@ -30,12 +30,12 @@ pub async fn handle(error: poise::FrameworkError<'_, Data, Report>) { event: _, framework: _, } => { - error!("error while handling event:\n{error:#?}"); + error!("Error while handling event:\n{error:#?}"); } error => { if let Err(e) = poise::builtins::on_error(error).await { - error!("error while handling an error: {}", e); + error!("Unhandled error occured:\n{e:#?}"); } } } diff --git a/src/handlers/event/message.rs b/src/handlers/event/message.rs index 88faf85..c90ae3a 100644 --- a/src/handlers/event/message.rs +++ b/src/handlers/event/message.rs @@ -1,8 +1,8 @@ -use crate::settings::Settings; +use crate::Settings; use crate::{consts, Data}; use color_eyre::eyre::{Report, Result}; -use log::*; +use log::info; use poise::serenity_prelude::{Context, Message}; use poise::FrameworkContext; @@ -10,9 +10,9 @@ pub async fn handle( ctx: &Context, framework: FrameworkContext<'_, Data, Report>, msg: &Message, - settings: &Settings, + data: &Data, ) -> Result<()> { - if should_echo(framework, msg, settings) { + if should_echo(framework, msg, &data.settings) { msg.reply(ctx, &msg.content).await?; } @@ -25,8 +25,12 @@ fn should_echo( settings: &Settings, ) -> bool { let gid = msg.guild_id.unwrap_or_default(); - if msg.author.id == framework.bot_id || !settings.is_guild_allowed(gid) { - info!("not running copypasta command in {gid}"); + if msg.author.id == framework.bot_id { + info!("I don't like repeating myself..."); + } + + if !settings.is_guild_allowed(gid) { + info!("Not echoing in guild {gid}"); return false; } diff --git a/src/handlers/event/mod.rs b/src/handlers/event/mod.rs index bbfc642..a587c77 100644 --- a/src/handlers/event/mod.rs +++ b/src/handlers/event/mod.rs @@ -16,18 +16,16 @@ pub async fn handle( ) -> Result<()> { match event { Event::Ready { data_about_bot } => { - log::info!("logged in as {}", data_about_bot.user.name) + log::info!("Logged in as {}!", data_about_bot.user.name) } Event::Message { new_message } => { - message::handle(ctx, framework, new_message, &data.settings).await? + message::handle(ctx, framework, new_message, data).await? } - Event::ChannelPinsUpdate { pin } => pinboard::handle(ctx, pin, &data.settings).await, + Event::ChannelPinsUpdate { pin } => pinboard::handle(ctx, pin, data).await?, - Event::ReactionAdd { add_reaction } => { - reactboard::handle(ctx, add_reaction, &data.settings).await? - } + Event::ReactionAdd { add_reaction } => reactboard::handle(ctx, add_reaction, data).await?, _ => {} } diff --git a/src/handlers/event/pinboard.rs b/src/handlers/event/pinboard.rs index 0c87a5b..33d2680 100644 --- a/src/handlers/event/pinboard.rs +++ b/src/handlers/event/pinboard.rs @@ -1,15 +1,15 @@ -use crate::settings::Settings; -use crate::utils; +use crate::{utils, Data}; +use color_eyre::eyre::{eyre, Context as _, Result}; use log::*; use poise::serenity_prelude::model::prelude::*; use poise::serenity_prelude::Context; -pub async fn handle(ctx: &Context, pin: &ChannelPinsUpdateEvent, settings: &Settings) { - if let Some(sources) = &settings.pinboard_sources { +pub async fn handle(ctx: &Context, pin: &ChannelPinsUpdateEvent, data: &Data) -> Result<()> { + if let Some(sources) = &data.settings.pinboard_sources { if !sources.contains(&pin.channel_id) { - warn!("can't access source of pin!"); - return; + warn!("Can't access source of pin!"); + return Ok(()); } } @@ -18,16 +18,23 @@ pub async fn handle(ctx: &Context, pin: &ChannelPinsUpdateEvent, settings: &Sett .channel_id .pins(&ctx.http) .await - .expect("couldn't get a list of pins!?"); + .expect("Couldn't get a list of pins!?"); for pin in pins { // We call `take` because it's supposed to be just for the latest message. - redirect(ctx, &pin, pinner.take(), settings.pinboard_target).await; - pin.unpin(&ctx).await.expect("couldn't unpin message"); + redirect(ctx, &pin, pinner.take(), data.settings.pinboard_target).await?; + pin.unpin(&ctx).await?; } + + Ok(()) } -async fn redirect(ctx: &Context, pin: &Message, pinner: Option<UserId>, target: ChannelId) { +async fn redirect( + ctx: &Context, + pin: &Message, + pinner: Option<UserId>, + target: ChannelId, +) -> Result<()> { let pinner = pinner.map_or("*someone*".to_owned(), |u| format!("<@{u}>")); let embed = utils::resolve_message_to_embed(ctx, pin).await; @@ -38,7 +45,9 @@ async fn redirect(ctx: &Context, pin: &Message, pinner: Option<UserId>, target: .set_embed(embed) }) .await - .expect("couldn't redirect message"); + .wrap_err_with(|| eyre!("couldn't redirect message"))?; + + Ok(()) } /// (Desperate, best-effort) attempt to get the user that pinned the last message diff --git a/src/handlers/event/reactboard.rs b/src/handlers/event/reactboard.rs index 3972931..2a417da 100644 --- a/src/handlers/event/reactboard.rs +++ b/src/handlers/event/reactboard.rs @@ -1,14 +1,32 @@ -use crate::{settings::Settings, utils}; +use crate::{utils, Data}; use color_eyre::eyre::{eyre, Context as _, Result}; use log::*; -use poise::serenity_prelude::{Context, Message, MessageReaction, Reaction}; +use poise::serenity_prelude::{ChannelId, Context, Message, MessageId, MessageReaction, Reaction}; +use redis::AsyncCommands as _; +use redis_macros::{FromRedisValue, ToRedisArgs}; +use serde::{Deserialize, Serialize}; -pub async fn handle(ctx: &Context, reaction: &Reaction, settings: &Settings) -> Result<()> { +#[derive(Clone, Debug, Serialize, Deserialize, FromRedisValue, ToRedisArgs)] +struct ReactBoardEntry { + original_id: MessageId, + reaction_count: u64, + channel_id: ChannelId, + message_id: MessageId, +} + +#[derive(Default, Serialize, Deserialize, FromRedisValue, ToRedisArgs)] +struct ReactBoardInfo { + reactions: Vec<ReactBoardEntry>, +} + +const REACT_BOARD_KEY: &str = "reactboard-v1"; + +pub async fn handle(ctx: &Context, reaction: &Reaction, data: &Data) -> Result<()> { let msg = reaction .message(&ctx.http) .await - .wrap_err("couldn't get reaction from message!")?; + .wrap_err_with(|| "Couldn't get reaction from message!")?; let matched = msg .clone() @@ -17,13 +35,13 @@ pub async fn handle(ctx: &Context, reaction: &Reaction, settings: &Settings) -> .find(|r| r.reaction_type == reaction.emoji) .ok_or_else(|| { eyre!( - "couldn't find any matching reactions for {} in message {}!", + "Couldn't find any matching reactions for {} in message {}!", reaction.emoji.as_data(), msg.id ) })?; - send_to_reactboard(ctx, &matched, &msg, settings).await?; + send_to_reactboard(ctx, &matched, &msg, data).await?; Ok(()) } @@ -32,32 +50,117 @@ async fn send_to_reactboard( ctx: &Context, reaction: &MessageReaction, msg: &Message, - settings: &Settings, + data: &Data, ) -> Result<()> { - if !settings.can_use_reaction(reaction) { - info!("reaction {} can't be used!", reaction.reaction_type); + // make sure everything is in order... + if !data.settings.can_use_reaction(reaction) { + info!("Reaction {} can't be used!", reaction.reaction_type); return Ok(()); } - if reaction.count == settings.reactboard_requirement.unwrap_or(5) { + if reaction.count < data.settings.reactboard_requirement.unwrap_or(5) { + info!( + "Ignoring message {} on reactboard, not enough reactions", + msg.id + ); + return Ok(()); + } + + let mut con = data.redis.get_async_connection().await?; + let req = con.get(REACT_BOARD_KEY).await; + + let mut reactboard: ReactBoardInfo = if let Err(why) = req { + // set the value to the default if the key is uninitialized + match why.kind() { + redis::ErrorKind::TypeError => { + warn!("Initializing {REACT_BOARD_KEY} key in Redis..."); + con.set(REACT_BOARD_KEY, ReactBoardInfo::default()).await?; + con.get(REACT_BOARD_KEY).await? + } + _ => return Err(why.into()), + } + } else { + req? + }; + + // try to find previous reactboard entry by the id of the original message + let old_index = reactboard + .reactions + .iter() + .position(|r| r.original_id == msg.id); + + let content = format!("{} **#{}**", reaction.reaction_type, reaction.count); + + // bump reaction count if previous entry exists + if let Some(old_index) = old_index { + let old_entry = reactboard.reactions[old_index].clone(); + + // bail if we don't need to edit anything + if old_entry.reaction_count >= reaction.count { + info!("Message {} doesn't need updating", msg.id); + return Ok(()); + } + + info!( + "Bumping {} reaction count from {} to {}", + msg.id, old_entry.reaction_count, reaction.count + ); + + ctx.http + .get_message( + *old_entry.channel_id.as_u64(), + *old_entry.message_id.as_u64(), + ) + .await + .wrap_err_with(|| { + format!( + "Couldn't get previous message from ReactBoardEntry {} in Redis DB!", + old_entry.original_id + ) + })? + .edit(ctx, |m| m.content(content)) + .await?; + + // update reaction count in redis + let mut new_entry = old_entry.clone(); + new_entry.reaction_count = reaction.count; + + reactboard.reactions.remove(old_index); + reactboard.reactions.push(new_entry.clone()); + + info!( + "Updating ReactBoard entry {} in {REACT_BOARD_KEY}\nOld:\n{old_entry:#?}\nNew:\n{new_entry:#?}", + msg.id + ); + con.set(REACT_BOARD_KEY, reactboard).await?; + // make new message and add entry to redis otherwise + } else { let embed = utils::resolve_message_to_embed(ctx, msg).await; - settings + let resp = data + .settings .reactboard_target - .send_message(&ctx.http, |m| { + .send_message(ctx, |m| { m.allowed_mentions(|am| am.empty_parse()) - .content(format!( - "{} **#{}**", - reaction.reaction_type, reaction.count - )) + .content(content) .set_embed(embed) }) .await?; - } else { + + let entry = ReactBoardEntry { + original_id: msg.id, + reaction_count: reaction.count, + channel_id: resp.channel_id, + message_id: resp.id, + }; + + reactboard.reactions.push(entry.clone()); + info!( - "not putting message {} on reactboard, not enough reactions", - msg.id - ) + "Creating new ReactBoard entry {} in {REACT_BOARD_KEY}:\n{:#?}", + msg.id, entry + ); + con.set(REACT_BOARD_KEY, reactboard).await?; } Ok(()) diff --git a/src/main.rs b/src/main.rs index 0e9400b..6dc99a1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,7 @@ type Context<'a> = poise::Context<'a, Data, Report>; #[derive(Clone)] pub struct Data { settings: Settings, + redis: redis::Client, } impl Data { @@ -27,7 +28,12 @@ impl Data { let settings = Settings::new().ok_or_else(|| eyre!("Couldn't create new settings object!"))?; - Ok(Self { settings }) + let redis_url = std::env::var("REDIS_URL") + .wrap_err_with(|| eyre!("Couldn't find Redis URL in environment!"))?; + + let redis = redis::Client::open(redis_url)?; + + Ok(Self { settings, redis }) } } diff --git a/src/settings.rs b/src/settings.rs index 5cf0dec..406b990 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -45,17 +45,17 @@ impl Settings { }) .unwrap_or_default(); - info!("pinboard target is {}", pinboard_target); + info!("PinBoard target is {}", pinboard_target); if let Some(sources) = &pinboard_sources { - info!("pinboard sources are {:#?}", sources); + info!("PinBoard sources are {:#?}", sources); } - info!("reactboard target is {}", reactboard_target); + info!("ReactBoard target is {}", reactboard_target); info!( - "reactboard custom reactions are {:#?}", + "ReactBoard custom reactions are {:#?}", reactboard_custom_reactions ); info!( - "reactboard unicode reactions are {:#?}", + "ReactBoard unicode reactions are {:#?}", reactboard_unicode_reactions ); diff --git a/src/utils.rs b/src/utils.rs index c0353f0..10140f4 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -24,7 +24,7 @@ pub fn random_choice<const N: usize>(arr: [&str; N]) -> Result<String> { let mut rng = rand::thread_rng(); let resp = arr .choose(&mut rng) - .ok_or_else(|| eyre!("couldn't choose from array!"))?; + .ok_or_else(|| eyre!("Couldn't choose random object from array:\n{arr:#?}!"))?; Ok((*resp).to_string()) } |
