added forum post

This commit is contained in:
liyunze 2026-01-07 20:42:20 +08:00
parent 79eb9d7492
commit b52d3b9477
7 changed files with 245 additions and 16 deletions

View file

@ -1,2 +1,3 @@
pub mod faq;
pub mod forum;
pub mod info;

View file

@ -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<S: Into<String>>(description: S) -> Result<String, E
fn faq_exists(conn: &Connection, faq: &str, guild_id: &u64) -> Result<bool, Error> {
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<Faq> = 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<Faq> = 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);

123
src/commands/forum.rs Normal file
View file

@ -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<S: Into<String>>(
conn: &Connection,
forum_id: S,
guild_id: S,
) -> Result<bool, Error> {
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(())
}

View file

@ -32,9 +32,6 @@ fn table_exists(conn: &Connection, table_name: &str) -> Result<bool> {
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(())

View file

@ -1 +1,2 @@
pub mod ready;
pub mod thread_create;

View file

@ -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<bool, Error> {
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(())
}

View file

@ -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<dyn std::error::Error + Send + Sync>;
type Context<'a> = poise::Context<'a, Data, Error>;
#[derive(Debug)]
pub struct Data {
conn: Arc<Mutex<Connection>>,
thread_cooldowns: Arc<Mutex<HashMap<serenity::ChannelId, Instant>>>,
}
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();