Skip to content

Commit

Permalink
Merge pull request #18 from Panini-Devs/0.0.1-staging-changes
Browse files Browse the repository at this point in the history
moderation prototyping
  • Loading branch information
paninizer authored Feb 13, 2024
2 parents fb1b3ad + 6da4402 commit a323fd7
Show file tree
Hide file tree
Showing 9 changed files with 330 additions and 5 deletions.
118 changes: 118 additions & 0 deletions src/commands/moderation.rs
Original file line number Diff line number Diff line change
@@ -1 +1,119 @@
use crate::{
utilities::{
messages, models,
modlog::{self, ModType},
},
Context, Error,
};

use chrono::Utc;
use poise::serenity_prelude::UserId;
use tracing::{error, info};

#[poise::command(
prefix_command,
slash_command,
category = "Moderator",
required_permissions = "BAN_MEMBERS",
required_bot_permissions = "BAN_MEMBERS | SEND_MESSAGES",
guild_only,
ephemeral
)]
pub async fn ban(
context: Context<'_>,
#[description = "The user to ban."]
#[rename = "user"]
user_id: UserId,
#[description = "Reason for the ban."]
#[max_length = 80]
reason: Option<String>,
) -> Result<(), Error> {
let database = &context.data().sqlite;

let user = models::user(context, user_id).await?;

let moderator = context.author();
let moderator_id = moderator.id;

if user.system {
let reply = messages::error_reply("Cannot ban a system user.", false);
context.send(reply).await?;
return Ok(());
}

if user_id == moderator_id {
let reply = messages::error_reply("Sorry, but you cannot ban yourself.", true);
context.send(reply).await?;

return Ok(());
}

let reason = reason.unwrap_or_else(|| "No reason provided.".to_string());

let reason_char_count = reason.chars().count();
if reason_char_count > 80 {
let reply = messages::info_reply("Reason must be no more than 80 characters long.", true);
context.send(reply).await?;

return Ok(());
}

let result = {
let (user_name, user_mention) = (&user.name, models::user_mention(context, user_id).await?);

let (moderator_name, moderator_mention) =
(&moderator.name, models::author_mention(context)?);

let (guild_id, guild_name) = {
let guild_id = context.guild_id().unwrap();
let guild = context.guild().unwrap();
(guild_id, guild.name.clone())
};

let created_at = Utc::now().naive_utc();

let mut user_mod_history = modlog::select_modlog_from_users(&user_id, database).await?;

let message = messages::info_message(format!(
"You've been banned from {guild_name} by {moderator_mention} for {reason}.",
));
let dm = user.direct_message(context, message).await;

if let Err(why) = dm {
error!("Couldn't send DM to @{user_name}: {why:?}");
}

match guild_id.ban_with_reason(context, user_id, 0, &reason).await {
Ok(_) => {
modlog::insert_modlog(
ModType::Ban,
&guild_id,
&user_id,
&moderator_id,
&reason,
created_at,
database,
)
.await?;

user_mod_history += 1;

modlog::update_users_set_modlog(&user_id, user_mod_history, database).await?;

info!("@{moderator_name} banned @{user_name} from {guild_name}: {reason}");
Ok(format!("{user_mention} has been banned."))
}
Err(why) => {
error!("Couldn't ban @{user_name}: {why:?}");
Err(format!("Sorry, but I couldn't ban {user_mention}."))
}
}
};

if let Err(why) = result {
let reply = messages::error_reply(&why, true);
context.send(reply).await?;
}

Ok(())
}
7 changes: 6 additions & 1 deletion src/commands/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,12 @@ pub async fn prefix(context: Context<'_>) -> Result<(), Error> {
required_bot_permissions = "SEND_MESSAGES",
guild_only = true
)]
pub async fn set(context: Context<'_>, prefix: Option<String>) -> Result<(), Error> {
pub async fn set(
context: Context<'_>,
#[description = "The new prefix."]
#[max_length = 5]
prefix: Option<String>,
) -> Result<(), Error> {
if let Some(guild_id) = context.guild_id() {
let prefix = prefix.unwrap_or_else(|| "+".to_string());

Expand Down
5 changes: 4 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use utilities::types::GuildSettings;
mod commands;
mod utilities;

use crate::commands::{info::*, math::*, owner::*, setup::*, utilities::*};
use crate::commands::{info::*, math::*, owner::*, setup::*, utilities::*, moderation::*};

use sqlx::SqlitePool;

Expand Down Expand Up @@ -181,6 +181,9 @@ async fn main() {
bot_stat(),
multiply(),
add(),
divide(),
subtract(),
ban(),
help(),
ping(),
servers(),
Expand Down
18 changes: 17 additions & 1 deletion src/utilities/embeds.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use chrono::NaiveDateTime;
use serenity::{
all::User,
all::{
colours::css,
User,
},
builder::{CreateEmbed, CreateEmbedAuthor},
model::Colour,
};
use std::fmt::Write;

Expand Down Expand Up @@ -61,3 +65,15 @@ pub fn warnings_command_embed(
.author(embed_author)
.fields(embed_fields)
}

pub fn error_message_embed(message: &String) -> CreateEmbed {
CreateEmbed::default()
.description(format!("{message}"))
.colour(css::DANGER)
}

pub fn info_message_embed(message: &String) -> CreateEmbed {
CreateEmbed::default()
.description(format!("{message}"))
.colour(Colour::BLUE)
}
36 changes: 36 additions & 0 deletions src/utilities/messages.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use poise::CreateReply;
use serenity::builder::{
CreateInteractionResponse, CreateInteractionResponseMessage, CreateMessage,
};

use super::embeds;

pub async fn error_response(
message: impl Into<String>,
ephemeral: bool,
) -> CreateInteractionResponse {
let embed = embeds::error_message_embed(&message.into());

let response_message = CreateInteractionResponseMessage::new()
.embed(embed)
.ephemeral(ephemeral);
CreateInteractionResponse::Message(response_message)
}

pub fn info_message(message: impl Into<String>) -> CreateMessage {
let embed = embeds::info_message_embed(&message.into());

CreateMessage::default().embed(embed)
}

pub fn error_reply(message: impl Into<String>, ephemeral: bool) -> CreateReply {
let embed = embeds::error_message_embed(&message.into());

CreateReply::default().embed(embed).ephemeral(ephemeral)
}

pub fn info_reply(message: impl Into<String>, ephemeral: bool) -> CreateReply {
let embed = embeds::info_message_embed(&message.into());

CreateReply::default().embed(embed).ephemeral(ephemeral)
}
3 changes: 3 additions & 0 deletions src/utilities/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
pub mod embeds;
pub mod event_handler;
pub mod git;
pub mod messages;
pub mod models;
pub mod modlog;
pub mod paginate;
pub mod types;
27 changes: 27 additions & 0 deletions src/utilities/models.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use crate::Context;
use poise::serenity_prelude::{model::ModelError, User, UserId};
use serenity::all::{Mention, Mentionable};
use tracing::error;

pub fn author(context: Context<'_>) -> Result<&User, ModelError> {
Ok(context.author())
}

pub fn author_mention(context: Context<'_>) -> Result<Mention, ModelError> {
let author = author(context)?;
Ok(author.mention())
}

pub async fn user(context: Context<'_>, user_id: UserId) -> Result<User, ModelError> {
match user_id.to_user(context).await {
Ok(user) => Ok(user),
Err(why) => {
error!("Couldn't get user: {why:?}");
return Err(ModelError::MemberNotFound);
}
}
}

pub async fn user_mention(context: Context<'_>, user_id: UserId) -> Result<Mention, ModelError> {
Ok(user(context, user_id).await?.mention())
}
55 changes: 53 additions & 2 deletions src/utilities/modlog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use poise::serenity_prelude as serenity;
use serenity::all::{GuildId, UserId};
use sqlx::{Row, SqlitePool};
use tokio::time::Instant;
use tracing::{error, info};
use tracing::{debug, error, info};
use uuid::Uuid;

pub enum ModType {
Expand Down Expand Up @@ -100,7 +100,7 @@ pub async fn delete_mod_log(
Ok(())
}

pub async fn insert_mod_log(
pub async fn insert_modlog(
action_type: ModType,
guild_id: &GuildId,
user_id: &UserId,
Expand Down Expand Up @@ -135,3 +135,54 @@ pub async fn insert_mod_log(

Ok(())
}

pub async fn select_modlog_from_users(
user_id: &UserId,
pool: &SqlitePool,
) -> Result<i32, sqlx::Error> {
let start_time = Instant::now();

let query =
sqlx::query("SELECT infractions FROM users WHERE user_id = ?").bind(i64::from(*user_id));
let row = match query.fetch_one(pool).await {
Ok(infractions) => infractions,
Err(why) => {
error!("Couldn't select infractions from Users: {why:?}");
return Err(why);
}
};

let infractions = match row.try_get::<i32, _>("infractions") {
Ok(infractions) => infractions,
Err(why) => {
error!("Couldn't get infractions: {why:?}");
return Err(why);
}
};

let elapsed_time = start_time.elapsed();
debug!("Selected infractions from Users in {elapsed_time:.2?}");

Ok(infractions)
}

pub async fn update_users_set_modlog(
user_id: &UserId,
infractions: i32,
pool: &SqlitePool,
) -> Result<(), sqlx::Error> {
let start_time = Instant::now();

let query = sqlx::query("UPDATE users SET infractions = ? WHERE user_id = ?")
.bind(infractions)
.bind(i64::from(*user_id));
if let Err(why) = query.execute(pool).await {
error!("Couldn't update infractions for user(s) in Users: {why:?}");
return Err(why);
}

let elapsed_time = start_time.elapsed();
debug!("Updated infractions for user(s) within Users in {elapsed_time:.2?}");

Ok(())
}
66 changes: 66 additions & 0 deletions src/utilities/paginate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use poise::serenity_prelude as serenity;
use uuid::Uuid;

use crate::Context;

/// Paginates a list of embeds using UUID as custom ID to identify the buttons.
pub async fn paginate<U, E>(
ctx: Context<'_>,
pages: Vec<serenity::all::CreateEmbed>,
) -> Result<(), serenity::Error> {
// Define some unique identifiers for the navigation buttons
let ctx_id = Uuid::new_v4();
let prev_button_id = format!("{}prev", ctx_id);
let next_button_id = format!("{}next", ctx_id);

// Send the embed with the first page as content
let reply = {
let components = serenity::CreateActionRow::Buttons(vec![
serenity::CreateButton::new(&prev_button_id).emoji('◀'),
serenity::CreateButton::new(&next_button_id).emoji('▶'),
]);

poise::CreateReply::default()
.embed(pages[0].clone())
.components(vec![components])
};

ctx.send(reply).await?;

// Loop through incoming interactions with the navigation buttons
let mut current_page = 0;
while let Some(press) = serenity::collector::ComponentInteractionCollector::new(ctx)
// We defined our button IDs to start with `ctx_id`. If they don't, some other command's
// button was pressed
.filter(move |press| press.data.custom_id.starts_with(&ctx_id.to_string()))
// Timeout when no navigation button has been pressed for 24 hours
.timeout(std::time::Duration::from_secs(3600 * 24))
.await
{
// Depending on which button was pressed, go to next or previous page
if press.data.custom_id == next_button_id {
current_page += 1;
if current_page >= pages.len() {
current_page = 0;
}
} else if press.data.custom_id == prev_button_id {
current_page = current_page.checked_sub(1).unwrap_or(pages.len() - 1);
} else {
// This is an unrelated button interaction
continue;
}

// Update the message with the new page contents
press
.create_response(
ctx.serenity_context(),
serenity::CreateInteractionResponse::UpdateMessage(
serenity::CreateInteractionResponseMessage::new()
.embed(pages[current_page].clone()),
),
)
.await?;
}

Ok(())
}

0 comments on commit a323fd7

Please sign in to comment.