Add moderation to Kon and refactor unrelated parts

This commit is contained in:
toast 2024-03-29 21:28:04 +11:00
parent 10dd8f7231
commit 6f60296e25
11 changed files with 676 additions and 39 deletions

134
Cargo.lock generated
View File

@ -58,7 +58,7 @@ checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.55", "syn 2.0.57",
] ]
[[package]] [[package]]
@ -308,7 +308,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"strsim", "strsim",
"syn 2.0.55", "syn 2.0.57",
] ]
[[package]] [[package]]
@ -319,7 +319,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [ dependencies = [
"darling_core", "darling_core",
"quote", "quote",
"syn 2.0.55", "syn 2.0.57",
] ]
[[package]] [[package]]
@ -398,7 +398,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.55", "syn 2.0.57",
] ]
[[package]] [[package]]
@ -528,7 +528,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.55", "syn 2.0.57",
] ]
[[package]] [[package]]
@ -952,11 +952,13 @@ dependencies = [
[[package]] [[package]]
name = "kon" name = "kon"
version = "0.2.7" version = "0.3.1"
dependencies = [ dependencies = [
"cargo_toml", "cargo_toml",
"gamedig", "gamedig",
"lazy_static",
"once_cell", "once_cell",
"parse_duration",
"poise", "poise",
"reqwest 0.12.2", "reqwest 0.12.2",
"serde", "serde",
@ -1117,12 +1119,79 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "num"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36"
dependencies = [
"num-bigint",
"num-complex",
"num-integer",
"num-iter",
"num-rational",
"num-traits",
]
[[package]]
name = "num-bigint"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-complex"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95"
dependencies = [
"autocfg",
"num-traits",
]
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.1.0" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef"
dependencies = [
"autocfg",
"num-bigint",
"num-integer",
"num-traits",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.18" version = "0.2.18"
@ -1180,7 +1249,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.55", "syn 2.0.57",
] ]
[[package]] [[package]]
@ -1224,6 +1293,17 @@ dependencies = [
"windows-targets 0.48.5", "windows-targets 0.48.5",
] ]
[[package]]
name = "parse_duration"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7037e5e93e0172a5a96874380bf73bc6ecef022e26fa25f2be26864d6b3ba95d"
dependencies = [
"lazy_static",
"num",
"regex",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"
@ -1260,7 +1340,7 @@ dependencies = [
"phf_shared", "phf_shared",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.55", "syn 2.0.57",
] ]
[[package]] [[package]]
@ -1289,14 +1369,14 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.55", "syn 2.0.57",
] ]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.13" version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
[[package]] [[package]]
name = "pin-utils" name = "pin-utils"
@ -1336,7 +1416,7 @@ dependencies = [
"darling", "darling",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.55", "syn 2.0.57",
] ]
[[package]] [[package]]
@ -1747,9 +1827,9 @@ dependencies = [
[[package]] [[package]]
name = "security-framework" name = "security-framework"
version = "2.9.2" version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"core-foundation", "core-foundation",
@ -1760,9 +1840,9 @@ dependencies = [
[[package]] [[package]]
name = "security-framework-sys" name = "security-framework-sys"
version = "2.9.1" version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef"
dependencies = [ dependencies = [
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@ -1794,7 +1874,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.55", "syn 2.0.57",
] ]
[[package]] [[package]]
@ -1980,9 +2060,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.55" version = "2.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" checksum = "11a6ae1e52eb25aab8f3fb9fca13be982a373b8f1157ca14b897a825ba4a2d35"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2066,7 +2146,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.55", "syn 2.0.57",
] ]
[[package]] [[package]]
@ -2117,9 +2197,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokenservice-client" name = "tokenservice-client"
version = "0.2.0" version = "0.2.1"
source = "sparse+https://git.toast-server.net/api/packages/toast/cargo/" source = "sparse+https://git.toast-server.net/api/packages/toast/cargo/"
checksum = "ce6d723554b87f8068463a7aae14ed604e2804a8a0827d251d7ac08850bac18e" checksum = "f406f95c4d818de69519f9ac3874d876a7ba0662d5965506ef6b2b5784acf4bb"
dependencies = [ dependencies = [
"reqwest 0.12.2", "reqwest 0.12.2",
"serde", "serde",
@ -2153,7 +2233,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.55", "syn 2.0.57",
] ]
[[package]] [[package]]
@ -2325,7 +2405,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.55", "syn 2.0.57",
] ]
[[package]] [[package]]
@ -2454,7 +2534,7 @@ checksum = "0b122284365ba8497be951b9a21491f70c9688eb6fddc582931a0703f6a00ece"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.55", "syn 2.0.57",
] ]
[[package]] [[package]]
@ -2601,7 +2681,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.55", "syn 2.0.57",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -2635,7 +2715,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.55", "syn 2.0.57",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]

View File

@ -1,12 +1,14 @@
[package] [package]
name = "kon" name = "kon"
version = "0.2.7" version = "0.3.1"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
cargo_toml = "0.19.2" cargo_toml = "0.19.2"
gamedig = "0.5.0" gamedig = "0.5.0"
lazy_static = "1.4.0"
once_cell = "1.19.0" once_cell = "1.19.0"
parse_duration = "2.1.1"
poise = "0.6.1" poise = "0.6.1"
reqwest = { version = "0.12.2", features = ["json"] } reqwest = { version = "0.12.2", features = ["json"] }
serde = "1.0.197" serde = "1.0.197"

View File

@ -2,3 +2,4 @@ pub mod ping;
pub mod status; pub mod status;
pub mod uptime; pub mod uptime;
pub mod gameserver; pub mod gameserver;
pub mod moderation;

203
src/commands/moderation.rs Normal file
View File

@ -0,0 +1,203 @@
use crate::{
Error,
internals::utils::capitalize_first,
models::moderation_events::{
Moderations,
ActionTypes
}
};
use poise::CreateReply;
use poise::serenity_prelude::Member;
use parse_duration::parse;
use std::time::SystemTime;
static FALLBACK_REASON: &str = "Reason unknown";
fn duration2epoch(duration: &str) -> Result<i64, Error> {
match parse(duration) {
Ok(dur) => {
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map_err(|_| Error::from("System time before Unix Epoch"))?;
Ok((now + dur).as_secs() as i64)
}
Err(_) => Err(Error::from("Invalid duration format"))
}
}
/// Subcommands collection for /case command
#[poise::command(
slash_command,
guild_only,
subcommands("update"),
default_member_permissions = "KICK_MEMBERS | BAN_MEMBERS | MODERATE_MEMBERS"
)]
pub async fn case(_: poise::Context<'_, (), Error>) -> Result<(), Error> {
Ok(())
}
/// Update a case with new reason
#[poise::command(slash_command, guild_only)]
pub async fn update(
ctx: poise::Context<'_, (), Error>,
#[description = "Case ID to update"] case_id: i32,
#[description = "New reason for the case"] reason: String
) -> Result<(), Error> {
match Moderations::update_case(
i64::from(ctx.guild_id().unwrap()),
case_id,
false,
Some(reason.clone())
).await {
Ok(_) => ctx.send(CreateReply::default().content(format!("Case #{} updated with new reason:\n`{}`", case_id, reason))).await?,
Err(e) => ctx.send(CreateReply::default().content(format!("Error updating case ID: {}\nError: {}", case_id, e))).await?
};
Ok(())
}
/// Kick a member
#[poise::command(
slash_command,
guild_only,
default_member_permissions = "KICK_MEMBERS",
required_bot_permissions = "KICK_MEMBERS"
)]
pub async fn kick(
ctx: poise::Context<'_, (), Error>,
#[description = "Member to be kicked"] member: Member,
#[description = "Reason for the kick"] reason: Option<String>
) -> Result<(), Error> {
let reason = reason.unwrap_or(FALLBACK_REASON.to_string());
let case = Moderations::create_case(
i64::from(ctx.guild_id().unwrap()),
ActionTypes::Kick,
false,
i64::from(member.user.id),
member.user.tag(),
reason.clone(),
i64::from(ctx.author().id),
ctx.author().tag(),
ctx.created_at().timestamp(),
None
).await?;
Moderations::generate_modlog(case.clone(), &ctx.http(), ctx.channel_id().into()).await?;
member.kick_with_reason(&ctx.http(), &reason).await?;
ctx.send(CreateReply::default().content(format!("Member: {}\nReason: `{}`\nType: {}\nModerator: {}", member.user.tag(), reason, capitalize_first(case.action_type.as_str()), ctx.author().tag()))).await?;
Ok(())
}
/// Ban a member
#[poise::command(
slash_command,
guild_only,
default_member_permissions = "BAN_MEMBERS",
required_bot_permissions = "BAN_MEMBERS"
)]
pub async fn ban(
ctx: poise::Context<'_, (), Error>,
#[description = "Member to be banned"] member: Member,
#[description = "Reason for the ban"] reason: Option<String>,
#[description = "Ban duration"] duration: Option<String>
) -> Result<(), Error> {
let reason = reason.unwrap_or(FALLBACK_REASON.to_string());
let duration = match duration {
Some(d) => Some(duration2epoch(&d)?),
None => None
};
let is_case_active = duration.is_some();
let case = Moderations::create_case(
i64::from(ctx.guild_id().unwrap()),
ActionTypes::Ban,
is_case_active,
i64::from(member.user.id),
member.user.tag(),
reason.clone(),
i64::from(ctx.author().id),
ctx.author().tag(),
ctx.created_at().timestamp(),
duration
).await?;
Moderations::generate_modlog(case.clone(), &ctx.http(), ctx.channel_id().into()).await?;
member.ban_with_reason(&ctx.http(), 0, &reason).await?;
ctx.send(CreateReply::default().content(format!("Member: {}\nReason: `{}`\nType: {}\nModerator: {}\nDuration: `{}`", member.user.tag(), reason, capitalize_first(case.action_type.as_str()), ctx.author().tag(), duration.unwrap()))).await?;
Ok(())
}
/// Timeout a member
#[poise::command(
slash_command,
guild_only,
default_member_permissions = "MODERATE_MEMBERS",
required_bot_permissions = "MODERATE_MEMBERS"
)]
pub async fn mute(
ctx: poise::Context<'_, (), Error>,
#[description = "Member to be muted"] mut member: Member,
#[description = "Mute duration"] duration: String,
#[description = "Reason for the mute"] reason: Option<String>
) -> Result<(), Error> {
let reason = reason.unwrap_or(FALLBACK_REASON.to_string());
let duration = Some(duration2epoch(&duration)?);
let is_case_active = duration.is_some();
let case = Moderations::create_case(
i64::from(ctx.guild_id().unwrap()),
ActionTypes::Mute,
is_case_active,
i64::from(member.user.id),
member.user.tag(),
reason.clone(),
i64::from(ctx.author().id),
ctx.author().tag(),
ctx.created_at().timestamp(),
duration
).await?;
println!("case.duration: {}", case.duration.unwrap().to_string().as_str());
let mute_time = poise::serenity_prelude::Timestamp::from_unix_timestamp(case.duration.unwrap()).expect("Failed to format timestamp");
member.disable_communication_until_datetime(&ctx.http(), mute_time).await?;
ctx.send(CreateReply::default().content(format!("Member: {}\nReason: `{}`\nType: {}\nModerator: {}\nDuration: `{}`", member.user.tag(), reason, capitalize_first(case.action_type.as_str()), ctx.author().tag(), mute_time))).await?;
Ok(())
}
/// Warn a member
#[poise::command(
slash_command,
guild_only,
default_member_permissions = "MODERATE_MEMBERS",
required_bot_permissions = "MODERATE_MEMBERS"
)]
pub async fn warn(
ctx: poise::Context<'_, (), Error>,
#[description = "Member to be warned"] member: Member,
#[description = "Reason for the warn"] reason: Option<String>
) -> Result<(), Error> {
let reason = reason.unwrap_or(FALLBACK_REASON.to_string());
let case = Moderations::create_case(
i64::from(ctx.guild_id().unwrap()),
ActionTypes::Warn,
false,
i64::from(member.user.id),
member.user.tag(),
reason.clone(),
i64::from(ctx.author().id),
ctx.author().tag(),
ctx.created_at().timestamp(),
None
).await?;
Moderations::generate_modlog(case.clone(), &ctx.http(), ctx.channel_id().into()).await?;
ctx.send(CreateReply::default().content(format!("Member: {}\nReason: `{}`\nType: {}\nModerator: {}", member.user.tag(), reason, capitalize_first(case.action_type.as_str()), ctx.author().tag()))).await?;
Ok(())
}

View File

@ -32,6 +32,33 @@ impl DatabaseController {
); );
").await?; ").await?;
// Guild Case IDs
client.batch_execute("
CREATE TABLE IF NOT EXISTS guild_case_ids (
guild_id BIGINT NOT NULL,
max_case_id INT NOT NULL DEFAULT 0,
PRIMARY KEY (guild_id)
);
").await?;
// ModerationEvents
client.batch_execute("
CREATE TABLE IF NOT EXISTS moderation_events (
guild_id BIGINT NOT NULL,
case_id INT NOT NULL,
action_type VARCHAR(255) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT FALSE,
user_id BIGINT NOT NULL,
user_tag VARCHAR(255) NOT NULL,
reason VARCHAR(1024) NOT NULL,
moderator_id BIGINT NOT NULL,
moderator_tag VARCHAR(255) NOT NULL,
time_created BIGINT NOT NULL,
duration BIGINT,
PRIMARY KEY (guild_id, case_id)
);
").await?;
Ok(DatabaseController { client }) Ok(DatabaseController { client })
} }
} }

View File

@ -1 +1,2 @@
pub mod database; pub mod database;
pub mod timers;

93
src/controllers/timers.rs Normal file
View File

@ -0,0 +1,93 @@
use crate::{
Error,
models::moderation_events::{
Moderations,
ActionTypes
}
};
use std::time::SystemTime;
use poise::serenity_prelude::{
Context,
model::{
user::CurrentUser,
id::{
UserId,
GuildId
}
},
};
use tokio::time::{
interval,
Duration
};
fn timer_failed(name: &str) -> String {
format!("Failed to start timer for {}", name)
}
pub async fn start_timers(discord_: &Context, bot_: CurrentUser) -> Result<(), Error> {
let ctx_clone = discord_.clone();
tokio::spawn(async move {
check_modlog_cases(&ctx_clone, bot_).await.expect(&timer_failed("moderation events"))
});
Ok(())
}
async fn check_modlog_cases(discord_: &Context, bot_: CurrentUser) -> Result<(), Error> {
let mut interval = interval(Duration::from_secs(15));
loop {
interval.tick().await;
let events = Moderations::get_active_events().await?;
for event in events {
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map_err(|_| Error::from("System time before Unix Epoch"))?;
let check_action_type = match event.action_type {
ActionTypes::Ban => ActionTypes::Unban,
ActionTypes::Mute => ActionTypes::Unmute,
_ => continue // Skip if not a timed action
};
if let Some(duration) = event.duration {
let duration = Duration::from_secs(duration as u64);
if now > duration {
Moderations::update_case(
event.guild_id,
event.case_id,
false,
None
).await?;
Moderations::generate_modlog(Moderations::create_case(
event.guild_id,
check_action_type,
false,
event.user_id,
event.user_tag.clone(),
format!("Duration for Case #{} has expired", event.case_id),
bot_.id.into(),
bot_.tag(),
SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as i64,
None
).await?, &discord_.http, 865673694184996888).await?;
match check_action_type {
ActionTypes::Unban => {
let guild_id = GuildId::new(event.guild_id as u64);
let user_id = UserId::new(event.user_id as u64);
discord_.http.remove_ban(guild_id, user_id, Some(format!("Duration for Case #{} has expired", event.case_id).as_str())).await?;
},
_ => {}
}
println!("ModerationTimer[CaseExpired]: ({}) #{} - {}", event.guild_id, event.case_id, event.user_tag)
}
}
}
}
}

View File

@ -17,6 +17,14 @@ pub fn concat_message(messages: Vec<String>) -> String {
messages.join("\n") messages.join("\n")
} }
pub fn capitalize_first(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + chars.as_str(),
}
}
pub fn format_duration(secs: u64) -> String { pub fn format_duration(secs: u64) -> String {
let days = secs / 86400; let days = secs / 86400;
let hours = (secs % 86400) / 3600; let hours = (secs % 86400) / 3600;

View File

@ -3,10 +3,6 @@ mod controllers;
mod models; mod models;
mod internals; mod internals;
use std::{
env::var,
error
};
use poise::serenity_prelude::{ use poise::serenity_prelude::{
builder::{ builder::{
CreateMessage, CreateMessage,
@ -21,10 +17,12 @@ use poise::serenity_prelude::{
GatewayIntents GatewayIntents
}; };
type Error = Box<dyn error::Error + Send + Sync>; type Error = Box<dyn std::error::Error + Send + Sync>;
static BOT_READY_NOTIFY: u64 = 865673694184996888; static BOT_READY_NOTIFY: u64 = 865673694184996888;
static CAN_DEPLOY_COMMANDS: bool = false;
async fn on_ready( async fn on_ready(
ctx: &Context, ctx: &Context,
ready: &Ready, ready: &Ready,
@ -32,6 +30,8 @@ async fn on_ready(
) -> Result<(), Error> { ) -> Result<(), Error> {
println!("Connected to API as {}", ready.user.name); println!("Connected to API as {}", ready.user.name);
controllers::timers::start_timers(&ctx, ready.user.to_owned()).await.expect("Failed to start timers");
let message = CreateMessage::new(); let message = CreateMessage::new();
let ready_embed = CreateEmbed::new() let ready_embed = CreateEmbed::new()
.color(internals::utils::EMBED_COLOR) .color(internals::utils::EMBED_COLOR)
@ -40,15 +40,14 @@ async fn on_ready(
ChannelId::new(BOT_READY_NOTIFY).send_message(&ctx.http, message.add_embed(ready_embed)).await?; ChannelId::new(BOT_READY_NOTIFY).send_message(&ctx.http, message.add_embed(ready_embed)).await?;
let register_commands = var("REGISTER_CMDS").unwrap_or_else(|_| String::from("true")).parse::<bool>().unwrap_or(true); if CAN_DEPLOY_COMMANDS {
if register_commands {
let builder = poise::builtins::create_application_commands(&framework.options().commands); let builder = poise::builtins::create_application_commands(&framework.options().commands);
let commands = Command::set_global_commands(&ctx.http, builder).await; let commands = Command::set_global_commands(&ctx.http, builder).await;
match commands { match commands {
Ok(cmdmap) => for command in cmdmap.iter() { Ok(cmdmap) => {
println!("Registered command globally: {}", command.name); let command_box: Vec<_> = cmdmap.iter().map(|cmd| cmd.name.clone()).collect();
println!("Registered commands globally: {}", command_box.join("\n- "));
}, },
Err(why) => println!("Error registering commands: {:?}", why) Err(why) => println!("Error registering commands: {:?}", why)
} }
@ -67,7 +66,14 @@ async fn main() {
commands::ping::ping(), commands::ping::ping(),
commands::uptime::uptime(), commands::uptime::uptime(),
commands::status::status(), commands::status::status(),
commands::gameserver::gameserver() commands::gameserver::gameserver(),
// Separator here to make it easier to read and update moderation stuff below
commands::moderation::case(),
commands::moderation::update(),
commands::moderation::ban(),
commands::moderation::kick(),
commands::moderation::mute(),
commands::moderation::warn(),
], ],
pre_command: |ctx| Box::pin(async move { pre_command: |ctx| Box::pin(async move {
let get_guild_name = match ctx.guild() { let get_guild_name = match ctx.guild() {

View File

@ -1 +1,2 @@
pub mod gameservers; pub mod gameservers;
pub mod moderation_events;

View File

@ -0,0 +1,215 @@
use crate::{
controllers::database::DatabaseController,
internals::utils::{
EMBED_COLOR,
capitalize_first
}
};
use poise::serenity_prelude::{
Http,
Error,
Timestamp,
ChannelId,
CreateMessage,
CreateEmbed
};
#[derive(Clone)]
pub struct Moderations {
pub guild_id: i64,
pub case_id: i32,
pub action_type: ActionTypes,
pub is_active: bool,
pub user_id: i64,
pub user_tag: String,
pub reason: String,
pub moderator_id: i64,
pub moderator_tag: String,
pub time_created: i64,
pub duration: Option<i64>
}
#[derive(Clone, Copy)]
pub enum ActionTypes {
Ban,
Kick,
Mute,
Warn,
Unban,
Unmute,
Unknown
}
impl ActionTypes {
pub fn as_str(&self) -> &'static str {
match *self {
ActionTypes::Ban => "ban",
ActionTypes::Kick => "kick",
ActionTypes::Mute => "mute",
ActionTypes::Warn => "warn",
ActionTypes::Unban => "unban",
ActionTypes::Unmute => "unmute",
ActionTypes::Unknown => "unknown"
}
}
}
impl Moderations {
pub async fn get_active_events() -> Result<Vec<Moderations>, tokio_postgres::Error> {
let _db = DatabaseController::new().await?.client;
_db.execute("BEGIN", &[]).await.expect("Failed to start transaction!");
let stmt = _db.prepare("
SELECT * FROM moderation_events
WHERE is_active = true
ORDER BY duration DESC, time_created DESC;
").await?;
_db.execute("COMMIT", &[]).await.expect("Failed to commit transaction!");
let rows = _db.query(&stmt, &[]).await?;
let mut moderations = Vec::new();
for row in rows {
moderations.push(Moderations {
guild_id: row.get("guild_id"),
case_id: row.get("case_id"),
action_type: match row.get::<_, &str>("action_type") {
"ban" => ActionTypes::Ban,
"kick" => ActionTypes::Kick,
"mute" => ActionTypes::Mute,
"warn" => ActionTypes::Warn,
"unban" => ActionTypes::Unban,
"unmute" => ActionTypes::Unmute,
_ => ActionTypes::Unknown
},
is_active: row.get("is_active"),
user_id: row.get("user_id"),
user_tag: row.get("user_tag"),
reason: row.get("reason"),
moderator_id: row.get("moderator_id"),
moderator_tag: row.get("moderator_tag"),
time_created: row.get("time_created"),
duration: row.get("duration")
});
}
Ok(moderations)
}
pub async fn generate_modlog(case: Moderations, http: &Http, channel_id: u64) -> Result<(), Error> {
let time_created_formatted = Timestamp::from_unix_timestamp(case.time_created).expect(" Failed to format timestamp!");
let modlog_embed = CreateEmbed::default()
.color(EMBED_COLOR)
.title(format!("{} • Case #{}", capitalize_first(case.action_type.as_str()), case.case_id))
.fields(vec![
("User", format!("{}\n<@{}>", case.user_tag, case.user_id), true),
("Moderator", format!("{}\n<@{}>", case.moderator_tag, case.moderator_id), true),
("\u{200B}", "\u{200B}".to_string(), true),
("Reason", format!("`{}`", case.reason), false)
])
.timestamp(time_created_formatted);
ChannelId::new(channel_id).send_message(http, CreateMessage::new().embed(modlog_embed)).await?;
Ok(())
}
pub async fn create_case(
guild_id: i64,
action_type: ActionTypes,
is_active: bool,
user_id: i64,
user_tag: String,
reason: String,
moderator_id: i64,
moderator_tag: String,
time_created: i64,
duration: Option<i64>
) -> Result<Moderations, tokio_postgres::Error> {
let _db = DatabaseController::new().await?.client;
// Get the current max case_id for the guild
let stmt = _db.prepare("
SELECT max_case_id FROM guild_case_ids WHERE guild_id = $1;
").await?;
let rows = _db.query(&stmt, &[&guild_id]).await?;
let mut max_case_id = if let Some(row) = rows.get(0) {
row.get::<_, i32>("max_case_id")
} else {
0
};
// Increment the max case_id for the guild
max_case_id += 1;
let stmt = _db.prepare("
INSERT INTO guild_case_ids (guild_id, max_case_id) VALUES ($1, $2)
ON CONFLICT (guild_id) DO UPDATE SET max_case_id = $2;
").await?;
_db.execute(&stmt, &[&guild_id, &max_case_id]).await?;
// Create a new case in database and return the case_id
let stmt = _db.prepare("
INSERT INTO moderation_events (
guild_id, case_id, action_type, is_active, user_id, user_tag, reason, moderator_id, moderator_tag, time_created, duration
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING case_id;
").await?;
let row = _db.query(&stmt, &[
&guild_id,
&max_case_id,
&action_type.as_str(),
&is_active,
&user_id,
&user_tag,
&reason,
&moderator_id,
&moderator_tag,
&time_created,
&duration
]).await?;
let moderations = Moderations {
guild_id,
case_id: row[0].get("case_id"),
action_type,
is_active,
user_id,
user_tag,
reason,
moderator_id,
moderator_tag,
time_created,
duration
};
Ok(moderations)
}
pub async fn update_case(
guild_id: i64,
case_id: i32,
is_active: bool,
reason: Option<String>
) -> Result<(), tokio_postgres::Error> {
let _db = DatabaseController::new().await?.client;
match reason {
Some(reason) => {
_db.execute("
UPDATE moderation_events
SET is_active = $3, reason = $4 WHERE guild_id = $1 AND case_id = $2;
", &[&guild_id, &case_id, &is_active, &reason]).await?;
},
None => {
_db.execute("
UPDATE moderation_events
SET is_active = $3 WHERE guild_id = $1 AND case_id = $2;
", &[&guild_id, &case_id, &is_active]).await?;
}
}
Ok(())
}
}