release 1.0 #15
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1121,7 +1121,7 @@ checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "p4bl0t"
|
name = "p4bl0t"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "p4bl0t"
|
name = "p4bl0t"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Lucien Cartier-Tilet <lucien@phundrak.com>"]
|
authors = ["Lucien Cartier-Tilet <lucien@phundrak.com>"]
|
||||||
license-file = "LICENSE.md"
|
license-file = "LICENSE.md"
|
||||||
@ -11,8 +11,6 @@ repository = "https://github.com/phundrak/p4bl0t"
|
|||||||
keywords = ["discord", "bot", "logging"]
|
keywords = ["discord", "bot", "logging"]
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
color-eyre = "0.6.2"
|
color-eyre = "0.6.2"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
|
@ -1,6 +1,14 @@
|
|||||||
-- Add migration script here
|
-- Add migration script here
|
||||||
|
|
||||||
|
-- Discord IDs are kept as INTEGERs and not unsigned INTEGERs despite
|
||||||
|
-- their Rust type being u64. In order to properly manage them, you'll
|
||||||
|
-- need to cast any u64 to i64 with `as i64` before writing them to
|
||||||
|
-- the database, and cast any i64 to u64 with `as u64` when reading
|
||||||
|
-- them from the database. This operation is noop in Rust and should
|
||||||
|
-- therefore not cost a single CPU cycle.
|
||||||
CREATE TABLE IF NOT EXISTS guild_log_channels (
|
CREATE TABLE IF NOT EXISTS guild_log_channels (
|
||||||
guild_id TEXT PRIMARY KEY,
|
guild_id INTEGER NOT NULL,
|
||||||
channel_id TEXT NOT NULL,
|
channel_id INTEGER NOT NULL,
|
||||||
UNIQUE(guild_id, channel_id)
|
UNIQUE(guild_id, channel_id)
|
||||||
);
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS guild_log_channels_guild_id ON guild_log_channels(guild_id);
|
||||||
|
59
src/db.rs
59
src/db.rs
@ -1,59 +0,0 @@
|
|||||||
use std::env;
|
|
||||||
|
|
||||||
use sqlx::SqlitePool;
|
|
||||||
|
|
||||||
pub struct Database {
|
|
||||||
pool: SqlitePool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Database {
|
|
||||||
pub async fn new() -> color_eyre::Result<Self> {
|
|
||||||
Ok(Self {
|
|
||||||
pool: SqlitePool::connect(&env::var("DATABASE_URL")?).await?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_logging_channel(
|
|
||||||
&self,
|
|
||||||
guild_id: u64,
|
|
||||||
) -> color_eyre::Result<Vec<u64>> {
|
|
||||||
let guild_str = guild_id.to_string();
|
|
||||||
let channels = sqlx::query!(
|
|
||||||
r#"
|
|
||||||
SELECT channel_id
|
|
||||||
FROM guild_log_channels
|
|
||||||
WHERE guild_id = ?1
|
|
||||||
"#,
|
|
||||||
guild_str
|
|
||||||
)
|
|
||||||
.fetch_all(&self.pool)
|
|
||||||
.await?;
|
|
||||||
Ok(channels
|
|
||||||
.iter()
|
|
||||||
.map(|id| id.channel_id.parse::<u64>().unwrap())
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn set_logging_channel(
|
|
||||||
&self,
|
|
||||||
guild_id: u64,
|
|
||||||
channel_id: u64,
|
|
||||||
) -> color_eyre::Result<()> {
|
|
||||||
let guild_str = guild_id.to_string();
|
|
||||||
let channel_str = channel_id.to_string();
|
|
||||||
let mut conn = self.pool.acquire().await?;
|
|
||||||
|
|
||||||
sqlx::query!(
|
|
||||||
r#"
|
|
||||||
INSERT INTO guild_log_channels (guild_id, channel_id)
|
|
||||||
VALUES ( ?1, ?2 )
|
|
||||||
"#,
|
|
||||||
guild_str,
|
|
||||||
channel_str
|
|
||||||
)
|
|
||||||
.execute(&mut *conn)
|
|
||||||
.await?
|
|
||||||
.last_insert_rowid();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
98
src/db/mod.rs
Normal file
98
src/db/mod.rs
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
#![allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)]
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
use poise::serenity_prelude::{ChannelId, GuildId};
|
||||||
|
use sqlx::SqlitePool;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
pub type Result<T> = ::std::result::Result<T, sqlx::Error>;
|
||||||
|
|
||||||
|
pub struct Database {
|
||||||
|
pool: SqlitePool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Database {
|
||||||
|
pub async fn new() -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
pool: SqlitePool::connect(
|
||||||
|
&env::var("DATABASE_URL")
|
||||||
|
.expect("Missing enviroment variable DATABASE_URL"),
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_logging_channels(
|
||||||
|
&self,
|
||||||
|
guild_id: GuildId,
|
||||||
|
) -> Result<Vec<u64>> {
|
||||||
|
let guild_id = guild_id.0 as i64;
|
||||||
|
let channels = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
SELECT channel_id
|
||||||
|
FROM guild_log_channels
|
||||||
|
WHERE guild_id = ?1
|
||||||
|
"#,
|
||||||
|
guild_id
|
||||||
|
)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.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())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_logging_channel(
|
||||||
|
&self,
|
||||||
|
guild_id: GuildId,
|
||||||
|
channel_id: 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?;
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO guild_log_channels (guild_id, channel_id)
|
||||||
|
VALUES ( ?1, ?2 )
|
||||||
|
"#,
|
||||||
|
guild_id,
|
||||||
|
channel_id
|
||||||
|
)
|
||||||
|
.execute(&mut *conn)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("Error setting channel {channel_id} as logger for guild {guild_id}: {e:?}");
|
||||||
|
e
|
||||||
|
})
|
||||||
|
.map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove_logging_channel(
|
||||||
|
&self,
|
||||||
|
guild_id: GuildId,
|
||||||
|
channel_id: 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?;
|
||||||
|
sqlx::query!(r#"
|
||||||
|
DELETE FROM guild_log_channels
|
||||||
|
WHERE guild_id = ?1 AND channel_id = ?2
|
||||||
|
"#,
|
||||||
|
guild_id,
|
||||||
|
channel_id)
|
||||||
|
.execute(&mut *conn)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("Error removing channel {channel_id} as a logger for guild {guild_id}: {e:?}");
|
||||||
|
e
|
||||||
|
})
|
||||||
|
.map(|_| ())
|
||||||
|
}
|
||||||
|
}
|
@ -1,17 +1,105 @@
|
|||||||
use super::{Context, Error};
|
use super::{Context, Result};
|
||||||
|
|
||||||
use super::utils::serenity;
|
use super::utils::serenity;
|
||||||
|
|
||||||
|
#[allow(clippy::unused_async)]
|
||||||
|
#[poise::command(
|
||||||
|
slash_command,
|
||||||
|
subcommands("add_channel", "list_channels", "remove_channel"),
|
||||||
|
required_permissions = "ADMINISTRATOR"
|
||||||
|
)]
|
||||||
|
pub async fn logging(_ctx: Context<'_>) -> Result {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[poise::command(slash_command)]
|
#[poise::command(slash_command)]
|
||||||
pub async fn add_logging_channel(
|
pub async fn add_channel(
|
||||||
ctx: Context<'_>,
|
ctx: Context<'_>,
|
||||||
#[description = "Selected channel"] channel: Option<serenity::Channel>,
|
#[description = "New logging channel"] channel: serenity::Channel,
|
||||||
) -> Result<(), Error> {
|
) -> Result {
|
||||||
let response = match channel {
|
let channel_id = channel.id();
|
||||||
None => "No channel selected. Please select one.".to_owned(),
|
let response = match ctx.guild_id() {
|
||||||
Some(chan) => {
|
None => "Error: Could not determine the guild's ID".to_owned(),
|
||||||
let channel_id = chan.id();
|
Some(guild_id) => {
|
||||||
format!("Selected channel <#{channel_id}>")
|
match ctx
|
||||||
|
.data()
|
||||||
|
.database
|
||||||
|
.set_logging_channel(guild_id, channel_id)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(()) => format!(
|
||||||
|
"Added channel <#{channel_id}> as a logging channel"
|
||||||
|
),
|
||||||
|
Err(e) => {
|
||||||
|
if let Some(db_error) = e.as_database_error() {
|
||||||
|
if db_error.is_unique_violation() {
|
||||||
|
format!("Channel <#{channel_id}> is already a logging channel")
|
||||||
|
} else {
|
||||||
|
format!("Error: {e:?}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"Something bad happened with the database: {e:?}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ctx.say(response).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[poise::command(slash_command)]
|
||||||
|
pub 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) => {
|
||||||
|
match ctx.data().database.get_logging_channels(guild_id).await {
|
||||||
|
Err(e) => format!("Could not retrieve loggers: {e:?}"),
|
||||||
|
Ok(channels) => {
|
||||||
|
if channels.is_empty() {
|
||||||
|
"No channels registered as loggers".to_owned()
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"Here are the channels currently set as loggers:\n{}",
|
||||||
|
channels
|
||||||
|
.iter()
|
||||||
|
.map(|channel| format!("- <#{channel}>"))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("\n")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ctx.say(response).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[poise::command(slash_command)]
|
||||||
|
pub async fn remove_channel(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "Logger channel to remove"] channel: serenity::Channel,
|
||||||
|
) -> Result {
|
||||||
|
let channel_id = channel.id();
|
||||||
|
let response = match ctx.guild_id() {
|
||||||
|
None => "Error: Could not determine the guild's ID".to_owned(),
|
||||||
|
Some(guild_id) => {
|
||||||
|
match ctx
|
||||||
|
.data()
|
||||||
|
.database
|
||||||
|
.remove_logging_channel(guild_id, channel_id)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(()) => {
|
||||||
|
format!("Removed channel <#{channel_id}> as a logger")
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
format!("Could not remove channel as a logger: {e:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
ctx.say(response).await?;
|
ctx.say(response).await?;
|
||||||
|
@ -0,0 +1,72 @@
|
|||||||
|
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(())
|
||||||
|
}
|
@ -5,14 +5,20 @@ pub mod utils;
|
|||||||
use poise::FrameworkBuilder;
|
use poise::FrameworkBuilder;
|
||||||
use utils::serenity;
|
use utils::serenity;
|
||||||
|
|
||||||
use commands::add_logging_channel;
|
use commands::logging;
|
||||||
use utils::{BotData, Context, Error};
|
use utils::{BotData, Context, Error};
|
||||||
|
|
||||||
pub async fn make_bot() -> color_eyre::Result<FrameworkBuilder<BotData, Error>>
|
use self::events::event_handler;
|
||||||
{
|
|
||||||
let framework = poise::Framework::builder()
|
pub type Result = ::std::result::Result<(), Error>;
|
||||||
|
|
||||||
|
pub fn make_bot() -> FrameworkBuilder<BotData, Error> {
|
||||||
|
poise::Framework::builder()
|
||||||
.options(poise::FrameworkOptions {
|
.options(poise::FrameworkOptions {
|
||||||
commands: vec![add_logging_channel()],
|
commands: vec![logging()],
|
||||||
|
event_handler: |ctx, event, framework, data| {
|
||||||
|
Box::pin(event_handler(ctx, event, framework, data))
|
||||||
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.token(std::env::var("DISCORD_TOKEN").expect("missing DISCORD_TOKEN"))
|
.token(std::env::var("DISCORD_TOKEN").expect("missing DISCORD_TOKEN"))
|
||||||
@ -26,6 +32,5 @@ pub async fn make_bot() -> color_eyre::Result<FrameworkBuilder<BotData, Error>>
|
|||||||
.await?;
|
.await?;
|
||||||
Ok(BotData::new().await?)
|
Ok(BotData::new().await?)
|
||||||
})
|
})
|
||||||
});
|
})
|
||||||
Ok(framework)
|
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ use crate::db::Database;
|
|||||||
|
|
||||||
pub use poise::serenity_prelude as serenity;
|
pub use poise::serenity_prelude as serenity;
|
||||||
pub struct BotData {
|
pub struct BotData {
|
||||||
database: Database,
|
pub database: Database,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BotData {
|
impl BotData {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#![warn(clippy::style, clippy::pedantic)]
|
||||||
|
|
||||||
mod utils;
|
mod utils;
|
||||||
mod db;
|
mod db;
|
||||||
mod discord;
|
mod discord;
|
||||||
@ -10,7 +12,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
color_eyre::install()?;
|
color_eyre::install()?;
|
||||||
utils::setup_logging();
|
utils::setup_logging();
|
||||||
|
|
||||||
let bot = discord::make_bot().await?;
|
let bot = discord::make_bot();
|
||||||
bot.run().await?;
|
bot.run().await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -3,7 +3,7 @@ use tracing_subscriber::FmtSubscriber;
|
|||||||
|
|
||||||
pub fn setup_logging() {
|
pub fn setup_logging() {
|
||||||
let subscriber = FmtSubscriber::builder()
|
let subscriber = FmtSubscriber::builder()
|
||||||
.with_max_level(Level::DEBUG)
|
.with_max_level(Level::INFO)
|
||||||
.finish();
|
.finish();
|
||||||
tracing::subscriber::set_global_default(subscriber)
|
tracing::subscriber::set_global_default(subscriber)
|
||||||
.expect("Setting default subscriber failed");
|
.expect("Setting default subscriber failed");
|
||||||
|
Loading…
Reference in New Issue
Block a user