diff --git a/lua/loader.lua b/lua/loader.lua index 144e991..c7516aa 100644 --- a/lua/loader.lua +++ b/lua/loader.lua @@ -5,17 +5,52 @@ sys.registerCommand({ command_category = commandCategory.info, -- this can be any string, but the bot provides a few categories already help = "Shows you helpful information for the bot. Seems you already know how to use it. :loafplink:", timeout = nil, -- time in milliseconds that the user must wait before running the command again - hidden = false, -- whether or not other commands should acknowledge its existance, for example we dont want dev commands showing up in the help info for regular users, regardless if they can use them or not + hidden = false, -- whether or not other commands should acknowledge its existence, for example we dont want dev commands showing up in the help info for regular users, regardless if they can use them or not permissions = nil, -- which discord permissions they need to run the command - func = function(context, message, guildid) - for k, v in pairs(commands) do - info("Help info for command", k) + arguments = { + { + type = "positional", + position = 0, + optional = true, + data_type = "string", + checker = function(var) + for index, value in ipairs(sys.getCommandTable()) do + if value == var then + return; -- exit normally because the command was found + end + end + + -- throw an error to whoever ran this function saying the command does not exist + error("Command `" .. var .. "` does not exist.", 2) + end, + }, + }, + + func = function(context, message, guildid, commandArguments) + if commandArguments.getArgument(0) ~= nil then + log.info("Help info for command `" .. commandArguments.getArgument(0) .. "`") + else + log.info("Here is a list of all commands:") + for index, value in pairs(sys.getCommandTable()) do + if not value.hidden then + log.info("\tCommand pretty name: " .. value.pretty_name) + log.info("\tCommand name: " .. value.name) + log.info("\tCommand aliases: " .. value.aliases) + log.info("\tCommand category: " .. value.category) + log.info("\tCommand help info: " .. value.help) + end + end end end }) -sys.executeCommand("help", "E") +sys.executeCommand("help", "help") +sys.executeCommand("help") +-- since the next usage will throw an error, catch the error +log.info(pcall(sys.executeCommand, "help", "invalid_command")) + +log.info("end") -- exit(0) -- local seen={} diff --git a/src/arguments.rs b/src/arguments.rs index 7fd6b19..fc082f3 100644 --- a/src/arguments.rs +++ b/src/arguments.rs @@ -1,75 +1,86 @@ 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>, -} +pub struct ArgumentStorage(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); - } - } - } + pub fn add_argument() {} } -#[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, +// #[derive(Debug, Clone, PartialEq, Default)] +// pub struct ArgumentStorage { +// // arguments: Vec, +// long_key_to_id: HashMap>, +// short_key_to_id: HashMap>, +// } - 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, -} +// impl ArgumentStorage { +// pub fn new() -> Self { +// Self::default() +// } -#[derive(Debug, Clone, PartialEq, PartialOrd)] -pub enum ArgumentContainer { - Value(String), - Argument(usize), -} +// 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/bot.rs b/src/bot.rs index 39c2dc7..a8cf74c 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -1,18 +1,18 @@ -use std::collections::HashMap; -use std::sync::atomic::Ordering; -use std::sync::{Arc, OnceLock}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Condvar, OnceLock}; use crate::commands::{self, COMMANDS}; -use crate::{constants, tasks, DO_SHUTDOWN, NO_DISCORD}; +use crate::{constants, tasks, CARG_NO_DISCORD}; use serenity::all::{ActivityData, Context, GuildId}; use serenity::all::{EventHandler, GatewayIntents, ShardManager}; use serenity::model::channel::Message; use serenity::model::gateway::Ready; use serenity::Client; -use tracing::*; +use tracing::{error, info, warn}; + +pub 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 pub static SHARD_MANAGER: OnceLock> = OnceLock::new(); - pub struct BotHandler; unsafe impl Sync for BotHandler {} @@ -122,7 +122,7 @@ pub async fn start() { | GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT; - if *NO_DISCORD.get().unwrap() { + if *CARG_NO_DISCORD.get().unwrap() { warn!("ABORTING CONNECTING TO DISCORD, BYE BYE!"); } else { let mut client = match Client::builder(&token, intents) diff --git a/src/commands.rs b/src/commands.rs index 4c9d02c..7f84adf 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, fmt::Debug, sync::LazyLock, time::Duration}; use serenity::all::{Context, GuildId, Message, Permissions}; use tokio::sync::RwLock; -use crate::arguments::{Argument, ArgumentStorage}; +use crate::arguments::ArgumentStorage; pub static COMMANDS: LazyLock>> = LazyLock::new(|| RwLock::new(HashMap::new())); @@ -90,10 +90,10 @@ impl Command { self.timeout = Some(timeout); self } - pub fn argument(mut self, argument: Argument) -> Self { - self.arguments.add(argument); - 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 diff --git a/src/lua.rs b/src/lua.rs index 1f97bc7..f6cb718 100644 --- a/src/lua.rs +++ b/src/lua.rs @@ -1,13 +1,37 @@ -use mlua::{Lua, LuaOptions, StdLib, Table, Variadic}; -use tracing::info; +use mlua::{Lua, LuaOptions, Result, StdLib, Table, Value, Variadic}; +use tracing::{debug, error, info, trace, warn}; -use crate::constants::{CAT_FUN, CAT_GEN, CAT_INF, CAT_MOD, VERSION}; +use crate::constants::{CAT_DEV, CAT_FUN, CAT_GEN, CAT_INF, CAT_MOD, VERSION}; +// if there is a better way please tell me or replace this garbage +pub fn value_to_string(value: Value) -> String { + match value { + Value::Nil => "".to_owned(), + Value::Boolean(boolean) => (if boolean { "true" } else { "false" }).to_owned(), + Value::LightUserData(ludata) => (ludata.0 as usize).to_string(), + Value::Integer(int) => int.to_string(), + Value::Number(num) => num.to_string(), + Value::Vector(vec) => vec.to_string(), + Value::String(string2) => String::from_utf8_lossy(string2.as_bytes()).to_string(), + Value::Table(table) => format!("{table:?}"), // could probably handle this one better but whatever + Value::Function(function) => { + let info = function.info(); + format!( + "`{}` at `{}`", + info.name.map_or_else(|| "Unnamed".to_owned(), |val| val), + info.line_defined + .map_or_else(|| "Unknown location".to_owned(), |val| val.to_string()), + ) + } + Value::Thread(thread) => { + format!("`{}`:`{:?}`", thread.to_pointer() as usize, thread.status()) + } + Value::UserData(userdata) => format!("`{userdata:?}`"), + Value::Error(err) => err.to_string(), + } +} -pub fn initialize() -> Lua { - // let mut runtimes = vec![]; - // for i in 0..1 { - +pub fn initialize() -> Result { // coroutines could be useful, but too complicated to throw onto the others immediately // DO NOT add io or os, both contain functions that can easily mess with the host system // investigate package and debug @@ -18,14 +42,14 @@ pub fn initialize() -> Lua { LuaOptions::new() .catch_rust_panics(true) .thread_pool_size(num_cpus::get()), - ) - .expect("Lua could not initialize properly"); + )?; + // .expect("Lua could not initialize properly"); // set max to 2mb - lua.set_memory_limit(2 * 1024 * 1024).unwrap(); + info!("Previous memory limit `{}`", lua.set_memory_limit(2 * 1024 * 1024)?); let globals = lua.globals(); - globals.set("NP_VERSION", VERSION).unwrap(); + globals.set("NP_VERSION", VERSION)?; clean_global( #[cfg(not(debug_assertions))] @@ -37,12 +61,9 @@ pub fn initialize() -> Lua { set_logging(&lua, &globals); - // runtimes.push(lua); - // } - drop(globals); - lua + Ok(lua) } fn clean_global(#[cfg(not(debug_assertions))] lua: &Lua, globals: &Table) { @@ -54,8 +75,8 @@ fn clean_global(#[cfg(not(debug_assertions))] lua: &Lua, globals: &Table) { } // disabling this for now as mlua should do a decent job of collecting itself, and if not we can manage it in rust, no need for the users to worry about it globals.raw_remove("collectgarbage").unwrap(); - // remove this for use later with the error log kind (it also panics the interpereter when used so thats a nono) - globals.raw_remove("error").unwrap(); + // keep error as lua needs trace errors instead of rust like return types (its easier to keep things the way they are so we dont confuse newcomers) + // globals.raw_remove("error").unwrap(); // depricated and unneeded globals.raw_remove("gcinfo").unwrap(); // undocumented as far as i can tell and serves no immediate user facing purpose @@ -65,87 +86,152 @@ fn clean_global(#[cfg(not(debug_assertions))] lua: &Lua, globals: &Table) { } fn prepare_global(lua: &Lua, globals: &Table) { - let table = lua.create_table().unwrap(); - table.set("info", CAT_INF).unwrap(); - table.set("fun", CAT_FUN).unwrap(); - table.set("general", CAT_GEN).unwrap(); - table.set("moderation", CAT_MOD).unwrap(); - globals.set("commandCategory", table).unwrap(); + let command_category_table = lua.create_table().unwrap(); + command_category_table.set("dev", CAT_DEV).unwrap(); + command_category_table.set("info", CAT_INF).unwrap(); + command_category_table.set("fun", CAT_FUN).unwrap(); + command_category_table.set("general", CAT_GEN).unwrap(); + command_category_table.set("moderation", CAT_MOD).unwrap(); + globals + .set("commandCategory", command_category_table) + .unwrap(); - let table = lua.create_table().unwrap(); - #[rustfmt::skip] - table.set("registerCommand", lua.create_function(rust_lua_functions::registerCommand).unwrap()).unwrap(); - #[rustfmt::skip] - table.set("executeCommand", lua.create_function(rust_lua_functions::executeCommand).unwrap()).unwrap(); + let sys_table = lua.create_table().unwrap(); + #[allow(clippy::deprecated_cfg_attr)] // shut up clippy thats unstable + #[cfg_attr(rustfmt, rustfmt_skip)] // wish this was comment based but whatever + { + sys_table.set("registerCommand", lua.create_function(rust_lua_functions::register_command).unwrap()).unwrap(); + sys_table.set("executeCommand", lua.create_function(rust_lua_functions::execute_command).unwrap()).unwrap(); + sys_table.set("luaValueToString", lua.create_function(rust_lua_functions::lua_value_to_string).unwrap()).unwrap(); + } - globals.set("sys", table).unwrap(); + globals.set("sys", sys_table).unwrap(); } fn set_logging(lua: &Lua, globals: &Table) { - let log = lua - .create_function(|_lua, strings: Variadic| { + // i wish i could loop these but i cant instantiate macros + let log_table = lua.create_table().unwrap(); + let trace = lua + .create_function(|_lua, strings: Variadic| { let mut st = String::new(); for i in strings { + st.push_str(value_to_string(i).as_str()); st.push(' '); - st.push_str(i.as_str()); } + st.pop(); + trace!("Lua: {st}"); + Ok(()) + }) + .unwrap(); + log_table.set("trace", trace).unwrap(); + let debug = lua + .create_function(|_lua, strings: Variadic| { + let mut st = String::new(); + for i in strings { + st.push_str(value_to_string(i).as_str()); + st.push(' '); + } + st.pop(); + debug!("Lua: {}", st); + Ok(()) + }) + .unwrap(); + log_table.set("debug", debug).unwrap(); + let info = lua + .create_function(|_lua, strings: Variadic| { + let mut st = String::new(); + for i in strings { + st.push_str(value_to_string(i).as_str()); + st.push(' '); + } + st.pop(); info!("Lua: {}", st); Ok(()) }) .unwrap(); - globals.set("info", log).unwrap(); + log_table.set("info", info).unwrap(); + let warn = lua + .create_function(|_lua, strings: Variadic| { + let mut st = String::new(); + for i in strings { + st.push_str(value_to_string(i).as_str()); + st.push(' '); + } + st.pop(); + warn!("Lua: {}", st); + Ok(()) + }) + .unwrap(); + log_table.set("warn", warn).unwrap(); + let error = lua + .create_function(|_lua, strings: Variadic| { + let mut st = String::new(); + for i in strings { + st.push_str(value_to_string(i).as_str()); + st.push(' '); + } + st.pop(); + error!("Lua: {}", st); + Ok(()) + }) + .unwrap(); + log_table.set("error", error).unwrap(); + globals.set("log", log_table).unwrap(); } -#[allow(non_snake_case)] mod rust_lua_functions { - use mlua::{ExternalError, Lua, Result, Value, Variadic}; + use mlua::prelude::LuaResult; + use mlua::{ExternalError, Lua, Value, Variadic}; use tracing::info; - pub fn executeCommand(_lua: &Lua, arg_values: Variadic) -> Result<()> { - let mut command_name = String::new(); + use crate::lua::value_to_string; + + pub fn execute_command(_lua: &Lua, mut arg_values: Variadic) -> LuaResult<()> { + let command_name = match arg_values.get(0).cloned() { + Some(name) => name, + None => return Err("executeCommand requires a command name to execute! please supply a valid command name as the first argument.".into_lua_err()), + }; + let mut command_args = vec![]; - for (idx, value) in arg_values.iter().enumerate() { + for value in arg_values.split_off(1) { match value { + Value::String(string) => { + command_args.push(string.to_string_lossy().to_string()); + } Value::Nil => { return Err( "Nil argument provided! executeCommand accepts no nil arguments" .into_lua_err(), ) } - Value::String(string) => { - if idx == 0 { - command_name = string.to_string_lossy().to_string(); - } else { - command_args.push(string.to_string_lossy().to_string()); - } - } Value::Table(_) => { return Err("Direct run commands are not supported yet.".into_lua_err()) } - // Value::Function(_) => todo!(), - _ => { - return Err("Invalid type used! Only `String` and `Table` are supported by executeCommand".into_lua_err()); - } - }; + _ => return Err( + "Invalid type used! Only `String` and `Table` are supported by executeCommand" + .into_lua_err(), + ), + } } info!( - "Running lua command `{:?}` with args `{:?}`", - command_name, command_args + "Running lua command `{}` with args `{:?}`", + value_to_string(command_name), + command_args ); Ok(()) } - pub fn registerCommand(_lua: &Lua, arg_values: Variadic) -> Result<()> { + pub fn register_command(_lua: &Lua, arg_values: Variadic) -> LuaResult<()> { let table = match arg_values.get(0) { + Some(Value::Table(table)) => table, Some(Value::Nil) | None => { return Err( "Nil argument provided! registerCommand accepts no nil arguments" .into_lua_err(), ) } - Some(Value::Table(table)) => table, - Some(_) => { + _ => { return Err( "Invalid type used! Only `Table` is supported by registerCommand" .into_lua_err(), @@ -153,8 +239,17 @@ mod rust_lua_functions { } }; + info!("{:?}", table); + Ok(()) } + + pub fn lua_value_to_string(_lua: &Lua, mut arg_values: Variadic) -> LuaResult { + match arg_values.len() { + 1 => Ok(value_to_string(arg_values.remove(0))), + _ => Err("luaValueToString requires exactly one argument.".into_lua_err()), + } + } } // pub mod luagen_module { diff --git a/src/main.rs b/src/main.rs index a7b44ee..e25c0b6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,8 @@ #![deny(clippy::unwrap_used)] #![deny(clippy::expect_used)] #![deny(clippy::pedantic)] +#![allow(clippy::unreadable_literal)] +#![allow(clippy::needless_pass_by_value)] mod arguments; mod bot; @@ -9,67 +11,82 @@ mod constants; mod lua; mod tasks; -use std::collections::HashMap; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Condvar, OnceLock}; +use std::process::exit; +use std::sync::atomic::Ordering; +use std::sync::OnceLock; + +use serenity::all::{Context, GuildId}; +use serenity::model::channel::Message; +use tracing::metadata::LevelFilter; + +use tracing::{error, info}; use commands::{Command, CommandFnKind, COMMANDS}; use constants::CAT_HID; -use metadata::LevelFilter; -use serenity::all::{Context, GuildId}; -use serenity::model::channel::Message; -use tracing::*; -pub static NO_DISCORD: 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 +pub static CARG_NO_DISCORD: OnceLock = OnceLock::new(); #[tokio::main] async fn main() { - tracing::subscriber::set_global_default( + if let Err(err) = tracing::subscriber::set_global_default( tracing_subscriber::fmt() .with_max_level(LevelFilter::INFO) .finish(), - ) - .unwrap(); + ) { + eprintln!("Encountered an error while initializing logging! `{err}`"); + exit(1); + }; info!("Logging initialized."); info!("Grabbing commandline input."); let mut nodiscord = false; for i in std::env::args() { + #[allow(clippy::single_match)] // here for convienience when adding additional flags match i.as_str() { "nodiscord" => nodiscord = true, _ => {} } } - NO_DISCORD.set(nodiscord).unwrap(); - - // let mut commands = HashMap::default(); + if let Err(err) = CARG_NO_DISCORD.set(nodiscord) { + error!("Encountered an error while setting the `nodiscord` flag! `{err}`"); + exit(1); + }; info!("Loading stock commands."); insert_stock().await; info!("Loading rust dynamic commands."); - insert_rust().await; + insert_rust(); info!("Initializing Lua runtime."); - let lua = lua::initialize(); // may not need to be mutable, but its fine for now + let lua = match lua::initialize() { + Ok(lua) => lua, + Err(err) => { + error!("Encountered an error while initializing lua! `{err}`"); + exit(1); + }, + }; // may not need to be mutable, but its fine for now - lua.load(include_str!("../lua/loader.lua")) - .set_name("lua init script") + if let Err(err) = lua + .load(include_str!("../lua/loader.lua")) + .set_name("loader.lua") .exec() - .unwrap(); + { + error!("Encountered an error while running loader.lua: {err}"); + }; info!("Loading lua commandlets."); - insert_lua(&lua).await; + insert_lua(&lua); + + if let Err(err) = lua.sandbox(true) { + error!("Encountered an error while setting the lua runtime to sandbox mode! `{err}`"); + exit(1); + }; - lua.sandbox(true).unwrap(); - bot::start().await; } /// The last set of commands, these are used by the hoster of the engine. -pub async fn insert_lua(_lua: &mlua::Lua) { +pub fn insert_lua(_lua: &mlua::Lua) { // commands.insert( // "stop".to_owned(), // Command::new("stop".to_owned()) @@ -84,7 +101,7 @@ pub async fn insert_lua(_lua: &mlua::Lua) { } /// Cannot use any command names of stock commands, but gets to pick before lua commands are loaded. -pub async fn insert_rust() { +pub fn insert_rust() { // commands.insert( // "stop".to_owned(), // Command::new("stop".to_owned()) @@ -118,13 +135,15 @@ fn stop_command(_: Context, msg: Message, _: Option) { if msg.author.id != 380045419381784576 { return; } - DO_SHUTDOWN.0.store(true, Ordering::SeqCst); - DO_SHUTDOWN.1.notify_all(); + bot::DO_SHUTDOWN.0.store(true, Ordering::SeqCst); + bot::DO_SHUTDOWN.1.notify_all(); let handle = tokio::runtime::Handle::current(); let _eg = handle.enter(); handle.spawn(async { + // this is literally impossible to fail as the SHARD_MANAGER is required to exist for this function to ever run + #[allow(clippy::unwrap_used)] bot::SHARD_MANAGER.get().unwrap().shutdown_all().await; }); }