diff --git a/Cargo.lock b/Cargo.lock index 620d6d7..63df3e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -375,6 +375,18 @@ dependencies = [ "version_check", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.3.0" @@ -403,6 +415,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -572,12 +590,30 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "http" version = "1.4.0" @@ -884,6 +920,17 @@ version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +[[package]] +name = "libsqlite3-sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -1140,6 +1187,7 @@ dependencies = [ "dotenv", "poise", "reqwest", + "rusqlite", "serde", "serde_json", "serenity", @@ -1419,6 +1467,20 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rusqlite" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rustc-hash" version = "2.1.1" diff --git a/Cargo.toml b/Cargo.toml index d33001b..ee1ccf0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,4 +10,5 @@ reqwest = "0.12.24" serde_json = "1.0.145" serenity = "0.12.5" tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread"] } -serde = "1.0.228" \ No newline at end of file +serde = "1.0.228" +rusqlite = { version = "0.37.0", features = ["bundled"] } \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9f1ba7a --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +Portfolio roast bot + +--- + +To do: +- [ ] Database + - [ ] FAQs +- [ ] Slash commands +- [ ] Portfolio Roast Forum: One tag only \ No newline at end of file diff --git a/bot_data.sqlite b/bot_data.sqlite new file mode 100644 index 0000000..041a60e Binary files /dev/null and b/bot_data.sqlite differ diff --git a/src/commands.rs b/src/commands.rs index 4b8757f..d2c0b87 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1 +1,2 @@ +pub mod faq; pub mod info; diff --git a/src/commands/faq.rs b/src/commands/faq.rs new file mode 100644 index 0000000..720f089 --- /dev/null +++ b/src/commands/faq.rs @@ -0,0 +1,24 @@ +use crate::{Context, Error}; + +#[poise::command(slash_command, subcommands("add", "child2"))] +pub async fn faq(_ctx: Context<'_>) -> Result<(), Error> { + Ok(()) +} + +/// A subcommand of `parent` +#[poise::command( + prefix_command, + slash_command, + required_permissions = "MANAGE_MESSAGES" // mod only +)] +pub async fn add(ctx: Context<'_>) -> Result<(), Error> { + ctx.say("You invoked the first child command!").await?; + Ok(()) +} + +/// Another subcommand of `parent` +#[poise::command(prefix_command, slash_command)] +pub async fn child2(ctx: Context<'_>) -> Result<(), Error> { + ctx.say("You invoked the second child command!").await?; + Ok(()) +} diff --git a/src/database.rs b/src/database.rs new file mode 100644 index 0000000..8fd0a6b --- /dev/null +++ b/src/database.rs @@ -0,0 +1 @@ +pub mod database; diff --git a/src/database/database.rs b/src/database/database.rs new file mode 100644 index 0000000..fc8a784 --- /dev/null +++ b/src/database/database.rs @@ -0,0 +1,51 @@ +use std::path::Path; + +// Using SQLite +use rusqlite::{Connection, OpenFlags, Result}; + +pub fn init_db(filename: &str) -> Result { + let filepath = Path::new(filename); + + let open_flags = OpenFlags::default(); + + let conn = Connection::open_with_flags(filepath, open_flags); + + conn +} + +fn table_exists(conn: &Connection, table_name: &str) -> Result { + let exists: bool = conn.query_row( + &format!( + "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type='table' AND name='{}')", + table_name + ), + [], + |row| row.get(0), + )?; + + Ok(exists) +} + +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 { + conn.execute( + "CREATE TABLE faqs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + description TEXT NOT NULL + )", + (), + ) + .expect("FAQ Table Creation error"); + } + + // if we need to create more tables we'll use this function + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 11f47fd..a47b7a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,38 @@ use dotenv; use poise::serenity_prelude as serenity; +use rusqlite::Connection; +use std::sync::Arc; +use tokio::sync::Mutex; mod commands; +mod database; mod events; - -struct Data {} +use crate::database::database::{init_db, init_tables}; // Types used by all command functions type Error = Box; type Context<'a> = poise::Context<'a, Data, Error>; +pub struct Data { + conn: Arc>, +} + +impl Data { + // Helper method to safely execute database operations + pub async fn execute_db(&self, f: F) -> Result + where + F: FnOnce(&Connection) -> Result + Send + 'static, + R: Send + 'static, + { + let conn = self.conn.clone(); + tokio::task::spawn_blocking(move || { + let conn = conn.blocking_lock(); + f(&conn) + }) + .await? + .map_err(|e| e.into()) + } +} + async fn event_handler( ctx: &serenity::Context, event: &serenity::FullEvent, @@ -24,9 +48,33 @@ async fn event_handler( Ok(()) } +async fn on_error(error: poise::FrameworkError<'_, Data, Error>) { + // This is our custom error handler + // They are many errors that can occur, so we only handle the ones we want to customize + // and forward the rest to the default handler + match error { + poise::FrameworkError::Setup { error, .. } => panic!("Failed to start bot: {:?}", error), + poise::FrameworkError::Command { error, ctx, .. } => { + println!("Error in command `{}`: {:?}", ctx.command().name, error,); + } + error => { + if let Err(e) = poise::builtins::on_error(error).await { + println!("Error while handling error: {}", e) + } + } + } +} + #[tokio::main] async fn main() { dotenv::dotenv().ok(); + + // SQLite + let conn = init_db("bot_data.sqlite").expect("Failed to start database"); + init_tables(&conn).expect("Failed to init tables"); + let conn = Arc::new(Mutex::new(conn)); + + // Discord bot let token = std::env::var("DISCORD_TOKEN").expect("missing DISCORD_TOKEN"); let intents = serenity::GatewayIntents::non_privileged(); @@ -36,12 +84,13 @@ async fn main() { event_handler: |ctx, event, framework, data| { Box::pin(event_handler(ctx, event, framework, data)) }, + on_error: |error| Box::pin(on_error(error)), ..Default::default() }) .setup(|ctx, _ready, framework| { Box::pin(async move { poise::builtins::register_globally(ctx, &framework.options().commands).await?; - Ok(Data {}) + Ok(Data { conn }) }) }) .build();