diff --git a/.gitignore b/.gitignore index 0b745e2..7a09d50 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target -.env \ No newline at end of file +.env +/data \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 237e42e..f036646 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -618,6 +618,7 @@ dependencies = [ "dotenvy", "once_cell", "openssl", + "poise", "postgres-openssl", "rand", "regex", @@ -686,6 +687,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "bytecount" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" + [[package]] name = "bytemuck" version = "1.14.2" @@ -713,6 +720,37 @@ dependencies = [ "bytes", ] +[[package]] +name = "camino" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "694c8807f2ae16faecc43dc17d74b3eb042482789fd0eb64b39a2e04e087053f" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", +] + [[package]] name = "cc" version = "1.0.83" @@ -1042,6 +1080,17 @@ dependencies = [ "serde", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -1198,6 +1247,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "version_check", +] + [[package]] name = "event-listener" version = "2.5.3" @@ -1537,6 +1595,12 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "h2" version = "0.3.23" @@ -2032,6 +2096,21 @@ dependencies = [ "unicase", ] +[[package]] +name = "mini-moka" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c325dfab65f261f386debee8b0969da215b3fa0037e74c8a1234db7ba986d803" +dependencies = [ + "crossbeam-channel", + "crossbeam-utils", + "dashmap", + "skeptic", + "smallvec", + "tagptr", + "triomphe", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2485,6 +2564,35 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" +[[package]] +name = "poise" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1819d5a45e3590ef33754abce46432570c54a120798bdbf893112b4211fa09a6" +dependencies = [ + "async-trait", + "derivative", + "futures-util", + "parking_lot", + "poise_macros", + "regex", + "serenity", + "tokio", + "tracing", +] + +[[package]] +name = "poise_macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fa2c123c961e78315cd3deac7663177f12be4460f5440dbf62a7ed37b1effea" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "postgres-openssl" version = "0.5.0" @@ -2626,6 +2734,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "pulldown-cmark" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" +dependencies = [ + "bitflags 2.4.1", + "memchr", + "unicase", +] + [[package]] name = "quote" version = "1.0.35" @@ -3312,6 +3431,7 @@ dependencies = [ "base64", "bitflags 2.4.1", "bytes", + "chrono", "command_attr", "dashmap", "flate2", @@ -3331,6 +3451,7 @@ dependencies = [ "tokio-tungstenite", "tracing", "typemap_rev", + "typesize", "url", "uwl", ] @@ -3434,6 +3555,21 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "skeptic" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8" +dependencies = [ + "bytecount", + "cargo_metadata", + "error-chain", + "glob", + "pulldown-cmark", + "tempfile", + "walkdir", +] + [[package]] name = "slab" version = "0.4.9" @@ -3763,6 +3899,12 @@ dependencies = [ "libc", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "tap" version = "1.0.1" @@ -4076,6 +4218,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "triomphe" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3" + [[package]] name = "try-lock" version = "0.2.5" @@ -4114,6 +4262,35 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "typesize" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb704842c709bc76f63e99e704cb208beeccca2abbabd0d9aec02e48ca1cee0f" +dependencies = [ + "chrono", + "dashmap", + "hashbrown 0.14.3", + "mini-moka", + "parking_lot", + "secrecy", + "serde_json", + "time", + "typesize-derive", + "url", +] + +[[package]] +name = "typesize-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "905e88c2a4cc27686bd57e495121d451f027e441388a67f773be729ad4be1ea8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "ulid" version = "1.1.2" diff --git a/Cargo.toml b/Cargo.toml index 5dbc8b7..8984307 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ rand = "0.8.5" walkdir = "2.4.0" surrealdb = "1.1.1" once_cell = "1.19.0" +poise = "0.6.1" [[bin]] diff --git a/README.md b/README.md index 48e240d..f97a23c 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,10 @@ Main lib used : [serenity-rs/serenity](https://github.com/serenity-rs/serenity) - The bot has to be able to be deployed on a k8s cluster - The bot has to be OPT-IN (the user has to enable the bot on his server with a command) +## Important to do + +- Migrate to Trace + ## previous project - [TestDiscord](https://git.weebo.fr/sandbox/TestDiscord): Bot that check if a world match with one in the image warehouse and answer with a picture diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..4119eb3 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,19 @@ +services: + surrealdb: + image: surrealdb/surrealdb:latest + container_name: mongo + environment: + SURREAL_AUTH: true + SURREAL_USER: bot_root + SURREAL_PASS: bot_password_azertdsq + SURREAL_PATH: file:/appdata/bot.db + user: "1000:1000" + command: + - start + ports: + - "8000:8000" + volumes: + - db:/appdata + +volumes: + db: \ No newline at end of file diff --git a/devfile.yaml b/devfile.yaml index 1251b3e..5c05b0c 100644 --- a/devfile.yaml +++ b/devfile.yaml @@ -17,10 +17,8 @@ variables: components: - name: tools container: - image: harbor.weebo.fr/batleforc/che-rust:0.1.13 - memoryLimit: 8Gi - command: ["tail"] - args: ["-f", "/dev/null"] + image: harbor.weebo.fr/batleforc/che-rust:latest + memoryLimit: 10Gi mountSources: true endpoints: - name: 5437-http diff --git a/src/bot/handler.rs b/src/bot/handler.rs index cb84580..2ee0ddf 100644 --- a/src/bot/handler.rs +++ b/src/bot/handler.rs @@ -14,6 +14,7 @@ use crate::{ img::config_file::{ConfigImgGlobal, KeyWordItem}, }; + pub struct Handler; #[async_trait] diff --git a/src/botv2/cmd/help.rs b/src/botv2/cmd/help.rs new file mode 100644 index 0000000..909c3da --- /dev/null +++ b/src/botv2/cmd/help.rs @@ -0,0 +1,34 @@ +use poise::samples::HelpConfiguration; +use crate::botv2::init::{Context,Error}; + +/// Show help message +#[poise::command(prefix_command, track_edits, category = "Utility")] +pub async fn help( + ctx: Context<'_>, + #[description = "Command to get help for"] + #[rest] + mut command: Option, +) -> Result<(), Error> { + // This makes it possible to just make `help` a subcommand of any command + // `/fruit help` turns into `/help fruit` + // `/fruit help apple` turns into `/help fruit apple` + if ctx.invoked_command_name() != "help" { + command = match command { + Some(c) => Some(format!("{} {}", ctx.invoked_command_name(), c)), + None => Some(ctx.invoked_command_name().to_string()), + }; + } + let extra_text_at_bottom = "\ +Provided by Batleforc with love ❤️ and too much coffee ☕"; + + let config = HelpConfiguration { + show_subcommands: true, + show_context_menu_commands: true, + ephemeral: true, + extra_text_at_bottom, + + ..Default::default() + }; + poise::builtins::help(ctx, command.as_deref(), config).await?; + Ok(()) +} \ No newline at end of file diff --git a/src/botv2/cmd/mod.rs b/src/botv2/cmd/mod.rs new file mode 100644 index 0000000..05a84c5 --- /dev/null +++ b/src/botv2/cmd/mod.rs @@ -0,0 +1,2 @@ +pub mod ping; +pub mod help; \ No newline at end of file diff --git a/src/botv2/cmd/ping.rs b/src/botv2/cmd/ping.rs new file mode 100644 index 0000000..09fc94e --- /dev/null +++ b/src/botv2/cmd/ping.rs @@ -0,0 +1,11 @@ +use crate::botv2::init::{Context,Error}; + +/// Ping command +#[poise::command( + slash_command, + prefix_command, +)] +pub async fn ping(ctx: Context<'_>) -> Result<(), Error> { + ctx.say("Pong from framework!").await?; + Ok(()) +} \ No newline at end of file diff --git a/src/botv2/handler.rs b/src/botv2/handler.rs new file mode 100644 index 0000000..1660a8e --- /dev/null +++ b/src/botv2/handler.rs @@ -0,0 +1,110 @@ +use tokio::fs::File; +use poise::serenity_prelude as serenity; +use serenity::all::{CreateAttachment,CreateMessage}; +use rand::Rng; +use crate::{db::user_image::UserImage, img::config_file::KeyWordItem}; +use super::init::{Data, Error}; + +pub async fn event_handler( + ctx: &serenity::Context, + event: &serenity::FullEvent, + _framework: poise::FrameworkContext<'_, Data, Error>, + data: &Data, +) -> Result<(), Error> { + match event { + serenity::FullEvent::Ready { data_about_bot, .. } => { + println!("{} is connected !", data_about_bot.user.name); + } + serenity::FullEvent::Message { new_message } => { + if new_message.author.bot || new_message.content.starts_with(&data.config.prefix.clone()) || new_message.content.len() == 0 { + return Ok(()); + } + let config_img = data.config_img.clone(); + let config = data.config.clone(); + let guild = match new_message.guild_id { + Some(guild) => guild, + None => return Ok(()), + }; + let user_in_db = match UserImage::find_by_server_id_user_id(&guild.get(), &new_message.author.id.get()).await { + Ok(Some(user_in_db)) => user_in_db.clone(), + Ok(None) => { + let user_in_db = UserImage::new(guild.get(), new_message.author.id.get(), false).unwrap(); + match user_in_db.create().await { + Ok(_) => user_in_db, + Err(e) => { + println!("Error saving user image: {:?}", e); + return Ok(()); + } + } + } + Err(e) => { + println!("Error finding user image: {:?}", e); + return Ok(()); + } + }; + if !user_in_db.enable { + println!("User image is not enable"); + return Ok(()); + } + if config_img.keyword.len() == 0 || new_message.content.len() > 50 { + return Ok(()); + } + + let folder_container = match config_img + .keyword + .iter() + .find(|keyword| keyword.does_value_match(new_message.content.clone())) + { + Some(keyword_matching) => { + println!("{} match {:?}", new_message.content, keyword_matching); + let keyword_path = match keyword_matching.path.len() { + 0 => keyword_matching.path[0].clone(), + _ => { + let id: usize = { + let mut rng = rand::thread_rng(); + rng.gen_range(0..keyword_matching.path.len()) + }; + keyword_matching.path[id].clone() + } + }; + + keyword_path.clone() + } + None => return Ok(()), + }; + + let path = format!("{}/{}", config.image.path.clone(), folder_container); + let file_folder = KeyWordItem::output_folder_content(path.clone()); + let id_rand: usize = { + let mut rng = rand::thread_rng(); + rng.gen_range(0..file_folder.len()) + }; + let filename = match file_folder.get(id_rand) { + Some(file) => file.file_name().to_str().unwrap(), + None => return Ok(()), + }; + let file_path = format!("{}/{}", path, filename); + let file = match File::open(file_path).await { + Ok(file) => file, + Err(why) => { + println!("Error opening file: {:?}", why); + return Ok(()); + } + }; + let attachment = match CreateAttachment::file(&file, filename).await { + Ok(attachment) => attachment, + Err(why) => { + println!("Error creating attachment: {:?}", why); + return Ok(()); + } + }; + let builder = CreateMessage::new().add_file(attachment); + + if let Err(why) = new_message.channel_id.send_message(&ctx.http, builder).await { + println!("Error sending message: {:?}", why); + } + } + _ => {} + } + Ok(()) +} \ No newline at end of file diff --git a/src/botv2/init.rs b/src/botv2/init.rs new file mode 100644 index 0000000..9652eb4 --- /dev/null +++ b/src/botv2/init.rs @@ -0,0 +1,115 @@ +use poise::serenity_prelude as serenity; +use std::fs; +use serenity::GatewayIntents; +use crate::botv2::cmd::{ping::ping,help::help}; +use crate::{botv2::handler::event_handler, img::config_file::ConfigFile}; +use crate::config::Config; +use tokio::sync::oneshot; +use tokio::task::spawn_blocking; +pub struct Data{ + pub config_img: ConfigFile, + pub config: Config, +} + +// Types used by all command functions +pub type Error = Box; +pub type Context<'a> = poise::Context<'a, Data, Error>; + +#[poise::command(slash_command, prefix_command)] +async fn age( + ctx: Context<'_>, + #[description = "Selected user"] user: Option, +) -> Result<(), Error> { + let u = user.as_ref().unwrap_or_else(|| ctx.author()); + let response = format!("{}'s account was created at {}", u.name, u.created_at()); + ctx.say(response).await?; + Ok(()) +} + + + +pub fn start_bot(config: Config, rx: oneshot::Receiver<()>){ + if config.token != "" { + spawn_blocking(move||{ + let rt = tokio::runtime::Handle::current(); + rt.block_on(async move{ + let local = tokio::task::LocalSet::new(); + let _ = local.run_until(async move{ + let config_img = match fs::read_to_string(format!( + "{}/config.yaml", + config.image.path + )) { + Ok(content) => content, + Err(err) => { + println!("Error while opening config.yaml : {:?}", err); + return; + } + }; + let config_parsed = match ConfigFile::parse_config(config_img) { + Ok(config) => config, + Err(err) => { + println!("Error while parsing config.yaml : {:?}", err); + return; + } + }; + let intents = GatewayIntents::GUILD_MESSAGES + | GatewayIntents::DIRECT_MESSAGES + | GatewayIntents::MESSAGE_CONTENT + | GatewayIntents::GUILD_VOICE_STATES + | GatewayIntents::GUILDS + | GatewayIntents::GUILD_MEMBERS + | GatewayIntents::GUILD_PRESENCES + | GatewayIntents::GUILD_MESSAGE_REACTIONS; + let token = config.token.clone(); + let prefix = config.prefix.clone(); + let framework = poise::Framework::builder() + .options(poise::FrameworkOptions { + commands: vec![age(), ping(),help()], + prefix_options: poise::PrefixFrameworkOptions { + prefix: Some(prefix.into()), + ..Default::default() + }, + event_handler: |ctx, event, framework, data| { + Box::pin(event_handler(ctx, event, framework, data)) + }, + ..Default::default() + }) + .setup(|ctx, _ready, framework| { + Box::pin(async move { + poise::builtins::register_globally(ctx, &framework.options().commands).await?; + Ok(Data { + config_img: config_parsed, + config: config.clone(), + }) + }) + }) + .build(); + let mut client = serenity::ClientBuilder::new(token, intents) + .framework(framework) + .await.expect("Error creating client"); + let shard_manager = client.shard_manager.clone(); + let client_start = client.start_autosharded(); + tokio::spawn(async move { + match rx.await { + Ok(_) => { + println!("Received shutdown signal"); + shard_manager.shutdown_all().await; + println!("Shutting down bot"); + } + Err(_) => { + println!("Channel dropped signal"); + shard_manager.shutdown_all().await; + println!("Shutting down bot"); + } + } + }); + println!("Bot is running..."); + if let Err(why) = client_start.await { + println!("Client error: {why:?}"); + } + println!("Bot is stopped..."); + }).await; + }) + }); + } +} \ No newline at end of file diff --git a/src/botv2/mod.rs b/src/botv2/mod.rs new file mode 100644 index 0000000..8c8c9b3 --- /dev/null +++ b/src/botv2/mod.rs @@ -0,0 +1,3 @@ +pub mod init; +pub mod handler; +pub mod cmd; \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index d07886b..dc01722 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,3 +2,4 @@ pub mod bot; pub mod config; pub mod db; pub mod img; +pub mod botv2; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 571f4f2..d11fa4b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,11 @@ -mod bot; -mod config; -mod db; -mod img; - +extern crate botdiscord; use std::{process, time::Duration}; - use actix_cors::Cors; use actix_web::{App, HttpServer}; -use bot::init::start_bot; -use config::parse_local_config; +use botdiscord::botv2::init::start_bot; +use botdiscord::config::parse_local_config; use tokio::{sync::oneshot, time::sleep}; +use botdiscord::db; #[tokio::main] // or #[actix_web::main] async fn main() -> std::io::Result<()> {