diff options
| author | seth <[email protected]> | 2023-12-05 05:17:49 -0500 |
|---|---|---|
| committer | seth <[email protected]> | 2023-12-15 16:41:13 -0500 |
| commit | 815cb0df3b3e3f9dd2078b00f85754da87b1d55e (patch) | |
| tree | 85099483f8ebb0586bc097b65f6c5a2b5997150e /src | |
| parent | 0ca61ddff6ec7404f0aeabc1c8c785bbc8db7fd5 (diff) | |
refactor: centralize storage handlers
Diffstat (limited to 'src')
| -rw-r--r-- | src/commands/general/ask.rs | 1 | ||||
| -rw-r--r-- | src/commands/general/bing.rs | 1 | ||||
| -rw-r--r-- | src/commands/general/convert.rs | 1 | ||||
| -rw-r--r-- | src/commands/general/random.rs | 1 | ||||
| -rw-r--r-- | src/commands/mod.rs | 2 | ||||
| -rw-r--r-- | src/commands/moderation/actions.rs | 7 | ||||
| -rw-r--r-- | src/commands/moderation/config.rs | 30 | ||||
| -rw-r--r-- | src/commands/optional/copypasta.rs | 8 | ||||
| -rw-r--r-- | src/commands/optional/teawiespam.rs | 6 | ||||
| -rw-r--r-- | src/handlers/event/guild.rs | 32 | ||||
| -rw-r--r-- | src/handlers/event/message.rs | 4 | ||||
| -rw-r--r-- | src/handlers/event/mod.rs | 14 | ||||
| -rw-r--r-- | src/handlers/event/pinboard.rs | 8 | ||||
| -rw-r--r-- | src/handlers/event/reactboard.rs | 57 | ||||
| -rw-r--r-- | src/main.rs | 40 | ||||
| -rw-r--r-- | src/settings.rs | 88 | ||||
| -rw-r--r-- | src/storage/mod.rs | 146 | ||||
| -rw-r--r-- | src/storage/reactboard.rs | 18 | ||||
| -rw-r--r-- | src/storage/settings.rs | 37 |
19 files changed, 301 insertions, 200 deletions
diff --git a/src/commands/general/ask.rs b/src/commands/general/ask.rs index 4bbf82e..e1f008a 100644 --- a/src/commands/general/ask.rs +++ b/src/commands/general/ask.rs @@ -1,4 +1,5 @@ use crate::{consts, utils, Context}; + use color_eyre::eyre::{Context as _, Result}; /// ask teawie a question! diff --git a/src/commands/general/bing.rs b/src/commands/general/bing.rs index fefbaf1..b80ebca 100644 --- a/src/commands/general/bing.rs +++ b/src/commands/general/bing.rs @@ -1,4 +1,5 @@ use crate::Context; + use color_eyre::eyre::Result; /// make sure the wie is alive diff --git a/src/commands/general/convert.rs b/src/commands/general/convert.rs index 60135c4..cbbf8dc 100644 --- a/src/commands/general/convert.rs +++ b/src/commands/general/convert.rs @@ -1,4 +1,5 @@ use crate::Context; + use bottomify::bottom; use color_eyre::eyre::Result; diff --git a/src/commands/general/random.rs b/src/commands/general/random.rs index 9aa282a..9595d09 100644 --- a/src/commands/general/random.rs +++ b/src/commands/general/random.rs @@ -1,4 +1,5 @@ use crate::{api, consts, utils, Context}; + use color_eyre::eyre::Result; #[poise::command(slash_command, subcommands("lore", "teawie", "shiggy"))] diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 5e6419c..833df38 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -20,6 +20,6 @@ pub fn to_global_commands() -> Vec<Command<Data, Report>> { ] } -pub fn to_guild_commands() -> Vec<Command<Data, Report>> { +pub fn to_optional_commands() -> Vec<Command<Data, Report>> { vec![optional::copypasta(), optional::teawiespam()] } diff --git a/src/commands/moderation/actions.rs b/src/commands/moderation/actions.rs index 1050656..4d4d0f8 100644 --- a/src/commands/moderation/actions.rs +++ b/src/commands/moderation/actions.rs @@ -2,6 +2,7 @@ use crate::colors::Colors; use crate::Context; use color_eyre::eyre::{eyre, Result}; +use log::*; use poise::serenity_prelude::{CreateEmbed, User}; fn create_moderation_embed( @@ -23,7 +24,7 @@ fn create_moderation_embed( |e: &mut CreateEmbed| e.title(title).fields(fields).color(Colors::Red) } -// ban a user +/// ban a user #[poise::command( slash_command, prefix_command, @@ -42,6 +43,7 @@ pub async fn ban_user( let reason = reason.unwrap_or("n/a".to_string()); + debug!("Banning user {} with reason {reason}", user.id); if reason != "n/a" { guild.ban_with_reason(ctx, &user, days, &reason).await?; } else { @@ -55,7 +57,7 @@ pub async fn ban_user( Ok(()) } -// kick a user +/// kick a user #[poise::command( slash_command, prefix_command, @@ -68,6 +70,7 @@ pub async fn kick_user(ctx: Context<'_>, user: User, reason: Option<String>) -> let reason = reason.unwrap_or("n/a".to_string()); + debug!("Kicking user {} for reason {reason}", user.id); if reason != "n/a" { guild.kick_with_reason(ctx, &user, &reason).await?; } else { diff --git a/src/commands/moderation/config.rs b/src/commands/moderation/config.rs index 2d1410c..d64c4cc 100644 --- a/src/commands/moderation/config.rs +++ b/src/commands/moderation/config.rs @@ -1,5 +1,5 @@ -use crate::settings::{Settings, SettingsProperties}; -use crate::Context; +use crate::{storage, Context}; +use storage::SettingsProperties; use color_eyre::eyre::{eyre, Context as _, ContextCompat, Result}; use log::*; @@ -33,9 +33,9 @@ pub async fn set( #[description = "Enables 'extra' commands like teawiespam and copypasta. Defaults to false."] optional_commands_enabled: Option<bool>, ) -> Result<()> { - let redis = &ctx.data().redis; + let storage = &ctx.data().storage; let gid = ctx.guild_id().unwrap_or_default(); - let mut settings = Settings::from_redis(redis, &gid).await?; + let mut settings = storage.get_guild_settings(&gid).await?; let previous_settings = settings.clone(); if let Some(channel) = pinboard_channel { @@ -48,24 +48,21 @@ pub async fn set( settings.pinboard_watch = Some(prev); } else { let new = Vec::from([watch.id]); - debug!("Setting pinboard_watch to {new:#?} for {} in Redis", gid); + debug!("Setting pinboard_watch to {new:#?} for {}", gid); settings.pinboard_watch = Some(new); } } if let Some(channel) = reactboard_channel { - debug!( - "Setting reactboard_channel to {channel} for {} in Redis", - gid - ); + debug!("Setting reactboard_channel to {channel} for {}", gid); settings.reactboard_channel = Some(channel.id); } if let Some(requirement) = reactboard_requirement { debug!( - "Setting reactboard_requirement to {requirement} for {} in Redis", + "Setting reactboard_requirement to {requirement} for {}", gid ); @@ -82,25 +79,24 @@ pub async fn set( settings.reactboard_reactions = Some(prev); } else { let new = Vec::from([emoji]); - debug!("Setting pinboard_watch to {new:#?} for {} in Redis", gid); + debug!("Setting pinboard_watch to {new:#?} for {}", gid); settings.reactboard_reactions = Some(new); } } if let Some(enabled) = optional_commands_enabled { - debug!( - "Setting optional_commands_enabled to {enabled} for {} in Redis", - gid - ); + debug!("Setting optional_commands_enabled to {enabled} for {}", gid); settings.optional_commands_enabled = enabled; } if previous_settings != settings { - settings.save(redis).await?; + debug!("Updating settings key for {gid}"); + storage.create_settings_key(settings).await?; ctx.reply("Configuration updated!").await?; } else { + debug!("Not updating settings key for {gid} since no changes were made"); ctx.reply("No changes made, so i'm not updating anything") .await?; } @@ -117,7 +113,7 @@ pub async fn get( .guild_id() .wrap_err_with(|| eyre!("Failed to get GuildId from context!"))?; - let settings = Settings::from_redis(&ctx.data().redis, gid).await?; + let settings = ctx.data().storage.get_guild_settings(gid).await?; let value = match setting { SettingsProperties::GuildId => settings.guild_id.to_string(), diff --git a/src/commands/optional/copypasta.rs b/src/commands/optional/copypasta.rs index ea23f5f..289a936 100644 --- a/src/commands/optional/copypasta.rs +++ b/src/commands/optional/copypasta.rs @@ -1,4 +1,4 @@ -use crate::{Context, Settings}; +use crate::Context; use std::collections::HashMap; @@ -66,11 +66,13 @@ pub async fn copypasta( ctx: Context<'_>, #[description = "the copypasta you want to send"] copypasta: Copypastas, ) -> Result<()> { + debug!("Running copypasta command with copypasta {copypasta}"); + let gid = ctx.guild_id().unwrap_or_default(); - let settings = Settings::from_redis(&ctx.data().redis, &gid).await?; + let settings = ctx.data().storage.get_guild_settings(&gid).await?; if !settings.optional_commands_enabled { - debug!("Not running copypasta command in {gid} since it's disabled"); + debug!("Exited copypasta command in {gid} since it's disabled"); return Ok(()); } diff --git a/src/commands/optional/teawiespam.rs b/src/commands/optional/teawiespam.rs index c1b3b29..bb8f32d 100644 --- a/src/commands/optional/teawiespam.rs +++ b/src/commands/optional/teawiespam.rs @@ -1,4 +1,4 @@ -use crate::{Context, Settings}; +use crate::Context; use color_eyre::eyre::Result; use log::*; @@ -6,8 +6,10 @@ use log::*; /// teawie will spam you. #[poise::command(slash_command, prefix_command)] pub async fn teawiespam(ctx: Context<'_>) -> Result<()> { + debug!("Running teawiespam command"); + let gid = ctx.guild_id().unwrap_or_default(); - let settings = Settings::from_redis(&ctx.data().redis, &gid).await?; + let settings = ctx.data().storage.get_guild_settings(&gid).await?; if !settings.optional_commands_enabled { debug!("Not running teawiespam in {gid} since it's disabled"); diff --git a/src/handlers/event/guild.rs b/src/handlers/event/guild.rs index b7a4028..3473276 100644 --- a/src/handlers/event/guild.rs +++ b/src/handlers/event/guild.rs @@ -2,25 +2,33 @@ use color_eyre::eyre::Result; use log::*; use poise::serenity_prelude::{Guild, UnavailableGuild}; -use crate::{Data, Settings}; +use crate::{storage, Data}; +use storage::settings::Settings; +use storage::Storage; -pub async fn handle_create(guild: &Guild, is_new: &bool, data: &Data) -> Result<()> { - if !is_new && Settings::from_redis(&data.redis, &guild.id).await.is_ok() { - debug!("Not recreating Redis key for {}", guild.id); +pub async fn handle_create(guild: &Guild, _is_new: &bool, data: &Data) -> Result<()> { + let storage = &data.storage; + let key = Storage::format_settings_key(guild.id); + + if storage.key_exists(&key).await? { + debug!("Not recreating settings key for {}", guild.id); return Ok(()); } - info!("Creating new Redis key for {}", guild.id); - Settings::new_redis(&data.redis, &guild.id).await?; + let settings = Settings { + guild_id: guild.id, + optional_commands_enabled: false, + ..Default::default() + }; + + warn!("Creating new settings key {key}:\n{settings:#?}"); + storage.create_settings_key(settings).await?; + Ok(()) } pub async fn handle_delete(guild: &UnavailableGuild, data: &Data) -> Result<()> { - let redis = &data.redis; - - info!("Deleting redis key for {}", guild.id); - let settings = Settings::from_redis(redis, &guild.id).await?; - settings.delete(redis).await?; - + let key = Storage::format_settings_key(guild.id); + data.storage.delete_key(&key).await?; Ok(()) } diff --git a/src/handlers/event/message.rs b/src/handlers/event/message.rs index dab2047..4fd1323 100644 --- a/src/handlers/event/message.rs +++ b/src/handlers/event/message.rs @@ -1,4 +1,4 @@ -use crate::{consts, Data, Settings}; +use crate::{consts, Data}; use color_eyre::eyre::{eyre, Report, Result}; use log::*; @@ -27,7 +27,7 @@ async fn should_echo(ctx: &Context, msg: &Message, data: &Data) -> Result<bool> let gid = msg .guild_id .ok_or_else(|| eyre!("Couldn't get GuildId from {}!", msg.id))?; - let settings = Settings::from_redis(&data.redis, &gid).await?; + let settings = data.storage.get_guild_settings(&gid).await?; if !settings.optional_commands_enabled { debug!("Not echoing in guild {gid}"); diff --git a/src/handlers/event/mod.rs b/src/handlers/event/mod.rs index 6dd5fe4..6b1fc9b 100644 --- a/src/handlers/event/mod.rs +++ b/src/handlers/event/mod.rs @@ -1,6 +1,8 @@ +use crate::storage::{ReactBoardInfo, REACT_BOARD_KEY}; use crate::Data; use color_eyre::eyre::{Report, Result}; +use log::*; use poise::serenity_prelude as serenity; use poise::{Event, FrameworkContext}; @@ -17,7 +19,17 @@ pub async fn handle( ) -> Result<()> { match event { Event::Ready { data_about_bot } => { - log::info!("Logged in as {}!", data_about_bot.user.name) + info!("Logged in as {}!", data_about_bot.user.name); + + // make sure react board is setup + let storage = &data.storage; + if !storage.key_exists(REACT_BOARD_KEY).await? { + warn!("Creating new ReactBoardInfo key {REACT_BOARD_KEY}"); + + storage + .create_reactboard_info_key(ReactBoardInfo::default()) + .await?; + } } Event::Message { new_message } => { diff --git a/src/handlers/event/pinboard.rs b/src/handlers/event/pinboard.rs index 21e8170..d95cfee 100644 --- a/src/handlers/event/pinboard.rs +++ b/src/handlers/event/pinboard.rs @@ -1,4 +1,4 @@ -use crate::{utils, Data, Settings}; +use crate::{utils, Data}; use color_eyre::eyre::{eyre, Context as _, Result}; use log::*; @@ -7,9 +7,9 @@ use poise::serenity_prelude::Context; pub async fn handle(ctx: &Context, pin: &ChannelPinsUpdateEvent, data: &Data) -> Result<()> { let gid = pin.guild_id.unwrap_or_default(); - let settings = Settings::from_redis(&data.redis, &gid).await?; + let settings = data.storage.get_guild_settings(&gid).await?; - let target = if let Some(target) = settings.reactboard_channel { + let target = if let Some(target) = settings.pinboard_channel { target } else { debug!("PinBoard is disabled in {gid}, ignoring"); @@ -94,7 +94,7 @@ async fn guess_pinner(ctx: &Context, pin: &ChannelPinsUpdateEvent) -> Option<Use .map(|first| first.user_id) } else { // TODO: mayyyyybe we can guess who pinned something in a DM...? - warn!("couldn't figure out who pinned in {}!", pin.channel_id); + warn!("Couldn't figure out who pinned in {}!", pin.channel_id); None } } diff --git a/src/handlers/event/reactboard.rs b/src/handlers/event/reactboard.rs index 2435976..53b51a7 100644 --- a/src/handlers/event/reactboard.rs +++ b/src/handlers/event/reactboard.rs @@ -1,26 +1,9 @@ -use crate::{utils, Data, Settings}; +use crate::{storage, utils, Data}; +use storage::{ReactBoardEntry, REACT_BOARD_KEY}; use color_eyre::eyre::{eyre, Context as _, Result}; use log::*; -use poise::serenity_prelude::{ChannelId, Context, Message, MessageId, MessageReaction, Reaction}; -use redis::AsyncCommands as _; -use redis_macros::{FromRedisValue, ToRedisArgs}; -use serde::{Deserialize, Serialize}; - -#[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"; +use poise::serenity_prelude::{Context, Message, MessageReaction, Reaction}; pub async fn handle(ctx: &Context, reaction: &Reaction, data: &Data) -> Result<()> { let msg = reaction @@ -52,8 +35,9 @@ async fn send_to_reactboard( msg: &Message, data: &Data, ) -> Result<()> { + let storage = &data.storage; let gid = msg.guild_id.unwrap_or_default(); - let settings = Settings::from_redis(&data.redis, &gid).await?; + let settings = storage.get_guild_settings(&gid).await?; // make sure everything is in order... let target = if let Some(target) = settings.reactboard_channel { @@ -76,22 +60,7 @@ async fn send_to_reactboard( 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? - }; + let mut reactboard = storage.get_reactboard_info().await?; // try to find previous reactboard entry by the id of the original message let old_index = reactboard @@ -107,11 +76,11 @@ async fn send_to_reactboard( // bail if we don't need to edit anything if old_entry.reaction_count >= reaction.count { - info!("Message {} doesn't need updating", msg.id); + debug!("Message {} doesn't need updating", msg.id); return Ok(()); } - info!( + debug!( "Bumping {} reaction count from {} to {}", msg.id, old_entry.reaction_count, reaction.count ); @@ -138,11 +107,11 @@ async fn send_to_reactboard( 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:#?}", + debug!( + "Updating ReactBoard entry {}\nOld entry:\n{old_entry:#?}\n\nNew:\n{new_entry:#?}\n", msg.id ); - con.set(REACT_BOARD_KEY, reactboard).await?; + storage.create_reactboard_info_key(reactboard).await?; // make new message and add entry to redis otherwise } else { let embed = utils::resolve_message_to_embed(ctx, msg).await; @@ -164,11 +133,11 @@ async fn send_to_reactboard( reactboard.reactions.push(entry.clone()); - info!( + debug!( "Creating new ReactBoard entry {} in {REACT_BOARD_KEY}:\n{:#?}", msg.id, entry ); - con.set(REACT_BOARD_KEY, reactboard).await?; + storage.create_reactboard_info_key(reactboard).await?; } Ok(()) diff --git a/src/main.rs b/src/main.rs index afedd1a..d355307 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,32 +5,31 @@ use log::*; use poise::{ serenity_prelude as serenity, EditTracker, Framework, FrameworkOptions, PrefixFrameworkOptions, }; -use redis::AsyncCommands; -use settings::Settings; +use storage::Storage; mod api; mod colors; mod commands; mod consts; mod handlers; -mod settings; +mod storage; mod utils; type Context<'a> = poise::Context<'a, Data, Report>; #[derive(Clone)] pub struct Data { - redis: redis::Client, + storage: Storage, } impl Data { pub fn new() -> Result<Self> { let redis_url = std::env::var("REDIS_URL") - .wrap_err_with(|| eyre!("Couldn't find Redis URL in environment!"))?; + .wrap_err_with(|| "Couldn't find Redis URL in environment!")?; - let redis = redis::Client::open(redis_url)?; + let storage = Storage::new(&redis_url)?; - Ok(Self { redis }) + Ok(Self { storage }) } } @@ -75,25 +74,18 @@ async fn main() -> Result<()> { info!("Registered global commands!"); // register "extra" commands in guilds that allow it - let mut con = data.redis.get_async_connection().await?; - - info!("Fetching all guild settings from Redis...this might take a while"); - let guilds: Vec<String> = con.keys(format!("{}:*", settings::ROOT_KEY)).await?; + info!("Fetching opted guilds"); + let guilds = data.storage.get_opted_guilds().await?; for guild in guilds { - let settings: Settings = con.get(guild).await?; - - if settings.optional_commands_enabled { - poise::builtins::register_in_guild( - ctx, - &commands::to_guild_commands(), - settings.guild_id, - ) - .await?; - info!("Registered guild commands to {}", settings.guild_id); - } else { - debug!("Not registering guild commands to {} since optional_commands_enabled is False", settings.guild_id); - } + poise::builtins::register_in_guild( + ctx, + &commands::to_optional_commands(), + guild, + ) + .await?; + + info!("Registered guild commands to {}", guild); } Ok(data) diff --git a/src/settings.rs b/src/settings.rs deleted file mode 100644 index 64cde1f..0000000 --- a/src/settings.rs +++ /dev/null @@ -1,88 +0,0 @@ -use color_eyre::eyre::{Context as _, Result}; -use poise::serenity_prelude::{ChannelId, GuildId, ReactionType}; -use redis::{AsyncCommands as _, Client}; -use redis_macros::{FromRedisValue, ToRedisArgs}; -use serde::{Deserialize, Serialize}; - -pub const ROOT_KEY: &str = "settings-v1"; - -#[derive(poise::ChoiceParameter)] -pub enum SettingsProperties { - GuildId, - PinBoardChannel, - PinBoardWatch, - ReactBoardChannel, - ReactBoardRequirement, - ReactBoardReactions, - OptionalCommandsEnabled, -} - -#[derive(Clone, Default, PartialEq, Serialize, Deserialize, FromRedisValue, ToRedisArgs)] -pub struct Settings { - pub guild_id: GuildId, - pub pinboard_channel: Option<ChannelId>, - pub pinboard_watch: Option<Vec<ChannelId>>, - pub reactboard_channel: Option<ChannelId>, - pub reactboard_requirement: Option<u64>, - pub reactboard_reactions: Option<Vec<ReactionType>>, - pub optional_commands_enabled: bool, -} - -impl Settings { - pub async fn new_redis(redis: &Client, gid: &GuildId) -> Result<()> { - let key = format!("{ROOT_KEY}:{gid}"); - let settings = Self { - guild_id: *gid, - optional_commands_enabled: false, - ..Default::default() - }; - - let mut con = redis.get_async_connection().await?; - con.set(&key, settings) - .await - .wrap_err_with(|| format!("Couldn't set key {key} in Redis!"))?; - - Ok(()) - } - - pub async fn from_redis(redis: &Client, gid: &GuildId) -> Result<Self> { - let key = format!("{ROOT_KEY}:{gid}"); - let mut con = redis.get_async_connection().await?; - - let settings: Settings = con - .get(&key) - .await - .wrap_err_with(|| format!("Couldn't get {key} from Redis!"))?; - - Ok(settings) - } - - pub async fn delete(&self, redis: &Client) -> Result<()> { - let key = format!("{ROOT_KEY}:{}", self.guild_id); - let mut con = redis.get_async_connection().await?; - - con.del(&key) - .await - .wrap_err_with(|| format!("Couldn't delete {key} from Redis!"))?; - - Ok(()) - } - - pub async fn save(&self, redis: &Client) -> Result<()> { - let key = format!("{ROOT_KEY}:{}", self.guild_id); - let mut con = redis.get_async_connection().await?; - - con.set(&key, self) - .await - .wrap_err_with(|| format!("Couldn't save {key} in Redis!"))?; - Ok(()) - } - - pub fn can_use_reaction(&self, reaction: &ReactionType) -> bool { - if let Some(reactions) = &self.reactboard_reactions { - reactions.iter().any(|r| r == reaction) - } else { - false - } - } -} diff --git a/src/storage/mod.rs b/src/storage/mod.rs new file mode 100644 index 0000000..e6f186a --- /dev/null +++ b/src/storage/mod.rs @@ -0,0 +1,146 @@ +use std::fmt::{Debug, Display}; + +use color_eyre::eyre::Result; +use log::*; +use poise::serenity_prelude::GuildId; +use redis::{AsyncCommands, Client, FromRedisValue, ToRedisArgs}; + +pub mod reactboard; +pub mod settings; + +pub use reactboard::*; +pub use settings::*; + +#[derive(Clone, Debug)] +pub struct Storage { + client: Client, +} + +impl Storage { + pub fn new(redis_url: &str) -> Result<Self> { + let client = Client::open(redis_url)?; + + Ok(Self { client }) + } + + pub async fn get_key<T>(&self, key: &str) -> Result<T> + where + T: FromRedisValue, + { + debug!("Getting key {key}"); + + let mut con = self.client.get_async_connection().await?; + let res: T = con.get(key).await?; + + Ok(res) + } + + pub async fn set_key<'a>( + &self, + key: &str, + value: impl ToRedisArgs + Debug + Send + Sync + 'a, + ) -> Result<()> { + debug!("Creating key {key}:\n{value:#?}"); + + let mut con = self.client.get_async_connection().await?; + con.set(key, value).await?; + + Ok(()) + } + + pub async fn key_exists(&self, key: &str) -> Result<bool> { + debug!("Checking if key {key} exists"); + + let mut con = self.client.get_async_connection().await?; + let exists: u64 = con.exists(key).await?; + + Ok(exists > 0) + } + + pub async fn delete_key(&self, key: &str) -> Result<()> { + debug!("Deleting key {key}"); + + let mut con = self.client.get_async_connection().await?; + con.del(key).await?; + + Ok(()) + } + + pub async fn add_to_index<'a>( + &self, + key: &str, + member: impl ToRedisArgs + Send + Sync + 'a, + ) -> Result<()> { + let index = format!("{key}:index"); + debug!("Appending index {index}"); + + let mut con = self.client.get_async_connection().await?; + con.sadd(index, member).await?; + + Ok(()) + } + + pub fn format_settings_key(subkey: impl Display) -> String { + format!("{}:{subkey}", SETTINGS_KEY) + } + + pub async fn create_settings_key(&self, settings: Settings) -> Result<()> { + let key = Self::format_settings_key(settings.guild_id); + + self.set_key(&key, &settings).await?; + self.add_to_index(SETTINGS_KEY, settings).await?; + + Ok(()) + } + + /// get guilds that have enabled optional commands + pub async fn get_opted_guilds(&self) -> Result<Vec<GuildId>> { + debug!("Fetching opted-in guilds"); + + let guilds = self.get_all_guild_settings().await?; + let opted: Vec<GuildId> = guilds + .iter() + .filter_map(|g| { + if g.optional_commands_enabled { + Some(g.guild_id) + } else { + None + } + }) + .collect(); + + Ok(opted) + } + + pub async fn get_all_guild_settings(&self) -> Result<Vec<Settings>> { + debug!("Fetching all guild settings"); + + let mut con = self.client.get_async_connection().await?; + let key = Self::format_settings_key("index"); + + let guilds: Vec<Settings> = con.smembers(key).await?; + + Ok(guilds) + } + + pub async fn get_guild_settings(&self, guild_id: &GuildId) -> Result<Settings> { + debug!("Fetching guild settings for {guild_id}"); + + let key = Self::format_settings_key(guild_id); + let settings: Settings = self.get_key(&key).await?; + + Ok(settings) + } + + pub async fn create_reactboard_info_key(&self, reactboard: ReactBoardInfo) -> Result<()> { + self.set_key(REACT_BOARD_KEY, reactboard).await?; + Ok(()) + } + + pub async fn get_reactboard_info(&self) -> Result<ReactBoardInfo> { + debug!("Fetching reactboard info"); + let reactboard: ReactBoardInfo = self.get_key(REACT_BOARD_KEY).await?; + + Ok(reactboard) + } +} diff --git a/src/storage/reactboard.rs b/src/storage/reactboard.rs new file mode 100644 index 0000000..e08aa54 --- /dev/null +++ b/src/storage/reactboard.rs @@ -0,0 +1,18 @@ +use poise::serenity_prelude::{ChannelId, MessageId}; +use redis_macros::{FromRedisValue, ToRedisArgs}; +use serde::{Deserialize, Serialize}; + +pub const REACT_BOARD_KEY: &str = "reactboard-v1"; + +#[derive(Clone, Debug, Serialize, Deserialize, FromRedisValue, ToRedisArgs)] +pub struct ReactBoardEntry { + pub original_id: MessageId, + pub reaction_count: u64, + pub channel_id: ChannelId, + pub message_id: MessageId, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, FromRedisValue, ToRedisArgs)] +pub struct ReactBoardInfo { + pub reactions: Vec<ReactBoardEntry>, +} diff --git a/src/storage/settings.rs b/src/storage/settings.rs new file mode 100644 index 0000000..c8a663d --- /dev/null +++ b/src/storage/settings.rs @@ -0,0 +1,37 @@ +use poise::serenity_prelude::{ChannelId, GuildId, ReactionType}; +use redis_macros::{FromRedisValue, ToRedisArgs}; +use serde::{Deserialize, Serialize}; + +pub const SETTINGS_KEY: &str = "settings-v1"; + +#[derive(poise::ChoiceParameter)] +pub enum SettingsProperties { + GuildId, + PinBoardChannel, + PinBoardWatch, + ReactBoardChannel, + ReactBoardRequirement, + ReactBoardReactions, + OptionalCommandsEnabled, +} + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, FromRedisValue, ToRedisArgs)] +pub struct Settings { + pub guild_id: GuildId, + pub pinboard_channel: Option<ChannelId>, + pub pinboard_watch: Option<Vec<ChannelId>>, + pub reactboard_channel: Option<ChannelId>, + pub reactboard_requirement: Option<u64>, + pub reactboard_reactions: Option<Vec<ReactionType>>, + pub optional_commands_enabled: bool, +} + +impl Settings { + pub fn can_use_reaction(&self, reaction: &ReactionType) -> bool { + if let Some(reactions) = &self.reactboard_reactions { + reactions.iter().any(|r| r == reaction) + } else { + false + } + } +} |
