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
This commit is contained in:
deepCurse 2025-02-18 16:46:25 -04:00
parent 991feb82a1
commit 578918b22f
Signed by: u1
GPG key ID: AD770D25A908AFF4
18 changed files with 1564 additions and 644 deletions

1027
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -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"
mlua = { version = "0.10.3", features = ["async", "luau"] }
serenity = "0.12.4"
num_cpus = "1.16.0"
rand = "0.8.5"
rand = "0.9.0"
regex = "1.10.4"
sqlite = { version = "0.36.1", features = ["bundled"] }
ctrlc = "3.4.5"
[features]

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

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,14 +1,13 @@
use std::{collections::HashMap, sync::Arc};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Default)]
pub struct ArgumentStorage(HashMap<String, String>);
impl ArgumentStorage {
pub fn new() -> Self {
Self::default()
}
pub fn new() -> Self { Self::default() }
pub fn add_argument() {}
pub fn add_argument() {
}
}
// #[derive(Debug, Clone, PartialEq, Default)]
@ -84,4 +83,3 @@ impl ArgumentStorage {
// Value(String),
// Argument(usize),
// }

View file

@ -1,18 +1,34 @@
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Condvar, OnceLock};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use std::sync::OnceLock;
use crate::commands::{self, COMMANDS};
use crate::{constants, tasks, CARG_NO_DISCORD};
use serenity::all::{ActivityData, Context, GuildId};
use serenity::all::{EventHandler, GatewayIntents, ShardManager};
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::{error, info, warn};
pub static DO_SHUTDOWN: (AtomicBool, Condvar) = (AtomicBool::new(false), Condvar::new()); // atombool and condvar combo to ensure maximum coverage when the bot needs to power off
use tracing::error;
use tracing::info;
use tracing::warn;
use crate::commands::COMMANDS;
use crate::commands::{self};
use crate::constants;
use crate::tasks;
use crate::CMD_ARGS;
pub static DO_SHUTDOWN: AtomicBool = AtomicBool::new(false);
// TODO remove from static memory
pub static SHARD_MANAGER: OnceLock<Arc<ShardManager>> = OnceLock::new();
// cant remember why this was here, it isnt used anywhere else so ill just remove it for now
//pub static BOT_STARTED: AtomicBool = AtomicBool::new(false);
pub struct BotHandler;
unsafe impl Sync for BotHandler {}
@ -26,65 +42,59 @@ impl EventHandler for BotHandler {
return;
}
// TODO remove regular expressions
let prefix_regex = format!(
r"^({}|{}|<@{}>)\s?",
constants::COMMAND_PREFIX,
"NPO_PFX",
constants::CONSTANT_PREFIX,
ctx.cache.current_user().id
);
let cmd_regex = format!(r"{}[A-Za-z0-9_\-]+", prefix_regex);
let cmd_regex = format!(r"{prefix_regex}[A-Za-z0-9_\-]+");
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
let Ok(result) = regex::Regex::new(cmd_regex.as_str()) else {
error!("The following regex function has failed to compile, this should not be possible. `{prefix_regex}`");
return;
};
if DO_SHUTDOWN.0.load(Ordering::SeqCst) {
let Some(result) = result.find(&msg.content) else {
return; // message not meant for us :(
};
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 Ok(target_cmd_name) = regex::Regex::new(prefix_regex.as_str()) else {
error!("The following regex function has failed to compile, this should not be possible. `{prefix_regex}`");
return;
};
let target_cmd_name = dbg!(target_cmd_name.replace(result.as_str(), "").to_string());
msg.reply(&ctx.http, target_cmd_name.to_string())
.await
.unwrap();
let _ = msg.reply(&ctx.http, target_cmd_name.to_string()).await;
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),
}
}
if let Some(command) = dbg!(COMMANDS.read().await).get(&target_cmd_name) {
match (msg.guild_id, &command.run_guild_command, &command.run_dm_command) {
//(Some(gid), Some(commands::CommandFnKind::Lua(())), _) => todo!(),
(Some(gid), Some(commands::CommandFnKind::Rust(gcmd)), _) => (gcmd)(ctx, msg, Some(gid)),
//(None, _, Some(commands::CommandFnKind::Lua(()))) => todo!(),
(None, _, Some(commands::CommandFnKind::Rust(dcmd))) => (dcmd)(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.")))
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
@ -122,18 +132,18 @@ pub async fn start() {
| GatewayIntents::GUILD_MESSAGES
| GatewayIntents::MESSAGE_CONTENT;
if *CARG_NO_DISCORD.get().unwrap() {
if CMD_ARGS.nodiscord {
warn!("ABORTING CONNECTING TO DISCORD, BYE BYE!");
} else {
let mut client = match Client::builder(&token, intents)
.event_handler(BotHandler)
.await
{
let mut client = match Client::builder(token, intents).event_handler(BotHandler).await {
Ok(client) => client,
Err(err) => panic!("Error starting client connection: `{err}`"),
};
SHARD_MANAGER.set(client.shard_manager.clone()).unwrap();
// just ignore the response, its impossible to fail here anyway
let _ = SHARD_MANAGER.set(client.shard_manager.clone());
//BOT_STARTED.store(true, std::sync::atomic::Ordering::SeqCst);
if let Err(why) = client.start_shards(2).await {
error!("Client error: {why:?}");
@ -142,4 +152,3 @@ pub async fn start() {
warn!("BOT EXITING");
}

View file

@ -1,19 +1,28 @@
use std::{collections::HashMap, fmt::Debug, sync::LazyLock, time::Duration};
use std::collections::HashMap;
use std::fmt::Debug;
use std::sync::LazyLock;
use std::time::Duration;
use serenity::all::Context;
use serenity::all::GuildId;
use serenity::all::Message;
use serenity::all::Permissions;
use serenity::all::{Context, GuildId, Message, Permissions};
use tokio::sync::RwLock;
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 +35,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 +43,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 +63,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,22 +87,27 @@ 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
@ -100,38 +118,38 @@ impl Command {
}
}
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 {
@ -139,3 +157,55 @@ impl Debug for Command {
// }
// }
/// 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 async 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();
};
COMMANDS.write().await.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,60 @@
#![allow(unused)]
pub const VERSION: u16 = 0;
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";
// 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 the fancy version
pub const BOT_NAME: &str = "nopalmo";
pub const BOT_NAME_FANCY: &str = "Nopalmo";
pub const COMMAND_PREFIX: &'static str = ";";
// pub const SHORT_ARGUMENT_PREFIX: &'static str = "-";
// pub const LONG_ARGUMENT_PREFIX: &'static str = "--";
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...
Arguments:
nodiscord Skips connecting to discord
Eventually this will connect to a simulated discord instance
dbuser The username to be used when connecting to a database type that requires one
dbpass The password used like dbuser
dbaddress The network address of the desired database
dbport The port to use with dbaddress
dbpath The path to a unix socket to substitute an address or the path to an SQLite database file
dbkind The kind of database we will be using
use_cli Enables a CLI management interface for the bot
use_gui The GUI version of the CLI
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
connect_remote Connect to a remote instance of the bot
host_remote Host an instance of the bot for remote connections
remote_address The remote address of the bot, or the address you wish to host the bot on
remote_port The port used like remote_address
remote_key The path or literal value of the key required to authenticate with the bot on a remote connection
remote_key_type Decides the key type for remote connections
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
";

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);
}

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

@ -0,0 +1,60 @@
use std::{
fmt::Debug,
panic::Location,
sync::{Arc, OnceLock},
};
use serenity::all::UserId;
use crate::errors::DatabaseStoredError;
pub mod access;
pub mod mariadb;
pub mod postgres;
pub mod sqlite;
#[derive(Debug)]
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,
sync::{Arc, Mutex},
};
use sqlite::Connection;
use tracing::{error, info};
use crate::{errors::DatabaseStoredError, CMD_ARGS};
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) = CMD_ARGS.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())
}
}

40
src/errors.rs Normal file
View file

@ -0,0 +1,40 @@
use std::{
fmt::Display,
panic::Location,
time::{SystemTime, 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,7 +1,25 @@
use mlua::{Lua, LuaOptions, Result, StdLib, Table, Value, Variadic};
use tracing::{debug, error, info, trace, warn};
// FIXME remove later, used for now while in active development
#![allow(clippy::unwrap_used)]
use crate::constants::{CAT_DEV, CAT_FUN, CAT_GEN, CAT_INF, CAT_MOD, VERSION};
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_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 {
@ -12,22 +30,25 @@ pub fn value_to_string(value: Value) -> String {
Value::Integer(int) => int.to_string(),
Value::Number(num) => num.to_string(),
Value::Vector(vec) => vec.to_string(),
Value::String(string2) => String::from_utf8_lossy(string2.as_bytes()).to_string(),
Value::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()),
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:?}"),
}
}
@ -39,40 +60,41 @@ pub fn initialize() -> Result<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()),
LuaOptions::new().catch_rust_panics(true).thread_pool_size(num_cpus::get()),
)?;
// .expect("Lua could not initialize properly");
// set max to 2mb
info!("Previous memory limit `{}`", lua.set_memory_limit(2 * 1024 * 1024)?);
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)?;
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);
drop(globals);
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();
// 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)
@ -85,6 +107,7 @@ 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 command_category_table = lua.create_table().unwrap();
command_category_table.set("dev", CAT_DEV).unwrap();
@ -92,24 +115,26 @@ fn prepare_global(lua: &Lua, globals: &Table) {
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();
globals.set("commandCategory", command_category_table).unwrap();
let sys_table = lua.create_table().unwrap();
#[allow(clippy::deprecated_cfg_attr)] // shut up clippy thats unstable
#[cfg_attr(rustfmt, rustfmt_skip)] // wish this was comment based but whatever
{
sys_table.set("registerCommand", lua.create_function(rust_lua_functions::register_command).unwrap()).unwrap();
sys_table.set("executeCommand", lua.create_function(rust_lua_functions::execute_command).unwrap()).unwrap();
sys_table.set("luaValueToString", lua.create_function(rust_lua_functions::lua_value_to_string).unwrap()).unwrap();
}
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) {
// 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>| {
@ -181,15 +206,17 @@ fn set_logging(lua: &Lua, globals: &Table) {
mod rust_lua_functions {
use mlua::prelude::LuaResult;
use mlua::{ExternalError, Lua, Value, Variadic};
use mlua::ExternalError;
use mlua::Lua;
use mlua::Value;
use mlua::Variadic;
use tracing::info;
use crate::lua::value_to_string;
pub fn execute_command(_lua: &Lua, mut arg_values: Variadic<Value>) -> LuaResult<()> {
let command_name = match arg_values.get(0).cloned() {
Some(name) => name,
None => return Err("executeCommand requires a command name to execute! please supply a valid command name as the first argument.".into_lua_err()),
let 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![];
@ -198,45 +225,23 @@ mod rust_lua_functions {
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(),
),
},
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
);
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.get(0) {
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(),
)
}
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(),
);
}
return Err("Invalid type used! Only `Table` is supported by registerCommand".into_lua_err());
},
};
info!("{:?}", table);
@ -353,4 +358,3 @@ mod rust_lua_functions {
// }
// }
// }

View file

@ -3,148 +3,247 @@
#![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::process::exit;
use std::sync::atomic::Ordering;
use std::sync::mpsc::Sender;
use std::sync::LazyLock;
use std::sync::OnceLock;
use serenity::all::{Context, GuildId};
use serenity::model::channel::Message;
use tracing::debug;
use tracing::error;
use tracing::info;
use tracing::metadata::LevelFilter;
use tracing::warn;
use tracing::{error, info};
use constants::HELP_STRING;
use commands::{Command, CommandFnKind, COMMANDS};
use constants::CAT_HID;
use databases::DBKind;
pub static CARG_NO_DISCORD: OnceLock<bool> = OnceLock::new();
#[derive(Debug, Clone)]
pub struct CmdArgs {
// TODO bool_bucket: u8,
pub nodiscord: bool,
pub dbuser: Option<String>,
pub dbpass: Option<String>,
pub dbaddress: Option<String>,
pub dbport: Option<String>,
pub dbpath: Option<String>,
pub dbkind: Option<String>,
pub use_cli: bool,
pub lua_engine_count: Option<String>,
pub connect_remote: bool,
pub remote_address: Option<String>,
pub remote_port: Option<String>,
pub remote_key: Option<String>,
pub remote_key_type: Option<String>,
}
#[tokio::main]
async fn main() {
if let Err(err) = tracing::subscriber::set_global_default(
tracing_subscriber::fmt()
.with_max_level(LevelFilter::INFO)
.finish(),
) {
eprintln!("Encountered an error while initializing logging! `{err}`");
exit(1);
};
info!("Logging initialized.");
/// 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 CMD_ARGS: LazyLock<CmdArgs> = LazyLock::new(get_cmd_args);
/// 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();
#[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}");
}
}
pub fn get_cmd_args() -> CmdArgs {
info!("Grabbing commandline input.");
let mut nodiscord = false;
for i in std::env::args() {
#[allow(clippy::single_match)] // here for convienience when adding additional flags
match i.as_str() {
"nodiscord" => nodiscord = true,
_ => {}
let mut dbuser = None;
let mut dbpass = None;
let mut dbaddress = None;
let mut dbport = None;
let mut dbpath = None;
let mut dbkind = None;
let mut use_cli = false;
let mut lua_engine_count = None;
let mut connect_remote = false;
let mut remote_address = None;
let mut remote_port = None;
let mut remote_key = None;
let mut remote_key_type = None;
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();
}
}
if let Err(err) = CARG_NO_DISCORD.set(nodiscord) {
error!("Encountered an error while setting the `nodiscord` flag! `{err}`");
exit(1);
while let Some(item) = args.next() {
match item.to_lowercase().trim() {
// Execution args
"help" | "-h" | "--help" | "-help" | "/help" | "/h" | "?" | "/?" => {
info!("{HELP_STRING}");
exit(0);
},
// Data args
"nodiscord" => nodiscord = true,
"dbuser" => dbuser = args.next(),
"dbpass" => dbpass = args.next(),
"dbaddress" => dbaddress = args.next(),
"dbport" => dbport = args.next(),
"dbpath" => dbpath = args.next(),
"dbkind" => dbkind = args.next(),
"use_cli" => use_cli = true,
"lua_engine_count" => lua_engine_count = args.next(),
"connect_remote" => connect_remote = true,
"remote_address" => remote_address = args.next(),
"remote_port" => remote_port = args.next(),
"remote_key" => remote_key = args.next(),
"remote_key_type" => remote_key_type = args.next(),
value => warn!("Unknown or misplaced value: {value}"), // TODO move help argument here?
}
}
CmdArgs {
nodiscord,
dbuser,
dbpass,
dbaddress,
dbport,
dbpath,
dbkind,
use_cli,
lua_engine_count,
connect_remote,
remote_address,
remote_port,
remote_key,
remote_key_type,
}
}
// 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]
async fn main() {
// why do we need to set global default again?
tracing_subscriber::fmt()
.compact()
.with_timer(tracing_subscriber::fmt::time::uptime())
.with_ansi(true)
.with_level(true)
.with_file(cfg!(debug_assertions))
.with_line_number(cfg!(debug_assertions))
.with_thread_names(cfg!(debug_assertions))
.with_max_level(if cfg!(debug_assertions) { LevelFilter::DEBUG } else { LevelFilter::INFO })
.init();
info!("Logging initialized.");
// Manually init the lazy lock
LazyLock::force(&CMD_ARGS);
debug!("{:?}", *CMD_ARGS);
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}");
}
let dbkind = if let Some(arg) = &CMD_ARGS.dbkind {
info!("Loading database accessor.");
match arg.to_lowercase().trim() {
"access" => DBKind::Access,
"postgresql" => DBKind::PostgreSQL,
"sqlite" => DBKind::SQLite,
"mariadb" => DBKind::MariaDB,
_ => {
error!("Invalid database kind! Choose one of `sqlite` `mariadb` `postgresql` `access`");
exit(-1);
},
}
} else {
warn!("You must select a database kind with `dbkind`");
exit(-1);
};
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().await;
info!("Loading rust dynamic commands.");
insert_rust();
commands::insert_rust();
info!("Initializing Lua runtime.");
let lua = match lua::initialize() {
Ok(lua) => lua,
Err(err) => {
error!("Encountered an error while initializing lua! `{err}`");
exit(1);
},
}; // may not need to be mutable, but its fine for now
// 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!
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}");
};
// TODO lua::init_lua();
//info!("Loading lua commandlets.");
//insert_lua(&lua);
info!("Loading lua commandlets.");
insert_lua(&lua);
// old
if let Err(err) = lua.sandbox(true) {
error!("Encountered an error while setting the lua runtime to sandbox mode! `{err}`");
exit(1);
};
//info!("Initializing Lua runtime.");
//let lua = match lua::initialize() {
// Ok(lua) => lua,
// Err(err) => {
// error!("Encountered an error while initializing lua! `{err}`");
// exit(1);
// },
//};
//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}");
//}
//info!("Loading lua commandlets.");
//insert_lua(&lua);
//if let Err(err) = lua.sandbox(true) {
// error!("Encountered an error while setting the lua runtime to sandbox mode! `{err}`");
// exit(1);
//}
if CMD_ARGS.use_cli {
// TODO create cli interface for bot
}
bot::start().await;
}
/// 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.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 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()),
);
}
fn stop_command(_: Context, msg: Message, _: Option<GuildId>) {
// hardcode my id for now
if msg.author.id != 380045419381784576 {
return;
}
bot::DO_SHUTDOWN.0.store(true, Ordering::SeqCst);
bot::DO_SHUTDOWN.1.notify_all();
let handle = tokio::runtime::Handle::current();
let _eg = handle.enter();
handle.spawn(async {
// this is literally impossible to fail as the SHARD_MANAGER is required to exist for this function to ever run
#[allow(clippy::unwrap_used)]
bot::SHARD_MANAGER.get().unwrap().shutdown_all().await;
});
}

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,64 @@ 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 {
interval.tick().await;
}
}
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 +104,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;