diff --git a/Cargo.toml b/Cargo.toml index 9e88281..2877ee8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,20 +28,23 @@ rand = "0.8.5" walkdir = "2.4.0" surrealdb = "1.1.1" once_cell = "1.19.0" -poise = "0.6.1" +poise = { version = "0.6.1", features = ["cache"] } tracing = "0.1" tracing-actix-web = "0.7.9" serde_repr = "0.1" -tracing-subscriber = { version = "0.3", features = ["registry", "env-filter","time"] } +tracing-subscriber = { version = "0.3", features = [ + "registry", + "env-filter", + "time", +] } time = "0.3.36" tracing-bunyan-formatter = "0.3" opentelemetry = "0.23" opentelemetry_sdk = { version = "0.23", features = ["rt-tokio"] } -opentelemetry-otlp = { version = "0.16"} +opentelemetry-otlp = { version = "0.16" } tracing-opentelemetry = "0.24" - [[bin]] name = "botdiscord" path = "src/main.rs" diff --git a/cog.toml b/cog.toml deleted file mode 100644 index e036f6c..0000000 --- a/cog.toml +++ /dev/null @@ -1,24 +0,0 @@ -from_latest_tag = false -ignore_merge_commits = false -disable_changelog = false -disable_bump_commit = false -generate_mono_repository_global_tag = true -branch_whitelist = [] -skip_ci = "[skip ci]" -skip_untracked = false -pre_bump_hooks = [] -post_bump_hooks = [] -pre_package_bump_hooks = [] -post_package_bump_hooks = [] - -[git_hooks] - -[commit_types] - -[changelog] -path = "CHANGELOG.md" -authors = [] - -[bump_profiles] - -[packages] diff --git a/src/api/apidocs.rs b/src/api/apidocs.rs new file mode 100644 index 0000000..43193e1 --- /dev/null +++ b/src/api/apidocs.rs @@ -0,0 +1,26 @@ +use utoipa::OpenApi; + +use crate::api::bot::info; + +#[derive(OpenApi)] +#[openapi( + info( + title = "BotDiscordApi", + version = "0.1.0", + description = "Api for discord bot", + ), + tags( + (name= "Auth", description = "Authentication"), + (name= "User", description = "User operations"), + (name= "Bot", description = "Bot operations") + ), + components( + schemas( + info::Info, + ) + ), + paths( + info::get_info, + ) +)] +pub struct ApiDocs; diff --git a/src/api/bot/info.rs b/src/api/bot/info.rs new file mode 100644 index 0000000..b30b522 --- /dev/null +++ b/src/api/bot/info.rs @@ -0,0 +1,40 @@ +use std::sync::Arc; + +use actix_web::{get, web, HttpResponse, Responder}; +use poise::serenity_prelude::Http; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use crate::config::Config; + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct Info { + pub name: String, + pub version: String, + pub description: String, + pub available_guids: Vec, +} + +#[utoipa::path( + tag= "Bot", + operation_id = "getInfo", + path = "/api/bot/info", + responses( + (status = 200, description = "Information about the bot", body = Info), + (status = 500, description = "Internal server error") + ) +)] +#[get("/info")] +#[tracing::instrument(name = "get_info", skip(config, http))] +pub async fn get_info(config: web::Data, http: web::Data>) -> impl Responder { + let list_guilds = http.get_guilds(None, None).await; + match list_guilds { + Ok(guilds) => HttpResponse::Ok().json(Info { + name: "BotDiscord".to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + description: config.bot_name.clone(), + available_guids: guilds.iter().map(|g| g.name.clone()).collect(), + }), + Err(e) => HttpResponse::InternalServerError().body(format!("Error getting guilds: {}", e)), + } +} diff --git a/src/api/bot/mod.rs b/src/api/bot/mod.rs new file mode 100644 index 0000000..fa5b28a --- /dev/null +++ b/src/api/bot/mod.rs @@ -0,0 +1,6 @@ +use actix_web::Scope; +pub mod info; + +pub fn init_bot() -> Scope { + Scope::new("/bot").service(info::get_info) +} diff --git a/src/api/init.rs b/src/api/init.rs new file mode 100644 index 0000000..17d71ab --- /dev/null +++ b/src/api/init.rs @@ -0,0 +1,9 @@ +use actix_web::Scope; + +use super::{bot, meme}; + +pub fn init_api() -> Scope { + Scope::new("/api") + .service(meme::init_meme()) + .service(bot::init_bot()) +} diff --git a/src/api/meme/mod.rs b/src/api/meme/mod.rs new file mode 100644 index 0000000..90e9cba --- /dev/null +++ b/src/api/meme/mod.rs @@ -0,0 +1,5 @@ +use actix_web::Scope; + +pub fn init_meme() -> Scope { + Scope::new("/meme") +} diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..1cca2d2 --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,4 @@ +pub mod apidocs; +pub mod bot; +pub mod init; +pub mod meme; diff --git a/src/bot/cmd/help.rs b/src/bot/cmd/help.rs deleted file mode 100644 index 6ea74f0..0000000 --- a/src/bot/cmd/help.rs +++ /dev/null @@ -1,28 +0,0 @@ -use serenity::client::Context; -use serenity::framework::standard::macros::help; -use serenity::framework::standard::{ - help_commands, Args, CommandGroup, CommandResult, HelpOptions, -}; -use serenity::model::prelude::{Message, UserId}; -use std::collections::HashSet; - -#[help] -#[individual_command_tip = "Hello! Pour plus d'information sur une commande, passe la en argument."] -#[command_not_found_text = "La commande `{}` n'a pas été trouver."] -#[max_levenshtein_distance(3)] -#[wrong_channel = "Strike"] -#[lacking_role = "Hide"] -#[lacking_permissions = "Hide"] -#[strikethrough_commands_tip_in_guild("")] -#[embed_success_colour = "#5F021F"] -async fn help( - context: &Context, - msg: &Message, - args: Args, - help_options: &'static HelpOptions, - groups: &[&'static CommandGroup], - owners: HashSet, -) -> CommandResult { - let _ = help_commands::with_embeds(context, msg, args, help_options, groups, owners).await; - Ok(()) -} diff --git a/src/bot/cmd/meme/answer.rs b/src/bot/cmd/meme/answer.rs deleted file mode 100644 index c86d57f..0000000 --- a/src/bot/cmd/meme/answer.rs +++ /dev/null @@ -1,151 +0,0 @@ -use rand::Rng; -use serenity::{ - all::Message, - builder::{CreateAttachment, CreateEmbed, CreateEmbedFooter, CreateMessage}, - client::Context, - framework::standard::{macros::command, Args, CommandResult}, -}; -use tokio::fs::File; - -use crate::{ - config::ConfigGlobal, - img::config_file::{ConfigImgGlobal, KeyWordItem}, -}; - -#[command] -#[description = "Answer your message with a meme, the keyword need to be put under quote"] -pub async fn answer(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - if let Ok(keyword) = args.single_quoted::() { - println!("Keyword: {}", keyword); - - let (config_img, config) = { - let data_read = ctx.data.read().await; - let config_img = data_read - .get::() - .expect("Config img not found") - .clone(); - let config = data_read - .get::() - .expect("Main config not found") - .clone(); - (config_img, config) - }; - let folder_container = match config_img - .keyword - .iter() - .find(|keyword_under_search| keyword_under_search.does_value_match(keyword.clone())) - { - Some(keyword_matching) => { - println!("{} match {:?}", keyword, 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 => { - if let Err(why) = msg - .channel_id - .say(&ctx.http, format!("No match for {}", keyword)) - .await - { - println!("Error sending message: {:?}", why) - } - 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 => { - if let Err(why) = msg - .channel_id - .say( - &ctx.http, - format!("Couldn't find file in folder for {}", keyword), - ) - .await - { - println!("Error sending message: {:?}", why) - } - return Ok(()); - } - }; - let file_path = format!("{}/{}", path, filename); - let file = match File::open(file_path).await { - Ok(file) => file, - Err(why) => { - if let Err(why) = msg - .channel_id - .say( - &ctx.http, - format!("Error opening file {} => {:?}", filename, why), - ) - .await - { - println!("Error sending message: {:?}", why) - } - return Ok(()); - } - }; - let attachment = match CreateAttachment::file(&file, filename).await { - Ok(attachment) => attachment, - Err(why) => { - if let Err(why) = msg - .channel_id - .say(&ctx.http, format!("Error creating attachment: {:?}", why)) - .await - { - println!("Error sending message: {:?}", why) - } - return Ok(()); - } - }; - if filename.ends_with(".mp3") || filename.ends_with(".mp4") { - let builder = CreateMessage::new() - .add_file(attachment) - .reference_message(msg); - if let Err(why) = msg.channel_id.send_message(&ctx.http, builder).await { - println!("Error sending message: {:?}", why); - } - return Ok(()); - } - let footer = CreateEmbedFooter::new("WeeboBot"); - let embed = CreateEmbed::new() - .title(keyword) - .footer(footer) - .image(format!("attachment://{}", filename)); - let builder = CreateMessage::new() - .embed(embed) - .add_file(attachment) - .reference_message(msg); - if let Err(why) = msg.channel_id.send_message(&ctx.http, builder).await { - println!("Error sending message: {:?}", why); - } - return Ok(()); - } - if let Err(why) = msg - .channel_id - .say( - &ctx.http, - "Please add keyword if you want to use this command. Example: `!meme answer \"keyword\"`", - ) - .await - { - println!("Error sending message: {:?}", why) - } - Ok(()) -} diff --git a/src/bot/cmd/meme/ask.rs b/src/bot/cmd/meme/ask.rs deleted file mode 100644 index 0f23cde..0000000 --- a/src/bot/cmd/meme/ask.rs +++ /dev/null @@ -1,21 +0,0 @@ -use serenity::builder::{CreateEmbed, CreateEmbedFooter, CreateMessage}; -use serenity::{ - all::Message, - client::Context, - framework::standard::{macros::command, CommandResult}, -}; - -#[command] -#[description = "Info to ask for new meme"] -pub async fn ask(ctx: &Context, msg: &Message) -> CommandResult { - let footer = CreateEmbedFooter::new("WeeboBot"); - let embed = CreateEmbed::new() - .title("New meme ?") - .field("Hello, si tu souhaites ajouter un nouveau meme au bot, tu dois demander à batleforc d'en ajouter un nouveau. Tu peux le faire en lui envoyant un message.".to_string(), String::new(), true) - .footer(footer); - let builder = CreateMessage::new().embed(embed); - if let Err(why) = msg.channel_id.send_message(&ctx.http, builder).await { - println!("Error sending message: {:?}", why) - } - Ok(()) -} diff --git a/src/bot/cmd/meme/enable.rs b/src/bot/cmd/meme/enable.rs deleted file mode 100644 index 6dbf568..0000000 --- a/src/bot/cmd/meme/enable.rs +++ /dev/null @@ -1,96 +0,0 @@ -use serenity::{ - all::Message, - builder::{CreateEmbed, CreateEmbedFooter, CreateMessage}, - client::Context, - framework::standard::{macros::command, Args, CommandResult}, - model::colour, -}; - -use crate::db::user_image::User; - -#[command] -#[description = "Enable/Disable the auto meme answer"] -pub async fn enable(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - let guild = match msg.guild_id { - Some(guild) => guild, - None => return Ok(()), - }; - let footer = CreateEmbedFooter::new("WeeboBot"); - let embed = CreateEmbed::new() - .title("Enable/Disable auto meme answer") - .footer(footer); - let mut user_in_db: User = - match User::find_by_server_id_user_id(&guild.get(), &msg.author.id.get()).await { - Ok(Some(user_in_db)) => user_in_db.clone(), - Ok(None) => { - let user_in_db = User::new(guild.get(), msg.author.id.get(), false).unwrap(); - match user_in_db.create().await { - Ok(_) => user_in_db, - Err(e) => { - let embed = embed - .field("Could't create user in db", e.to_string(), false) - .color(colour::Color::RED); - let builder = CreateMessage::new().embed(embed).reference_message(msg); - if let Err(why) = msg.channel_id.send_message(&ctx.http, builder).await { - println!("Error sending message: {:?}", why) - } - println!("Error saving user image: {:?}", e); - return Ok(()); - } - } - } - Err(e) => { - let embed = embed - .field("Error finding user image", e.to_string(), false) - .color(colour::Color::RED); - let builder = CreateMessage::new().embed(embed).reference_message(msg); - if let Err(why) = msg.channel_id.send_message(&ctx.http, builder).await { - println!("Error sending message: {:?}", why) - } - println!("Error finding user image: {:?}", e); - return Ok(()); - } - }; - if let Ok(new_status) = args.single::() { - if user_in_db.enable == new_status { - let embed = embed.field( - format!("Auto answer the same : {}", new_status), - String::new(), - false, - ); - let builder = CreateMessage::new().embed(embed).reference_message(msg); - if let Err(why) = msg.channel_id.send_message(&ctx.http, builder).await { - println!("Error sending message: {:?}", why) - } - return Ok(()); - } - user_in_db.enable = new_status; - match user_in_db.update().await { - Ok(_) => { - let embed = embed.field("Auto answer", new_status.to_string(), false); - let builder = CreateMessage::new().embed(embed).reference_message(msg); - if let Err(why) = msg.channel_id.send_message(&ctx.http, builder).await { - println!("Error sending message: {:?}", why) - } - } - Err(e) => { - let embed = embed.field("Couldn't update user in db", e.to_string(), false); - let builder = CreateMessage::new().embed(embed).reference_message(msg); - if let Err(why) = msg.channel_id.send_message(&ctx.http, builder).await { - println!("Error sending message: {:?}", why) - } - println!("Error saving user image: {:?}", e); - return Ok(()); - } - } - return Ok(()); - } else { - let embed = embed.field("Auto answer", user_in_db.enable.to_string(), false); - let builder = CreateMessage::new().embed(embed).reference_message(msg); - if let Err(why) = msg.channel_id.send_message(&ctx.http, builder).await { - println!("Error sending message: {:?}", why) - } - } - - Ok(()) -} diff --git a/src/bot/cmd/meme/init.rs b/src/bot/cmd/meme/init.rs deleted file mode 100644 index 217c5e2..0000000 --- a/src/bot/cmd/meme/init.rs +++ /dev/null @@ -1,15 +0,0 @@ -use super::{answer::ANSWER_COMMAND, ask::ASK_COMMAND, enable::ENABLE_COMMAND, list::LIST_COMMAND}; -use serenity::all::standard::macros::group; - -// Include the meme commands -// - enable/disable the meme answer -// - trigger a one time meme answer (default command) -// - make a request to add a new meme -// - list meme categories -#[group] -#[prefixes("meme", "m")] -#[summary = "Meme related commands"] -#[description = "Commands to handle the meme related stuffs, it allow you to enable/disable the meme answer or to trigger a one time meme answer."] -#[commands(enable, answer, ask, list)] -#[default_command(answer)] -pub struct Meme; diff --git a/src/bot/cmd/meme/list.rs b/src/bot/cmd/meme/list.rs deleted file mode 100644 index 7a2cb60..0000000 --- a/src/bot/cmd/meme/list.rs +++ /dev/null @@ -1,44 +0,0 @@ -use serenity::builder::{CreateEmbed, CreateEmbedFooter, CreateMessage}; -use serenity::{ - all::Message, - client::Context, - framework::standard::{macros::command, CommandResult}, -}; - -use crate::img::config_file::ConfigImgGlobal; - -#[command] -#[description = "List supported meme keywords"] -pub async fn list(ctx: &Context, msg: &Message) -> CommandResult { - let config_img = { - let data_read = ctx.data.read().await; - data_read - .get::() - .expect("Config img not found") - .clone() - }; - let list_value: Vec<(String, String, bool)> = config_img - .keyword - .iter() - .map(|x| (x.value.clone().join(", "), String::new(), true)) - .collect(); - if list_value.is_empty() { - let builder = CreateMessage::new().content("No meme keyword found"); - if let Err(why) = msg.channel_id.send_message(&ctx.http, builder).await { - println!("Error sending message: {:?}", why) - } - return Ok(()); - } - for chunks in list_value.chunks(25) { - let footer = CreateEmbedFooter::new("WeeboBot"); - let embed = CreateEmbed::new() - .title("Meme List") - .fields(chunks.to_vec()) - .footer(footer); - let builder = CreateMessage::new().embed(embed); - if let Err(why) = msg.channel_id.send_message(&ctx.http, builder).await { - println!("Error sending message: {:?}", why) - } - } - Ok(()) -} diff --git a/src/bot/cmd/meme/mod.rs b/src/bot/cmd/meme/mod.rs deleted file mode 100644 index 59905a0..0000000 --- a/src/bot/cmd/meme/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod answer; -pub mod ask; -pub mod enable; -pub mod init; -pub mod list; diff --git a/src/bot/cmd/mod.rs b/src/bot/cmd/mod.rs deleted file mode 100644 index 56bf971..0000000 --- a/src/bot/cmd/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod help; -pub mod meme; -pub mod ping; diff --git a/src/bot/cmd/ping.rs b/src/bot/cmd/ping.rs deleted file mode 100644 index 5cd1de2..0000000 --- a/src/bot/cmd/ping.rs +++ /dev/null @@ -1,14 +0,0 @@ -use serenity::{ - all::Message, - client::Context, - framework::standard::{macros::command, CommandResult}, -}; - -#[command] -#[description = "Pong!"] -pub async fn ping(ctx: &Context, msg: &Message) -> CommandResult { - if let Err(why) = msg.channel_id.say(&ctx.http, "Pong from framework!").await { - println!("Error sending message: {:?}", why) - } - Ok(()) -} diff --git a/src/bot/handler.rs b/src/bot/handler.rs deleted file mode 100644 index c8f7aad..0000000 --- a/src/bot/handler.rs +++ /dev/null @@ -1,125 +0,0 @@ -use rand::Rng; -use serenity::prelude::*; -use serenity::{ - all::{Message, Ready}, - async_trait, - builder::{CreateAttachment, CreateMessage}, - client::Context, -}; -use tokio::fs::File; - -use crate::{ - config::ConfigGlobal, - db::user_image::User, - img::config_file::{ConfigImgGlobal, KeyWordItem}, -}; - - -pub struct Handler; - -#[async_trait] -impl EventHandler for Handler { - async fn message(&self, ctx: Context, msg: Message) { - if msg.author.bot || msg.content.starts_with("!") || msg.content.len() == 0 { - return; - } - let guild = match msg.guild_id { - Some(guild) => guild, - None => return, - }; - let user_in_db: User = - match User::find_by_server_id_user_id(&guild.get(), &msg.author.id.get()).await { - Ok(Some(user_in_db)) => user_in_db.clone(), - Ok(None) => { - let user_in_db = - User::new(guild.get(), msg.author.id.get(), false).unwrap(); - match user_in_db.create().await { - Ok(_) => user_in_db, - Err(e) => { - println!("Error saving user image: {:?}", e); - return; - } - } - } - Err(e) => { - println!("Error finding user image: {:?}", e); - return; - } - }; - if !user_in_db.enable { - println!("User image is not enable"); - return; - } - let (config_img, config) = { - let data_read = ctx.data.read().await; - let config_img = data_read - .get::() - .expect("Config img not found") - .clone(); - let config = data_read - .get::() - .expect("Main config not found") - .clone(); - (config_img, config) - }; - - if config_img.keyword.len() == 0 || msg.content.len() > 50 { - return; - } - let folder_container = match config_img - .keyword - .iter() - .find(|keyword| keyword.does_value_match(msg.content.clone())) - { - Some(keyword_matching) => { - println!("{} match {:?}", msg.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, - }; - 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, - }; - 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; - } - }; - let attachment = match CreateAttachment::file(&file, filename).await { - Ok(attachment) => attachment, - Err(why) => { - println!("Error creating attachment: {:?}", why); - return; - } - }; - let builder = CreateMessage::new().add_file(attachment); - if let Err(why) = msg.channel_id.send_message(&ctx.http, builder).await { - println!("Error sending message: {:?}", why); - } - } - - async fn ready(&self, _: Context, ready: Ready) { - println!("{} is connected!", ready.user.name); - } -} diff --git a/src/bot/init.rs b/src/bot/init.rs deleted file mode 100644 index cbd2322..0000000 --- a/src/bot/init.rs +++ /dev/null @@ -1,124 +0,0 @@ -use super::cmd::meme::init::MEME_GROUP; -use super::cmd::{help::HELP, ping::PING_COMMAND}; -use super::handler::Handler; -use crate::config::{Config, ConfigGlobal}; -use crate::img::config_file::{ConfigFile, ConfigImgGlobal}; -use serenity::framework::standard::Configuration; -use serenity::{ - all::GatewayIntents, - framework::{standard::macros::group, StandardFramework}, - http::Http, - Client, -}; -use std::collections::HashSet; -use std::fs; -use tokio::sync::oneshot; -use tokio::task::spawn_blocking; - -#[group] -#[commands(ping)] -struct General; - -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 http = Http::new(&config.token); - let (owners, _bot_id) = match http.get_current_application_info().await { - Ok(info) => { - let mut owners = HashSet::new(); - match info.owner { - Some(user) => { - owners.insert(user.id); - println!("{}", user.global_name.unwrap()) - } - None => {} - } - - (owners, info.id) - } - Err(why) => panic!("Could not access application info: {:?}", why), - }; - - let framework = StandardFramework::new() - .help(&HELP) - .group(&GENERAL_GROUP) - .group(&MEME_GROUP); - - framework.configure( - Configuration::new() - .with_whitespace(true) - .prefix(config.prefix.clone()) - .owners(owners), - ); - - let mut client = Client::builder(&config.token, intents) - .event_handler(Handler) - .framework(framework) - .await - .expect("Err creating client"); - - { - // Open the data lock in write mode, so keys can be inserted to it. - let mut data = client.data.write().await; - - data.insert::(config_parsed.clone()); - data.insert::(config.clone()); - } - - 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"); - } - } - }); - - if let Err(why) = client_start.await { - println!("Client error: {why:?}"); - } - println!("Bot stopped."); - }) - .await; - }) - }); - } -} diff --git a/src/bot/mod.rs b/src/bot/mod.rs deleted file mode 100644 index 15832b0..0000000 --- a/src/bot/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod cmd; -pub mod handler; -pub mod init; diff --git a/src/botv2/cmd/meme/answer.rs b/src/botv2/cmd/meme/answer.rs index c47c23a..069d6b8 100644 --- a/src/botv2/cmd/meme/answer.rs +++ b/src/botv2/cmd/meme/answer.rs @@ -1,6 +1,6 @@ -use crate::{ - botv2::init::{Context, Error}, +use crate::botv2::{ domain::meme::answer::{answer_meme, AnswerResult}, + init::{Context, Error}, }; use poise::CreateReply; use tracing::instrument; diff --git a/src/botv2/cmd/meme/enable.rs b/src/botv2/cmd/meme/enable.rs index 2bb615d..4f82c21 100644 --- a/src/botv2/cmd/meme/enable.rs +++ b/src/botv2/cmd/meme/enable.rs @@ -1,6 +1,6 @@ -use crate::{ - botv2::init::{Context, Error}, +use crate::botv2::{ domain::meme::change_auto_meme::change_auto_meme, + init::{Context, Error}, }; use poise::CreateReply; use tracing::instrument; @@ -16,7 +16,11 @@ pub async fn enable( Some(guild) => guild, None => return Ok(()), }; - let embed = change_auto_meme(enable, ctx.author().id.get(), guild.get()).await; + let entity_name = ctx.data().entity_name.clone(); + let embed: Result< + Vec, + crate::botv2::domain::meme::change_auto_meme::MemeChangeAutoMemeError, + > = change_auto_meme(enable, ctx.author().id.get(), guild.get(), entity_name).await; let mut reply = CreateReply::default(); if let Ok(embed) = embed { reply.embeds = embed; diff --git a/src/domain/meme/answer.rs b/src/botv2/domain/meme/answer.rs similarity index 100% rename from src/domain/meme/answer.rs rename to src/botv2/domain/meme/answer.rs diff --git a/src/domain/meme/change_auto_meme.rs b/src/botv2/domain/meme/change_auto_meme.rs similarity index 59% rename from src/domain/meme/change_auto_meme.rs rename to src/botv2/domain/meme/change_auto_meme.rs index bbe835f..96cc526 100644 --- a/src/domain/meme/change_auto_meme.rs +++ b/src/botv2/domain/meme/change_auto_meme.rs @@ -16,33 +16,33 @@ pub async fn change_auto_meme( enable: Option, user_id: u64, guild_id: u64, + bot_entity: String, ) -> Result, MemeChangeAutoMemeError> { - let footer = CreateEmbedFooter::new("WeeboBot"); + let footer = CreateEmbedFooter::new(bot_entity.clone()); let mut embed = CreateEmbed::new() .title("Enable/Disable auto meme answer") .footer(footer); - let mut user_in_db: User = - match User::find_by_server_id_user_id(&guild_id, &user_id).await { - Ok(Some(user_in_db)) => user_in_db.clone(), - Ok(None) => { - let user_in_db = User::new(guild_id, user_id, false).unwrap(); - match user_in_db.create().await { - Ok(_) => user_in_db, - Err(e) => { - let embed = embed - .field("Could't create user in db", e.to_string(), false) - .color(colour::Color::RED); - return Ok(vec![embed]); - } + let mut user_in_db: User = match User::find_by_server_id_user_id(&guild_id, &user_id).await { + Ok(Some(user_in_db)) => user_in_db.clone(), + Ok(None) => { + let user_in_db = User::new(guild_id, user_id, false).unwrap(); + match user_in_db.create().await { + Ok(_) => user_in_db, + Err(e) => { + let embed = embed + .field("Could't create user in db", e.to_string(), false) + .color(colour::Color::RED); + return Ok(vec![embed]); } } - Err(e) => { - let embed = embed - .field("Error finding user image", e.to_string(), false) - .color(colour::Color::RED); - return Ok(vec![embed]); - } - }; + } + Err(e) => { + let embed = embed + .field("Error finding user image", e.to_string(), false) + .color(colour::Color::RED); + return Ok(vec![embed]); + } + }; if let Some(new_status) = enable { if user_in_db.enable == new_status { @@ -66,4 +66,4 @@ pub async fn change_auto_meme( embed = embed.field("Auto answer", user_in_db.enable.to_string(), false); } return Ok(vec![embed]); -} \ No newline at end of file +} diff --git a/src/domain/meme/mod.rs b/src/botv2/domain/meme/mod.rs similarity index 100% rename from src/domain/meme/mod.rs rename to src/botv2/domain/meme/mod.rs diff --git a/src/domain/mod.rs b/src/botv2/domain/mod.rs similarity index 100% rename from src/domain/mod.rs rename to src/botv2/domain/mod.rs diff --git a/src/botv2/handler.rs b/src/botv2/handler.rs index 3ba873d..7704047 100644 --- a/src/botv2/handler.rs +++ b/src/botv2/handler.rs @@ -4,6 +4,7 @@ use poise::serenity_prelude as serenity; use rand::Rng; use serenity::all::{CreateAttachment, CreateMessage}; use tokio::fs::File; +use tracing::info; pub async fn event_handler( ctx: &serenity::Context, @@ -13,7 +14,7 @@ pub async fn event_handler( ) -> Result<(), Error> { match event { serenity::FullEvent::Ready { data_about_bot, .. } => { - println!("{} is connected !", data_about_bot.user.name); + info!("{} is connected !", data_about_bot.user.name); } serenity::FullEvent::Message { new_message } => { if new_message.author.bot diff --git a/src/botv2/init.rs b/src/botv2/init.rs index f9f16f9..617cb79 100644 --- a/src/botv2/init.rs +++ b/src/botv2/init.rs @@ -2,12 +2,13 @@ use crate::botv2::cmd::meme::{answer::answer, enable::enable, list::list}; use crate::botv2::cmd::{help::help, ping::ping}; use crate::config::Config; use crate::{botv2::handler::event_handler, img::config_file::ConfigFile}; -use poise::serenity_prelude as serenity; +use poise::serenity_prelude::{self as serenity, Http}; use serenity::GatewayIntents; use std::fs; +use std::sync::Arc; use tokio::sync::oneshot; -use tokio::task::spawn_blocking; -use tracing::instrument; +use tracing::{info, instrument}; + pub struct Data { pub config_img: ConfigFile, pub config: Config, @@ -30,96 +31,84 @@ async fn age( 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(), list(), enable(), answer()], - 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(), - entity_name: config.bot_name.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; +pub async fn start_bot(config: Config, rx: oneshot::Receiver<()>) -> Arc { + let config_img = match fs::read_to_string(format!("{}/config.yaml", config.image.path)) { + Ok(content) => content, + Err(err) => { + tracing::error!("Error while opening config.yaml : {:?}", err); + panic!("Error while opening config.yaml : {:?}", err) + } + }; + let config_parsed = match ConfigFile::parse_config(config_img) { + Ok(config) => config, + Err(err) => { + tracing::error!("Error while parsing config.yaml : {:?}", err); + panic!("Error while parsing config.yaml : {:?}", err) + } + }; + 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(), list(), enable(), answer()], + 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(), + entity_name: format!( + "{}-{}", + config.bot_name.clone(), + env!("CARGO_PKG_VERSION").to_string() + ), + }) }) - }); - } + }) + .build(); + let mut client = serenity::ClientBuilder::new(token, intents) + .framework(framework) + .await + .expect("Error creating client"); + let http = client.http.clone(); + let shard_manager = client.shard_manager.clone(); + tokio::spawn(async move { + match rx.await { + Ok(_) => { + tracing::info!("Received shutdown signal"); + shard_manager.shutdown_all().await; + tracing::info!("Shutting down bot"); + } + Err(_) => { + tracing::info!("Channel dropped signal"); + shard_manager.shutdown_all().await; + tracing::info!("Shutting down bot"); + } + } + }); + tokio::spawn(async move { + info!("Bot is running..."); + if let Err(why) = client.start_autosharded().await { + tracing::error!("Client error: {why:?}"); + } + info!("Bot is stopped..."); + }); + http } diff --git a/src/botv2/mod.rs b/src/botv2/mod.rs index 8c8c9b3..621ee96 100644 --- a/src/botv2/mod.rs +++ b/src/botv2/mod.rs @@ -1,3 +1,4 @@ -pub mod init; +pub mod cmd; +pub mod domain; pub mod handler; -pub mod cmd; \ No newline at end of file +pub mod init; diff --git a/src/config.rs b/src/config.rs index c1b1554..ff6b25f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,10 +1,11 @@ +use crate::tracing::tracing_kind::Tracing; use dotenvy::dotenv; -use serde::Deserialize; use poise::serenity_prelude::prelude::TypeMapKey; +use serde::Deserialize; use std::env; use std::fs::read_to_string; use std::path::PathBuf; -use crate::tracing::tracing_kind::Tracing; +use tracing::{info, trace}; const DB_URL: &str = "DB_URL"; const DB_USER: &str = "DB_USER"; @@ -60,10 +61,10 @@ pub struct PersistenceConfig { pub fn parse_local_config() -> Config { let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - println!(env!("CARGO_MANIFEST_DIR")); + trace!(env!("CARGO_MANIFEST_DIR")); d.push("resources/config.toml"); match dotenv() { - Ok(_) => println!("Loaded .env file"), + Ok(_) => info!("Loaded .env file"), Err(err) => println!("No .env file found: {:?}", err), } parse_config(d) diff --git a/src/lib.rs b/src/lib.rs index 8864b9b..f7b60fc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ +pub mod api; +pub mod botv2; pub mod config; pub mod db; pub mod img; -pub mod botv2; -pub mod domain; -pub mod tracing; \ No newline at end of file +pub mod tracing; diff --git a/src/main.rs b/src/main.rs index 1d1b14a..4e89c05 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,19 @@ extern crate botdiscord; -use std::{process, time::Duration}; +use ::tracing::info; use actix_cors::Cors; -use actix_web::{App, HttpServer}; -use botdiscord::{botv2::init::start_bot, tracing}; +use actix_web::dev::Service; +use actix_web::http::header; +use actix_web::{web, App, HttpServer}; +use botdiscord::api::apidocs::ApiDocs; +use botdiscord::api::init::init_api; use botdiscord::config::parse_local_config; -use tokio::{sync::oneshot, time::sleep}; use botdiscord::db; +use botdiscord::{botv2::init::start_bot, tracing}; +use std::{process, time::Duration}; +use tokio::{sync::oneshot, time::sleep}; +use tracing_actix_web::{RequestId, TracingLogger}; +use utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; #[tokio::main] // or #[actix_web::main] async fn main() -> std::io::Result<()> { @@ -20,21 +28,50 @@ async fn main() -> std::io::Result<()> { } } let (tx, rx) = oneshot::channel(); - start_bot(config.clone(), rx); - HttpServer::new(|| { + let http = start_bot(config.clone(), rx).await; + info!("API Server started on port {}", port); + let mut openapi = ApiDocs::openapi(); + openapi.info.title = format!("{} Api", config.bot_name.clone()); + openapi.info.version = env!("CARGO_PKG_VERSION").to_string(); + let api_config = config.clone(); + HttpServer::new(move || { let cors = Cors::default() .allow_any_header() .allow_any_method() .allow_any_origin(); - App::new().wrap(cors) + let swagger_ui = + SwaggerUi::new("/api/docs/{_:.*}").url("/api/docs/docs.json", openapi.clone()); + App::new() + .app_data(web::Data::new(api_config.clone())) + .app_data(web::Data::new(http.clone())) + .wrap(cors) + .service(swagger_ui) + .service( + init_api() + .wrap_fn(|mut req, srv| { + let request_id_asc = req.extract::(); + let fut = srv.call(req); + async move { + let mut res = fut.await?; + let request_id: RequestId = request_id_asc.await.unwrap(); + let request_id_str = format!("{}", request_id); + let headers = res.headers_mut(); + headers.insert( + header::HeaderName::from_static("x-request-id"), + header::HeaderValue::from_str(request_id_str.as_str()).unwrap(), + ); + Ok(res) + } + }) + .wrap(TracingLogger::default()), + ) }) .bind(("0.0.0.0", port))? .run() .await?; - println!("API Server stopped."); + info!("API Server stopped."); tx.send(()).unwrap(); tracing::init::stop_tracing(config.tracing.clone(), config.bot_name.clone()); - println!("Signal sent to bot."); sleep(Duration::from_secs(2)).await; process::exit(1); // This is a workaround to stop the bot, it should be replaced by a better solution }