From 0f40bc98c3efd81e7e46ca5de315371e70ccd5e7 Mon Sep 17 00:00:00 2001 From: deepCurse Date: Mon, 9 Sep 2024 07:40:00 -0300 Subject: [PATCH] lua testing --- Cargo.lock | 11 ++ Cargo.toml | 7 +- lua/help.lua | 18 +++ lua/loader.lua | 40 +++++++ src/bot.rs | 99 +++++++++++++++++ src/constants.rs | 6 +- src/lua.rs | 99 +++++++++++++++++ src/main.rs | 281 +++++++++++++++++++---------------------------- 8 files changed, 389 insertions(+), 172 deletions(-) create mode 100644 lua/help.lua create mode 100644 lua/loader.lua create mode 100644 src/bot.rs create mode 100644 src/lua.rs diff --git a/Cargo.lock b/Cargo.lock index 5d6ded4..1aa24f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -826,6 +826,7 @@ name = "nopalmo" version = "0.1.0" dependencies = [ "mlua", + "num_cpus", "rand", "regex", "serenity", @@ -860,6 +861,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "object" version = "0.32.2" diff --git a/Cargo.toml b/Cargo.toml index 202fe3c..c6744c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,13 +8,14 @@ tokio = { version = "1.37.0", features = ["full"] } tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["chrono"] } +mlua = { version = "0.9.9", features = ["async", "luau"] } +serenity = "0.12.1" + +num_cpus = "1.16.0" 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 diff --git a/lua/help.lua b/lua/help.lua new file mode 100644 index 0000000..e740fba --- /dev/null +++ b/lua/help.lua @@ -0,0 +1,18 @@ +COMMAND["help"] = { + aliases = { "h" }, -- other strings that you can run the command from + pretty_name = "Help", -- the name of the command as it shows up in menues and other commands that use its pretty name + name = "help", -- the main required string used to call the command + 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 + permissions = nil, -- which discord permissions they need to run the command + + FUNC = function(context, message, guildid) + for t in COMMAND do + print("Help info for command " + t) + end + end +} + +COMMAND["help"].FUNC() \ No newline at end of file diff --git a/lua/loader.lua b/lua/loader.lua new file mode 100644 index 0000000..4a352bb --- /dev/null +++ b/lua/loader.lua @@ -0,0 +1,40 @@ +commands.help = { + aliases = { "h" }, -- other strings that you can run the command from + pretty_name = "Help", -- the name of the command as it shows up in menues and other commands that use its pretty name + name = "help", -- the main required string used to call the command + 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 + 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) + end + end +} + +commands["help"].func() + +-- exit(0) +-- local seen={} +-- local function dump(t,i) +-- seen[t]=true +-- local s={} +-- local n=0 +-- for k, v in pairs(t) do +-- n=n+1 +-- s[n]=tostring(k) +-- end +-- table.sort(s) +-- for k,v in ipairs(s) do +-- print(i .. v) +-- v=t[v] +-- if type(v)=="table" and not seen[v] then +-- dump(v,i.."\t") +-- end +-- end +-- end + +-- dump(_G,"") \ No newline at end of file diff --git a/src/bot.rs b/src/bot.rs new file mode 100644 index 0000000..658829a --- /dev/null +++ b/src/bot.rs @@ -0,0 +1,99 @@ +use std::collections::HashMap; +use std::sync::atomic::Ordering; + +use crate::commands::{self, Command}; +use crate::{constants, tasks, DO_SHUTDOWN}; +use serenity::all::EventHandler; +use serenity::all::{ActivityData, Context, GuildId}; +use serenity::model::channel::Message; +use serenity::model::gateway::Ready; +use tracing::*; + +pub struct BotHandler { + // TODO use data field instead? + pub commands: HashMap, +} + +unsafe impl Sync for BotHandler {} +unsafe impl Send for BotHandler {} + +#[serenity::async_trait] +impl EventHandler for BotHandler { + 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, + "NPO_PFX", + ctx.cache.current_user().id + ); + let cmd_regex = format!(r"{}[A-Za-z0-9_\-]+", prefix_regex); + + let result = match regex::Regex::new(cmd_regex.as_str()) + .unwrap() + .find(&msg.content) + { + Some(result) => result, + None => return, // silently exit because not every message is meant for the bot + }; + + if DO_SHUTDOWN.0.load(Ordering::SeqCst) { + let _ = msg + .channel_id + .say( + &ctx.http, + "Sorry! Your request was cancelled because the bot is shutting down.", + ) + .await; + return; + } + + let target_cmd_name = regex::Regex::new(prefix_regex.as_str()) + .unwrap() + .replace(result.as_str(), "") + .to_string(); + + msg.reply(&ctx.http, target_cmd_name.to_string()) + .await + .unwrap(); + + 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), + } + } + } + } + } + + /// 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."); + } +} diff --git a/src/constants.rs b/src/constants.rs index 11c57b5..bf4d161 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,3 +1,5 @@ +pub const VERSION: u16 = 0; + pub const COMMAND_PREFIX: &'static str = ";"; -pub const SHORT_ARGUMENT_PREFIX: &'static str = "-"; -pub const LONG_ARGUMENT_PREFIX: &'static str = "--"; +// pub const SHORT_ARGUMENT_PREFIX: &'static str = "-"; +// pub const LONG_ARGUMENT_PREFIX: &'static str = "--"; diff --git a/src/lua.rs b/src/lua.rs new file mode 100644 index 0000000..897400f --- /dev/null +++ b/src/lua.rs @@ -0,0 +1,99 @@ +use mlua::{Lua, LuaOptions, StdLib, Table, Variadic}; +use tracing::{debug, error, info, trace, warn}; + +use crate::{constants::VERSION, CAT_FUN, CAT_GEN, CAT_HID, CAT_INF, CAT_MOD}; + +pub fn initialize() -> Lua { + // let mut runtimes = vec![]; + // for i in 0..1 { + + // 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 + + // may need debug to set limitations to the runtime such as execution time and iteration count + let lua = Lua::new_with( + StdLib::BIT | StdLib::BUFFER | StdLib::MATH | StdLib::STRING | StdLib::TABLE | StdLib::UTF8, + LuaOptions::new() + .catch_rust_panics(true) + .thread_pool_size(num_cpus::get()), + ) + .expect("Lua could not initialize properly"); + + // set max to 2mb + lua.set_memory_limit(2 * 1024 * 1024).unwrap(); + + let globals = lua.globals(); + globals.set("NP_VERSION", VERSION).unwrap(); + + clean_global( + #[cfg(not(debug_assertions))] + &lua, + &globals, + ); + + prepare_global(&lua, &globals); + + set_logging(&lua, &globals); + + lua.load(include_str!("../lua/loader.lua")) + .set_name("lua init script") + .exec() + .unwrap(); + + lua.sandbox(true).unwrap(); + + // return; + + // runtimes.push(lua); + // } + + drop(globals); + + lua +} + +fn clean_global(#[cfg(not(debug_assertions))] lua: &Lua, globals: &Table) { + // allows us to assert things in debug mode, and keep the asserts in release without hard crashing + #[cfg(not(debug_assertions))] + { + let assert = lua.create_function(|_lua, ()| Ok(())).unwrap(); + globals.set("assert", assert).unwrap(); + } + // 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(); + // depricated and unneeded + globals.raw_remove("gcinfo").unwrap(); + // undocumented as far as i can tell and serves no immediate user facing purpose + globals.raw_remove("newproxy").unwrap(); + // we should be using the logging functions instead of raw print + globals.raw_remove("print").unwrap(); +} +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 table = lua.create_table().unwrap(); + globals.set("commands", table).unwrap(); +} + +fn set_logging(lua: &Lua, globals: &Table) { + let log = lua + .create_function(|_lua, strings: Variadic| { + let mut st = String::new(); + for i in strings { + st.push(' '); + st.push_str(i.as_str()); + } + info!("Lua: {}", st); + Ok(()) + }) + .unwrap(); + globals.set("info", log).unwrap(); +} diff --git a/src/main.rs b/src/main.rs index 64bbdd7..4d6bea8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,20 +3,22 @@ #![deny(clippy::pedantic)] mod arguments; +mod bot; mod commands; mod constants; +mod lua; mod tasks; use std::collections::HashMap; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Condvar, OnceLock}; -use commands::Command; +use bot::BotHandler; +use commands::{Command, CommandFnKind}; use metadata::LevelFilter; -use serenity::all::{ActivityData, Context, GuildId, ShardManager}; -use serenity::all::{EventHandler, GatewayIntents}; +use serenity::all::GatewayIntents; +use serenity::all::{Context, GuildId, ShardManager}; use serenity::model::channel::Message; -use serenity::model::gateway::Ready; use serenity::Client; use tracing::*; @@ -31,95 +33,6 @@ 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? - commands: HashMap, -} - -unsafe impl Sync for Handler {} -unsafe impl Send for Handler {} - -#[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, - "NPO_PFX", - ctx.cache.current_user().id - ); - let cmd_regex = format!(r"{}[A-Za-z0-9_\-]+", prefix_regex); - - let result = match regex::Regex::new(cmd_regex.as_str()) - .unwrap() - .find(&msg.content) - { - Some(result) => result, - None => return, // silently exit because not every message is meant for the bot - }; - - if DO_SHUTDOWN.0.load(Ordering::SeqCst) { - let _ = msg - .channel_id - .say( - &ctx.http, - "Sorry! Your request was cancelled because the bot is shutting down.", - ) - .await; - return; - } - - let target_cmd_name = regex::Regex::new(prefix_regex.as_str()) - .unwrap() - .replace(result.as_str(), "") - .to_string(); - - msg.reply(&ctx.http, target_cmd_name.to_string()) - .await - .unwrap(); - - 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), - } - } - } - } - } - - /// 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( @@ -129,86 +42,120 @@ async fn main() { ) .unwrap(); - // TODO load this at runtime so the key will not be stored in the binary? - #[cfg(not(debug_assertions))] - let token = { - info!("Initializing bot with production token."); - include_str!("bot_token.prod") - }; - #[cfg(debug_assertions)] - let token = { - info!("Initializing bot with development token."); - include_str!("bot_token.dev") - }; - - let intents = GatewayIntents::DIRECT_MESSAGES - | GatewayIntents::DIRECT_MESSAGE_REACTIONS - | GatewayIntents::GUILDS - | GatewayIntents::GUILD_MODERATION - // | GatewayIntents::GUILD_EMOJIS_AND_STICKERS - | GatewayIntents::GUILD_MEMBERS - | GatewayIntents::GUILD_MESSAGE_REACTIONS - | GatewayIntents::GUILD_MESSAGES - | GatewayIntents::MESSAGE_CONTENT; - + info!("Logging initialized."); + let mut commands = HashMap::default(); - stock_commands::insert_all(&mut commands); + info!("Loading stock commands."); + insert_stock(&mut commands); + info!("Loading rust dynamic commands."); + insert_rust(&mut commands); - let mut client = match Client::builder(&token, intents) - .event_handler(Handler { commands }) - .await - { - Ok(client) => client, - Err(err) => panic!("Error starting client connection: `{err}`"), - }; + info!("Initializing Lua runtime."); + let mut lua = lua::initialize(); // may not need to be mutable, but its fine for now - SHARD_MANAGER.set(client.shard_manager.clone()).unwrap(); + info!("Loading lua commandlets."); + insert_lua(&mut commands, &mut lua); - if let Err(why) = client.start_shards(2).await { - error!("Client error: {why:?}"); - } + warn!("ABORTING CONNECTING TO DISCORD, BYE BYE!"); + return; - warn!("EXITING"); + // // TODO load this at runtime so the key will not be stored in the binary? + // #[cfg(not(debug_assertions))] + // let token = { + // info!("Initializing bot with production token."); + // include_str!("bot_token.prod") + // }; + // #[cfg(debug_assertions)] + // let token = { + // info!("Initializing bot with development token."); + // include_str!("bot_token.dev") + // }; + + // let intents = GatewayIntents::DIRECT_MESSAGES + // | GatewayIntents::DIRECT_MESSAGE_REACTIONS + // | GatewayIntents::GUILDS + // | GatewayIntents::GUILD_MODERATION + // // | GatewayIntents::GUILD_EMOJIS_AND_STICKERS + // | GatewayIntents::GUILD_MEMBERS + // | GatewayIntents::GUILD_MESSAGE_REACTIONS + // | GatewayIntents::GUILD_MESSAGES + // | GatewayIntents::MESSAGE_CONTENT; + + // let mut client = match Client::builder(&token, intents) + // .event_handler(BotHandler { commands }) + // .await + // { + // Ok(client) => client, + // Err(err) => panic!("Error starting client connection: `{err}`"), + // }; + + // SHARD_MANAGER.set(client.shard_manager.clone()).unwrap(); + + // if let Err(why) = client.start_shards(2).await { + // error!("Client error: {why:?}"); + // } + + // warn!("MAIN FUNCTION 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; - }); - } +/// The last set of commands, these are used by the hoster of the engine. +pub fn insert_lua(_commands: &mut HashMap, _lua: &mut mlua::Lua) { + // 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()), + // ); +} + +/// Cannot use any command names of stock commands, but gets to pick before lua commands are loaded. +pub fn insert_rust(_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()), + // ); +} + +/// Will never fail, gets first pick of command names and properties. It is up to the maintainer to make damn sure this works. +pub fn insert_stock(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; + }); }