Compare commits
3 Commits
master
...
module/mod
Author | SHA1 | Date | |
---|---|---|---|
8a3d90789d | |||
ca26eb6fd1 | |||
6d3033b3e7 |
82
Cargo.lock
generated
82
Cargo.lock
generated
@ -958,11 +958,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kon"
|
name = "kon"
|
||||||
version = "0.2.7"
|
version = "0.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cargo_toml",
|
"cargo_toml",
|
||||||
"gamedig",
|
"gamedig",
|
||||||
|
"lazy_static",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"parse_duration",
|
||||||
"poise",
|
"poise",
|
||||||
"reqwest 0.12.3",
|
"reqwest 0.12.3",
|
||||||
"serde",
|
"serde",
|
||||||
@ -1123,12 +1125,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"
|
||||||
@ -1230,6 +1299,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"
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kon"
|
name = "kon"
|
||||||
version = "0.2.7"
|
version = "0.3.2"
|
||||||
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.3", features = ["json"] }
|
reqwest = { version = "0.12.3", features = ["json"] }
|
||||||
serde = "1.0.197"
|
serde = "1.0.197"
|
||||||
|
@ -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
203
src/commands/moderation.rs
Normal 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(())
|
||||||
|
}
|
@ -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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +1,2 @@
|
|||||||
pub mod database;
|
pub mod database;
|
||||||
|
pub mod timers;
|
||||||
|
95
src/controllers/timers.rs
Normal file
95
src/controllers/timers.rs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
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(6));
|
||||||
|
|
||||||
|
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?;
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let guild_id = GuildId::new(event.guild_id as u64);
|
||||||
|
let cached_guild_data = discord_.cache.guild(guild_id);
|
||||||
|
|
||||||
|
println!("ModerationTimer[CaseExpired]: {}:#{}:{}:{}", cached_guild_data.unwrap().name.to_owned(), event.case_id, event.user_tag, event.reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
28
src/main.rs
28
src/main.rs
@ -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() {
|
||||||
|
@ -1 +1,2 @@
|
|||||||
pub mod gameservers;
|
pub mod gameservers;
|
||||||
|
pub mod moderation_events;
|
||||||
|
215
src/models/moderation_events.rs
Normal file
215
src/models/moderation_events.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user