Public Access
Add proper logging, make commands modularized, add new commands and add event handler
Run cargo test / Run tests (push) Has been cancelled
Run cargo test / Run tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
TG_BOT_DISCORD_TOKEN=your-token-here
|
||||
|
||||
# Database Configuration
|
||||
TG_BOT_POSTGRES_USER=postgres
|
||||
TG_BOT_POSTGRES_PASSWORD=changeme
|
||||
TG_BOT_POSTGRES_DB=tg_dev_srv_bot
|
||||
TG_BOT_DATABASE_URL=postgres://postgres:changeme@localhost:5432/tg_dev_srv_bot
|
||||
|
||||
# Required for sqlx
|
||||
DATABASE_URL=postgres://postgres:postgres@localhost:5432/tg_dev_srv_bot
|
||||
|
||||
# Logging configuration (trace, debug, info, warn, error)
|
||||
TG_BOT_LOG=info
|
||||
@@ -0,0 +1,45 @@
|
||||
name: Run cargo test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
DATABASE_URL: postgres://postgres:postgres@postgres:5432/tg_dev_srv_bot
|
||||
DISCORD_TOKEN: testing
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: tg_dev_srv_bot
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgmp-dev libmpfr-dev libmpc-dev
|
||||
|
||||
- name: Set up Rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
+5
-1
@@ -6,9 +6,13 @@ edition = "2024"
|
||||
[dependencies]
|
||||
dotenv = "0.15.0"
|
||||
exmex = "0.21.0"
|
||||
f128 = "0.2.9"
|
||||
poise = "0.6.2"
|
||||
rug = "1.30.0"
|
||||
serenity = "0.12.5"
|
||||
sqlx = { version = "0.9.0", features = ["postgres", "runtime-tokio", "macros"] }
|
||||
tokio = { version = "1.52.3", features = ["full"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.10.2"
|
||||
|
||||
@@ -4,9 +4,9 @@ services:
|
||||
container_name: tg-dev-srv-bot-postgres
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-tg_dev_srv_bot}
|
||||
POSTGRES_USER: ${TG_BOT_POSTGRES_USER:-postgres}
|
||||
POSTGRES_PASSWORD: ${TG_BOT_POSTGRES_PASSWORD:-postgres}
|
||||
POSTGRES_DB: ${TG_BOT_POSTGRES_DB:-tg_dev_srv_bot}
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
|
||||
+7
-1
@@ -1,6 +1,7 @@
|
||||
CREATE TABLE IF NOT EXISTS auto_roles (
|
||||
guild_id BIGINT NOT NULL,
|
||||
role_id BIGINT NOT NULL
|
||||
role_id BIGINT NOT NULL,
|
||||
enabled BOOLEAN NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS reaction_role (
|
||||
@@ -21,3 +22,8 @@ CREATE TABLE IF NOT EXISTS admin_roles (
|
||||
role_id BIGINT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
guild_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
settings_get_messaged_about_errors BOOLEAN NOT NULL
|
||||
);
|
||||
+27
-2
@@ -1,5 +1,6 @@
|
||||
use crate::{Context, Error};
|
||||
use poise::ChoiceParameter;
|
||||
use tracing::{debug, trace, info};
|
||||
use crate::shared_functions::trace_message;
|
||||
|
||||
#[derive(Debug, poise::ChoiceParameter)]
|
||||
pub enum Precision {
|
||||
@@ -17,14 +18,38 @@ pub async fn calc(ctx: Context<'_>,
|
||||
#[description = "Precision"]
|
||||
precision: Option<Precision>
|
||||
) -> Result<(), Error> {
|
||||
debug!("{} has requested to calculate {}", ctx.author().name, expression);
|
||||
let msg = "Calculating...".to_string();
|
||||
trace_message(msg.clone(), ctx.channel_id().to_string(), ctx.guild_id().unwrap().to_string()).await;
|
||||
let response_message = ctx.say(msg).await?;
|
||||
let start_time = std::time::Instant::now();
|
||||
trace!("Saving start time: {:?}", start_time);
|
||||
let value = match precision {
|
||||
Some(Precision::Bits32) => exmex::eval_str::<f32>(&expression).map_err(|err| err.to_string())?.to_string(),
|
||||
Some(Precision::Bits64) => exmex::eval_str::<f64>(&expression).map_err(|err| err.to_string())?.to_string(),
|
||||
None => exmex::eval_str::<f64>(&expression).map_err(|err| err.to_string())?.to_string(),
|
||||
};
|
||||
let end_time = std::time::Instant::now();
|
||||
trace!("Saving end time: {:?}", end_time);
|
||||
let duration = end_time - start_time;
|
||||
ctx.say(format!("{} = {} \n -# Precision: {} Compute time : {:?}", expression, value, precision.map(|p| p.name()).unwrap_or("64-bit"), duration)).await?;
|
||||
trace!("Saving duration: {:?}", duration);
|
||||
let precision_str = match precision {
|
||||
Some(Precision::Bits32) => "32-bit".to_string(),
|
||||
Some(Precision::Bits64) => "64-bit".to_string(),
|
||||
None => "64-bit".to_string(),
|
||||
};
|
||||
let msg = format!("{} = {} \n-# Precision: {} Compute time : {:?}", expression, value, precision_str, duration);
|
||||
trace_message(msg.clone(), ctx.channel_id().to_string(), ctx.guild_id().unwrap().to_string()).await;
|
||||
edit_response_message(&response_message, ctx, msg, false).await?;
|
||||
debug!("Calculation perfomed for {} with result {} in {:?} by {}", expression, value, duration, ctx.author().name);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn edit_response_message<'a>(response_message: &poise::ReplyHandle<'a>, ctx: Context<'_>, content: String, silent: bool) -> Result<(), Error> {
|
||||
if silent {
|
||||
response_message.edit(ctx, poise::CreateReply::default().content(content).allowed_mentions(serenity::all::CreateAllowedMentions::new().empty_users())).await?;
|
||||
} else {
|
||||
response_message.edit(ctx, poise::CreateReply::default().content(content)).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::{Context, Error};
|
||||
use crate::shared_functions::get_all_admin_users;
|
||||
use poise::serenity_prelude as sere;
|
||||
use tracing::{error, warn, info, debug, trace};
|
||||
|
||||
#[derive(Debug, poise::ChoiceParameter)]
|
||||
pub enum AdminOperationType {
|
||||
@@ -19,7 +21,7 @@ pub enum AdminOperationType {
|
||||
|
||||
/// Admin command
|
||||
#[poise::command(slash_command, prefix_command)]
|
||||
pub async fn manage_users(ctx: Context<'_>,
|
||||
pub async fn manage_admins(ctx: Context<'_>,
|
||||
#[description = "Operation to perform"]
|
||||
operation: Option<AdminOperationType>,
|
||||
#[description = "User"]
|
||||
@@ -27,9 +29,11 @@ pub async fn manage_users(ctx: Context<'_>,
|
||||
#[description = "Role"]
|
||||
role: Option<sere::Role>
|
||||
) -> Result<(), Error> {
|
||||
trace!("manage_admins command called by user {}", ctx.author().id.get());
|
||||
let response_message = ctx.say("Running command...").await?;
|
||||
|
||||
if !is_admin(ctx).await? {
|
||||
debug!("User {} is not an admin, denying access", ctx.author().id.get());
|
||||
response_message.edit(ctx, poise::CreateReply::default().content("You are not allowed to run this command.")).await?;
|
||||
return Ok(());
|
||||
}
|
||||
@@ -78,59 +82,6 @@ pub async fn manage_users(ctx: Context<'_>,
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_all_admin_users(ctx: Context<'_>) -> Result<Vec<sere::User>, Error> {
|
||||
let guild_id = ctx.guild_id().unwrap();
|
||||
let pool = &ctx.data().pool;
|
||||
let db_guild_id = guild_id.get() as i64;
|
||||
|
||||
let admin_users_records = sqlx::query!("SELECT user_id FROM admin_users WHERE guild_id = $1;", db_guild_id)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
let admin_roles_records = sqlx::query!("SELECT role_id FROM admin_roles WHERE guild_id = $1;", db_guild_id)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
let admin_role_ids: std::collections::HashSet<_> = admin_roles_records
|
||||
.into_iter()
|
||||
.map(|record| sere::RoleId::new(record.role_id as u64))
|
||||
.collect();
|
||||
|
||||
let mut admin_users = Vec::new();
|
||||
let mut admin_user_ids = std::collections::HashSet::new();
|
||||
|
||||
for record in admin_users_records {
|
||||
let user_id = sere::UserId::new(record.user_id as u64);
|
||||
if admin_user_ids.insert(user_id) {
|
||||
if let Ok(user) = user_id.to_user(ctx.http()).await {
|
||||
admin_users.push(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut last_member_id = None;
|
||||
loop {
|
||||
let members = guild_id.members(ctx.http(), Some(1000), last_member_id).await?;
|
||||
if members.is_empty() {
|
||||
break;
|
||||
}
|
||||
last_member_id = Some(members.last().unwrap().user.id);
|
||||
|
||||
for member in members {
|
||||
let has_role = member.roles.iter().any(|role_id| admin_role_ids.contains(role_id));
|
||||
let has_admin_perms = ctx.guild()
|
||||
.map(|g| g.member_permissions(&member).administrator())
|
||||
.unwrap_or(false);
|
||||
|
||||
if (has_role || has_admin_perms) && admin_user_ids.insert(member.user.id) {
|
||||
admin_users.push(member.user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(admin_users)
|
||||
}
|
||||
|
||||
async fn is_admin(ctx: Context<'_>) -> Result<bool, Error> {
|
||||
let user_running_command = ctx.author().id.get() as i64;
|
||||
let admins = get_all_admin_users(ctx).await?;
|
||||
@@ -144,16 +95,20 @@ async fn is_admin(ctx: Context<'_>) -> Result<bool, Error> {
|
||||
}
|
||||
|
||||
async fn add_admin_user<'a>(ctx: Context<'a>, msg: &poise::ReplyHandle<'a>, user: sere::Member) -> Result<(), Error> {
|
||||
trace!("Executing add_admin_user for user {}", user.user.name);
|
||||
if is_target_admin_user(ctx, &user).await? {
|
||||
trace!("User {} is already an admin", user.user.name);
|
||||
edit_response_message(msg, ctx, format!("{} is already an admin", user.user.name), false).await?;
|
||||
return Ok(());
|
||||
}
|
||||
let query = sqlx::query!("INSERT INTO admin_users (guild_id, user_id) VALUES ($1, $2);", ctx.guild_id().unwrap().get() as i64, user.user.id.get() as i64).execute(&ctx.data().pool).await;
|
||||
match query {
|
||||
Ok(_) => {
|
||||
info!("Added user {} as admin in guild {}", user.user.name, ctx.guild_id().unwrap().get());
|
||||
edit_response_message(msg, ctx, format!("Added {} as admin", user.user.name), false).await?;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to add user {} as admin in guild {}: {:?}", user.user.name, ctx.guild_id().unwrap().get(), e);
|
||||
edit_response_message(msg, ctx, format!("Failed to add {} as admin: {}", user.user.name, e), false).await?;
|
||||
}
|
||||
}
|
||||
@@ -181,16 +136,20 @@ async fn is_target_admin_user(ctx: Context<'_>, user: &sere::Member) -> Result<b
|
||||
|
||||
|
||||
async fn remove_admin_user<'a>(ctx: Context<'a>, msg: &poise::ReplyHandle<'a>, user: sere::Member) -> Result<(), Error> {
|
||||
trace!("Executing remove_admin_user for user {}", user.user.name);
|
||||
if !is_target_admin_user(ctx, &user).await? {
|
||||
trace!("User {} is not an admin", user.user.name);
|
||||
edit_response_message(msg, ctx, format!("{} is not an admin", user.user.name), false).await?;
|
||||
return Ok(());
|
||||
}
|
||||
let query = sqlx::query!("DELETE FROM admin_users WHERE guild_id = $1 AND user_id = $2;", ctx.guild_id().unwrap().get() as i64, user.user.id.get() as i64).execute(&ctx.data().pool).await;
|
||||
match query {
|
||||
Ok(_) => {
|
||||
info!("Removed user {} as admin in guild {}", user.user.name, ctx.guild_id().unwrap().get());
|
||||
edit_response_message(msg, ctx, format!("Removed {} as admin", user.user.name), false).await?;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to remove user {} as admin in guild {}: {:?}", user.user.name, ctx.guild_id().unwrap().get(), e);
|
||||
edit_response_message(msg, ctx, format!("Failed to remove {} as admin: {}", user.user.name, e), false).await?;
|
||||
}
|
||||
}
|
||||
@@ -205,16 +164,20 @@ async fn list_admin_users<'a>(ctx: Context<'a>, msg: &poise::ReplyHandle<'a>) ->
|
||||
}
|
||||
|
||||
async fn add_admin_role<'a>(ctx: Context<'a>, msg: &poise::ReplyHandle<'a>, role: sere::Role) -> Result<(), Error> {
|
||||
trace!("Executing add_admin_role for role {}", role.name);
|
||||
if is_target_admin_role(ctx, &role).await? {
|
||||
trace!("Role {} is already an admin role", role.name);
|
||||
edit_response_message(msg, ctx, format!("{} is already an admin role", role.name), false).await?;
|
||||
return Ok(());
|
||||
}
|
||||
let query = sqlx::query!("INSERT INTO admin_roles (guild_id, role_id) VALUES ($1, $2);", ctx.guild_id().unwrap().get() as i64, role.id.get() as i64).execute(&ctx.data().pool).await;
|
||||
match query {
|
||||
Ok(_) => {
|
||||
info!("Added role {} as admin role in guild {}", role.name, ctx.guild_id().unwrap().get());
|
||||
edit_response_message(msg, ctx, format!("Added {} as admin role", role.name), false).await?;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to add role {} as admin role in guild {}: {:?}", role.name, ctx.guild_id().unwrap().get(), e);
|
||||
edit_response_message(msg, ctx, format!("Failed to add {} as admin role: {}", role.name, e), false).await?;
|
||||
}
|
||||
}
|
||||
@@ -222,16 +185,20 @@ async fn add_admin_role<'a>(ctx: Context<'a>, msg: &poise::ReplyHandle<'a>, role
|
||||
}
|
||||
|
||||
async fn remove_admin_role<'a>(ctx: Context<'a>, msg: &poise::ReplyHandle<'a>, role: sere::Role) -> Result<(), Error> {
|
||||
trace!("Executing remove_admin_role for role {}", role.name);
|
||||
if !is_target_admin_role(ctx, &role).await? {
|
||||
trace!("Role {} is not an admin role", role.name);
|
||||
edit_response_message(msg, ctx, format!("{} is not an admin role", role.name), false).await?;
|
||||
return Ok(());
|
||||
}
|
||||
let query = sqlx::query!("DELETE FROM admin_roles WHERE guild_id = $1 AND role_id = $2;", ctx.guild_id().unwrap().get() as i64, role.id.get() as i64).execute(&ctx.data().pool).await;
|
||||
match query {
|
||||
Ok(_) => {
|
||||
info!("Removed role {} as admin role in guild {}", role.name, ctx.guild_id().unwrap().get());
|
||||
edit_response_message(msg, ctx, format!("Removed {} as admin role", role.name), false).await?;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to remove role {} as admin role in guild {}: {:?}", role.name, ctx.guild_id().unwrap().get(), e);
|
||||
edit_response_message(msg, ctx, format!("Failed to remove {} as admin role: {}", role.name, e), false).await?;
|
||||
}
|
||||
}
|
||||
@@ -262,4 +229,4 @@ async fn is_target_admin_role<'a>(ctx: Context<'a>, role: &sere::Role) -> Result
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
use crate::{Context, Error};
|
||||
use crate::shared_functions::get_all_admin_users;
|
||||
use poise::serenity_prelude as sere;
|
||||
use tracing::{error, warn, info, debug, trace};
|
||||
|
||||
#[derive(Debug, poise::ChoiceParameter)]
|
||||
pub enum AutoRoleOperationType {
|
||||
#[name = "Enable auto-role"]
|
||||
EnableAutoRole,
|
||||
#[name = "Set auto-role (does not enable the feature)"]
|
||||
SetAutoRole,
|
||||
#[name = "Disable auto-role"]
|
||||
DisableAutoRole,
|
||||
#[name = "Show auto-role"]
|
||||
ShowAutoRole,
|
||||
}
|
||||
|
||||
/// Auto role command
|
||||
#[poise::command(slash_command, prefix_command)]
|
||||
pub async fn manage_auto_role(ctx: Context<'_>,
|
||||
#[description = "Operation to perform"]
|
||||
operation: Option<AutoRoleOperationType>,
|
||||
#[description = "Role"]
|
||||
role: Option<sere::Role>
|
||||
) -> Result<(), Error> {
|
||||
trace!("manage_auto_role command called by user {}", ctx.author().id.get());
|
||||
let response_message = ctx.say("Running command...").await?;
|
||||
|
||||
if !is_admin(ctx).await? {
|
||||
debug!("User {} is not an admin, denying access", ctx.author().id.get());
|
||||
edit_response_message(&response_message, ctx, "You are not allowed to run this command.".to_string(), false).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match operation {
|
||||
Some(AutoRoleOperationType::EnableAutoRole) => {
|
||||
if let Some(s_role) = role {
|
||||
enable_auto_role(ctx, &response_message, s_role).await?;
|
||||
} else {
|
||||
edit_response_message(&response_message, ctx, "Please provide a role".to_string(), false).await?;
|
||||
}
|
||||
}
|
||||
Some(AutoRoleOperationType::SetAutoRole) => {
|
||||
if let Some(s_role) = role {
|
||||
set_auto_role(ctx, &response_message, s_role).await?;
|
||||
} else {
|
||||
edit_response_message(&response_message, ctx, "Please provide a role".to_string(), false).await?;
|
||||
}
|
||||
}
|
||||
Some(AutoRoleOperationType::DisableAutoRole) => {
|
||||
if let Some(s_role) = role {
|
||||
disable_auto_role(ctx, &response_message, s_role).await?;
|
||||
} else {
|
||||
edit_response_message(&response_message, ctx, "Please provide a role".to_string(), false).await?;
|
||||
}
|
||||
}
|
||||
Some(AutoRoleOperationType::ShowAutoRole) => {
|
||||
show_auto_role(ctx, &response_message).await?;
|
||||
}
|
||||
None => {
|
||||
edit_response_message(&response_message, ctx, "Please provide an operation".to_string(), false).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn is_admin(ctx: Context<'_>) -> Result<bool, Error> {
|
||||
let user_running_command = ctx.author().id.get() as i64;
|
||||
let admins = get_all_admin_users(ctx).await?;
|
||||
|
||||
for user in admins {
|
||||
if user.id.get() as i64 == user_running_command {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn enable_auto_role(ctx: Context<'_>, response_message: &poise::ReplyHandle<'_>, role: sere::Role) -> Result<(), Error> {
|
||||
trace!("Executing enable_auto_role for role {}", role.name);
|
||||
if !is_any_role_set_in_guild(ctx).await? {
|
||||
trace!("No role is set in guild {}", ctx.guild_id().unwrap().get());
|
||||
edit_response_message(response_message, ctx, "No role is set. Please set a role first.".to_string(), false).await?;
|
||||
return Ok(());
|
||||
}
|
||||
if !does_role_exist_in_guild(ctx, &role).await? {
|
||||
trace!("Role {} does not exist in guild {}", role.name, ctx.guild_id().unwrap().get());
|
||||
edit_response_message(response_message, ctx, "This role does not exist".to_string(), false).await?;
|
||||
return Ok(());
|
||||
}
|
||||
if is_auto_role_enabled(ctx, &role).await? {
|
||||
trace!("Auto role {} is already enabled in guild {}", role.name, ctx.guild_id().unwrap().get());
|
||||
edit_response_message(response_message, ctx, "Auto role is already enabled".to_string(), false).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let query = sqlx::query!("UPDATE auto_roles SET enabled = true WHERE guild_id = $1 AND role_id = $2;", ctx.guild_id().unwrap().get() as i64, role.id.get() as i64).execute(&ctx.data().pool).await;
|
||||
match query {
|
||||
Ok(_) => {
|
||||
info!("Enabled auto role {} in guild {}", role.name, ctx.guild_id().unwrap().get());
|
||||
edit_response_message(response_message, ctx, format!("Enabled auto role: <@&{}>", role.id.get()), false).await?;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to enable {} as auto role in guild {}: {}", role.id.get(), ctx.guild_id().unwrap().get(), e);
|
||||
edit_response_message(response_message, ctx, format!("Failed to enable {} as auto role: {}", role.id.get(), e), false).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn is_any_role_set_in_guild(ctx: Context<'_>) -> Result<bool, Error> {
|
||||
let query = sqlx::query!("SELECT * FROM auto_roles WHERE guild_id = $1", ctx.guild_id().unwrap().get() as i64).fetch_optional(&ctx.data().pool).await?;
|
||||
if query.is_some() {
|
||||
return Ok(true);
|
||||
}
|
||||
return Ok(false)
|
||||
}
|
||||
|
||||
async fn does_role_exist_in_guild(ctx: Context<'_>, target_role: &sere::Role) -> Result<bool, Error> {
|
||||
if let Some(guild) = ctx.guild() {
|
||||
if guild.roles.contains_key(&target_role.id) {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
return Ok(false)
|
||||
}
|
||||
|
||||
async fn set_auto_role(ctx: Context<'_>, response_message: &poise::ReplyHandle<'_>, role: sere::Role) -> Result<(), Error> {
|
||||
trace!("Executing set_auto_role for role {}", role.name);
|
||||
if !does_role_exist_in_guild(ctx, &role).await? {
|
||||
trace!("Role {} does not exist in guild {}", role.name, ctx.guild_id().unwrap().get());
|
||||
edit_response_message(response_message, ctx, "This role does not exist".to_string(), false).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let query = sqlx::query!("INSERT INTO auto_roles (guild_id, role_id, enabled) VALUES ($1, $2, false) ON CONFLICT (guild_id) DO UPDATE SET role_id = $2;", ctx.guild_id().unwrap().get() as i64, role.id.get() as i64).execute(&ctx.data().pool).await;
|
||||
match query {
|
||||
Ok(_) => {
|
||||
info!("Set {} as auto role in guild {}", role.name, ctx.guild_id().unwrap().get());
|
||||
edit_response_message(response_message, ctx, format!("Set {} as auto role", role.name), false).await?;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to set {} as auto role in guild {}: {}", role.id.get(), ctx.guild_id().unwrap().get(), e);
|
||||
edit_response_message(response_message, ctx, format!("Failed to set {} as auto role: {}", role.id.get(), e), false).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn disable_auto_role(ctx: Context<'_>, response_message: &poise::ReplyHandle<'_>, role: sere::Role) -> Result<(), Error> {
|
||||
trace!("Executing disable_auto_role for role {}", role.name);
|
||||
if !is_any_role_set_in_guild(ctx).await? {
|
||||
trace!("No role is set in guild {}", ctx.guild_id().unwrap().get());
|
||||
edit_response_message(response_message, ctx, "No auto role is set. Please set a role first.".to_string(), false).await?;
|
||||
return Ok(());
|
||||
}
|
||||
if !is_auto_role_enabled(ctx, &role).await? {
|
||||
trace!("Auto role {} is already disabled in guild {}", role.name, ctx.guild_id().unwrap().get());
|
||||
edit_response_message(response_message, ctx, "Auto role is already disabled".to_string(), false).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let query = sqlx::query!("UPDATE auto_roles SET enabled = false WHERE guild_id = $1 AND role_id = $2;", ctx.guild_id().unwrap().get() as i64, role.id.get() as i64).execute(&ctx.data().pool).await;
|
||||
match query {
|
||||
Ok(_) => {
|
||||
info!("Disabled auto role {} in guild {}", role.name, ctx.guild_id().unwrap().get());
|
||||
edit_response_message(response_message, ctx, format!("Disabled auto role: {}", role.name), false).await?;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to disable {} as auto role in guild {}: {}", role.id.get(), ctx.guild_id().unwrap().get(), e);
|
||||
edit_response_message(response_message, ctx, format!("Failed to disable {} as auto role: {}", role.id.get(), e), false).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn edit_response_message<'a>(response_message: &poise::ReplyHandle<'a>, ctx: Context<'_>, content: String, silent: bool) -> Result<(), Error> {
|
||||
if silent {
|
||||
response_message.edit(ctx, poise::CreateReply::default().content(content).allowed_mentions(serenity::all::CreateAllowedMentions::new().empty_users())).await?;
|
||||
} else {
|
||||
response_message.edit(ctx, poise::CreateReply::default().content(content)).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn is_auto_role_enabled(ctx: Context<'_>, target_role: &sere::Role) -> Result<bool, Error> {
|
||||
let query = sqlx::query!("SELECT enabled FROM auto_roles WHERE guild_id = $1 AND role_id = $2;", ctx.guild_id().unwrap().get() as i64, target_role.id.get() as i64).fetch_optional(&ctx.data().pool).await?;
|
||||
if query.is_none() {
|
||||
return Ok(false);
|
||||
}
|
||||
return Ok(query.unwrap().enabled)
|
||||
}
|
||||
|
||||
async fn is_any_auto_role_enabled(ctx: Context<'_>) -> Result<bool, Error> {
|
||||
let query = sqlx::query!("SELECT role_id FROM auto_roles WHERE guild_id = $1 AND enabled = true", ctx.guild_id().unwrap().get() as i64).fetch_optional(&ctx.data().pool).await?;
|
||||
if query.is_none() {
|
||||
return Ok(false);
|
||||
}
|
||||
return Ok(true)
|
||||
}
|
||||
|
||||
async fn show_auto_role(ctx: Context<'_>, response_message: &poise::ReplyHandle<'_>) -> Result<(), Error> {
|
||||
trace!("Executing show_auto_role");
|
||||
if !is_any_role_set_in_guild(ctx).await? {
|
||||
trace!("No role is set in guild {}", ctx.guild_id().unwrap().get());
|
||||
edit_response_message(response_message, ctx, "No role has been set yet.".to_string(), false).await?;
|
||||
return Ok(());
|
||||
}
|
||||
if !is_any_auto_role_enabled(ctx).await? {
|
||||
trace!("Auto role is disabled in guild {}", ctx.guild_id().unwrap().get());
|
||||
edit_response_message(response_message, ctx, "Auto role is disabled.".to_string(), false).await?;
|
||||
return Ok(());
|
||||
}
|
||||
let role_id = sqlx::query!("SELECT role_id FROM auto_roles WHERE guild_id = $1", ctx.guild_id().unwrap().get() as i64).fetch_optional(&ctx.data().pool).await?;
|
||||
edit_response_message(response_message, ctx, format!("Currently, <@&{}> is set as auto role and is enabled.", role_id.unwrap().role_id), false).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// async fn list_admin_users<'a>(ctx: Context<'a>, msg: &poise::ReplyHandle<'a>) -> Result<(), Error> {
|
||||
// let admins = get_all_admin_users(ctx).await?;
|
||||
// let admin_strings: Vec<String> = admins.iter().map(|user| format!("<@{}>", user.id.get())).collect();
|
||||
// edit_response_message(msg, ctx, format!("Admin users: {}", admin_strings.join(", ")), true).await?;
|
||||
// Ok(())
|
||||
// }
|
||||
+4
-2
@@ -1,7 +1,9 @@
|
||||
pub mod ping;
|
||||
pub mod calc;
|
||||
pub mod manage_users;
|
||||
pub mod manage_admins;
|
||||
pub mod manage_auto_role;
|
||||
|
||||
pub use ping::ping;
|
||||
pub use calc::calc;
|
||||
pub use manage_users::manage_users;
|
||||
pub use manage_admins::manage_admins;
|
||||
pub use manage_auto_role::manage_auto_role;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
use crate::{
|
||||
Data, Error
|
||||
};
|
||||
use poise::serenity_prelude as sere;
|
||||
|
||||
pub async fn event_handler(
|
||||
ctx: &sere::Context,
|
||||
event: &sere::FullEvent,
|
||||
_framework: poise::FrameworkContext<'_, Data, Error>,
|
||||
user_data: &Data,
|
||||
) -> Result<(), Error> {
|
||||
match event {
|
||||
sere::FullEvent::GuildMemberAddition { new_member } => {
|
||||
crate::events::user_join::user_join(ctx, new_member, &user_data.pool).await;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod user_join;
|
||||
|
||||
pub use user_join::user_join;
|
||||
@@ -0,0 +1,40 @@
|
||||
use poise::serenity_prelude as sere;
|
||||
use tracing::{error, warn, info, debug, trace};
|
||||
|
||||
pub async fn user_join(ctx: &sere::Context, new_member: &sere::Member, pool: &sqlx::PgPool) {
|
||||
trace!("User joined event handler called for guild {}", new_member.guild_id);
|
||||
// Assign role to new user if the feature is enabled
|
||||
let guild_id = new_member.guild_id.get() as u64;
|
||||
|
||||
let role_result = sqlx::query!(
|
||||
"SELECT role_id FROM auto_roles WHERE guild_id = $1 AND enabled = true",
|
||||
guild_id as i64
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await;
|
||||
|
||||
trace!("Checking if auto role is enabled for guild {}", guild_id);
|
||||
|
||||
match role_result {
|
||||
Ok(Some(row)) => {
|
||||
trace!("Auto role enabled for guild {}", guild_id);
|
||||
let role_id = row.role_id as u64;
|
||||
trace!("Adding role {} to user {}", role_id, new_member.user.name);
|
||||
give_role_to_user(ctx, new_member, role_id).await;
|
||||
|
||||
}
|
||||
Ok(None) => {
|
||||
trace!("No auto role enabled for guild {}", guild_id);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Database error fetching auto role for guild {}: {:?}", guild_id, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn give_role_to_user(ctx: &sere::Context, member: &sere::Member, role_id: u64) {
|
||||
if let Err(e) = member.add_role(&ctx.http, sere::RoleId::new(role_id)).await {
|
||||
error!("Error adding role to member: {:?}", e);
|
||||
}
|
||||
debug!("Role {} added to user {} in guild {}", role_id, member.user.name, member.guild_id);
|
||||
}
|
||||
+49
-20
@@ -1,6 +1,11 @@
|
||||
use std::env;
|
||||
use poise::serenity_prelude as sere;
|
||||
use tracing::{info, warn, error, debug, trace};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
pub mod commands;
|
||||
pub mod event_handler;
|
||||
pub mod events;
|
||||
pub mod shared_functions;
|
||||
|
||||
pub struct Data {
|
||||
pub pool: sqlx::PgPool,
|
||||
@@ -14,27 +19,60 @@ pub type Context<'a> = poise::Context<'a, Data, Error>;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Load env from file
|
||||
dotenv::dotenv().ok();
|
||||
|
||||
let log_level = env::var("TG_BOT_LOG").unwrap_or_else(|_| "info".to_string());
|
||||
let filter = EnvFilter::new(format!("tg_dev_srv_bot={}", log_level));
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(filter)
|
||||
.with_file(true)
|
||||
.with_thread_ids(true)
|
||||
.with_thread_names(true)
|
||||
.with_line_number(true)
|
||||
.with_target(true)
|
||||
.init();
|
||||
|
||||
trace!("Logging system started");
|
||||
info!("Log Level: {}", log_level);
|
||||
|
||||
trace!("Env loaded");
|
||||
|
||||
// Grab envs
|
||||
let database_url = env::var("DATABASE_URL").expect("Expected a database url in the environment");
|
||||
let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment");
|
||||
trace!("Loading database url");
|
||||
let database_url = env::var("TG_BOT_DATABASE_URL").expect("Expected a database url in the environment");
|
||||
trace!("Database url loaded");
|
||||
|
||||
trace!("Loading token");
|
||||
let token = env::var("TG_BOT_DISCORD_TOKEN").expect("Expected a token in the environment");
|
||||
trace!("Token loaded");
|
||||
|
||||
|
||||
|
||||
info!("Starting bot...");
|
||||
|
||||
// Initialize database
|
||||
debug!("Initalizing database");
|
||||
let pool = sqlx::postgres::PgPoolOptions::new().connect(&database_url).await.expect("Failed to connect to database");
|
||||
debug!("Database connected");
|
||||
|
||||
trace!("Applying sqlschema");
|
||||
let _res = sqlx::raw_sql(include_str!("../sqlschema.txt")).execute(&pool).await.expect("Failed to execute sqlschema");
|
||||
debug!("Sqlschema applied");
|
||||
|
||||
trace!("Loading intents");
|
||||
let intents = sere::GatewayIntents::non_privileged()
|
||||
| sere::GatewayIntents::MESSAGE_CONTENT;
|
||||
| sere::GatewayIntents::MESSAGE_CONTENT
|
||||
| sere::GatewayIntents::GUILD_MEMBERS;
|
||||
trace!("Intents loaded");
|
||||
|
||||
trace!("Initalizing framework");
|
||||
let framework = poise::Framework::builder()
|
||||
.options(poise::FrameworkOptions {
|
||||
event_handler: |ctx, event, framework, user_data| {
|
||||
Box::pin(event_handler(ctx, event, framework, user_data))
|
||||
Box::pin(event_handler::event_handler(ctx, event, framework, user_data))
|
||||
},
|
||||
commands: vec![commands::ping(), commands::calc(), commands::manage_users()],
|
||||
commands: vec![commands::ping(), commands::calc(), commands::manage_admins(), commands::manage_auto_role()],
|
||||
..Default::default()
|
||||
})
|
||||
.setup(|ctx, _ready, framework| {
|
||||
@@ -46,23 +84,14 @@ async fn main() {
|
||||
})
|
||||
})
|
||||
.build();
|
||||
trace!("Framework initialized");
|
||||
|
||||
debug!("Connecting to Discord");
|
||||
let mut client = sere::ClientBuilder::new(&token, intents).framework(framework).await.expect("Err creating client");
|
||||
debug!("Connected to Discord");
|
||||
|
||||
|
||||
info!("Bot running");
|
||||
if let Err(why) = client.start().await {
|
||||
println!("Client error: {why:?}");
|
||||
error!("Client error: {why:?}");
|
||||
}
|
||||
}
|
||||
|
||||
async fn event_handler(
|
||||
ctx: &sere::Context,
|
||||
event: &sere::FullEvent,
|
||||
framework: poise::FrameworkContext<'_, Data, Error>,
|
||||
user_data: &Data,
|
||||
) -> Result<(), Error> {
|
||||
if let sere::FullEvent::ChannelCreate {channel} = event {
|
||||
println!("Channel created: {channel:?}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use crate::{Context, Error};
|
||||
use poise::serenity_prelude as sere;
|
||||
use tracing::trace;
|
||||
|
||||
pub async fn get_all_admin_users(ctx: Context<'_>) -> Result<Vec<sere::User>, Error> {
|
||||
let guild_id = ctx.guild_id().unwrap();
|
||||
let pool = &ctx.data().pool;
|
||||
let db_guild_id = guild_id.get() as i64;
|
||||
|
||||
let admin_users_records = sqlx::query!("SELECT user_id FROM admin_users WHERE guild_id = $1;", db_guild_id)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
let admin_roles_records = sqlx::query!("SELECT role_id FROM admin_roles WHERE guild_id = $1;", db_guild_id)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
let admin_role_ids: std::collections::HashSet<_> = admin_roles_records
|
||||
.into_iter()
|
||||
.map(|record| sere::RoleId::new(record.role_id as u64))
|
||||
.collect();
|
||||
|
||||
let mut admin_users = Vec::new();
|
||||
let mut admin_user_ids = std::collections::HashSet::new();
|
||||
|
||||
for record in admin_users_records {
|
||||
let user_id = sere::UserId::new(record.user_id as u64);
|
||||
if admin_user_ids.insert(user_id) {
|
||||
if let Ok(user) = user_id.to_user(ctx.http()).await {
|
||||
admin_users.push(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut last_member_id = None;
|
||||
loop {
|
||||
let members = guild_id.members(ctx.http(), Some(1000), last_member_id).await?;
|
||||
if members.is_empty() {
|
||||
break;
|
||||
}
|
||||
last_member_id = Some(members.last().unwrap().user.id);
|
||||
|
||||
for member in members {
|
||||
let has_role = member.roles.iter().any(|role_id| admin_role_ids.contains(role_id));
|
||||
let has_admin_perms = ctx.guild()
|
||||
.map(|g| g.member_permissions(&member).administrator())
|
||||
.unwrap_or(false);
|
||||
|
||||
if (has_role || has_admin_perms) && admin_user_ids.insert(member.user.id) {
|
||||
admin_users.push(member.user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(admin_users)
|
||||
}
|
||||
|
||||
pub async fn trace_message(msg: String, channel: String, guild: String) {
|
||||
trace!("Saying \"{}\" in channel {} in guild {}", msg, channel, guild);
|
||||
}
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
# AI WRITTEN
|
||||
set -e
|
||||
|
||||
# Ensure we run from the script's directory
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Load environment variables if they exist
|
||||
if [ -f .env ]; then
|
||||
set -a
|
||||
source .env
|
||||
set +a
|
||||
fi
|
||||
|
||||
echo "Wiping database volume and restarting containers..."
|
||||
docker compose -f docker-compose-dev.yml down -v
|
||||
docker compose -f docker-compose-dev.yml up -d
|
||||
|
||||
echo "Waiting for PostgreSQL to be ready..."
|
||||
until [ "$(docker inspect -f '{{.State.Health.Status}}' tg-dev-srv-bot-postgres 2>/dev/null)" = "healthy" ]; do
|
||||
printf "."
|
||||
sleep 1
|
||||
done
|
||||
echo ""
|
||||
echo "PostgreSQL is healthy!"
|
||||
|
||||
echo "Applying database schema from sqlschema.txt..."
|
||||
docker compose -f docker-compose-dev.yml exec -T postgres psql -U "${TG_BOT_POSTGRES_USER:-postgres}" -d "${TG_BOT_POSTGRES_DB:-tg_dev_srv_bot}" -c "$(cat sqlschema.txt)"
|
||||
|
||||
echo "Database reset and schema applied successfully!"
|
||||
Reference in New Issue
Block a user