From b52d3b9477272473f94bf0d067469e454ab76df0 Mon Sep 17 00:00:00 2001 From: liyunze <50455574+liyunze-coding@users.noreply.github.com> Date: Wed, 7 Jan 2026 20:42:20 +0800 Subject: [PATCH] added forum post --- src/commands.rs | 1 + src/commands/faq.rs | 17 +++-- src/commands/forum.rs | 123 ++++++++++++++++++++++++++++++++++++ src/database/database.rs | 18 +++++- src/events.rs | 1 + src/events/thread_create.rs | 81 ++++++++++++++++++++++++ src/main.rs | 20 ++++-- 7 files changed, 245 insertions(+), 16 deletions(-) create mode 100644 src/commands/forum.rs create mode 100644 src/events/thread_create.rs diff --git a/src/commands.rs b/src/commands.rs index d2c0b87..619a5f7 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,2 +1,3 @@ pub mod faq; +pub mod forum; pub mod info; diff --git a/src/commands/faq.rs b/src/commands/faq.rs index bf362ce..6da7bb0 100644 --- a/src/commands/faq.rs +++ b/src/commands/faq.rs @@ -6,8 +6,8 @@ use serenity::all::CreateEmbed; // Define your FAQ struct struct Faq { - id: i64, - guild_id: String, + // id: i64, + // guild_id: String, title: String, description: String, } @@ -20,7 +20,7 @@ fn format_embed_description>(description: S) -> Result Result { let guild_id = guild_id.to_string(); let exists: bool = conn.query_row( - "SELECT EXISTS(SELECT 1 FROM faqs WHERE title = ?1 AND description = ?2)", + "SELECT EXISTS(SELECT 1 FROM faqs WHERE title = ?1 AND guild_id = ?2)", [faq, &guild_id], |row| row.get(0), )?; @@ -47,8 +47,8 @@ async fn autocomplete_faq_titles<'a>( let faqs: Vec = stmt .query_map([], |row| { Ok(Faq { - id: row.get(0)?, - guild_id: row.get(1)?, + // id: row.get(0)?, + // guild_id: row.get(1)?, title: row.get(2)?, description: row.get(3)?, }) @@ -99,8 +99,8 @@ pub async fn view( let faqs: Vec = stmt .query_map([], |row| { Ok(Faq { - id: row.get(0)?, - guild_id: row.get(1)?, + // id: row.get(0)?, + // guild_id: row.get(1)?, title: row.get(2)?, description: row.get(3)?, }) @@ -158,9 +158,8 @@ pub async fn add( }) .await?; - let embed_description = format_embed_description(description).expect("Format error"); - if result == 1 { + let embed_description = format_embed_description(description).expect("Format error"); let embed = CreateEmbed::new() .title(format!("Added FAQ: {}", &title)) .description(embed_description); diff --git a/src/commands/forum.rs b/src/commands/forum.rs new file mode 100644 index 0000000..8647273 --- /dev/null +++ b/src/commands/forum.rs @@ -0,0 +1,123 @@ +use crate::{Context, Error}; +use poise::CreateReply; +use rusqlite::Connection; +use serenity::all::{CreateEmbed, GuildChannel}; + +// struct Forum { +// id: i64, +// guild_id: String, +// forum_id: String, +// } + +#[poise::command(slash_command, subcommands("add", "delete"), guild_only)] +pub async fn forum(_ctx: Context<'_>) -> Result<(), Error> { + Ok(()) +} + +fn forum_exists>( + conn: &Connection, + forum_id: S, + guild_id: S, +) -> Result { + let exists: bool = conn.query_row( + "SELECT EXISTS(SELECT 1 FROM forums WHERE forum_id = ?1 AND guild_id = ?2)", + [forum_id.into(), guild_id.into()], + |row| row.get(0), + )?; + + Ok(exists) +} + +/// Add forum +#[poise::command( + prefix_command, + slash_command, + required_permissions = "MANAGE_MESSAGES" +)] +pub async fn add( + ctx: Context<'_>, + #[description = "FAQ Title"] + #[channel_types("Forum")] + forum_channel: GuildChannel, +) -> Result<(), Error> { + let data = ctx.data(); + + let result = data + .execute_db({ + let guild_id = ctx.guild_id().expect("Guild ID not found").to_string(); + move |conn| { + let exists = forum_exists(conn, forum_channel.id.to_string(), guild_id.clone()) + .unwrap_or(false); + + // add + if !exists { + conn.execute( + "INSERT INTO forums (guild_id, forum_id) VALUES (?1, ?2)", + [&guild_id, &forum_channel.id.to_string()], + ) + } else { + Ok(0) + } + } + }) + .await?; + + if result == 1 { + let embed = CreateEmbed::new() + .title("Added new forum channel to monitor") + .description(format!("<#{}>", forum_channel.id)); + ctx.send(CreateReply::default().embed(embed)).await?; + } else { + let embed = CreateEmbed::new() + .title("Error adding forum channel") + .description("Forum channel already exists!"); + ctx.send(CreateReply::default().embed(embed)).await?; + } + Ok(()) +} + +/// Delete forum +#[poise::command( + prefix_command, + slash_command, + required_permissions = "MANAGE_MESSAGES" +)] +pub async fn delete( + ctx: Context<'_>, + #[description = "Forum channel"] + #[channel_types("Forum")] + forum_channel: GuildChannel, +) -> Result<(), Error> { + let data = ctx.data(); + let server_guild_id = ctx.guild_id().expect("Guild ID not found"); + let guild_id = server_guild_id.to_string(); + + let result = data + .execute_db({ + let forum_id = forum_channel.id.to_string(); + move |conn| { + conn.execute( + "DELETE FROM faqs WHERE forum_id = ?1 AND guild_id = ?2", + [forum_id, guild_id], + ) + } + }) + .await?; + + if result == 1 { + let embed = CreateEmbed::new() + .title("Deleted Forum") + .description(format!( + "Forum <#{}> has been removed from the database", + forum_channel.id + )); + ctx.send(CreateReply::default().embed(embed)).await?; + } else { + let embed = CreateEmbed::new() + .title("Error deleting forum") + .description(format!("{} rows were affected", result)); + ctx.send(CreateReply::default().embed(embed)).await?; + } + + Ok(()) +} diff --git a/src/database/database.rs b/src/database/database.rs index 6c426b2..9516faa 100644 --- a/src/database/database.rs +++ b/src/database/database.rs @@ -32,9 +32,6 @@ fn table_exists(conn: &Connection, table_name: &str) -> Result { pub fn init_tables(conn: &Connection) -> Result<()> { // FAQ table - // ID: Primary Key - // FAQ title: string - // FAQ description: string let faq_table_exists = table_exists(conn, "faqs").expect("FAQ table exists error"); if !faq_table_exists { @@ -50,6 +47,21 @@ pub fn init_tables(conn: &Connection) -> Result<()> { .expect("FAQ Table Creation error"); } + let forums_table_exists = + table_exists(conn, "forums").expect("Forum channels table exists error"); + + if !forums_table_exists { + conn.execute( + "CREATE TABLE forums ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + guild_id TEXT NOT NULL, + forum_id TEXT NOT NULL + )", + (), + ) + .expect("Forum Channels Table Creation error"); + } + // if we need to create more tables we'll use this function Ok(()) diff --git a/src/events.rs b/src/events.rs index 849debc..53ae029 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1 +1,2 @@ pub mod ready; +pub mod thread_create; diff --git a/src/events/thread_create.rs b/src/events/thread_create.rs new file mode 100644 index 0000000..7bb34ac --- /dev/null +++ b/src/events/thread_create.rs @@ -0,0 +1,81 @@ +use std::time::{Duration, Instant}; + +use crate::{Data, Error}; +use ::serenity::all::CreateMessage; +use poise::serenity_prelude as serenity; +use rusqlite::Connection; + +fn forum_exists(conn: &Connection, forum_id: &str, guild_id: &str) -> Result { + let guild_id = guild_id.to_string(); + let exists: bool = conn.query_row( + "SELECT EXISTS(SELECT 1 FROM forums WHERE forum_id = ?1 AND guild_id = ?2)", + [forum_id, &guild_id], + |row| row.get(0), + )?; + + Ok(exists) +} + +pub async fn on_thread_create( + ctx: &serenity::Context, + thread: &serenity::GuildChannel, + data: &Data, +) -> Result<(), Error> { + const COOLDOWN_DURATION: Duration = Duration::from_secs(2); + { + let mut cooldowns = data.thread_cooldowns.lock().await; + if let Some(last_call) = cooldowns.get(&thread.id) { + if last_call.elapsed() < COOLDOWN_DURATION { + println!("Thread {} is on cooldown, skipping", thread.id); + return Ok(()); + } + } + cooldowns.insert(thread.id, Instant::now()); + } + + // check if forum channel ID is in database + let forum_exists = data + .execute_db({ + let forum_channel_id = thread + .parent_id + .expect("Thread Parent ID not found") + .to_string(); + + let guild_id = thread.guild_id.to_string(); + + move |conn| { + let exists = forum_exists(conn, &forum_channel_id, &guild_id).unwrap_or(false); + + Ok(exists) + } + }) + .await + .expect("Checking forum error"); + + if !forum_exists { + return Ok(()); + } + + // check if there's more than one tag + if thread.applied_tags.len() <= 1 { + return Ok(()); + } + + // go into the channel and ping the user to only use one tag + let thread_post = thread.id; + let thread_owner = thread + .owner_id + .expect("Failed to get thread owner ID") + .to_string(); + + let message_builder = CreateMessage::new().content(format!( + "<@{}> Please select only ONE (1) tag (personal-only or career-oriented)", + thread_owner + )); + + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + thread_post.send_message(ctx, message_builder).await?; + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 5f50af0..a91d7dd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use dotenv; use poise::serenity_prelude as serenity; use rusqlite::Connection; -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc, time::Instant}; use tokio::sync::Mutex; mod commands; mod database; @@ -12,8 +12,10 @@ use crate::database::database::{init_db, init_tables}; type Error = Box; type Context<'a> = poise::Context<'a, Data, Error>; +#[derive(Debug)] pub struct Data { conn: Arc>, + thread_cooldowns: Arc>>, } impl Data { @@ -37,12 +39,15 @@ async fn event_handler( ctx: &serenity::Context, event: &serenity::FullEvent, _framework: poise::FrameworkContext<'_, Data, Error>, - _data: &Data, + data: &Data, ) -> Result<(), Error> { match event { serenity::FullEvent::Ready { data_about_bot, .. } => { events::ready::on_ready(ctx, data_about_bot).await?; } + serenity::FullEvent::ThreadCreate { thread } => { + events::thread_create::on_thread_create(ctx, thread, data).await?; + } _ => {} } Ok(()) @@ -80,7 +85,11 @@ async fn main() { let framework = poise::Framework::builder() .options(poise::FrameworkOptions { - commands: vec![commands::info::help(), commands::faq::faq()], + commands: vec![ + commands::info::help(), + commands::faq::faq(), + commands::forum::forum(), + ], event_handler: |ctx, event, framework, data| { Box::pin(event_handler(ctx, event, framework, data)) }, @@ -90,7 +99,10 @@ async fn main() { .setup(|ctx, _ready, framework| { Box::pin(async move { poise::builtins::register_globally(ctx, &framework.options().commands).await?; - Ok(Data { conn }) + Ok(Data { + conn, + thread_cooldowns: Arc::new(Mutex::new(HashMap::new())), + }) }) }) .build();