Fix woodpecker missing branch and fix cargo fmt
Some checks failed
ci/woodpecker/push/build Pipeline failed

This commit is contained in:
2026-06-03 21:40:02 +01:00
parent f03e47d9f5
commit eef2cb7558
5 changed files with 194 additions and 161 deletions

View File

@@ -34,4 +34,4 @@ steps:
platforms: linux/amd64 platforms: linux/amd64
when: when:
- event: push - event: push
branch: main branch: master

View File

@@ -17,99 +17,92 @@ use serenity::{
use std::sync::Arc; use std::sync::Arc;
pub async fn register_commands(ctx: &Context, guild_id: GuildId) -> Result<(), serenity::Error> { pub async fn register_commands(ctx: &Context, guild_id: GuildId) -> Result<(), serenity::Error> {
let commands = vec![ let commands = vec![CreateCommand::new("config")
CreateCommand::new("config") .kind(CommandType::ChatInput)
.kind(CommandType::ChatInput) .description("Configure the anti-spam bot")
.description("Configure the anti-spam bot") .default_member_permissions(Permissions::ADMINISTRATOR)
.default_member_permissions(Permissions::ADMINISTRATOR) .add_option(
.add_option( CreateCommandOption::new(
CreateCommandOption::new( CommandOptionType::SubCommand,
CommandOptionType::SubCommand, "mod-channel",
"mod-channel", "Set the mod notification channel",
"Set the mod notification channel",
)
.add_sub_option(
CreateCommandOption::new(
CommandOptionType::Channel,
"channel",
"The channel for notifications",
)
.required(true),
),
) )
.add_option( .add_sub_option(
CreateCommandOption::new( CreateCommandOption::new(
CommandOptionType::SubCommand, CommandOptionType::Channel,
"watched-channel-add", "channel",
"Add a channel to the watchlist", "The channel for notifications",
) )
.add_sub_option( .required(true),
CreateCommandOption::new( ),
CommandOptionType::Channel, )
"channel", .add_option(
"The channel to watch", CreateCommandOption::new(
) CommandOptionType::SubCommand,
.required(true), "watched-channel-add",
), "Add a channel to the watchlist",
) )
.add_option( .add_sub_option(
CreateCommandOption::new( CreateCommandOption::new(
CommandOptionType::SubCommand, CommandOptionType::Channel,
"watched-channel-remove", "channel",
"Remove a channel from the watchlist", "The channel to watch",
) )
.add_sub_option( .required(true),
CreateCommandOption::new( ),
CommandOptionType::Channel, )
"channel", .add_option(
"The channel to stop watching", CreateCommandOption::new(
) CommandOptionType::SubCommand,
.required(true), "watched-channel-remove",
), "Remove a channel from the watchlist",
) )
.add_option( .add_sub_option(
CreateCommandOption::new( CreateCommandOption::new(
CommandOptionType::SubCommand, CommandOptionType::Channel,
"watched-channel-list", "channel",
"List all watched channels", "The channel to stop watching",
),
)
.add_option(
CreateCommandOption::new(
CommandOptionType::SubCommand,
"ban-reason",
"Set the ban reason",
) )
.add_sub_option( .required(true),
CreateCommandOption::new( ),
CommandOptionType::String, )
"reason", .add_option(CreateCommandOption::new(
"The ban reason text", CommandOptionType::SubCommand,
) "watched-channel-list",
.required(true), "List all watched channels",
), ))
.add_option(
CreateCommandOption::new(
CommandOptionType::SubCommand,
"ban-reason",
"Set the ban reason",
) )
.add_option( .add_sub_option(
CreateCommandOption::new( CreateCommandOption::new(
CommandOptionType::SubCommand, CommandOptionType::String,
"immune-role", "reason",
"Set a role that makes users immune from auto-ban", "The ban reason text",
) )
.add_sub_option( .required(true),
CreateCommandOption::new( ),
CommandOptionType::Role, )
"role", .add_option(
"The immune role (or none to clear)", CreateCommandOption::new(
) CommandOptionType::SubCommand,
.required(false), "immune-role",
), "Set a role that makes users immune from auto-ban",
) )
.add_option( .add_sub_option(
CreateCommandOption::new( CreateCommandOption::new(
CommandOptionType::SubCommand, CommandOptionType::Role,
"unban", "role",
"Unban a user by ID", "The immune role (or none to clear)",
) )
.required(false),
),
)
.add_option(
CreateCommandOption::new(CommandOptionType::SubCommand, "unban", "Unban a user by ID")
.add_sub_option( .add_sub_option(
CreateCommandOption::new( CreateCommandOption::new(
CommandOptionType::String, CommandOptionType::String,
@@ -118,8 +111,7 @@ pub async fn register_commands(ctx: &Context, guild_id: GuildId) -> Result<(), s
) )
.required(true), .required(true),
), ),
), )];
];
guild_id.set_commands(&ctx.http, commands).await?; guild_id.set_commands(&ctx.http, commands).await?;
Ok(()) Ok(())
@@ -148,66 +140,85 @@ pub async fn handle_interaction(
let content = match name { let content = match name {
"mod-channel" => get_channel_option(sub_opts, "channel") "mod-channel" => get_channel_option(sub_opts, "channel")
.and_then(|cid| db.set_mod_channel(guild_id, cid).ok().map(|_| "Mod notification channel set!".to_string())) .and_then(|cid| {
db.set_mod_channel(guild_id, cid)
.ok()
.map(|_| "Mod notification channel set!".to_string())
})
.unwrap_or_else(|| "Failed to set mod channel.".to_string()), .unwrap_or_else(|| "Failed to set mod channel.".to_string()),
"watched-channel-add" => get_channel_option(sub_opts, "channel") "watched-channel-add" => get_channel_option(sub_opts, "channel")
.and_then(|cid| db.add_watched_channel(guild_id, cid).ok().map(|_| "Channel added to watchlist!".to_string())) .and_then(|cid| {
db.add_watched_channel(guild_id, cid)
.ok()
.map(|_| "Channel added to watchlist!".to_string())
})
.unwrap_or_else(|| "Failed to add channel.".to_string()), .unwrap_or_else(|| "Failed to add channel.".to_string()),
"watched-channel-remove" => get_channel_option(sub_opts, "channel") "watched-channel-remove" => get_channel_option(sub_opts, "channel")
.and_then(|cid| db.remove_watched_channel(guild_id, cid).ok().map(|_| "Channel removed from watchlist!".to_string())) .and_then(|cid| {
db.remove_watched_channel(guild_id, cid)
.ok()
.map(|_| "Channel removed from watchlist!".to_string())
})
.unwrap_or_else(|| "Failed to remove channel.".to_string()), .unwrap_or_else(|| "Failed to remove channel.".to_string()),
"watched-channel-list" => { "watched-channel-list" => {
let channels = db.get_watched_channels(guild_id).unwrap_or_default(); let channels = db.get_watched_channels(guild_id).unwrap_or_default();
if channels.is_empty() { if channels.is_empty() {
"No watched channels.".to_string() "No watched channels.".to_string()
} else { } else {
format!("Watched channels: {}", channels.iter().map(|c| format!("<#{}>", c)).collect::<Vec<_>>().join(", ")) format!(
"Watched channels: {}",
channels
.iter()
.map(|c| format!("<#{}>", c))
.collect::<Vec<_>>()
.join(", ")
)
} }
} }
"ban-reason" => get_string_option(sub_opts, "reason") "ban-reason" => get_string_option(sub_opts, "reason")
.and_then(|r| db.set_ban_reason(guild_id, &r).ok().map(|_| format!("Ban reason set to: {}", r))) .and_then(|r| {
db.set_ban_reason(guild_id, &r)
.ok()
.map(|_| format!("Ban reason set to: {}", r))
})
.unwrap_or_else(|| "Failed to set ban reason.".to_string()), .unwrap_or_else(|| "Failed to set ban reason.".to_string()),
"immune-role" => { "immune-role" => match get_role_option(sub_opts, "role") {
match get_role_option(sub_opts, "role") { Some(rid) => db
Some(rid) => db.set_immune_role(guild_id, rid) .set_immune_role(guild_id, rid)
.map(|_| format!("Immune role set to <@&{}>", rid)) .map(|_| format!("Immune role set to <@&{}>", rid))
.unwrap_or_else(|_| "Failed to set immune role.".to_string()), .unwrap_or_else(|_| "Failed to set immune role.".to_string()),
None => db.clear_immune_role(guild_id) None => db
.map(|_| "Immune role cleared. All users will be auto-banned.".to_string()) .clear_immune_role(guild_id)
.unwrap_or_else(|_| "Failed to clear immune role.".to_string()), .map(|_| "Immune role cleared. All users will be auto-banned.".to_string())
} .unwrap_or_else(|_| "Failed to clear immune role.".to_string()),
} },
"unban" => { "unban" => match get_string_option(sub_opts, "user_id") {
match get_string_option(sub_opts, "user_id") { Some(uid_str) => {
Some(uid_str) => { let uid: u64 = match uid_str.parse() {
let uid: u64 = match uid_str.parse() { Ok(id) => id,
Ok(id) => id, Err(_) => {
Err(_) => { respond(ctx, interaction, "Invalid user ID. Must be a number.").await;
respond(ctx, interaction, "Invalid user ID. Must be a number.").await; return;
return; }
} };
}; let _ = db.remove_ban_logs(guild_id, uid);
let _ = db.remove_ban_logs(guild_id, uid); match guild_id.unban(&ctx.http, UserId::new(uid)).await {
match guild_id.unban(&ctx.http, UserId::new(uid)).await { Ok(_) => format!("User <@{}> has been unbanned and ban logs cleared.", uid),
Ok(_) => format!("User <@{}> has been unbanned and ban logs cleared.", uid), Err(e) => {
Err(e) => { eprintln!("Unban error: {:?}", e);
eprintln!("Unban error: {:?}", e); "Ban logs cleared, but Discord unban failed. The user may not be currently banned.".to_string()
"Ban logs cleared, but Discord unban failed. The user may not be currently banned.".to_string()
}
} }
} }
None => "No user ID provided.".to_string(),
} }
} None => "No user ID provided.".to_string(),
},
_ => "Unknown subcommand. Use /config to see available options.".to_string(), _ => "Unknown subcommand. Use /config to see available options.".to_string(),
}; };
respond(ctx, interaction, &content).await; respond(ctx, interaction, &content).await;
} }
fn extract_subcommand( fn extract_subcommand(options: &[CommandDataOption]) -> Option<(&str, &[CommandDataOption])> {
options: &[CommandDataOption],
) -> Option<(&str, &[CommandDataOption])> {
let opt = options.first()?; let opt = options.first()?;
match &opt.value { match &opt.value {
CommandDataOptionValue::SubCommand(opts) => Some((&opt.name, opts.as_slice())), CommandDataOptionValue::SubCommand(opts) => Some((&opt.name, opts.as_slice())),
@@ -215,10 +226,7 @@ fn extract_subcommand(
} }
} }
fn get_channel_option( fn get_channel_option(options: &[CommandDataOption], name: &str) -> Option<ChannelId> {
options: &[CommandDataOption],
name: &str,
) -> Option<ChannelId> {
options.iter().find(|o| o.name == name).and_then(|opt| { options.iter().find(|o| o.name == name).and_then(|opt| {
if let CommandDataOptionValue::Channel(c) = &opt.value { if let CommandDataOptionValue::Channel(c) = &opt.value {
Some(*c) Some(*c)
@@ -228,10 +236,7 @@ fn get_channel_option(
}) })
} }
fn get_role_option( fn get_role_option(options: &[CommandDataOption], name: &str) -> Option<RoleId> {
options: &[CommandDataOption],
name: &str,
) -> Option<RoleId> {
options.iter().find(|o| o.name == name).and_then(|opt| { options.iter().find(|o| o.name == name).and_then(|opt| {
if let CommandDataOptionValue::Role(r) = &opt.value { if let CommandDataOptionValue::Role(r) = &opt.value {
Some(*r) Some(*r)
@@ -241,10 +246,7 @@ fn get_role_option(
}) })
} }
fn get_string_option( fn get_string_option(options: &[CommandDataOption], name: &str) -> Option<String> {
options: &[CommandDataOption],
name: &str,
) -> Option<String> {
options.iter().find(|o| o.name == name).and_then(|opt| { options.iter().find(|o| o.name == name).and_then(|opt| {
if let CommandDataOptionValue::String(s) = &opt.value { if let CommandDataOptionValue::String(s) = &opt.value {
Some(s.clone()) Some(s.clone())
@@ -254,11 +256,7 @@ fn get_string_option(
}) })
} }
async fn respond( async fn respond(ctx: &Context, interaction: &CommandInteraction, content: &str) {
ctx: &Context,
interaction: &CommandInteraction,
content: &str,
) {
let _ = interaction let _ = interaction
.create_response( .create_response(
&ctx.http, &ctx.http,
@@ -267,4 +265,4 @@ async fn respond(
), ),
) )
.await; .await;
} }

View File

@@ -13,7 +13,9 @@ impl Database {
rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE | rusqlite::OpenFlags::SQLITE_OPEN_CREATE, rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE | rusqlite::OpenFlags::SQLITE_OPEN_CREATE,
)?; )?;
conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")?; conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")?;
Ok(Database { conn: Mutex::new(conn) }) Ok(Database {
conn: Mutex::new(conn),
})
} }
pub fn init(&self) -> Result<()> { pub fn init(&self) -> Result<()> {
@@ -73,9 +75,8 @@ impl Database {
pub fn get_watched_channels(&self, guild_id: GuildId) -> Result<Vec<ChannelId>> { pub fn get_watched_channels(&self, guild_id: GuildId) -> Result<Vec<ChannelId>> {
let gid = guild_id.get() as i64; let gid = guild_id.get() as i64;
let guard = self.conn.lock().unwrap(); let guard = self.conn.lock().unwrap();
let mut stmt = guard.prepare( let mut stmt =
"SELECT channel_id FROM watched_channels WHERE guild_id = ?", guard.prepare("SELECT channel_id FROM watched_channels WHERE guild_id = ?")?;
)?;
let rows = stmt.query_map([gid], |row| row.get::<_, i64>(0))?; let rows = stmt.query_map([gid], |row| row.get::<_, i64>(0))?;
let channels: Vec<ChannelId> = rows let channels: Vec<ChannelId> = rows
.filter_map(|r| r.ok()) .filter_map(|r| r.ok())
@@ -152,7 +153,13 @@ impl Database {
} }
} }
pub fn log_ban(&self, guild_id: GuildId, user_id: u64, channel_id: ChannelId, message_snippet: &str) -> Result<()> { pub fn log_ban(
&self,
guild_id: GuildId,
user_id: u64,
channel_id: ChannelId,
message_snippet: &str,
) -> Result<()> {
let gid = guild_id.get() as i64; let gid = guild_id.get() as i64;
let uid = user_id as i64; let uid = user_id as i64;
let cid = channel_id.get() as i64; let cid = channel_id.get() as i64;
@@ -164,11 +171,11 @@ impl Database {
} }
pub fn total_bans(&self) -> Result<i64> { pub fn total_bans(&self) -> Result<i64> {
let count: i64 = self.conn.lock().unwrap().query_row( let count: i64 =
"SELECT COUNT(*) FROM ban_log", self.conn
[], .lock()
|row| row.get(0), .unwrap()
)?; .query_row("SELECT COUNT(*) FROM ban_log", [], |row| row.get(0))?;
Ok(count) Ok(count)
} }
@@ -181,4 +188,4 @@ impl Database {
)?; )?;
Ok(()) Ok(())
} }
} }

View File

@@ -1,20 +1,16 @@
use crate::commands::{handle_interaction, register_commands}; use crate::commands::{handle_interaction, register_commands};
use crate::db::Database; use crate::db::Database;
use serenity::all::ActivityData; use serenity::all::ActivityData;
use serenity::model::channel::MessageFlags;
use serenity::{ use serenity::{
async_trait, async_trait,
builder::{CreateAttachment, CreateMessage}, builder::{CreateAttachment, CreateMessage},
client::Context, client::Context,
model::{ model::{
application::Interaction, application::Interaction, channel::Message, gateway::Ready, guild::Guild, id::GuildId,
channel::Message,
gateway::Ready,
guild::Guild,
id::GuildId,
user::OnlineStatus, user::OnlineStatus,
}, },
}; };
use serenity::model::channel::MessageFlags;
use std::sync::Arc; use std::sync::Arc;
pub struct Handler { pub struct Handler {
@@ -24,7 +20,9 @@ pub struct Handler {
#[async_trait] #[async_trait]
impl serenity::client::EventHandler for Handler { impl serenity::client::EventHandler for Handler {
async fn ready(&self, _ctx: Context, ready: Ready) { async fn ready(&self, _ctx: Context, ready: Ready) {
let shard_info = ready.shard.map_or("N/A".to_string(), |s| format!("{}/{}", s.id.0, s.total)); let shard_info = ready
.shard
.map_or("N/A".to_string(), |s| format!("{}/{}", s.id.0, s.total));
println!("{} is connected! [Shard {}]", ready.user.name, shard_info); println!("{} is connected! [Shard {}]", ready.user.name, shard_info);
} }
@@ -41,7 +39,10 @@ impl serenity::client::EventHandler for Handler {
guild_count, total_bans guild_count, total_bans
); );
ctx.set_presence( ctx.set_presence(
Some(ActivityData::watching(format!("{} guilds | {} bans", guild_count, total_bans))), Some(ActivityData::watching(format!(
"{} guilds | {} bans",
guild_count, total_bans
))),
OnlineStatus::Online, OnlineStatus::Online,
); );
} }
@@ -61,7 +62,10 @@ impl serenity::client::EventHandler for Handler {
_ => return, _ => return,
} }
if let Err(e) = self.handle_watched_channel_message(&ctx, &msg, guild_id).await { if let Err(e) = self
.handle_watched_channel_message(&ctx, &msg, guild_id)
.await
{
eprintln!("Error handling watched channel message: {:?}", e); eprintln!("Error handling watched channel message: {:?}", e);
} }
} }
@@ -121,16 +125,40 @@ impl Handler {
if member.roles.is_empty() { if member.roles.is_empty() {
String::new() String::new()
} else { } else {
format!("\nRoles: {}", member.roles.iter().map(|r| format!("<@&{}>", r)).collect::<Vec<_>>().join(" ")) format!(
"\nRoles: {}",
member
.roles
.iter()
.map(|r| format!("<@&{}>", r))
.collect::<Vec<_>>()
.join(" ")
)
} }
} else { } else {
String::new() String::new()
}; };
let copy = format!("**{}** ({}):{}{}", msg.author.name, msg.author.id, roles_line, if msg.content.is_empty() { String::new() } else { format!("\n{}", msg.content) }); let copy = format!(
"**{}** ({}):{}{}",
msg.author.name,
msg.author.id,
roles_line,
if msg.content.is_empty() {
String::new()
} else {
format!("\n{}", msg.content)
}
);
let _ = mod_channel_id let _ = mod_channel_id
.send_message(&ctx.http, CreateMessage::new().content(&copy).files(files).flags(MessageFlags::SUPPRESS_EMBEDS)) .send_message(
&ctx.http,
CreateMessage::new()
.content(&copy)
.files(files)
.flags(MessageFlags::SUPPRESS_EMBEDS),
)
.await; .await;
let _ = msg.delete(&ctx.http).await; let _ = msg.delete(&ctx.http).await;
@@ -153,4 +181,4 @@ impl Handler {
Ok(()) Ok(())
} }
} }

View File

@@ -37,4 +37,4 @@ async fn main() {
if let Err(why) = client.start().await { if let Err(why) = client.start().await {
println!("Client error: {:?}", why); println!("Client error: {:?}", why);
} }
} }