portfolio-roast-bot/src/commands/faq.rs
2026-09-24 20:28:06 +08:00

320 lines
9.2 KiB
Rust

use crate::{Context, Error};
use futures::{Stream, StreamExt};
use poise::CreateReply;
use rusqlite::Connection;
use serenity::all::CreateEmbed;
// Define your FAQ struct
struct Faq {
title: String,
description: String,
}
fn format_embed_description<S: Into<String>>(description: S) -> Result<String, Error> {
let string_val = description.into();
Ok(str::replace(&string_val, "\\n", "\n"))
}
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 guild_id = ?2)",
[faq, &guild_id],
|row| row.get(0),
)?;
Ok(exists)
}
async fn autocomplete_faq_titles<'a>(
ctx: Context<'_>,
partial: &'a str,
) -> impl Stream<Item = String> + 'a {
let data = ctx.data();
let fetch_faqs = data
.execute_db({
let server_guild_id = ctx.guild_id().expect("Guild ID not found");
let guild_id = server_guild_id.to_string();
move |conn| {
let mut stmt = conn.prepare(
"SELECT id, guild_id, title, description FROM faqs WHERE guild_id = ?1",
)?;
let faqs: Vec<Faq> = stmt
.query_map([&guild_id], |row| {
Ok(Faq {
title: row.get(2)?,
description: row.get(3)?,
})
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(faqs)
}
})
.await;
let faqs: Vec<Faq> = fetch_faqs.unwrap_or(vec![]);
// Now you can use the faqs Vec
// ctx.say(format!("Found {} FAQs", faqs.len())).await?;
let partial_lowercase = partial.to_lowercase();
futures::stream::iter(faqs)
.filter(move |faq| {
futures::future::ready(faq.title.to_lowercase().contains(&partial_lowercase))
})
.map(|faq| faq.title)
}
#[poise::command(
slash_command,
subcommands("view", "preview", "add", "edit", "delete"),
guild_only
)]
pub async fn faq(_ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}
/// View FAQ
#[poise::command(prefix_command, slash_command)]
pub async fn view(
ctx: Context<'_>,
#[description = "FAQ Title"]
#[autocomplete = "autocomplete_faq_titles"]
faq_title: String,
) -> Result<(), Error> {
let data = ctx.data();
let faqs = data
.execute_db({
let server_guild_id = ctx.guild_id().expect("Guild ID not found");
let guild_id = server_guild_id.to_string();
move |conn| {
let mut stmt = conn.prepare(
"SELECT id, guild_id, title, description FROM faqs WHERE title = ?1 AND guild_id = ?2"
)?;
let faqs: Vec<Faq> = stmt
.query_map([&faq_title, &guild_id], |row| {
Ok(Faq {
// id: row.get(0)?,
// guild_id: row.get(1)?,
title: row.get(2)?,
description: row.get(3)?,
})
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(faqs)
}})
.await?;
let faq = faqs.first().unwrap();
let embed_description = format_embed_description(&faq.description).expect("Format error");
let embed = CreateEmbed::new()
.title(&faq.title)
.description(embed_description);
ctx.send(CreateReply::default().embed(embed)).await?;
Ok(())
}
/// Preview FAQ
#[poise::command(
prefix_command,
slash_command,
required_permissions = "MANAGE_MESSAGES" // mod only
)]
pub async fn preview(
ctx: Context<'_>,
#[description = "FAQ Title"]
#[autocomplete = "autocomplete_faq_titles"]
faq_title: String,
) -> Result<(), Error> {
let data = ctx.data();
let faqs = data
.execute_db({
let server_guild_id = ctx.guild_id().expect("Guild ID not found");
let guild_id = server_guild_id.to_string();
move |conn| {
let mut stmt = conn.prepare(
"SELECT id, guild_id, title, description FROM faqs WHERE title = ?1 AND guild_id = ?2"
)?;
let faqs: Vec<Faq> = stmt
.query_map([&faq_title, &guild_id], |row| {
Ok(Faq {
// id: row.get(0)?,
// guild_id: row.get(1)?,
title: row.get(2)?,
description: row.get(3)?,
})
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(faqs)
}})
.await?;
let faq = faqs.first().unwrap();
// let embed_description = format_embed_description(&faq.description).expect("Format error");
let embed = CreateEmbed::new().title(&faq.title).description(format!(
"```
{}
```",
&faq.description
));
ctx.send(CreateReply::default().embed(embed)).await?;
Ok(())
}
/// Add FAQ
#[poise::command(
prefix_command,
slash_command,
required_permissions = "MANAGE_MESSAGES" // mod only
)]
pub async fn add(
ctx: Context<'_>,
#[description = "FAQ title"] title: String,
#[description = "FAQ description"] description: String,
) -> Result<(), Error> {
let data: &crate::Data = ctx.data();
let result = data
.execute_db({
let title = title.clone();
let description = description.clone();
let server_guild_id = ctx.guild_id().expect("Guild ID not found");
let guild_id = server_guild_id.get();
move |conn| {
let exists = faq_exists(conn, &title, &guild_id).unwrap_or(false);
if !exists {
conn.execute(
"INSERT INTO faqs (guild_id, title, description) VALUES (?1, ?2, ?3)",
[guild_id.to_string(), title, description],
)
} else {
Ok(0)
}
}
})
.await?;
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);
ctx.send(CreateReply::default().embed(embed)).await?;
} else {
let embed = CreateEmbed::new()
.title(format!("Error adding FAQ: {}", &title))
.description("Title already exists!");
ctx.send(CreateReply::default().embed(embed)).await?;
}
Ok(())
}
/// Edit FAQ
#[poise::command(
prefix_command,
slash_command,
required_permissions = "MANAGE_MESSAGES"
)]
pub async fn edit(
ctx: Context<'_>,
#[description = "FAQ Title"]
#[autocomplete = "autocomplete_faq_titles"]
title: String,
#[description = "FAQ description"] description: String,
) -> Result<(), Error> {
let data = ctx.data();
let result = data
.execute_db({
let title = title.clone();
let description = description.clone();
let server_guild_id = ctx.guild_id().expect("Guild ID not found");
let guild_id = server_guild_id.get();
move |conn| {
conn.execute(
"UPDATE faqs SET description = ?3 WHERE title = ?1 AND guild_id = ?2",
[title, guild_id.to_string(), description],
)
}
})
.await?;
let embed_description = format_embed_description(description).expect("Format error");
if result == 1 {
let embed = CreateEmbed::new()
.title(format!("Edited FAQ: {}", &title))
.description(&embed_description);
ctx.send(CreateReply::default().embed(embed)).await?;
} else {
let embed = CreateEmbed::new()
.title(format!("Error editing FAQ: {}", &title))
.description(format!("{} rows were affected", result));
ctx.send(CreateReply::default().embed(embed)).await?;
}
Ok(())
}
/// Delete FAQ
#[poise::command(
prefix_command,
slash_command,
required_permissions = "MANAGE_MESSAGES"
)]
pub async fn delete(
ctx: Context<'_>,
#[description = "FAQ Title"]
#[autocomplete = "autocomplete_faq_titles"]
title: String,
) -> 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 title = title.clone();
move |conn| {
conn.execute(
"DELETE FROM faqs WHERE title = ?1 AND guild_id = ?2",
[title, guild_id],
)
}
})
.await?;
if result == 1 {
let embed = CreateEmbed::new().title(format!("Deleted FAQ: {}", &title));
ctx.send(CreateReply::default().embed(embed)).await?;
} else {
let embed = CreateEmbed::new()
.title(format!("Error deleting FAQ: {}", &title))
.description(format!("{} rows were affected", result));
ctx.send(CreateReply::default().embed(embed)).await?;
}
Ok(())
}