diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index d2b1c51..f956aec 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -48,6 +48,8 @@ jobs: REGISTRY: ghcr.io/${{ github.repository }} BOT_PREFIX: ${{ secrets.BOT_PREFIX }} BOT_TOKEN: ${{ secrets.BOT_TOKEN }} + CANDIDATE_ROLE: ${{ secrets.CANDIDATE_ROLE }} + MODERATOR_ROLE: ${{ secrets.MODERATOR_ROLE }} steps: - name: Checkout Develop uses: actions/checkout@v3 diff --git a/compose.yaml b/compose.yaml index 7a2b1d4..5715ce9 100644 --- a/compose.yaml +++ b/compose.yaml @@ -8,6 +8,8 @@ services: DATABASE_URL: "sqlite:database/data.db?mode=rwc" BOT_PREFIX: ${BOT_PREFIX} BOT_TOKEN: ${BOT_TOKEN} + CANDIDATE_ROLE: ${CANDIDATE_ROLE} + MODERATOR_ROLE: ${MODERATOR_ROLE} restart: always volumes: diff --git a/src/check/is_moderator.rs b/src/check/is_moderator.rs new file mode 100644 index 0000000..74d0974 --- /dev/null +++ b/src/check/is_moderator.rs @@ -0,0 +1,38 @@ +use anyhow::{Result, anyhow}; +use poise::CreateReply; + +use crate::{Context, Message}; + +#[tracing::instrument] +pub async fn is_moderator(ctx: Context<'_>) -> Result { + let config = &ctx.data().config; + + let Some(guild) = ctx.guild_id() else { + return Err(anyhow!("Must be in a guild")); + }; + let roles = guild.roles(ctx.http()).await?; + let role_id = roles.iter().find_map(|(id, role)| { + if role.name == config.moderator_role { + Some(id) + } else { + None + } + }); + let Some(role_id) = role_id else { + return Err(anyhow!("No role with given name")); + }; + + if ctx.author().has_role(ctx.http(), guild, role_id).await? { + return Ok(true); + } + + ctx.send( + CreateReply::default() + .ephemeral(true) + .reply(true) + .content(Message::Unauthorized), + ) + .await?; + + Ok(false) +} diff --git a/src/check/mod.rs b/src/check/mod.rs new file mode 100644 index 0000000..7805080 --- /dev/null +++ b/src/check/mod.rs @@ -0,0 +1,3 @@ +mod is_moderator; + +pub use is_moderator::is_moderator; diff --git a/src/command/add.rs b/src/command/add.rs index 4e7c78d..753a825 100644 --- a/src/command/add.rs +++ b/src/command/add.rs @@ -1,7 +1,7 @@ use anyhow::Result; use poise::serenity_prelude as serenity; -use crate::{Context, Message, database, util}; +use crate::{Context, Message, check, database, util}; /// Add one or more candidate IDs to the candidates database from a text file. #[tracing::instrument] @@ -9,7 +9,7 @@ use crate::{Context, Message, database, util}; slash_command, prefix_command, ephemeral, - required_permissions = "MANAGE_MESSAGES | MANAGE_THREADS" + check = "check::is_moderator" )] pub async fn add( ctx: Context<'_>, diff --git a/src/command/delete.rs b/src/command/delete.rs index e151db1..55bbdda 100644 --- a/src/command/delete.rs +++ b/src/command/delete.rs @@ -1,6 +1,6 @@ use anyhow::Result; -use crate::{Context, Message, database, util}; +use crate::{Context, Message, check, database, util}; /// Delete a candidate by ID from the candidates database. #[tracing::instrument] @@ -8,7 +8,7 @@ use crate::{Context, Message, database, util}; slash_command, prefix_command, ephemeral, - required_permissions = "MANAGE_MESSAGES | MANAGE_THREADS" + check = "check::is_moderator" )] pub async fn delete(ctx: Context<'_>, id: String) -> Result<()> { let pool = &ctx.data().pool; diff --git a/src/command/verify.rs b/src/command/verify.rs index b00c0c5..9dacd2c 100644 --- a/src/command/verify.rs +++ b/src/command/verify.rs @@ -3,13 +3,12 @@ use poise::serenity_prelude::EditRole; use crate::{Context, Message, database, util}; -const ROLE: &str = "Round 1: Challenger"; - /// Verify a candidate and assign role upon success. #[tracing::instrument] #[poise::command(slash_command, prefix_command, ephemeral)] pub async fn verify(ctx: Context<'_>, id: String) -> Result<()> { let pool = &ctx.data().pool; + let config = &ctx.data().config; if !util::is_valid_id(&id) { ctx.reply(Message::InvalidId).await?; @@ -52,7 +51,10 @@ pub async fn verify(ctx: Context<'_>, id: String) -> Result<()> { return Err(anyhow!("Must be in a guild")); }; let role = guild - .create_role(ctx.http(), EditRole::new().name(ROLE)) + .create_role( + ctx.http(), + EditRole::new().name(config.candidate_role.clone()), + ) .await?; member.add_role(ctx.http(), role.id).await?; diff --git a/src/config.rs b/src/config.rs index 69ffba3..5d968e4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,7 +9,15 @@ fn default_bot_prefix() -> String { "!".to_string() } -#[derive(Debug, Deserialize)] +fn default_candidate_role() -> String { + "Round 1: Challenger".to_string() +} + +fn default_moderator_role() -> String { + "Moderator".to_string() +} + +#[derive(Debug, Deserialize, Clone)] pub struct Config { #[serde(default = "default_database_url")] pub database_url: String, @@ -18,6 +26,12 @@ pub struct Config { pub bot_prefix: String, pub bot_token: String, + + #[serde(default = "default_candidate_role")] + pub candidate_role: String, + + #[serde(default = "default_moderator_role")] + pub moderator_role: String, } impl Config { diff --git a/src/main.rs b/src/main.rs index 1e0f0db..03e0faf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +pub mod check; pub mod command; pub mod config; pub mod database; @@ -19,6 +20,8 @@ pub type Context<'a> = poise::Context<'a, State, anyhow::Error>; pub async fn build_bot() -> anyhow::Result<()> { let config = Config::new()?; + let prefix = config.bot_prefix.clone(); + let token = config.bot_token.clone(); let options = FrameworkOptions { commands: vec![ @@ -29,7 +32,7 @@ pub async fn build_bot() -> anyhow::Result<()> { command::verify(), ], prefix_options: poise::PrefixFrameworkOptions { - prefix: Some(config.bot_prefix), + prefix: Some(prefix), edit_tracker: Some(Arc::new(poise::EditTracker::for_timespan( Duration::from_secs(3600), ))), @@ -43,7 +46,7 @@ pub async fn build_bot() -> anyhow::Result<()> { Box::pin(async move { poise::builtins::register_globally(ctx, &framework.options().commands).await?; - State::new(&config.database_url).await + State::new(config).await }) }) .options(options) @@ -51,7 +54,7 @@ pub async fn build_bot() -> anyhow::Result<()> { let intents = GatewayIntents::all() | GatewayIntents::GUILD_MESSAGE_REACTIONS; - let mut client = Client::builder(&config.bot_token, intents) + let mut client = Client::builder(&token, intents) .framework(framework) .await?; diff --git a/src/message.rs b/src/message.rs index 67e3662..84cb14d 100644 --- a/src/message.rs +++ b/src/message.rs @@ -10,6 +10,7 @@ pub enum Message<'a> { NotRegistered, InvalidName, Verified(Option), + Unauthorized, Error, } @@ -23,6 +24,7 @@ impl From> for String { Message::Verified(None) => "Bạn đã verify thành công".to_string(), Message::Verified(Some(time)) => format!("Bạn đã verify thành công vào {}.", util::format_datetime(time)), Message::InvalidName => "Vui lòng đặt tên đúng quy tắc.".to_string(), + Message::Unauthorized => "Lệnh dành riêng cho moderator".to_string(), Message::Error => "Có một số lỗi đã xảy ra bạn thử lại trong ít phút hoặc tạo ticket để được hỗ trợ nhé!".to_string(), } } diff --git a/src/state.rs b/src/state.rs index e991465..14edf1f 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,17 +1,20 @@ use anyhow::Result; use sqlx::SqlitePool; +use crate::Config; + #[derive(Debug)] pub struct State { pub pool: SqlitePool, + pub config: Config, } impl State { #[tracing::instrument] - pub async fn new(database_url: &str) -> Result { - let pool = SqlitePool::connect(database_url).await?; + pub async fn new(config: Config) -> Result { + let pool = SqlitePool::connect(&config.database_url).await?; sqlx::migrate!().run(&pool).await.unwrap(); - Ok(State { pool }) + Ok(State { pool, config }) } }