refactor: simplify code, better organize it, and comment it
This commit is contained in:
@@ -8,45 +8,74 @@ use tracing::error;
|
||||
|
||||
pub type Result<T> = ::std::result::Result<T, sqlx::Error>;
|
||||
|
||||
pub struct Database {
|
||||
pool: SqlitePool,
|
||||
}
|
||||
pub struct Database(SqlitePool);
|
||||
|
||||
impl Database {
|
||||
/// Initialize Sqlite database.
|
||||
///
|
||||
/// The Sqlite database should already exist and have its
|
||||
/// migrations already executed.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the environment variable `DATABASE_URL` is not set.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if the Sqlite pool fails to
|
||||
/// create.
|
||||
pub async fn new() -> Result<Self> {
|
||||
Ok(Self {
|
||||
pool: SqlitePool::connect(
|
||||
Ok(Self(
|
||||
SqlitePool::connect(
|
||||
&env::var("DATABASE_URL")
|
||||
.expect("Missing enviroment variable DATABASE_URL"),
|
||||
)
|
||||
.await?,
|
||||
})
|
||||
))
|
||||
}
|
||||
|
||||
/// Return from database all channels registered as loggers for a
|
||||
/// guild.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if `sqlx` does so.
|
||||
pub async fn get_logging_channels(
|
||||
&self,
|
||||
guild_id: GuildId,
|
||||
) -> Result<Vec<u64>> {
|
||||
) -> Result<Vec<ChannelId>> {
|
||||
let guild_id = guild_id.0 as i64;
|
||||
let channels = sqlx::query!(
|
||||
sqlx::query!(
|
||||
r#"
|
||||
SELECT channel_id
|
||||
FROM guild_log_channels
|
||||
WHERE guild_id = ?1
|
||||
"#,
|
||||
WHERE guild_id = ?1"#,
|
||||
guild_id
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.fetch_all(&self.0)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(
|
||||
"Error getting logging channels for guild {guild_id}: {e:?}"
|
||||
);
|
||||
e
|
||||
})?;
|
||||
Ok(channels.iter().map(|id| id.channel_id as u64).collect())
|
||||
})
|
||||
.map(|channels| {
|
||||
channels
|
||||
.iter()
|
||||
.map(|id| ChannelId(id.channel_id as u64))
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// Adds a channel as a logger for a guild.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if `sqlx` does so. This may
|
||||
/// be either a database issue, or a channel is already registered
|
||||
/// as a guild's logger, therefore violating the unicity
|
||||
/// constraint for guild ID and channel ID pairs.
|
||||
pub async fn set_logging_channel(
|
||||
&self,
|
||||
guild_id: GuildId,
|
||||
@@ -54,13 +83,11 @@ WHERE guild_id = ?1
|
||||
) -> Result<()> {
|
||||
let guild_id = guild_id.0 as i64;
|
||||
let channel_id = channel_id.0 as i64;
|
||||
let mut conn = self.pool.acquire().await?;
|
||||
let mut conn = self.0.acquire().await?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
sqlx::query!(r#"
|
||||
INSERT INTO guild_log_channels (guild_id, channel_id)
|
||||
VALUES ( ?1, ?2 )
|
||||
"#,
|
||||
VALUES ( ?1, ?2 )"#,
|
||||
guild_id,
|
||||
channel_id
|
||||
)
|
||||
@@ -73,18 +100,25 @@ VALUES ( ?1, ?2 )
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
/// Unregister a channel as a logger for a guild.
|
||||
///
|
||||
/// This function will return a success value even if `channel`
|
||||
/// was not a logger of `guild` already.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if `sqlx` does so.
|
||||
pub async fn remove_logging_channel(
|
||||
&self,
|
||||
guild_id: GuildId,
|
||||
channel_id: ChannelId,
|
||||
guild: GuildId,
|
||||
channel: ChannelId,
|
||||
) -> Result<()> {
|
||||
let guild_id = guild_id.0 as i64;
|
||||
let channel_id = channel_id.0 as i64;
|
||||
let mut conn = self.pool.acquire().await?;
|
||||
let guild_id = guild.0 as i64;
|
||||
let channel_id = channel.0 as i64;
|
||||
let mut conn = self.0.acquire().await?;
|
||||
sqlx::query!(r#"
|
||||
DELETE FROM guild_log_channels
|
||||
WHERE guild_id = ?1 AND channel_id = ?2
|
||||
"#,
|
||||
WHERE guild_id = ?1 AND channel_id = ?2"#,
|
||||
guild_id,
|
||||
channel_id)
|
||||
.execute(&mut *conn)
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
use super::{Context, Result};
|
||||
use super::super::{Context, Result};
|
||||
|
||||
use super::utils::serenity;
|
||||
use poise::serenity_prelude as serenity;
|
||||
|
||||
/// Main command for logging subcommands.
|
||||
///
|
||||
/// This command cannot be called on its own and will do nothing by
|
||||
/// itself.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This command will never error out, even if its signature says it
|
||||
/// can.
|
||||
#[allow(clippy::unused_async)]
|
||||
#[poise::command(
|
||||
slash_command,
|
||||
@@ -12,8 +21,13 @@ pub async fn logging(_ctx: Context<'_>) -> Result {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a channel as a logger.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if .
|
||||
#[poise::command(slash_command)]
|
||||
pub async fn add_channel(
|
||||
async fn add_channel(
|
||||
ctx: Context<'_>,
|
||||
#[description = "New logging channel"] channel: serenity::Channel,
|
||||
) -> Result {
|
||||
@@ -50,8 +64,16 @@ pub async fn add_channel(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List all channels registered as loggers for a guild.
|
||||
///
|
||||
/// This will list all channels that are logger channels in the server
|
||||
/// from which the command was executed.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if the database returns one.
|
||||
#[poise::command(slash_command)]
|
||||
pub async fn list_channels(ctx: Context<'_>) -> Result {
|
||||
async fn list_channels(ctx: Context<'_>) -> Result {
|
||||
let response = match ctx.guild_id() {
|
||||
None => "Error: Could not determine the guild's ID".to_owned(),
|
||||
Some(guild_id) => {
|
||||
@@ -78,8 +100,18 @@ pub async fn list_channels(ctx: Context<'_>) -> Result {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a channel as a logger in a guild.
|
||||
///
|
||||
/// This will remove a channel from the list of logger channels in the
|
||||
/// guild from which the command was executed. If the channel is not a
|
||||
/// logger, the bot will still consider unsetting the channel as a
|
||||
/// logger a success.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if the database errors.
|
||||
#[poise::command(slash_command)]
|
||||
pub async fn remove_channel(
|
||||
async fn remove_channel(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Logger channel to remove"] channel: serenity::Channel,
|
||||
) -> Result {
|
||||
3
src/discord/commands/mod.rs
Normal file
3
src/discord/commands/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod logging;
|
||||
|
||||
pub(crate) use logging::logging;
|
||||
24
src/discord/error.rs
Normal file
24
src/discord/error.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use std::error::Error as StdError;
|
||||
use std::fmt::{self, Display};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Error {
|
||||
GuildIdNotFound,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn boxed(self) -> Box<Self> {
|
||||
Box::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
// write!(f, "")
|
||||
match self {
|
||||
Self::GuildIdNotFound => write!(f, "Guild ID not found!"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {}
|
||||
@@ -1,72 +0,0 @@
|
||||
use crate::db::Database;
|
||||
|
||||
use super::{utils::BotData, Error, Result};
|
||||
|
||||
use poise::{serenity_prelude as serenity, Event};
|
||||
use tracing::{error, info};
|
||||
|
||||
async fn handle_everyone_mention(
|
||||
ctx: &serenity::Context,
|
||||
database: &Database,
|
||||
message: &serenity::Message,
|
||||
) -> Result {
|
||||
use serenity::ChannelId;
|
||||
if let Some(guild_id) = message.guild_id {
|
||||
if message.mention_everyone {
|
||||
let author = message.author.clone();
|
||||
let message_channel = message.channel_id;
|
||||
let channels: Vec<ChannelId> = database
|
||||
.get_logging_channels(guild_id)
|
||||
.await?
|
||||
.iter()
|
||||
.map(|channel_id| serenity::ChannelId(channel_id.to_owned()))
|
||||
.collect();
|
||||
for channel in &channels {
|
||||
channel
|
||||
.send_message(&ctx, |m| {
|
||||
m.embed(|e| {
|
||||
e.title("Someone mentioned everyone!")
|
||||
.field("Author", author.clone(), true)
|
||||
.field(
|
||||
"When",
|
||||
message.timestamp.naive_local().to_string(),
|
||||
true,
|
||||
)
|
||||
.field(
|
||||
"Channel",
|
||||
format!("<#{message_channel}>"),
|
||||
true,
|
||||
)
|
||||
.field("Link", format!("https://discord.com/channels/{guild_id}/{}/{}", channel.0, message.id), false)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to send message: {e:?}");
|
||||
e
|
||||
})?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("Could not determine guild id of message {message:?}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn event_handler(
|
||||
ctx: &serenity::Context,
|
||||
event: &Event<'_>,
|
||||
_framework: poise::FrameworkContext<'_, BotData, Error>,
|
||||
data: &BotData,
|
||||
) -> Result {
|
||||
match event {
|
||||
Event::Ready { data_about_bot } => {
|
||||
info!("Logged in as {}", data_about_bot.user.name);
|
||||
}
|
||||
Event::Message { new_message } => {
|
||||
handle_everyone_mention(ctx, &data.database, new_message).await?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
68
src/discord/events/everyone.rs
Normal file
68
src/discord/events/everyone.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use crate::db::Database;
|
||||
|
||||
use super::super::Result;
|
||||
|
||||
use super::super::error::Error as DiscordError;
|
||||
|
||||
use poise::serenity_prelude::{self as serenity, CreateEmbed};
|
||||
use tracing::{error, info};
|
||||
|
||||
fn message_for_everyone_mention(
|
||||
embed: &mut CreateEmbed,
|
||||
message: &serenity::Message,
|
||||
guild_id: u64,
|
||||
) {
|
||||
let author = message.author.clone();
|
||||
let message_channel = message.channel_id.0;
|
||||
embed
|
||||
.title("Someone mentioned everyone!")
|
||||
.field("Author", author.clone(), true)
|
||||
.field("When", message.timestamp.naive_local().to_string(), true)
|
||||
.field("Channel", format!("<#{message_channel}>"), true)
|
||||
.field(
|
||||
"Link",
|
||||
format!(
|
||||
"https://discord.com/channels/{guild_id}/{message_channel}/{}",
|
||||
message.id
|
||||
),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
/// Handle messages mentioning everyone.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if a message fails to be sent,
|
||||
/// if retrieving the list of channels registered as loggers fails, or
|
||||
/// if there is not guild ID that can be retrieved from the message.
|
||||
pub async fn handle_everyone_mention(
|
||||
ctx: &serenity::Context,
|
||||
database: &Database,
|
||||
message: &serenity::Message,
|
||||
) -> Result {
|
||||
info!("Message mentioning everyone: {message:?}");
|
||||
if !message.mention_everyone {
|
||||
return Ok(());
|
||||
}
|
||||
if message.guild_id.is_none() {
|
||||
error!("Message without a guild_id! {message:?}");
|
||||
return Err(DiscordError::GuildIdNotFound.boxed());
|
||||
}
|
||||
let guild_id = message.guild_id.unwrap();
|
||||
let channels: Vec<serenity::ChannelId> =
|
||||
database.get_logging_channels(guild_id).await?;
|
||||
for channel in &channels {
|
||||
// Ignore result, it'll be in the bot's logger
|
||||
let _ = channel
|
||||
.send_message(&ctx, |m| {
|
||||
m.embed(|e| {
|
||||
message_for_everyone_mention(e, message, guild_id.0);
|
||||
e
|
||||
})
|
||||
})
|
||||
.await
|
||||
.map_err(|e| error!("Failed to send message: {e:?}"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
34
src/discord/events/mod.rs
Normal file
34
src/discord/events/mod.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use super::{utils::BotData, Error, Result};
|
||||
|
||||
use poise::{
|
||||
serenity_prelude::{self as serenity},
|
||||
Event,
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
mod everyone;
|
||||
use everyone::handle_everyone_mention;
|
||||
|
||||
/// Function handling events the bot can see.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if one of the functions error
|
||||
/// themselves.
|
||||
pub async fn event_handler(
|
||||
ctx: &serenity::Context,
|
||||
event: &Event<'_>,
|
||||
_framework: poise::FrameworkContext<'_, BotData, Error>,
|
||||
data: &BotData,
|
||||
) -> Result {
|
||||
match event {
|
||||
Event::Ready { data_about_bot } => {
|
||||
info!("Logged in as {}", data_about_bot.user.name);
|
||||
}
|
||||
Event::Message { new_message } => {
|
||||
handle_everyone_mention(ctx, &data.database, new_message).await?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
mod commands;
|
||||
mod events;
|
||||
pub mod utils;
|
||||
pub mod error;
|
||||
|
||||
use poise::FrameworkBuilder;
|
||||
use utils::serenity;
|
||||
@@ -12,6 +13,11 @@ use self::events::event_handler;
|
||||
|
||||
pub type Result = ::std::result::Result<(), Error>;
|
||||
|
||||
/// Bootstraps the Discord bot.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the environment `DISCORD_TOKEN` is unavailable.
|
||||
pub fn make_bot() -> FrameworkBuilder<BotData, Error> {
|
||||
poise::Framework::builder()
|
||||
.options(poise::FrameworkOptions {
|
||||
|
||||
@@ -6,6 +6,14 @@ pub struct BotData {
|
||||
}
|
||||
|
||||
impl BotData {
|
||||
/// Initialize state data for bot.
|
||||
///
|
||||
/// For now, this only includes a connector to its database.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if the database fails to
|
||||
/// initialize.
|
||||
pub async fn new() -> color_eyre::Result<Self> {
|
||||
Ok(Self {
|
||||
database: Database::new().await?,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
use tracing::Level;
|
||||
use tracing_subscriber::FmtSubscriber;
|
||||
|
||||
/// Initialize logging for the project.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the logger fails to initialize.
|
||||
pub fn setup_logging() {
|
||||
let subscriber = FmtSubscriber::builder()
|
||||
.with_max_level(Level::INFO)
|
||||
|
||||
Reference in New Issue
Block a user