Compare commits

...
Sign in to create a new pull request.

5 commits
main ... dev

Author SHA1 Message Date
fcd2c3c35d
Polishing the turd 2025-03-25 01:18:05 -03:00
baa14a47e6
.env file support
introduced a macro to make management of program properties easier
program properties are obtained in this order: environment, .env file, commandline arguments
a configuration file will be included at a later date and will be loaded between .env and commandline args
2025-03-24 18:45:43 -03:00
3da1cfc1f5
Sync 2025-03-23 13:53:22 -03:00
578918b22f
Updated dependencies
Updated formatting file
Changed the command line argument system
Updated error checking and matching logic to be easier to work with
Moved groups of functions to more correct modules
Updated the constants.rs file and added a help string for use with the new argument system
Disabled lua for now, the rust side of the bot takes priority
Changed the shutdown handler to be easier to work with
Added shutdown monitor task that gets started after the bot is ready, allowing a graceful shutdown from anywhere in the program, authentication is expected to be handled at the place of calling, not within the shutdown function
Added a basic database interface, SQLite will be implemented first as it is the simplest and most useful to the spiff team
2025-02-18 16:46:25 -04:00
991feb82a1
dev sync attempt 2 2024-10-11 01:46:51 -03:00
22 changed files with 2192 additions and 781 deletions

2
.gitignore vendored
View file

@ -1,7 +1,9 @@
bot_token*
.env
/target
/.vscode
/.rust-analyzer
**/*.rs.bk
*.pdb

1055
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,22 +1,30 @@
[package]
name = "nopalmo"
version = "0.1.0"
edition = "2021"
edition = "2024"
[dependencies]
tokio = { version = "1.37.0", features = ["full"] }
# Logging
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["chrono"] }
log = "0.4.26"
mlua = { version = "0.9.9", features = ["async", "luau"] }
serenity = "0.12.1"
mlua = { version = "0.10.3", features = ["async", "luau"] }
# Tokio will be useful at a later date but as of now i will be limiting its usage
tokio = { version = "1.37.0", features = ["full"] }
# tossing this lib at some point, it makes too many assumptions about how i code and i dislike how often im forced to async
serenity = "0.12.4"
num_cpus = "1.16.0"
rand = "0.8.5"
regex = "1.10.4"
rand = "0.9.0"
#regex = "1.10.4"
sqlite = { version = "0.36.1", features = ["bundled"] }
ctrlc = "3.4.5"
[features]
dot_env_file = []
default = ["dot_env_file"]
# 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 = []

51
docs/db-structure.md Normal file
View file

@ -0,0 +1,51 @@
---
# Preface
### Primitive Data Types
#### Null
This value type can override any other type and essentially means "nothing" not even 0, its the lack of a value whatsoever
If you open a box thats supposed to have a number in it, and you found null, the box would just be empty
#### Boolean
A boolean value is true or false, it cannot be anything else
See [Sqlite](sqlite.md#Boolean) for more information on boolean values within sqlite.
#### Integer
An integer is any valid number without a decimal, such as 1, 5, 68, 421, and even negative numbers like -42, -99999, or -0 in some cases
#### Real Number (floating point number)
A real number is any real number, such as 1, 73, 45, 0.6, 255, -9.8, 200000000, and so on, it may use a decimal value unlike integers.
We should prefer Integers over Real Numbers unless a decimal place is absolutely required:
There are resolution limitations to consider, the number of bits a computer can store in a single value is finite, a 32 bit number only has 32 bits, meaning there can only be about 2 billion and some possible combinations.
Real numbers do not change this, there are only 2 billion or so different values to a real number, so the smaller you go, the more likely you are to accidentally "snap" to a nearby number that can actually be stored,
The same thing happens when you go high enough.
Here are some good videos on the topic.
Simple [Computerphile](https://www.youtube.com/watch?v=PZRI1IfStY0)
Technical [Spanning Tree](https://www.youtube.com/watch?v=bbkcEiUjehk)
### Complex Data Types
#### Users
- UID: Integer
- isDeveloper: Boolean
---
# Structure
### Stuff, eventually

10
docs/sqlite.md Normal file
View file

@ -0,0 +1,10 @@
This file will store any corrections or nuance about using sqlite.
### Boolean
Due to a restriction of sqlite, we must use Integer data types to represent a boolean value.
To do so the value must be either 0 meaning false, or 1 meaning true.
The database accessor will do this conversion for you automatically, so this will only matter to people working on the database accessor.

View file

@ -1,18 +1,18 @@
COMMAND["help"] = {
Metadata = {
name = "help", -- the main required string used to call the command
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:",
command_category = commandCategory.info, -- this can be any string, but the bot provides a few categories already
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()
function Command(context, message, guildid)
for t in COMMAND do
print("Help info for command " + t)
end
end

View file

@ -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={}
@ -38,3 +73,4 @@ sys.executeCommand("help", "E")
-- end
-- dump(_G,"")

2
rust-toolchain.toml Normal file
View file

@ -0,0 +1,2 @@
[toolchain]
channel = "stable"

View file

@ -1 +1,33 @@
hard_tabs = true
# i use tabs so stop bugging me and go elsewhere already
hard_tabs=true
binop_separator="Back"
condense_wildcard_suffixes = true
empty_item_single_line = false
enum_discrim_align_threshold = 30
struct_field_align_threshold = 30
short_array_element_width_threshold = 30
inline_attribute_width = 50
fn_params_layout = "Compressed"
fn_single_line = true
format_code_in_doc_comments = true
format_macro_matchers = true
hex_literal_case = "Upper"
imports_indent = "Visual"
imports_layout = "Vertical"
# indent_style = "Visual"
match_arm_blocks = false
match_block_trailing_comma = true
max_width = 160
imports_granularity = "Item"
newline_style = "Unix"
normalize_doc_attributes = true
overflow_delimited_expr = true
reorder_impl_items = true
# group_imports = "StdExternalCrate"
space_after_colon = false
# trailing_comma = "Always"
type_punctuation_density = "Compressed"
use_field_init_shorthand = true
use_try_shorthand = true
where_single_line = true

View file

@ -1,75 +1,86 @@
use std::{collections::HashMap, sync::Arc};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Default)]
pub struct ArgumentStorage {
// arguments: Vec<Argument>,
long_key_to_id: HashMap<String, Arc<Argument>>,
short_key_to_id: HashMap<String, Arc<Argument>>,
}
pub struct ArgumentStorage(HashMap<String, String>);
impl ArgumentStorage {
pub fn new() -> Self {
Self::default()
Self(HashMap::new())
}
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<String>, short: Option<String>) {
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<Argument>,
// long_key_to_id: HashMap<String, Arc<Argument>>,
// short_key_to_id: HashMap<String, Arc<Argument>>,
// }
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<String>, short: Option<String>) {
// 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),
// }

View file

@ -1,16 +1,31 @@
use std::collections::HashMap;
use std::sync::atomic::Ordering;
use std::sync::{Arc, OnceLock};
use std::sync::Arc;
use std::sync::OnceLock;
use std::sync::atomic::AtomicBool;
use crate::commands::{self, COMMANDS};
use crate::{constants, tasks, DO_SHUTDOWN, NO_DISCORD};
use serenity::all::{ActivityData, Context, GuildId};
use serenity::all::{EventHandler, GatewayIntents, ShardManager};
use serenity::Client;
use serenity::all::ActivityData;
use serenity::all::Context;
use serenity::all::EventHandler;
use serenity::all::GatewayIntents;
use serenity::all::GuildId;
use serenity::all::ShardManager;
use serenity::model::channel::Message;
use serenity::model::gateway::Ready;
use serenity::Client;
use tracing::*;
use tracing::error;
use tracing::info;
use tracing::warn;
use crate::PROPERTIES;
use crate::commands;
use crate::commands::COMMANDS;
use crate::constants;
use crate::tasks;
// TODO remove from static memory
pub static DO_SHUTDOWN: AtomicBool = AtomicBool::new(false);
// TODO remove from static memory
pub static SHARD_MANAGER: OnceLock<Arc<ShardManager>> = OnceLock::new();
pub struct BotHandler;
@ -18,6 +33,7 @@ pub struct BotHandler;
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) {
@ -26,65 +42,62 @@ impl EventHandler for BotHandler {
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 offset;
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 msg.content.starts_with(constants::COMMAND_PREFIX) {
offset = constants::COMMAND_PREFIX.len();
} else if msg.content.starts_with(constants::CONSTANT_PREFIX) {
offset = constants::CONSTANT_PREFIX.len();
} else if msg.content.starts_with(format!("<@{}>", ctx.cache.current_user().id).as_str()) {
// Magic numbers: +1 is for ilog10, +3 is for the <@> characters
offset = ctx.cache.current_user().id.get().checked_ilog10().unwrap_or(0) as usize + 1 + 3;
} else {
return;
}
if DO_SHUTDOWN.0.load(Ordering::SeqCst) {
// whitespace should be handled by group()
//if msg.content.chars().nth(offset).map_or(false, char::is_whitespace) {
// offset += 1;
//}
dbg!(&offset);
if DO_SHUTDOWN.load(std::sync::atomic::Ordering::SeqCst) {
let _ = msg
.channel_id
.say(
&ctx.http,
"Sorry! Your request was cancelled because the bot is shutting down.",
)
.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();
//let grouped = group(&&msg.content[offset..]);
//let bashed = bash(grouped);
msg.reply(&ctx.http, target_cmd_name.to_string())
.await
.unwrap();
let target_cmd_name = dbg!(&msg.content[offset..]);
if let Some(command) = COMMANDS.read().await.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),
}
}
let _ = msg.reply(&ctx.http, target_cmd_name.to_string()).await;
// TODO fix this unwrap
if let Some(command) = COMMANDS.read().unwrap().get(target_cmd_name) {
match (msg.guild_id, &command.run_guild_command, &command.run_dm_command) {
(Some(gid), Some(commands::CommandFnKind::Rust(gcmd)), _) => (gcmd)(ctx, msg, Some(gid)),
(None, _, Some(commands::CommandFnKind::Rust(dcmd))) => (dcmd)(ctx, msg, None),
(Some(gid), Some(commands::CommandFnKind::Lua(gcmd)), _) => todo!(),
(None, _, Some(commands::CommandFnKind::Lua(dcmd))) => todo!(),
_ => (),
}
}
}
/// 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.")))
let Some(shart) = ready.shard else {
error!("Bot ready function called while shards are not ready, this should be impossible.");
return;
};
info!("Shart `{}` is connected!", shart.id);
ctx.set_activity(Some(ActivityData::custom("Initializing.")));
}
/// Runs once when all shards are ready
@ -99,17 +112,18 @@ impl EventHandler for BotHandler {
}
}
pub async fn start() {
// 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.");
pub fn start() {
info!(
"Initializing bot with the {} token.",
if cfg!(debug_assertions) { "development" } else { "production" }
);
// Load this at runtime so the key will not be stored in the binary? at the very least we could disguise the string as garbage data so it cannot be easily extracted via static analysis, it seems like the bot library only uses references as well, so it should not be stored plaintext in memory either
// Will need a protected storage medium for the token, if we get to that point the user will need to suggest something, storing the token is not my responsibility, databases might be a good alternative?
const TOKEN: &str = if cfg!(debug_assertions) {
include_str!("bot_token.dev")
} else {
include_str!("bot_token.prod")
};
let intents = GatewayIntents::DIRECT_MESSAGES
@ -122,20 +136,24 @@ pub async fn start() {
| GatewayIntents::GUILD_MESSAGES
| GatewayIntents::MESSAGE_CONTENT;
if *NO_DISCORD.get().unwrap() {
if PROPERTIES.nodiscord {
// TODO fake discord api? we are probably tossing out serenity at some point anyway, could emulate something
warn!("ABORTING CONNECTING TO DISCORD, BYE BYE!");
} else {
let mut client = match Client::builder(&token, intents)
.event_handler(BotHandler)
.await
{
Ok(client) => client,
Err(err) => panic!("Error starting client connection: `{err}`"),
};
// TODO proper error handling
let reactor = tokio::runtime::Builder::new_multi_thread().enable_io().enable_time().build().unwrap();
SHARD_MANAGER.set(client.shard_manager.clone()).unwrap();
let mut client = reactor.block_on(async {
match Client::builder(TOKEN, intents).event_handler(BotHandler).await {
Ok(client) => client,
Err(err) => panic!("Error starting client connection: `{err}`"),
}
});
if let Err(why) = client.start_shards(2).await {
// just ignore the response, its impossible to fail here anyway
let _ = SHARD_MANAGER.set(client.shard_manager.clone());
if let Err(why) = reactor.block_on(client.start_shards(2)) {
error!("Client error: {why:?}");
}
}

View file

@ -1,19 +1,27 @@
use std::{collections::HashMap, fmt::Debug, sync::LazyLock, time::Duration};
use std::collections::HashMap;
use std::fmt::Debug;
use std::sync::LazyLock;
use std::sync::RwLock;
use std::time::Duration;
use serenity::all::{Context, GuildId, Message, Permissions};
use tokio::sync::RwLock;
use serenity::all::Context;
use serenity::all::GuildId;
use serenity::all::Message;
use serenity::all::Permissions;
use crate::arguments::{Argument, ArgumentStorage};
use crate::arguments::ArgumentStorage;
use crate::constants::CAT_HID;
pub static COMMANDS: LazyLock<RwLock<HashMap<String, Command>>> =
LazyLock::new(|| RwLock::new(HashMap::new()));
pub static COMMANDS: LazyLock<RwLock<HashMap<String, BotCommand>>> = LazyLock::new(|| RwLock::new(HashMap::new()));
#[derive(Debug)]
pub enum CommandFnKind {
Lua(()),
Rust(fn(Context, Message, Option<GuildId>)),
}
pub struct Command {
#[derive(Debug)]
pub struct BotCommand {
pub run_dm_command: Option<CommandFnKind>,
pub run_guild_command: Option<CommandFnKind>,
pub aliases: Vec<String>,
@ -26,7 +34,7 @@ pub struct Command {
pub hidden: bool,
pub arguments: ArgumentStorage,
pub required_caller_discord_permissions: ::serenity::all::Permissions,
pub required_caller_discord_permissions: serenity::all::Permissions,
#[cfg(feature = "nsfw_features")]
pub is_nsfw: bool,
@ -34,7 +42,7 @@ pub struct Command {
pub premium_kind: usize,
}
impl Command {
impl BotCommand {
pub fn new(name: String) -> Self {
Self {
run_dm_command: None,
@ -54,18 +62,22 @@ impl Command {
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
@ -74,67 +86,126 @@ impl Command {
self.aliases.push(alias);
self
}
pub fn aliases(mut self, aliases: &mut Vec<String>) -> 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 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 {}
unsafe impl Sync for BotCommand {}
unsafe impl Send for BotCommand {}
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 Debug for BotCommand {
// 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 {
// }
// }
/// The last set of commands, these are used by the hoster of the engine.
pub fn insert_lua(_lua: &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.blocking_write().insert(
// "stop2".to_owned(),
// BotCommand::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() {
let shutdown_command = |_, msg: Message, _| {
// hardcode my id for now
if crate::databases::get_db().is_dev(msg.author.id) {
return;
}
crate::shutdown_handler();
};
// TODO fix this unwrap
COMMANDS.write().unwrap().insert(
"stop".to_owned(),
BotCommand::new("stop".to_owned())
.alias("svs".to_owned())
.category(CAT_HID.to_owned())
.hidden(true)
.dm_command(CommandFnKind::Rust(shutdown_command))
.guild_command(CommandFnKind::Rust(shutdown_command))
.pretty_name("Stop the bot".to_owned())
.help("Stops the bot. Does nothing unless you are a developer.".to_owned()),
);
}

View file

@ -1,12 +1,106 @@
pub const VERSION: u16 = 0;
#![allow(unused)]
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";
/// TODO semver
pub const VERSION: u16 = 20; // setup reminder or git hook of some kind to inc this?
pub const COMMAND_PREFIX: &'static str = ";";
// pub const SHORT_ARGUMENT_PREFIX: &'static str = "-";
// pub const LONG_ARGUMENT_PREFIX: &'static str = "--";
// This variable should never change for any reason, it will be used to identify this instance of the bot
//pub const BOT_INTERNAL: &str = "npobot";
/// Should be limited to lowercase alphanumeric characters with periods, dashes, and underscores only
/// If you want to stylize the bot name use `BOT_NAME_FANCY`
pub const BOT_NAME: &str = "nopalmo";
pub const BOT_NAME_FANCY: &str = "Nopalmo";
pub const CAT_DEV: &str = "Dev";
pub const CAT_FUN: &str = "Fun";
pub const CAT_MOD: &str = "Moderation";
pub const CAT_GEN: &str = "General";
pub const CAT_INF: &str = "Info";
pub const CAT_HID: &str = "Hidden";
pub const EMOJI_THUMB_UP: &str = "<:thumbsup:1339301449645166665>";
pub const CONSTANT_PREFIX: &str = "NPO_PFX";
pub const COMMAND_PREFIX: &str = ";";
pub const SHORT_ARGUMENT_PREFIX: &str = "-";
pub const LONG_ARGUMENT_PREFIX: &str = "--";
pub const HELP_STRING: &str = r"
Nopalmo command help page:
Description:
Lorem Ipsum Dolar Sit Amet...
See Usage section for more information about how this help menu functions
Configuration:
print_help Prints this help information
h, help, -h, --help, -help, /h, /help, /?, ?
nodiscord Shuts down the program when the first discord connection usually takes place
Eventually this will connect to a simulated discord instance
-N, --no-discord
dbuser The username to be used when connecting to a database type that requires one
-U, --db-user <string>
dbpass The password used like dbuser
-P, --db-pass <string>
dbaddress The network address of the desired database
-A, --db-address <int.int.int.int>
dbport The port to use with dbaddress
-P, --db-port <int>
dbpath The path to a unix socket to substitute an address or the path to an SQLite database file
-F, --db-path <path>
dbkind The kind of database we will be using, one of `sqlite` `mariadb` `postgresql` `access`
-D, --db-kind <dbkind>
use_cli Enables a CLI management interface for the bot
-i, --interactive
use_gui The GUI version of the CLI
-g, --gui
lua_engine_count The number of lua execution engines to create, This defaults to the number of cores the computer has
This allows executing multiple lua tasks at once, including user commands and timer tasks
-l, --lua-engine-count <int>
log_level The level the logging system will use, one of `trace` `debug` `info` `warning` `error`
-L, --log-level <loglevel>
connect_remote Connect to a remote instance of the bot
-C, --remote-connection
host_remote Host an instance of the bot for remote connections
-H, --host-remote-connection
remote_address The remote address of the bot, or the address you wish to host the bot on
-a, --remote-address <int.int.int.int>
remote_port The port used like remote_address
-p, --remote-port <int>
remote_key The path of the key required to authenticate with the bot on a remote connection
The remote_key_type flag decides what this value is used for
-k, --remote-key <path>
remote_key_type Decides the key type for remote connections, one of `database` `password` or default `file`
-K, --remote-key-type <keytype>
Remote Connections:
Remote access is disabled by default and must be enabled via the `connect_remote` or `host_remote` flags
A key is required for remote connections, if you just need something quick use the `password` key type, the password must be the same for both server and client
The key can be managed by the database as well, and the key type `database` will disable the `remote_key` variable and instead load authentication information from the server
Usage:
./nopalmo --help
./nopalmo -H -a 0.0.0.0 -p 2022 -K file -k ./private.key
./nopalmo -C -a 127.0.0.1 -p 2022 -K file -k ./public.key
The Configuration section is layout like this:
Name Description
Commandline opts Option arguments
Each configurable value can be changed in multiple ways for ease of use by the user
The order of configuration application is like so:
1. System Environment
2. .env file (if enabled by your maintainer using the dot_env_file feature)
3. Configuration file (currently not supported)
4. Commandline options
Environment Variables:
Environment Variables may be set in addition or in place of commandline arguments
";

11
src/databases/access.rs Normal file
View file

@ -0,0 +1,11 @@
use std::process::exit;
use tracing::error;
pub fn create_interface() -> Result<(), Box<dyn std::error::Error>> {
//warn!("You must provide both a username and password via `dbuser` and `dbpass`");
//exit(-1);
error!("Sorry! this database kind is not implemented right now.");
exit(-1);
}

11
src/databases/mariadb.rs Normal file
View file

@ -0,0 +1,11 @@
use std::process::exit;
use tracing::error;
pub fn create_interface() -> Result<(), Box<dyn std::error::Error>> {
//warn!("You must provide both a username and password via `dbuser` and `dbpass`");
//exit(-1);
error!("Sorry! this database kind is not implemented right now.");
exit(-1);
}

58
src/databases/mod.rs Normal file
View file

@ -0,0 +1,58 @@
use std::fmt::Debug;
use std::sync::Arc;
use std::sync::OnceLock;
use serenity::all::UserId;
use crate::errors::DatabaseStoredError;
pub mod access;
pub mod mariadb;
pub mod postgres;
pub mod sqlite;
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
pub enum DBKind {
Access,
SQLite,
MariaDB,
PostgreSQL,
}
static DATABASE_ACCESSOR: OnceLock<Arc<Box<dyn DatabaseAccessor>>> = OnceLock::new();
/// # Panics
/// This function will panic if used after the database has already been set
///
/// You should not worry about this as this function only ever gets called once at the start of the program and you are doing something wrong if you need to call this a second time
pub fn set_db(dba: Box<dyn DatabaseAccessor>) {
assert!(DATABASE_ACCESSOR.set(Arc::new(dba)).is_ok(), "attempted to set database accessor after init");
}
/// # Panics
/// This function will panic if used before the database has been set
///
/// You should not worry about this as this function only ever gets called after the database has been set and you are doing something wrong if you are calling this before the database is live
pub fn get_db() -> Arc<Box<dyn DatabaseAccessor>> {
#[allow(clippy::expect_used)]
DATABASE_ACCESSOR.get().expect("attempted to get database before init").clone()
}
/// This trait will provide a very high level interface to all supported databases
/// Its implementations should also sanitize all inputs regardless of what type
/// The implementation may be multithreaded/multiconnection, but for sqlite it is limited to a single thread/locked direct access to the connection via a mutex
/// If you need more performance use a different database type
pub trait DatabaseAccessor: Sync + Send {
// TODO make a db upgrade table
fn get_db_version(&self);
fn check_db_health(&self);
fn fix_db_health(&self);
fn is_dev(&self, user_id: UserId) -> bool;
fn set_dev(&self, user_id: UserId);
fn store_error(&self, err: &DatabaseStoredError);
}

11
src/databases/postgres.rs Normal file
View file

@ -0,0 +1,11 @@
use std::process::exit;
use tracing::error;
pub fn create_interface() -> Result<(), Box<dyn std::error::Error>> {
//warn!("You must provide both a username and password via `dbuser` and `dbpass`");
//exit(-1);
error!("Sorry! this database kind is not implemented right now.");
exit(-1);
}

72
src/databases/sqlite.rs Normal file
View file

@ -0,0 +1,72 @@
use std::fmt::Debug;
use std::sync::Mutex;
use sqlite::Connection;
use tracing::error;
use tracing::info;
use crate::PROPERTIES;
use crate::errors::DatabaseStoredError;
use super::DatabaseAccessor;
struct SQLiteDatabase {
connection: Mutex<Connection>,
}
impl SQLiteDatabase {
//fn lock(&self) {
//}
}
impl Debug for SQLiteDatabase {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SQLiteDatabase")/*.field("connection", &"<SQLiteConnection>")*/.finish()
}
}
impl DatabaseAccessor for SQLiteDatabase {
fn get_db_version(&self) {
todo!()
}
fn check_db_health(&self) {
todo!()
}
fn fix_db_health(&self) {
todo!()
}
fn is_dev(&self, user_id: serenity::all::UserId) -> bool {
todo!()
}
fn set_dev(&self, user_id: serenity::all::UserId) {
todo!()
}
fn store_error(&self, err: &DatabaseStoredError) {
todo!()
}
}
/// # Panics
/// This function will panic if used after the database has already been set
///
/// You should not worry about this as this function only ever gets called once at the start of the program and you are doing something wrong if you need to call this a second time
pub fn create_interface() -> Result<(), Box<dyn std::error::Error>> {
info!("Loading SQLite.");
if let Some(db_path) = PROPERTIES.dbpath.clone() {
let connection = sqlite::open(&db_path).map_err(|err| format!("Could not open file {db_path:?} with error: {err}"))?;
super::set_db(Box::new(SQLiteDatabase {
connection: /*Arc::new(*/Mutex::new(connection)/*)*/,
}));
Ok(())
} else {
Err("You must provide a path using `dbpath` for sqlite to function!".into())
}
}

39
src/errors.rs Normal file
View file

@ -0,0 +1,39 @@
use std::fmt::Display;
use std::panic::Location;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use crate::databases;
#[derive(Debug)]
pub struct DatabaseStoredError {
epoch: u64,
caller: std::panic::Location<'static>,
cause: String,
}
impl Display for DatabaseStoredError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("")
}
}
impl std::error::Error for DatabaseStoredError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
}
impl DatabaseStoredError {
#[track_caller]
pub fn new(cause: impl ToString) -> Self {
let caller = *Location::caller();
let cause = cause.to_string();
#[allow(clippy::unwrap_used)] // Since epoch is 0, now cannot be less than 0 as the datatype is unsigned, making this unwrap impossible to fail.
let epoch = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
let err = Self { epoch, caller, cause };
databases::get_db().store_error(&err);
err
}
}

View file

@ -1,13 +1,58 @@
use mlua::{Lua, LuaOptions, StdLib, Table, Variadic};
// FIXME remove later, used for now while in active development
#![allow(clippy::unwrap_used)]
use mlua::Lua;
use mlua::LuaOptions;
use mlua::Result;
use mlua::StdLib;
use mlua::Table;
use mlua::Value;
use mlua::Variadic;
use tracing::debug;
use tracing::error;
use tracing::info;
use tracing::trace;
use tracing::warn;
use crate::constants::{CAT_FUN, CAT_GEN, CAT_INF, CAT_MOD, VERSION};
use crate::constants::CAT_DEV;
use crate::constants::CAT_FUN;
use crate::constants::CAT_GEN;
use crate::constants::CAT_INF;
use crate::constants::CAT_MOD;
use crate::constants::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 => "<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) => string2.to_string_lossy(),
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(),
Value::Buffer(buffer) => format!("{buffer:?}"),
// TODO figure out a better way to figure out what the reference is
// or just leave it as is because if youre converting a generic value ref to a string, what else were you expecting to happen
Value::Other(value_ref) => format!("{value_ref:?}"),
}
}
pub fn initialize() -> Lua {
// let mut runtimes = vec![];
// for i in 0..1 {
pub fn initialize() -> Result<Lua> {
// 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
@ -15,47 +60,45 @@ pub fn initialize() -> Lua {
// 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");
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 limit = 2 * 1024 * 1024;
info!("Setting memory limit to `{limit}` The previous limit was: `{}`", lua.set_memory_limit(limit)?);
// TODO make a mapping file that is processed at compile time that dictates what gets mapped where inside the lua engines
// This can be used to easily modify what is present in the sandbox and where it is/how to use it
// Once this mapping file is stable make a plugin or editor that can use the mapping file and associated type information
// To give syntax highlighting and autofill information, as well as documentation
let globals = lua.globals();
globals.set("NP_VERSION", VERSION).unwrap();
globals.set("NP_VERSION", VERSION)?;
clean_global(
#[cfg(not(debug_assertions))]
&lua,
&globals,
);
// TODO asserts should be handled differently
// 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();
//}
clean_global(&globals);
prepare_global(&lua, &globals);
set_logging(&lua, &globals);
// runtimes.push(lua);
// }
drop(globals);
lua
Ok(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();
}
/// Remove definitions that either A: should not be present in a sandbox, or B: we supply an alternative for to better suit the program
fn clean_global(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
@ -64,97 +107,154 @@ fn clean_global(#[cfg(not(debug_assertions))] lua: &Lua, globals: &Table) {
globals.raw_remove("print").unwrap();
}
/// Fill global with our new definitions meant to either extend functionality or suppliment things we have removed
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();
globals.set("sys", table).unwrap();
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", sys_table).unwrap();
}
fn set_logging(lua: &Lua, globals: &Table) {
let log = lua
.create_function(|_lua, strings: Variadic<String>| {
// i wish i could loop these but i cant instantiate macros
// TODO maybe i can wrap each log tool in a function and use function pointers
let log_table = lua.create_table().unwrap();
let trace = lua
.create_function(|_lua, strings: Variadic<Value>| {
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<Value>| {
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<Value>| {
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<Value>| {
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<Value>| {
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::ExternalError;
use mlua::Lua;
use mlua::Value;
use mlua::Variadic;
use mlua::prelude::LuaResult;
use tracing::info;
pub fn executeCommand(_lua: &Lua, arg_values: Variadic<Value>) -> Result<()> {
let mut command_name = String::new();
let mut command_args = vec![];
use crate::lua::value_to_string;
for (idx, value) in arg_values.iter().enumerate() {
match value {
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());
}
};
}
info!(
"Running lua command `{:?}` with args `{:?}`",
command_name, command_args
);
Ok(())
}
pub fn registerCommand(_lua: &Lua, arg_values: Variadic<Value>) -> Result<()> {
let table = match arg_values.get(0) {
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(),
);
}
pub fn execute_command(_lua: &Lua, mut arg_values: Variadic<Value>) -> LuaResult<()> {
let Some(command_name) = arg_values.first().cloned() else {
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 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::Table(_) => return Err("Direct run commands are not supported yet.".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 `{:?}`", value_to_string(command_name), command_args);
Ok(())
}
pub fn register_command(_lua: &Lua, arg_values: Variadic<Value>) -> LuaResult<()> {
let table = match arg_values.first() {
Some(Value::Table(table)) => table,
Some(Value::Nil) | None => return Err("Nil argument provided! registerCommand accepts no nil arguments".into_lua_err()),
_ => {
return Err("Invalid type used! Only `Table` is supported by registerCommand".into_lua_err());
},
};
info!("{:?}", table);
Ok(())
}
pub fn lua_value_to_string(_lua: &Lua, mut arg_values: Variadic<Value>) -> LuaResult<String> {
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 {

View file

@ -1,130 +1,487 @@
#![deny(clippy::unwrap_used)]
#![deny(clippy::expect_used)]
#![deny(clippy::pedantic)]
#![allow(clippy::unreadable_literal)]
#![allow(clippy::needless_pass_by_value)]
#![allow(clippy::unused_async)]
mod arguments;
mod bot;
mod commands;
mod constants;
mod databases;
mod errors;
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::LazyLock;
use std::sync::OnceLock;
use std::sync::mpsc::Sender;
use commands::{Command, CommandFnKind, COMMANDS};
use constants::CAT_HID;
use metadata::LevelFilter;
use serenity::all::{Context, GuildId};
use serenity::model::channel::Message;
use tracing::*;
use tracing::debug;
use tracing::error;
use tracing::info;
use tracing::metadata::LevelFilter;
use tracing::trace;
use tracing::warn;
pub static NO_DISCORD: OnceLock<bool> = OnceLock::new();
use constants::HELP_STRING;
// 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
use databases::DBKind;
#[tokio::main]
async fn main() {
tracing::subscriber::set_global_default(
tracing_subscriber::fmt()
.with_max_level(LevelFilter::INFO)
.finish(),
)
.unwrap();
info!("Logging initialized.");
// TODO remove this from static memory
/// In most cases this would just be a once lock or similar
/// however i dont like needing to unwrap it everywhere in the program after i know for a fact it was already initialized
/// at the start of execution and will never be written to again
///
/// So we just force the once lock when we want to load the args like we would for a once lock
pub static PROPERTIES: LazyLock<ProgramProperties> = LazyLock::new(get_cmd_args);
info!("Grabbing commandline input.");
let mut nodiscord = false;
for i in std::env::args() {
match i.as_str() {
"nodiscord" => nodiscord = true,
_ => {}
/// A simple no questions asked way to shut down the bot gracefully
// maybe we should include a reason enum that includes other info? no that should be handled wherever the message is sent from
// TODO custom panic handler?
pub static SHUTDOWN_SENDER: OnceLock<Sender<()>> = OnceLock::new();
fn string_to_bool(str: String) -> bool {
str.to_lowercase().trim() == "true"
}
fn string_to_log_level(str: String) -> Option<LevelFilter> {
match str.to_lowercase().trim() {
"trace" => Some(tracing_subscriber::filter::LevelFilter::TRACE),
"debug" => Some(tracing_subscriber::filter::LevelFilter::DEBUG),
"info" => Some(tracing_subscriber::filter::LevelFilter::INFO),
"warn" => Some(tracing_subscriber::filter::LevelFilter::WARN),
"error" => Some(tracing_subscriber::filter::LevelFilter::ERROR),
_ => {
error!("Invalid log level! {str} Choose one of `trace` `debug` `info` `warn` `error`");
None
},
}
}
fn string_to_db_kind(str: String) -> Option<DBKind> {
match str.to_lowercase().trim() {
"access" => Some(DBKind::Access),
"postgresql" => Some(DBKind::PostgreSQL),
"sqlite" => Some(DBKind::SQLite),
"mariadb" => Some(DBKind::MariaDB),
_ => {
error!("Invalid database kind! {str} Choose one of `sqlite` `mariadb` `postgresql` `access`");
None
},
}
}
/// ### This macro has a "header" of sorts which is used to configure the macro
/// The variables will also be initialized with the system enviroment values, so try not to use unwrap or expect, use a sane default value
/// ---
/// This is the first line of an example usage
/// ```
/// #[derive(Debug, Clone)] pub struct ProgramProperties;
/// ```
/// `#[]` is required and you may put `,` seperated attributes inside
///
/// `pub` is omittable but will be the visibility of the generated struct
///
/// `struct` is required for readability sake
///
/// `ProgramProperties` is any identifier you want and will be used to name the generated structure
///
/// ---
/// The second line is identical in all ways to the first line, however instead of creating a struct we create a function
/// ```
/// #[] pub fn get_cmd_args;
/// ```
///
/// ---
/// These two lines are used to name variables used later in the macro, the selected names being `item` and `arglist`
/// ```
/// envfixer item;
/// argumentfixer arglist;
/// ```
///
/// ---
/// Just like with the fn and struct you may change the visibility before the items name
///
/// The type within the structure is defined afterwards just like a normal struct
///
/// However the next item must be `,` and afterwards we shift to assigning data
///
/// `optnames`: is an 'array' of &str values which will be transparently used in a match statement when reading `env::args`
///
/// `envfixer`: is a 'datafixer' used to map the data to the correct type, optionally using the variable name defined in the header
///
/// `argumentfixer`: is identical to `envfixer` but used for the `env::args` input, in this case all we want is whether the option is present or not
/// ```
/// pub print_help: bool,
/// optnames: ["h", "help","-h", "--help", "-help","/h", "/help","/?", "?"],
/// envfixer: item.is_some_and(|x| x.to_lowercase().trim() == "true"),
/// argumentfixer: true;
/// pub nodiscord: bool,
/// optnames: [],
/// envfixer: item.is_some_and(string_to_bool),
/// argumentfixer: true;
/// pub dbuser : Option<String>,
/// optnames: [],
/// envfixer: item,
/// argumentfixer: arglist.next();
/// pub dbpass : Option<String>,
/// optnames: [],
/// envfixer: item,
/// argumentfixer: arglist.next();
/// pub dbaddress : Option<String>,
/// optnames: [],
/// envfixer: item,
/// argumentfixer: arglist.next();
/// pub dbport: Option<String>,
/// optnames: [],
/// envfixer: item,
/// argumentfixer: arglist.next();
/// pub dbpath: Option<String>,
/// optnames: [],
/// envfixer: item,
/// argumentfixer: arglist.next();
/// pub dbkind: Option<String>,
/// optnames: [],
/// envfixer: item,
/// argumentfixer: arglist.next();
/// ```
///
/// This macro is licensed under [The Unlicense](https://spdx.org/licenses/Unlicense.html)
///
macro_rules! program_properties {
{
#[$($sattr:meta),*] $svis:vis struct $sident:ident;
#[$($fattr:meta),*] $fvis:vis fn $fident:ident;
envfixer $vname:ident;
argumentfixer $vargsname:ident;
$($itemvis:vis $itemident:ident: $itemtype:ty, optnames: [$($options:literal),*], envfixer: $envfixer:expr, argumentfixer: $argumentfixer:expr;)+
} => {
$(#[$sattr])* $svis struct $sident {
$($itemvis $itemident: $itemtype,)+
}
$(#[$fattr])* $fvis fn $fident() -> $sident {
debug!("Initializing with env variables.");
let mut resval = $sident {
$($itemident: {
trace!("Reading env: {}", stringify!($itemident));
#[allow(unused)]
let $vname = std::env::var(stringify!($itemident)).ok();
$envfixer
},)+
};
#[cfg(feature = "dot_env_file")] {
debug!("Grabbing .env file input.");
if let Ok(cdir) = std::env::current_dir() {
if let Ok(string) = std::fs::read_to_string(cdir.join(".env")) {
let mut strbuf = vec![];
let mut bucket = String::new();
for c in string.chars() {
if c == '\n' {
strbuf.push(bucket);
bucket = String::new();
} else {
bucket.push(c);
}
}
if !bucket.trim().is_empty() {
strbuf.push(bucket);
}
let mut eqvec = vec![];
for s in &strbuf {
if let Some(eq) = s.split_once('=') {
eqvec.push(eq);
} else {
warn!("Invalid value in .env file! {s}");
}
}
for (lhs,rhs) in eqvec {
match lhs.trim() {
$(stringify!($itemident) => {
trace!("Reading .env file: {}", stringify!($itemident));
#[allow(unused)]
let $vname = Some(rhs.to_string());
resval.$itemident = $envfixer;
},)+
value => {
warn!("Unknown or misplaced value: {value}={rhs}"); // TODO make custom tracing message formatter that moves newlines forward to match the message offset from header info
warn!("Use argument help for more information.");
},
}
}
}
};
}
debug!("Grabbing commandline options.");
let mut args = std::env::args().peekable();
// The first arg is the path to the executable on all systems that i know of
// But this is not always guaranteed, however this program does not expect file paths as the first element so we can safely skip it if the path exists
// We could check this to see if it matches our environment executable path but that needs too much error handling for something like this
if let Some(argpath) = args.peek() {
if std::path::PathBuf::from(argpath).exists() {
args.next();
}
}
while let Some(item) = args.next() {
match item.trim() {
$($($options)|* | stringify!($itemident) => {
trace!("Reading commandline options: {}", stringify!($itemident));
#[allow(unused)]
let $vargsname = &mut args;
resval.$itemident = $argumentfixer;
},)+
value => {
warn!("Unknown or misplaced value: {value}"); // TODO make custom tracing message formatter that moves newlines forward to match the message offset from header info
warn!("Use argument help for more information.");
},
}
};
resval
}
};
}
// TODO basic type/validity/enum checking via helper functions
program_properties! {
#[derive(Debug, Clone)] pub struct ProgramProperties;
#[] pub fn get_cmd_args;
envfixer item;
argumentfixer arglist;
pub print_help: bool,
optnames: ["h", "help","-h", "--help", "-help","/h", "/help","/?", "?"],
envfixer: item.is_some_and(string_to_bool),
argumentfixer: true;
pub nodiscord: bool,
optnames: ["-N", "--no-discord"],
envfixer: item.is_some_and(string_to_bool),
argumentfixer: true;
pub dbuser : Option<String>,
optnames: ["-U", "--db-user"],
envfixer: item,
argumentfixer: arglist.next();
pub dbpass : Option<String>,
optnames: ["-P","--db-pass"],
envfixer: item,
argumentfixer: arglist.next();
pub dbaddress : Option<String>,
optnames: ["-A", "--db-address"],
envfixer: item,
argumentfixer: arglist.next();
pub dbport: Option<String>,
optnames: ["-P", "--db-port"],
envfixer: item,
argumentfixer: arglist.next();
pub dbpath: Option<String>,
optnames: ["-F","--db-path"],
envfixer: item,
argumentfixer: arglist.next();
pub dbkind: Option<DBKind>,
optnames: ["-D", "--db-kind"],
envfixer: item.and_then(string_to_db_kind),
argumentfixer: arglist.next().and_then(string_to_db_kind);
pub use_cli: bool,
optnames: ["-i", "--interactive"],
envfixer: item.is_some_and(string_to_bool),
argumentfixer: true;
pub use_gui: bool,
optnames: ["-g", "--gui"],
envfixer: item.is_some_and(string_to_bool),
argumentfixer: true;
pub lua_engine_count: Option<String>,
optnames: ["-l", "--lua-engine-count"],
envfixer: item,
argumentfixer: arglist.next();
pub log_level: Option<LevelFilter>,
optnames: ["-L", "--log-level"],
envfixer: item.and_then(string_to_log_level),
argumentfixer: arglist.next().and_then(string_to_log_level);
pub connect_remote: bool,
optnames: ["-C", "--remote-connection"],
envfixer: item.is_some_and(string_to_bool),
argumentfixer: true;
pub host_remote: bool,
optnames: ["-H", "--host-remote-connection"],
envfixer: item.is_some_and(string_to_bool),
argumentfixer: true;
pub remote_address: Option<String>,
optnames: ["-a", "--remote-address"],
envfixer: item,
argumentfixer: arglist.next();
pub remote_port: Option<String>,
optnames: ["-p", "--remote-port"],
envfixer: item,
argumentfixer: arglist.next();
pub remote_key: Option<String>,
optnames: ["-k", "--remote-key"],
envfixer: item,
argumentfixer: arglist.next();
pub remote_key_type: Option<String>,
optnames: ["-K", "--remote-key-type"],
envfixer: item,
argumentfixer: arglist.next();
}
#[allow(clippy::missing_panics_doc)]
pub fn shutdown_handler() {
static SHUTDOWN_COUNT: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(1);
if SHUTDOWN_COUNT.fetch_add(1, std::sync::atomic::Ordering::SeqCst) >= 3 {
warn!("3 exit events have piled up. Forcing exit.");
std::process::exit(-1);
}
#[allow(clippy::unwrap_used)] // it is impossible for this to be an error
if let Err(err) = SHUTDOWN_SENDER.get().unwrap().send(()) {
error!("Failed to send shutdown signal: {err}");
}
debug!("Shutdown requested!");
}
// TODO remove async from the main function and restrict its usage to just the discord bot
// We can manually manage worker threads everywhere else, especially with the lua engines
//#[tokio::main]
fn main() {
// why do we need to set global default again?
// TODO ansi color support
let (level_filter, level_filter_reload_handle) = tracing_subscriber::reload::Layer::new({
if let Ok(var) = std::env::var("log_level") {
let Some(var) = string_to_log_level(var) else {
eprintln!("CRITICAL: Could not read `log_level` system environment variable for initialization phase!");
eprintln!("CRITICAL: The value must be one of `trace` `debug` `info` `warn` `error` or it must not be present!");
exit(-1);
};
var
} else if cfg!(debug_assertions) {
LevelFilter::DEBUG
} else {
LevelFilter::INFO
}
});
{
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
tracing_subscriber::registry()
.with(level_filter)
.with(
tracing_subscriber::fmt::Layer::default()
.compact()
.with_level(true)
.with_file(cfg!(debug_assertions))
.with_line_number(cfg!(debug_assertions))
// TODO make configurable
.with_timer(tracing_subscriber::fmt::time::uptime())
// TODO detect ansi capabilities do something to allow colors if none supported, if fails just disable colors
.with_ansi(true)
// TODO allow in release?
.with_thread_names(cfg!(debug_assertions)),
)
.init();
}
// TODO set logging output to a file via a reload handle like above, store logged data and flush to file when file is registered and opened for writing as to not lose any data
// Manually init the lazy lock here rather than later for reliability sake
LazyLock::force(&PROPERTIES);
#[cfg(debug_assertions)]
debug!("{:?}", *PROPERTIES);
// TODO make this a global function
if let Some(nval) = PROPERTIES.log_level {
if let Some(cval) = level_filter_reload_handle.clone_current() {
if cval != nval {
trace!("Switching from log level {cval} to {nval}");
level_filter_reload_handle.modify(|filter| *filter = nval).unwrap();
}
}
}
NO_DISCORD.set(nodiscord).unwrap();
// let mut commands = HashMap::default();
let (shutdown_tx, shutdown_rx) = std::sync::mpsc::channel();
#[allow(clippy::unwrap_used)] // it is impossible for this to be an error
SHUTDOWN_SENDER.set(shutdown_tx.clone()).unwrap();
#[allow(clippy::unwrap_used)] // it is impossible for this to be an error
tasks::SHUTDOWN_RECEIVER.set(tokio::sync::Mutex::new(shutdown_rx)).unwrap();
if let Err(err) = ctrlc::set_handler(shutdown_handler) {
error!("Error setting Ctrl-C handler: {err}");
}
if PROPERTIES.print_help {
info!("{HELP_STRING}");
exit(0);
}
let Some(dbkind) = PROPERTIES.dbkind else {
warn!("You must select a database kind with `dbkind`");
exit(-1);
};
info!("Loading database accessor.");
if let Err(err) = match dbkind {
DBKind::Access => databases::access::create_interface(),
DBKind::MariaDB => databases::mariadb::create_interface(),
DBKind::PostgreSQL => databases::postgres::create_interface(),
DBKind::SQLite => databases::sqlite::create_interface(),
} {
error!("Could not start database {dbkind:?} with error: {err}");
exit(-1);
}
// TODO if the db is new fill it with data
// TODO if the db health check fails or is old attempt backup and upgrade
info!("Loading stock commands.");
insert_stock().await;
commands::insert_stock();
info!("Loading rust dynamic commands.");
insert_rust().await;
commands::insert_rust();
info!("Initializing Lua runtime.");
let lua = lua::initialize(); // may not need to be mutable, but its fine for now
// TODO lua mpmc with N lua engines, use the fire and forget strategy, lua engines should handle their own errors! and using a custom panic hook they should be replaced on death!
lua.load(include_str!("../lua/loader.lua"))
.set_name("lua init script")
.exec()
.unwrap();
// TODO lua::init_lua();
//info!("Loading lua commandlets.");
//insert_lua(&lua);
info!("Loading lua commandlets.");
insert_lua(&lua).await;
// old
lua.sandbox(true).unwrap();
bot::start().await;
}
//info!("Initializing Lua runtime.");
//let lua = match lua::initialize() {
// Ok(lua) => lua,
// Err(err) => {
// error!("Encountered an error while initializing lua! `{err}`");
// exit(1);
// },
//};
/// The last set of commands, these are used by the hoster of the engine.
pub async fn insert_lua(_lua: &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()),
// );
}
//if let Err(err) = lua.load(include_str!("../lua/loader.lua")).set_name("loader.lua").exec() {
// error!("Encountered an error while running loader.lua: {err}");
//}
/// Cannot use any command names of stock commands, but gets to pick before lua commands are loaded.
pub async fn insert_rust() {
// 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()),
// );
}
//info!("Loading lua commandlets.");
//insert_lua(&lua);
/// Will never fail, gets first pick of command names and properties. It is up to the maintainer to make damn sure this works.
pub async fn insert_stock() {
COMMANDS.write().await.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()),
);
}
//if let Err(err) = lua.sandbox(true) {
// error!("Encountered an error while setting the lua runtime to sandbox mode! `{err}`");
// exit(1);
//}
fn stop_command(_: Context, msg: Message, _: Option<GuildId>) {
// hardcode my id for now
if msg.author.id != 380045419381784576 {
return;
// Spawn new thread for cli, if no terminal is detected exit program
if PROPERTIES.use_cli {
// TODO create cli interface for bot
}
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 {
bot::SHARD_MANAGER.get().unwrap().shutdown_all().await;
});
bot::start();
}

View file

@ -1,17 +1,31 @@
use crate::{bot::SHARD_MANAGER, constants};
use std::{
sync::{Arc, LazyLock},
time::Duration,
};
use std::collections::hash_map::HashMap;
use std::sync::Arc;
use std::sync::LazyLock;
use std::sync::OnceLock;
use std::time::Duration;
use ::std::collections::hash_map::HashMap;
use rand::{distributions::uniform::SampleRange, rngs::OsRng};
use serenity::all::{ActivityData, Cache, Context, ShardId, ShardRunnerInfo};
use tokio::{sync::Mutex, task::JoinHandle};
use rand::Rng;
use rand::SeedableRng;
use serenity::all::ActivityData;
use serenity::all::Cache;
use serenity::all::Context;
use serenity::all::ShardId;
use serenity::all::ShardRunnerInfo;
use tokio::sync::Mutex;
use tokio::task::JoinHandle;
use tracing::error;
use tracing::info;
use tracing::warn;
static TASK_HANDLES: LazyLock<Arc<Mutex<TaskContainer>>> =
LazyLock::new(|| Arc::new(Mutex::new(TaskContainer::default())));
use crate::bot::SHARD_MANAGER;
use crate::constants;
// TODO remove SHUTDOWN_RECEIVER from static memory
pub static SHUTDOWN_RECEIVER: OnceLock<Mutex<std::sync::mpsc::Receiver<()>>> = OnceLock::new();
static TASK_HANDLES: LazyLock<Arc<Mutex<TaskContainer>>> = LazyLock::new(|| Arc::new(Mutex::new(TaskContainer::default())));
#[derive(Debug, Default)]
pub struct TaskContainer {
@ -21,24 +35,67 @@ pub struct TaskContainer {
pub async fn start_tasks(ctx: Context) {
info!("Starting MISC tasks.");
info!("Starting activity switcher task.");
info!("Starting activity switcher task...");
let Some(sharts) = SHARD_MANAGER.get() else {
error!("Could not obtain the shard manager.");
return;
};
TASK_HANDLES
.lock()
.await
.misc
.push(tokio::spawn(status_timer(
SHARD_MANAGER.get().unwrap().runners.clone(),
ctx.cache,
)));
.push(tokio::spawn(status_timer(sharts.runners.clone(), ctx.cache)));
info!("Starting forum task...");
TASK_HANDLES.lock().await.misc.push(tokio::spawn(forum_checker()));
info!("Starting shutdown monitor task...");
TASK_HANDLES.lock().await.misc.push(tokio::spawn(shutdown_monitor()));
info!("MISC tasks started.");
warn!("Task health checker is currently unimplemented!");
warn!("If a task panics we will not be able to restart it or fix any issues!");
}
pub async fn status_timer(
shard_runners: Arc<Mutex<HashMap<ShardId, ShardRunnerInfo>>>,
cache: Arc<Cache>,
) {
let mut rand = OsRng::default();
async fn forum_checker() {
let mut interval = tokio::time::interval(Duration::from_millis(16));
loop {
//check_forum().await;
interval.tick().await;
}
}
// check forums listed in DB, grab all messages, rank based on emotes, mark old/completed posts
pub async fn check_forum() {}
async fn shutdown_monitor() {
// TODO tokio interval waiting may be more efficient than just a recv on an std mpsc
// tokio mpsc may be better than both as the std mpsc may halt the tokio task
// without letting it yeild properly
//let mut interval = tokio::time::interval(Duration::from_millis(16));
#[allow(clippy::unwrap_used)] // it is impossible for this to be an error
let lock = SHUTDOWN_RECEIVER.get().unwrap().lock().await;
//loop {
if lock.recv().is_ok() {
warn!("The bot is shutting down now!");
crate::bot::DO_SHUTDOWN.store(true, std::sync::atomic::Ordering::SeqCst);
// TODO wait till all existing actions are completed or timeout
// impossible to fail
#[allow(clippy::unwrap_used)]
crate::bot::SHARD_MANAGER.get().unwrap().shutdown_all().await;
//break;
}
//interval.tick().await;
//}
}
async fn status_timer(shard_runners: Arc<Mutex<HashMap<ShardId, ShardRunnerInfo>>>, cache: Arc<Cache>) {
// TODO make this a vec and able to be updated from external sources like lua engines, maybe static
let activities = [
ActivityData::watching("my developer eat a watermelon whole."),
ActivityData::watching(format!(
@ -50,13 +107,12 @@ pub async fn status_timer(
ActivityData::listening("Infected Mushroom"),
];
let mut interval = tokio::time::interval(Duration::from_secs(20 * 60));
let mut rand = rand::rngs::SmallRng::from_os_rng();
loop {
let activity = SampleRange::sample_single(0..activities.len() - 1, &mut rand);
let activity = rand.random_range(0..activities.len() - 1);
for (_shard_id, shard_info) in shard_runners.lock().await.iter() {
shard_info
.runner_tx
.set_activity(Some(activities[activity].clone()));
shard_info.runner_tx.set_activity(Some(activities[activity].clone()));
}
interval.tick().await;