summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/commands/general/ask.rs1
-rw-r--r--src/commands/general/bing.rs1
-rw-r--r--src/commands/general/convert.rs1
-rw-r--r--src/commands/general/random.rs1
-rw-r--r--src/commands/mod.rs2
-rw-r--r--src/commands/moderation/actions.rs7
-rw-r--r--src/commands/moderation/config.rs30
-rw-r--r--src/commands/optional/copypasta.rs8
-rw-r--r--src/commands/optional/teawiespam.rs6
-rw-r--r--src/handlers/event/guild.rs32
-rw-r--r--src/handlers/event/message.rs4
-rw-r--r--src/handlers/event/mod.rs14
-rw-r--r--src/handlers/event/pinboard.rs8
-rw-r--r--src/handlers/event/reactboard.rs57
-rw-r--r--src/main.rs40
-rw-r--r--src/settings.rs88
-rw-r--r--src/storage/mod.rs146
-rw-r--r--src/storage/reactboard.rs18
-rw-r--r--src/storage/settings.rs37
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
+ }
+ }
+}