diff --git a/Cargo.lock b/Cargo.lock index 04ffcae..5d6ded4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,6 +88,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -109,6 +115,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bstr" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -191,9 +207,9 @@ dependencies = [ [[package]] name = "command_attr" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f08c85a02e066b7b4f7dcb60eee6ae0793ef7d6452a3547d1f19665df070a9" +checksum = "88da8d7e9fe6f30d8e3fcf72d0f84102b49de70fece952633e8439e89bdc7631" dependencies = [ "proc-macro2", "quote", @@ -640,15 +656,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "info" -version = "0.1.0" -dependencies = [ - "nopalmo", - "tokio", - "tracing", -] - [[package]] name = "ipnet" version = "2.9.0" @@ -670,6 +677,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "levenshtein" version = "1.0.5" @@ -714,6 +727,15 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "luau0-src" +version = "0.10.2+luau635" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8bd6bc70c84fdd4e89b71b528f7ab7db7b97adf759faa8dbe15c5011468e290" +dependencies = [ + "cc", +] + [[package]] name = "memchr" version = "2.7.2" @@ -772,17 +794,55 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "mlua" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d111deb18a9c9bd33e1541309f4742523bfab01d276bfa9a27519f6de9c11dc7" +dependencies = [ + "bstr", + "futures-util", + "libloading", + "mlua-sys", + "num-traits", + "once_cell", + "rustc-hash", +] + +[[package]] +name = "mlua-sys" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab7a5b4756b8177a2dfa8e0bbcde63bd4000afbc4ab20cbb68d114a25470f29" +dependencies = [ + "cc", + "cfg-if", + "luau0-src", + "pkg-config", +] + [[package]] name = "nopalmo" version = "0.1.0" dependencies = [ - "libloading", - "phf", + "mlua", "rand", "regex", "serenity", + "sqlite", "tokio", "tracing", + "tracing-subscriber", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", ] [[package]] @@ -815,6 +875,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking_lot" version = "0.12.2" @@ -844,48 +910,6 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "phf" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" -dependencies = [ - "phf_macros", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" -dependencies = [ - "phf_shared", - "rand", -] - -[[package]] -name = "phf_macros" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.65", -] - -[[package]] -name = "phf_shared" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" -dependencies = [ - "siphasher", -] - [[package]] name = "pin-project-lite" version = "0.2.14" @@ -898,6 +922,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + [[package]] name = "powerfmt" version = "0.2.0" @@ -1013,7 +1043,7 @@ version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "encoding_rs", "futures-core", @@ -1072,6 +1102,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + [[package]] name = "rustix" version = "0.38.34" @@ -1117,7 +1153,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64", + "base64 0.21.7", ] [[package]] @@ -1206,6 +1242,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_cow" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7bbbec7196bfde255ab54b65e34087c0849629280028238e67ee25d6a4b7da" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" version = "1.0.202" @@ -1242,13 +1287,13 @@ dependencies = [ [[package]] name = "serenity" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64da29158bb55d70677cacd4f4f8eab1acef005fb830d9c3bea411b090e96a9" +checksum = "880a04106592d0a8f5bdacb1d935889bfbccb4a14f7074984d9cd857235d34ac" dependencies = [ "arrayvec", "async-trait", - "base64", + "base64 0.22.1", "bitflags 2.5.0", "bytes", "chrono", @@ -1264,6 +1309,7 @@ dependencies = [ "reqwest", "secrecy", "serde", + "serde_cow", "serde_json", "static_assertions", "time", @@ -1287,6 +1333,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -1296,12 +1351,6 @@ dependencies = [ "libc", ] -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - [[package]] name = "skeptic" version = "0.13.7" @@ -1348,6 +1397,34 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "sqlite" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfe6fb16f2bee6452feeb4d12bfa404fbcd3cfc121b2950e501d1ae9cae718e" +dependencies = [ + "sqlite3-sys", +] + +[[package]] +name = "sqlite3-src" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "174d4a6df77c27db281fb23de1a6d968f3aaaa4807c2a1afa8056b971f947b4a" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "sqlite3-sys" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3901ada7090c3c3584dc92ec7ef1b7091868d13bfe6d7de9f0bcaffee7d0ade5" +dependencies = [ + "sqlite3-src", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -1447,6 +1524,16 @@ dependencies = [ "syn 2.0.65", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.3.36" @@ -1608,6 +1695,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "chrono", + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -1744,6 +1858,12 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4bf03e0ca70d626ecc4ba6b0763b934b6f2976e8c744088bb3c1d646fbb1ad0" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "version_check" version = "0.9.4" @@ -1879,6 +1999,22 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.8" @@ -1888,6 +2024,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 5cc8254..202fe3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,28 +3,20 @@ name = "nopalmo" version = "0.1.0" edition = "2021" - -[lib] -name = "nopalmo_lib" -path = "src/lib/lib.rs" - -[[bin]] -name = "nopalmo" -path = "src/main.rs" - -[workspace] -members = ["commands/info"] - [dependencies] -libloading = "0.8.5" -phf = { version = "0.11.2", features = ["phf_macros"] } -rand = "0.8.5" -regex = "1.10.4" -# rs-cord = { git = "https://github.com/jay3332/rs-cord" } -serenity = "0.12.1" tokio = { version = "1.37.0", features = ["full"] } tracing = "0.1.40" +tracing-subscriber = { version = "0.3.18", features = ["chrono"] } + +rand = "0.8.5" +regex = "1.10.4" +sqlite = { version = "0.36.1", features = ["bundled"] } + +mlua = { version = "0.9.9", features = ["async", "luau"] } +serenity = "0.12.1" [features] + +# slated for removal, originally this engine was going to be a multipurpose and publicly available bot, however plans change and im limiting the scope nsfw_features = [] premium_features = [] diff --git a/commands/info/Cargo.toml b/commands/info/Cargo.toml deleted file mode 100644 index 6fd3ba5..0000000 --- a/commands/info/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "info" -version = "0.1.0" -edition = "2021" -crate-type = ["cdylib"] - -[dependencies] -tokio = { version = "1.39.2", features = ["full"] } -tracing = "0.1.40" -nopalmo = { path = "../../" } \ No newline at end of file diff --git a/commands/info/src/lib.rs b/commands/info/src/lib.rs deleted file mode 100644 index 1def1f0..0000000 --- a/commands/info/src/lib.rs +++ /dev/null @@ -1,43 +0,0 @@ -use nopalmo_lib::PluginError; -use std::collections::HashMap; -use std::sync::{Arc, Mutex}; -use tokio::task; - -type AsyncFn = Box task::JoinHandle> + Send + Sync>; - -static FUNCTION_MAP: ::std::sync::OnceLock>>> = - ::std::sync::OnceLock::new(); - -#[allow(improper_ctypes_definitions)] -#[no_mangle] -pub extern "C" fn register_functions(function_map: Arc>>) { - if let Err(_) = FUNCTION_MAP.set(Arc::clone(&function_map)) { - tracing::error!("Could not set function map oncelock in `info` plugin."); - return; - } - - let example_function: AsyncFn = Box::new(|| { - task::spawn(async { - tracing::info!("example_function is running"); - // Simulate some async work - tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; - tracing::info!("example_function completed"); - // Return an error for demonstration - Err(PluginError::FunctionError( - "Something went wrong".to_string(), - )) - }) - }); - - function_map - .lock() - .unwrap() - .insert("example_function".to_string(), example_function); -} - -#[no_mangle] -pub extern "C" fn unregister_functions() { - if let Some(function_map) = FUNCTION_MAP.get() { - function_map.lock().unwrap().remove("example_function"); - } -} diff --git a/commands/info/src/ping.rs b/commands/info/src/ping.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/arguments.rs b/src/arguments.rs new file mode 100644 index 0000000..7fd6b19 --- /dev/null +++ b/src/arguments.rs @@ -0,0 +1,75 @@ +use std::{collections::HashMap, sync::Arc}; + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct ArgumentStorage { + // arguments: Vec, + long_key_to_id: HashMap>, + short_key_to_id: HashMap>, +} + +impl ArgumentStorage { + pub fn new() -> Self { + Self::default() + } + + pub fn add(&mut self, argument: Argument) { + let argument = Arc::new(argument); + + if let Some(long) = argument.long_name { + self.long_key_to_id + .insert(long.to_string(), argument.clone()); + } + if let Some(short) = argument.short_name { + self.long_key_to_id.insert(short.to_string(), argument); + } + } + + pub fn remove(&mut self, long: Option, short: Option) { + match (long, short) { + (None, None) => todo!(), // what + (None, Some(short_key)) => { + let argument = self.short_key_to_id.remove(&short_key); + + if let Some(argument) = argument { + if let Some(long_name) = argument.long_name { + self.long_key_to_id.remove(long_name); + } + } // TODO case where name is invalid? + } + (Some(long_key), None) => { + let argument = self.long_key_to_id.remove(&long_key); + + if let Some(argument) = argument { + if let Some(short_name) = argument.short_name { + self.short_key_to_id.remove(short_name); + } + } // TODO case where name is invalid? + } + (Some(long_key), Some(short_key)) => { + self.long_key_to_id.remove(&long_key); + self.short_key_to_id.remove(&short_key); + } + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd)] +pub struct Argument { + pub pretty_name: &'static str, + // we will use usize as an argument id system, the arguments will be available inside the argument cache + // the bool is for if its required or not + pub sub_arguments: Vec<(ArgumentContainer, bool)>, + pub requires_prefix: bool, + + pub wildcard_name: Option<&'static str>, + pub long_name: Option<&'static str>, // change to vec? + pub short_name: Option<&'static str>, // change to vec? + // /// use 0 for any position, 1 or more for positionals + // pub position: usize, +} + +#[derive(Debug, Clone, PartialEq, PartialOrd)] +pub enum ArgumentContainer { + Value(String), + Argument(usize), +} diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..7b092af --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,139 @@ +use std::{fmt::Debug, time::Duration}; + +use serenity::all::{Context, GuildId, Message, Permissions}; + +use crate::arguments::{Argument, ArgumentStorage}; + +// pub type DmCommand = fn(Context, Message); +// pub type GuildCommand = fn(Context, Message, GuildId); + +pub enum CommandFnKind { + Lua(()), + Rust(fn(Context, Message, Option)), +} + +pub struct Command { + pub run_dm_command: Option, + pub run_guild_command: Option, + pub aliases: Vec, + pub pretty_name: Option, + pub name: String, + pub command_category: Option, + pub help: Option, + // pub usage: String, // will be calculated from arguments automatically + pub timeout: Option, // TODO make this dynamic? + pub hidden: bool, + + pub arguments: ArgumentStorage, + pub required_caller_discord_permissions: ::serenity::all::Permissions, + + #[cfg(feature = "nsfw_features")] + pub is_nsfw: bool, + #[cfg(feature = "premium_features")] + pub premium_kind: usize, +} + +impl Command { + pub fn new(name: String) -> Self { + Self { + run_dm_command: None, + run_guild_command: None, + aliases: vec![], + pretty_name: None, + name, + hidden: false, + command_category: None, + help: None, + timeout: None, + arguments: ArgumentStorage::new(), + required_caller_discord_permissions: Permissions::empty(), + #[cfg(feature = "nsfw_features")] + is_nsfw: false, + #[cfg(feature = "premium_features")] + premium_kind: 0, + } + } + pub fn dm_command(mut self, dm_command: CommandFnKind) -> Self { + self.run_dm_command = Some(dm_command); + self + } + pub fn guild_command(mut self, guild_command: CommandFnKind) -> Self { + self.run_guild_command = Some(guild_command); + self + } + pub fn pretty_name(mut self, pretty_name: String) -> Self { + self.pretty_name = Some(pretty_name); + self + } + // pub fn name(mut self, name: String) -> Self { + // self.name = name; + // self + // } + pub fn alias(mut self, alias: String) -> Self { + self.aliases.push(alias); + self + } + pub fn aliases(mut self, aliases: &mut Vec) -> Self { + self.aliases.append(aliases); + self + } + pub fn category(mut self, category: String) -> Self { + self.command_category = Some(category); + self + } + pub fn help(mut self, help: String) -> Self { + self.help = Some(help); + self + } + pub fn timeout(mut self, timeout: Duration) -> Self { + self.timeout = Some(timeout); + self + } + pub fn argument(mut self, argument: Argument) -> Self { + self.arguments.add(argument); + self + } + pub fn hidden(mut self, hidden: bool) -> Self { + self.hidden = hidden; + self + } +} + +unsafe impl Sync for Command {} +unsafe impl Send for Command {} + +impl Debug for Command { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut binding = f.debug_struct("Command"); + binding + .field( + "run_dm_command", + &::std::any::type_name_of_val(&self.run_dm_command), + ) + .field( + "run_guild_command", + &::std::any::type_name_of_val(&self.run_guild_command), + ) + .field("aliases", &self.aliases) + .field("name", &self.name) + .field("pretty_name", &self.pretty_name) + .field("command_type", &self.command_category) + .field("help", &self.help) + .field("hidden", &self.hidden) + // .field("usage", &self.usage) + .field("timeout", &self.timeout) + .field("arguments", &self.arguments) + .field("permissions", &self.required_caller_discord_permissions); + #[cfg(feature = "nsfw_features")] + binding.field("is_nsfw", &self.is_nsfw); + #[cfg(feature = "premium_features")] + binding.field("premium_kind", &self.premium_kind); + binding.finish() + } +} + +// impl Default for Command { +// fn default() -> Self { + +// } +// } diff --git a/src/lib/arguments.rs b/src/lib/arguments.rs deleted file mode 100644 index ff64b37..0000000 --- a/src/lib/arguments.rs +++ /dev/null @@ -1,109 +0,0 @@ -use std::collections::HashMap; - -#[derive(Debug, Clone, PartialEq, Default)] -pub struct ArgumentStorage { - arguments: Vec, - long_key_to_id: HashMap, - short_key_to_id: HashMap, -} - -impl ArgumentStorage { - pub fn new() -> Self { - Self::default() - } - - pub fn add(&mut self, long: Option, short: Option, argument: Argument) { - let mut index = None; - for i in 0..self.arguments.len() { - if self.arguments.get(i).is_none() { - index = Some(i); - } - } - - if let Some(index) = index { - self.arguments[index] = argument; - - if let Some(long) = long { - self.long_key_to_id.insert(long, index); - } - if let Some(short) = short { - self.long_key_to_id.insert(short, index); - } - } - } - - pub fn remove(&mut self, long: Option, short: Option) { - - match (long, short) { - (None, None) => todo!(), - (None, Some(short_key)) => { - let short_index = *self.short_key_to_id.get(&short_key).unwrap(); - self.short_key_to_id.remove(&short_key); - - let mut long_to_remove = None; - - for (long_key, long_index) in &self.long_key_to_id { - // hate cloning here but i couldnt figure out how to avoid it - if short_index == *long_index { - long_to_remove = Some(long_key.clone()); - } - } - - if let Some(long_to_remove) = long_to_remove { - self.long_key_to_id.remove(&long_to_remove); - } - } - (Some(long_key), None) => { - let long_index = *self.long_key_to_id.get(&long_key).unwrap(); - self.long_key_to_id.remove(&long_key); - - let mut short_to_remove = None; - - for (short_key, short_index) in &self.short_key_to_id { - // hate cloning here but i couldnt figure out how to avoid it - if long_index == *short_index { - short_to_remove = Some(short_key.clone()); - } - } - - if let Some(short_to_remove) = short_to_remove { - self.short_key_to_id.remove(&short_to_remove); - } - } - (Some(long_key), Some(short_key)) => { - - - - self.long_key_to_id.remove(&long_key); - self.short_key_to_id.remove(&short_key); - - } - } - } -} - -pub enum ArgumentKind { - Short, - Long, - WildCard, -} - -#[derive(Debug, Clone, PartialEq, PartialOrd)] -pub struct Argument { - pub pretty_name: &'static str, - // we will use usize as an argument id system, the arguments will be available inside the argument cache - // the bool is for if its required or not - pub sub_arguments: Vec<(ArgumentContainer, bool)>, - pub requires_prefix: bool, - - pub long_name: Option<&'static str>, // change to vec? - pub short_name: Option<&'static str>, // change to vec? - // /// use 0 for any position, 1 or more for positionals - // pub position: usize, -} - -#[derive(Debug, Clone, PartialEq, PartialOrd)] -pub enum ArgumentContainer { - Value(String), - Argument(usize) -} \ No newline at end of file diff --git a/src/lib/lib.rs b/src/lib/lib.rs deleted file mode 100644 index 72c3dd3..0000000 --- a/src/lib/lib.rs +++ /dev/null @@ -1,73 +0,0 @@ -use std::{fmt::Debug, time::Duration}; - -pub mod arguments; - -use arguments::ArgumentStorage; -use serenity::all::{Context, GuildId, Message}; - -#[derive(Debug)] -pub enum PluginError { - FunctionError(String), - Other(String), -} - -impl core::fmt::Display for PluginError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - PluginError::FunctionError(msg) => write!(f, "Function error: {}", msg), - PluginError::Other(msg) => write!(f, "Other error: {}", msg), - } - } -} - -impl std::error::Error for PluginError {} - -pub struct Command { - pub run_dm_command: Box CommandReturn>, - pub run_guild_command: Box CommandReturn>, - pub aliases: Vec, - pub name: String, - pub command_category: String, - pub help: String, - pub usage: String, - pub timeout: Duration, // TODO make this dynamic? - - pub arguments: ArgumentStorage, - pub required_caller_discord_permissions: ::serenity::all::Permissions, - - #[cfg(feature = "nsfw_features")] - pub is_nsfw: bool, - #[cfg(feature = "premium_features")] - pub premium_kind: usize, -} - -unsafe impl Sync for Command {} -unsafe impl Send for Command {} - -impl Debug for Command { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut binding = f.debug_struct("Command"); - binding - .field( - "run_dm_command", - &::std::any::type_name_of_val(&self.run_dm_command), - ) - .field( - "run_guild_command", - &::std::any::type_name_of_val(&self.run_guild_command), - ) - .field("aliases", &self.aliases) - .field("name", &self.name) - .field("command_type", &self.command_category) - .field("help", &self.help) - .field("usage", &self.usage) - .field("timeout", &self.timeout) - .field("arguments", &self.arguments) - .field("permissions", &self.required_caller_discord_permissions); - #[cfg(feature = "nsfw_features")] - binding.field("is_nsfw", &self.is_nsfw); - #[cfg(feature = "premium_features")] - binding.field("premium_kind", &self.premium_kind); - binding.finish() - } -} diff --git a/src/main.rs b/src/main.rs index 73ca4f8..64bbdd7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,88 +2,51 @@ #![deny(clippy::expect_used)] #![deny(clippy::pedantic)] -//#![feature(async_fn_traits)] - -// mod commands; -// use nopalmo_lib; - +mod arguments; +mod commands; mod constants; -// mod permissions; -mod system_messages; mod tasks; -// use ::std::collections::hash_map::HashMap; +use std::collections::HashMap; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Condvar, OnceLock}; -// use std::error::Error; -// use std::future::Future; -// use std::pin::Pin; -use std::sync::atomic::AtomicBool; -// use std::sync::Arc; -// use std::time::Duration; - -// use discord_permissions::DiscordPermission; -// use nopalmo_lib::Command; -// use serenity::all::GuildId; -// use commands::Command; +use commands::Command; +use metadata::LevelFilter; +use serenity::all::{ActivityData, Context, GuildId, ShardManager}; +use serenity::all::{EventHandler, GatewayIntents}; use serenity::model::channel::Message; use serenity::model::gateway::Ready; -use serenity::prelude::*; -// use tokio::sync::mpsc; - -// type DMCommandArguments = Handler; -// type GCommandArguments = Handler; -// pub type CommandReturn = Pin>>; -// pub type CommandReturn = (); - - -// pub mod discord_permissions; - -// use self::arguments::Argument; -use ::serenity::all::Context; - -// use discord_permissions::DiscordPermission; - -// #[cfg(feature = "premium_features")] -// pub enum PremiumLevel { -// Free, -// Tier1, -// Tier2, -// Tier3, -// Super, -// } - -// pub enum CommandType { -// General, -// Moderation, -// Fun, -// Info, -// Extra, -// Unknown, -// } +use serenity::Client; +use tracing::*; +// pub const CAT_DEV: &'static str = "Dev"; +pub const CAT_FUN: &'static str = "Fun"; +pub const CAT_MOD: &'static str = "Moderation"; +pub const CAT_GEN: &'static str = "General"; +pub const CAT_INF: &'static str = "Info"; +pub const CAT_HID: &'static str = "Hidden"; +static SHARD_MANAGER: OnceLock> = OnceLock::new(); +// static SHARDS_READY: AtomicBool = AtomicBool::new(false); +static DO_SHUTDOWN: (AtomicBool, Condvar) = (AtomicBool::new(false), Condvar::new()); // atombool and condvar combo to ensure maximum coverage when the bot needs to power off struct Handler { // TODO use data field instead? - // system_sender: Mutex>, - // system_receiver: Mutex>, - do_shutdown: AtomicBool, - // task_count: AtomicUsize, - // command_map: ::std::collections::hash_map::HashMap< - // String, - // Arc>, - // >, + commands: HashMap, } unsafe impl Sync for Handler {} unsafe impl Send for Handler {} -// unsafe impl Sync for CommandReturn {} -// unsafe impl Send for CommandReturn {} - #[serenity::async_trait] impl EventHandler for Handler { async fn message(&self, ctx: Context, msg: Message) { + // We do not reply to bots https://en.wikipedia.org/wiki/Email_storm + if msg.author.bot { + return; + } + let prefix_regex = format!( r"^({}|{}|<@{}>)\s?", constants::COMMAND_PREFIX, @@ -100,7 +63,7 @@ impl EventHandler for Handler { None => return, // silently exit because not every message is meant for the bot }; - if self.do_shutdown.load(std::sync::atomic::Ordering::SeqCst) { + if DO_SHUTDOWN.0.load(Ordering::SeqCst) { let _ = msg .channel_id .say( @@ -113,34 +76,70 @@ impl EventHandler for Handler { let target_cmd_name = regex::Regex::new(prefix_regex.as_str()) .unwrap() - .replace(result.as_str(), ""); - - dbg!(&target_cmd_name); + .replace(result.as_str(), "") + .to_string(); msg.reply(&ctx.http, target_cmd_name.to_string()) .await .unwrap(); - //if let Some(command) = self.command_map.get(target_cmd_name) { - // if let Some(guild_id) = msg.guild_id { - // (command.run_guild_command)(&self, ctx, msg, guild_id); - // } else { - // (command.run_dm_command)(&self, ctx, msg); - // } - //} + if let Some(command) = self.commands.get(&target_cmd_name) { + if let Some(guild_id) = msg.guild_id { + if let Some(run_guild_command) = &command.run_guild_command { + match run_guild_command { + commands::CommandFnKind::Lua(_) => todo!(), + commands::CommandFnKind::Rust(cmd) => (cmd)(ctx, msg, Some(guild_id)), + } + } + } else { + if let Some(run_dm_command) = &command.run_dm_command { + match run_dm_command { + commands::CommandFnKind::Lua(_) => todo!(), + commands::CommandFnKind::Rust(cmd) => (cmd)(ctx, msg, None), + } + } + } + } } - async fn ready(&self, _: Context, ready: Ready) { - println!("Shart `{}` is connected!", ready.shard.unwrap().id); + /// Runs once for every shard once its ready + async fn ready(&self, ctx: Context, ready: Ready) { + info!("Shart `{}` is connected!", ready.shard.unwrap().id); + ctx.set_activity(Some(ActivityData::custom("Initializing."))) + } + + /// Runs once when all shards are ready + async fn shards_ready(&self, ctx: Context, total_shards: u32) { + tasks::start_tasks(ctx).await; + info!("{total_shards} shards ready."); + } + + // Runs once for every shard when its cache is ready + async fn cache_ready(&self, _ctx: Context, _guild_id_list: Vec) { + info!("Cache ready."); } } #[tokio::main] async fn main() { + tracing::subscriber::set_global_default( + tracing_subscriber::fmt() + .with_max_level(LevelFilter::INFO) + .finish(), + ) + .unwrap(); + + // TODO load this at runtime so the key will not be stored in the binary? #[cfg(not(debug_assertions))] - let token = include_str!("bot_token.prod"); + let token = { + info!("Initializing bot with production token."); + include_str!("bot_token.prod") + }; #[cfg(debug_assertions)] - let token = include_str!("bot_token.dev"); + let token = { + info!("Initializing bot with development token."); + include_str!("bot_token.dev") + }; let intents = GatewayIntents::DIRECT_MESSAGES | GatewayIntents::DIRECT_MESSAGE_REACTIONS @@ -152,89 +151,64 @@ async fn main() { | GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT; - // let (system_sender, system_receiver) = mpsc::channel(256); - // let system_sender = Mutex::new(system_sender); - // let system_receiver = Mutex::new(system_receiver); + let mut commands = HashMap::default(); - // let mut command_map = HashMap::new(); - - // fn ping_dm_run(handler: &Handler, ctx: Context, msg: Message) -> () {} - // fn ping_guild_run(handler: &Handler, ctx: Context, msg: Message, guild_id: GuildId) -> () { - // // MessageChannel channel = blob.getChannel(); - - // channel - // .sendMessage("Pong!\n" + ctx. + "ms\n") - // .queue(); - - // // if (argumentMap.get("all") != null) { - - // // channel.sendMessage("Gathering data. . .").queue(msg -> { - // // long timeToProcess = System.currentTimeMillis(); - - // // long jdaPing = blob.getJDA().getGatewayPing(); - // // long googlePing = -1; - // // try { - // // googlePing = UptimePing.sendPing("www.google.com"); - // // } catch (Exception e) { - // // e.printStackTrace(); - // // } - // // long discordPing = -1; - // // try { - // // discordPing = UptimePing.sendPing("www.discord.com"); - // // } catch (Exception e) { - // // e.printStackTrace(); - // // } - - // // String out = "Ping:\n" - // // + (googlePing > 0 ? "Google: " + googlePing + "ms\n" : "Could not connect to www.google.com\n") - // // + (discordPing > 0 ? "Discord: " + discordPing + "ms\n" - // // : "Could not connect to www.discord.com\n") - // // + "JDA-Discord heartbeat: " + jdaPing + "ms"; - - // // msg.editMessage(out + "\nTime to process: " + (System.currentTimeMillis() - timeToProcess) + "ms") - // // .queue(); - // // }); - // // } - // } - - // command_map.insert( - // "ping".to_owned(), - // Arc::new(Command { - // aliases: vec!["ping".to_owned()], - // arguments: HashMap::new(), - // command_type: nopalmo_lib::CommandType::General, - // help: "Some help info".to_owned(), - // name: "Ping".to_owned(), - // usage: "ping".to_owned(), - // permissions: vec![], - // timeout: Duration::from_secs(0), - // run_dm_command: Box::new(ping_dm_run), - // run_guild_command: Box::new(ping_guild_run), - // }), - // ); + stock_commands::insert_all(&mut commands); let mut client = match Client::builder(&token, intents) - .event_handler(Handler { - // system_receiver, - // system_sender, - do_shutdown: AtomicBool::new(false), - // task_count: AtomicUsize::new(0), - // command_map, - }) + .event_handler(Handler { commands }) .await { Ok(client) => client, Err(err) => panic!("Error starting client connection: `{err}`"), }; - let status_timer = tokio::spawn(tasks::status_timer( - client.shard_manager.runners.clone(), - client.cache.clone(), - )); + SHARD_MANAGER.set(client.shard_manager.clone()).unwrap(); if let Err(why) = client.start_shards(2).await { - println!("Client error: {why:?}"); + error!("Client error: {why:?}"); } - status_timer.abort(); + warn!("EXITING"); +} + +mod stock_commands { + use std::{collections::HashMap, sync::atomic::Ordering}; + + use serenity::all::{Context, GuildId, Message}; + + use crate::{ + commands::{Command, CommandFnKind}, + CAT_HID, DO_SHUTDOWN, SHARD_MANAGER, + }; + + pub fn insert_all(commands: &mut HashMap) { + commands.insert( + "stop".to_owned(), + Command::new("stop".to_owned()) + .alias("svs".to_owned()) + .category(CAT_HID.to_owned()) + .hidden(true) + .dm_command(CommandFnKind::Rust(stop_command)) + .guild_command(CommandFnKind::Rust(stop_command)) + .pretty_name("Stop the bot".to_owned()) + .help("Stops the bot. Does nothing unless you are a developer.".to_owned()), + ); + } + + fn stop_command(_: Context, msg: Message, _: Option) { + // hardcode my id for now + if msg.author.id != 380045419381784576 { + return; + } + DO_SHUTDOWN.0.store(true, Ordering::SeqCst); + DO_SHUTDOWN.1.notify_all(); + + let handle = tokio::runtime::Handle::current(); + let _eg = handle.enter(); + + handle.spawn(async { + SHARD_MANAGER.get().unwrap().shutdown_all().await; + }); + } } diff --git a/src/system_messages.rs b/src/system_messages.rs deleted file mode 100644 index c9b836d..0000000 --- a/src/system_messages.rs +++ /dev/null @@ -1,4 +0,0 @@ - -pub enum SystemMessage { - -} diff --git a/src/tasks.rs b/src/tasks.rs index 166745c..5b95627 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -1,34 +1,64 @@ use crate::constants; -use std::{sync::Arc, time::Duration}; +use std::{ + sync::{Arc, LazyLock}, + time::Duration, +}; use ::std::collections::hash_map::HashMap; use rand::{distributions::uniform::SampleRange, rngs::OsRng}; -use serenity::all::{ActivityData, Cache, ShardId, ShardRunnerInfo}; -use tokio::sync::Mutex; +use serenity::all::{ActivityData, Cache, Context, ShardId, ShardRunnerInfo}; +use tokio::{sync::Mutex, task::JoinHandle}; +use tracing::info; + +static TASK_HANDLES: LazyLock>> = + LazyLock::new(|| Arc::new(Mutex::new(TaskContainer::default()))); + +#[derive(Debug, Default)] +pub struct TaskContainer { + pub misc: Vec>, +} + +pub async fn start_tasks(ctx: Context) { + info!("Starting MISC tasks."); + + info!("Starting activity switcher task."); + TASK_HANDLES + .lock() + .await + .misc + .push(tokio::spawn(status_timer( + crate::SHARD_MANAGER.get().unwrap().runners.clone(), + ctx.cache, + ))); + + info!("MISC tasks started."); +} pub async fn status_timer( shard_runners: Arc>>, cache: Arc, ) { let mut rand = OsRng::default(); - let activity_list = [ + let activities = [ ActivityData::watching("my developer eat a watermelon whole."), ActivityData::watching(format!( "{} users in {} guilds.", - cache.user_count(), - cache.guild_count() + cache.user_count() as u64 + cache.unknown_members(), + cache.guild_count() + cache.unavailable_guilds().len() )), ActivityData::watching(format!("for {}help", constants::COMMAND_PREFIX)), ActivityData::listening("Infected Mushroom"), ]; + let mut interval = tokio::time::interval(Duration::from_secs(20 * 60)); loop { + let activity = SampleRange::sample_single(0..activities.len() - 1, &mut rand); for (_shard_id, shard_info) in shard_runners.lock().await.iter() { - shard_info.runner_tx.set_activity(Some( - activity_list[SampleRange::sample_single(0..activity_list.len() - 1, &mut rand)] - .clone(), - )); + shard_info + .runner_tx + .set_activity(Some(activities[activity].clone())); } - tokio::time::sleep(Duration::from_secs(20 * 60)).await; + + interval.tick().await; } } diff --git a/testdb b/testdb new file mode 100644 index 0000000..c639805 Binary files /dev/null and b/testdb differ