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>(description: S) -> Result { let string_val = description.into(); Ok(str::replace(&string_val, "\\n", "\n")) } fn faq_exists(conn: &Connection, faq: &str, guild_id: &u64) -> Result { 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 + '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 = stmt .query_map([&guild_id], |row| { Ok(Faq { title: row.get(2)?, description: row.get(3)?, }) })? .collect::, _>>()?; Ok(faqs) } }) .await; let faqs: Vec = 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 = 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::, _>>()?; 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 = 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::, _>>()?; 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(()) }