Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
38 changes: 38 additions & 0 deletions src/check/is_moderator.rs
Original file line number Diff line number Diff line change
@@ -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<bool> {
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)
}
3 changes: 3 additions & 0 deletions src/check/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod is_moderator;

pub use is_moderator::is_moderator;
4 changes: 2 additions & 2 deletions src/command/add.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
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]
#[poise::command(
slash_command,
prefix_command,
ephemeral,
required_permissions = "MANAGE_MESSAGES | MANAGE_THREADS"
check = "check::is_moderator"
)]
pub async fn add(
ctx: Context<'_>,
Expand Down
4 changes: 2 additions & 2 deletions src/command/delete.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
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]
#[poise::command(
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;
Expand Down
8 changes: 5 additions & 3 deletions src/command/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?;
Expand Down Expand Up @@ -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?;

Expand Down
16 changes: 15 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down
9 changes: 6 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod check;
pub mod command;
pub mod config;
pub mod database;
Expand All @@ -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![
Expand All @@ -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),
))),
Expand All @@ -43,15 +46,15 @@ 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)
.build();

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?;

Expand Down
2 changes: 2 additions & 0 deletions src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub enum Message<'a> {
NotRegistered,
InvalidName,
Verified(Option<NaiveDateTime>),
Unauthorized,
Error,
}

Expand All @@ -23,6 +24,7 @@ impl From<Message<'_>> 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(),
}
}
Expand Down
9 changes: 6 additions & 3 deletions src/state.rs
Original file line number Diff line number Diff line change
@@ -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<Self> {
let pool = SqlitePool::connect(database_url).await?;
pub async fn new(config: Config) -> Result<Self> {
let pool = SqlitePool::connect(&config.database_url).await?;
sqlx::migrate!().run(&pool).await.unwrap();

Ok(State { pool })
Ok(State { pool, config })
}
}