diff --git a/cog.toml b/cog.toml new file mode 100644 index 0000000..e036f6c --- /dev/null +++ b/cog.toml @@ -0,0 +1,24 @@ +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/resources/config.toml b/resources/config.toml index f281f04..6a72621 100644 --- a/resources/config.toml +++ b/resources/config.toml @@ -19,7 +19,7 @@ path = "/projects/base-image" [[tracing]] kind = "Console" name = "console" -level = 1 +level = 2 [tracing.additional] @@ -29,4 +29,4 @@ name = "otel" level = 2 [tracing.additional] -endpoint = "http://localhost:4317" \ No newline at end of file +endpoint = "http://localhost:4317" diff --git a/src/botv2/cmd/meme/answer.rs b/src/botv2/cmd/meme/answer.rs new file mode 100644 index 0000000..c47c23a --- /dev/null +++ b/src/botv2/cmd/meme/answer.rs @@ -0,0 +1,51 @@ +use crate::{ + botv2::init::{Context, Error}, + domain::meme::answer::{answer_meme, AnswerResult}, +}; +use poise::CreateReply; +use tracing::instrument; + +#[instrument(skip(ctx), level = "info", fields(channel = ctx.channel_id().get(), guild = ?ctx.guild_id().unwrap().get()))] +#[poise::command(slash_command, prefix_command, category = "meme")] +pub async fn answer( + ctx: Context<'_>, + #[description = "keyword thaat you want to search"] keyword: String, +) -> Result<(), Error> { + let guild_id = match ctx.guild_id() { + Some(guild) => guild.get(), + None => return Ok(()), + }; + let user_id = ctx.author().id.get(); + let config_img = ctx.data().config_img.clone(); + let base_path = ctx.data().config.image.path.clone(); + let bot_entity = ctx.data().entity_name.clone(); + let answer = answer_meme( + user_id, guild_id, keyword, base_path, config_img, bot_entity, + ) + .await; + if let Ok(answer) = answer { + match answer { + AnswerResult::Embed(embeds, attachments) => { + let mut reply = CreateReply::default(); + reply.embeds = embeds; + reply.attachments = attachments; + if let Err(why) = ctx.send(reply).await { + tracing::error!("Error sending message: {:?}", why); + } + } + AnswerResult::Animated(attachments) => { + let mut reply = CreateReply::default(); + reply.attachments = attachments; + if let Err(why) = ctx.send(reply).await { + tracing::error!("Error sending message: {:?}", why); + } + } + } + } else { + let builder = CreateReply::default().content("No meme keyword found"); + if let Err(why) = ctx.send(builder).await { + tracing::error!("Error sending message: {:?}", why); + } + } + Ok(()) +} diff --git a/src/botv2/cmd/meme/enable.rs b/src/botv2/cmd/meme/enable.rs index 0b40413..2bb615d 100644 --- a/src/botv2/cmd/meme/enable.rs +++ b/src/botv2/cmd/meme/enable.rs @@ -1,19 +1,17 @@ -use crate::{botv2::init::{Context,Error}, domain::meme::change_auto_meme::change_auto_meme}; +use crate::{ + botv2::init::{Context, Error}, + domain::meme::change_auto_meme::change_auto_meme, +}; use poise::CreateReply; use tracing::instrument; /// Enable/Disable auto answer available meme keywords -#[instrument(skip(ctx),level="info")] -#[poise::command( - slash_command, - prefix_command, - category = "meme" -)] +#[instrument(skip(ctx), level = "info", fields(channel = ctx.channel_id().get(), guild = ?ctx.guild_id().unwrap().get()))] +#[poise::command(slash_command, prefix_command, category = "meme")] pub async fn enable( ctx: Context<'_>, - #[description = "Enable or diable "] enable: Option + #[description = "Enable or diable "] enable: Option, ) -> Result<(), Error> { - tracing::info!(channel = ctx.channel_id().get(), guild = ?ctx.guild_id().unwrap().get(),"Enable command called"); let guild = match ctx.guild_id() { Some(guild) => guild, None => return Ok(()), @@ -27,4 +25,4 @@ pub async fn enable( tracing::error!("Error sending message: {:?}", why); } Ok(()) -} \ No newline at end of file +} diff --git a/src/botv2/cmd/meme/list.rs b/src/botv2/cmd/meme/list.rs index cef7a27..4e2a5c6 100644 --- a/src/botv2/cmd/meme/list.rs +++ b/src/botv2/cmd/meme/list.rs @@ -1,17 +1,16 @@ -use crate::botv2::init::{Context,Error}; -use poise::{serenity_prelude::{CreateEmbed, CreateEmbedFooter, CreateMessage}, CreateReply}; +use crate::botv2::init::{Context, Error}; +use poise::{ + serenity_prelude::{CreateEmbed, CreateEmbedFooter, CreateMessage}, + CreateReply, +}; use tracing::instrument; /// List available meme keywords -#[instrument(skip(ctx),level="info")] -#[poise::command( - slash_command, - prefix_command, - category = "meme" -)] +#[instrument(skip(ctx), level = "info", fields(channel = ctx.channel_id().get(), guild = ?ctx.guild_id().unwrap().get()))] +#[poise::command(slash_command, prefix_command, category = "meme")] pub async fn list(ctx: Context<'_>) -> Result<(), Error> { - tracing::info!(channel = ctx.channel_id().get(), guild = ?ctx.guild_id().unwrap().get(),"List command called"); let config_img = ctx.data().config_img.clone(); + let bot_name = ctx.data().entity_name.clone(); let list_value: Vec<(String, String, bool)> = config_img .keyword .iter() @@ -25,7 +24,7 @@ pub async fn list(ctx: Context<'_>) -> Result<(), Error> { return Ok(()); } let embed_vec = list_value.chunks(25).map(|chunks| { - let footer = CreateEmbedFooter::new("WeeboBot"); + let footer = CreateEmbedFooter::new(bot_name.clone()); CreateEmbed::new() .title("Meme List") .fields(chunks.to_vec()) @@ -40,4 +39,4 @@ pub async fn list(ctx: Context<'_>) -> Result<(), Error> { Ok(()) } -// https://github.com/serenity-rs/poise/blob/current/examples/fluent_localization/main.rs \ No newline at end of file +// https://github.com/serenity-rs/poise/blob/current/examples/fluent_localization/main.rs diff --git a/src/botv2/cmd/meme/mod.rs b/src/botv2/cmd/meme/mod.rs index e58847f..a669103 100644 --- a/src/botv2/cmd/meme/mod.rs +++ b/src/botv2/cmd/meme/mod.rs @@ -1,2 +1,3 @@ pub mod list; -pub mod enable; \ No newline at end of file +pub mod enable; +pub mod answer; \ No newline at end of file diff --git a/src/botv2/handler.rs b/src/botv2/handler.rs index b024783..3ba873d 100644 --- a/src/botv2/handler.rs +++ b/src/botv2/handler.rs @@ -1,9 +1,9 @@ -use tokio::fs::File; -use poise::serenity_prelude as serenity; -use serenity::all::{CreateAttachment,CreateMessage}; -use rand::Rng; -use crate::{db::user_image::User, img::config_file::KeyWordItem}; use super::init::{Data, Error}; +use crate::{db::user_image::User, img::config_file::KeyWordItem}; +use poise::serenity_prelude as serenity; +use rand::Rng; +use serenity::all::{CreateAttachment, CreateMessage}; +use tokio::fs::File; pub async fn event_handler( ctx: &serenity::Context, @@ -16,7 +16,10 @@ pub async fn event_handler( 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 { + 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(); @@ -25,25 +28,28 @@ pub async fn event_handler( Some(guild) => guild, None => return Ok(()), }; - let user_in_db = match User::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 = User::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(()); + let user_in_db = + match User::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 = + User::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(()); - } - }; + 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 { @@ -100,11 +106,15 @@ pub async fn event_handler( }; let builder = CreateMessage::new().add_file(attachment); - if let Err(why) = new_message.channel_id.send_message(&ctx.http, builder).await { + 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 index cb7044e..f9f16f9 100644 --- a/src/botv2/init.rs +++ b/src/botv2/init.rs @@ -1,24 +1,24 @@ -use poise::serenity_prelude as serenity; -use tracing::instrument; -use std::fs; -use serenity::GatewayIntents; -use crate::botv2::cmd::meme::enable::enable; -use crate::botv2::cmd::{ping::ping,help::help, meme::list::list}; -use crate::{botv2::handler::event_handler, img::config_file::ConfigFile}; +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 serenity::GatewayIntents; +use std::fs; use tokio::sync::oneshot; use tokio::task::spawn_blocking; -pub struct Data{ +use tracing::instrument; +pub struct Data { pub config_img: ConfigFile, pub config: Config, + pub entity_name: String, } // Types used by all command functions pub type Error = Box; pub type Context<'a> = poise::Context<'a, Data, Error>; - -#[instrument(skip(ctx),level="info")] +#[instrument(skip(ctx), level = "info")] #[poise::command(slash_command, prefix_command)] async fn age( ctx: Context<'_>, @@ -30,88 +30,96 @@ async fn age( Ok(()) } -pub fn start_bot(config: Config, rx: oneshot::Receiver<()>){ +pub fn start_bot(config: Config, rx: oneshot::Receiver<()>) { if config.token != "" { - spawn_blocking(move||{ + spawn_blocking(move || { let rt = tokio::runtime::Handle::current(); - rt.block_on(async move{ + 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()], - prefix_options: poise::PrefixFrameworkOptions { - prefix: Some(prefix.into()), + 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() - }, - 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(), + }) + .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"); + .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 running..."); - if let Err(why) = client_start.await { - println!("Client error: {why:?}"); - } - println!("Bot is stopped..."); - }).await; + println!("Bot is stopped..."); + }) + .await; }) }); } -} \ No newline at end of file +} diff --git a/src/domain/meme/answer.rs b/src/domain/meme/answer.rs new file mode 100644 index 0000000..8f0c2ae --- /dev/null +++ b/src/domain/meme/answer.rs @@ -0,0 +1,121 @@ +use crate::img::config_file::{ConfigFile, KeyWordItem}; +use poise::serenity_prelude::{model::colour, CreateAttachment, CreateEmbed, CreateEmbedFooter}; +use rand::Rng; +use tokio::fs::File; +use tracing::error; +use tracing::instrument; + +pub enum AnswerError { + ConfigGlobal, + ConfigImgGlobal, + KeyWordItem, + UnknownError(String), +} + +pub enum AnswerResult { + Embed(Vec, Vec), + Animated(Vec), +} + +#[instrument(level = "info", skip(config_img, base_path))] +pub async fn answer_meme( + user_id: u64, + guild_id: u64, + keyword: String, + base_path: String, + config_img: ConfigFile, + bot_entity: String, +) -> Result { + let footer = CreateEmbedFooter::new(bot_entity); + + let folder_container = match config_img + .keyword + .iter() + .find(|keyword_under_search| keyword_under_search.does_value_match(keyword.clone())) + { + Some(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(AnswerResult::Embed( + vec![CreateEmbed::new() + .title("No match found") + .footer(footer) + .color(colour::Color::RED)], + vec![], + )); + } + }; + let path = format!("{}/{}", base_path, 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 => { + error!(folder = path, "Couldn't find a file to send"); + return Ok(AnswerResult::Embed( + vec![CreateEmbed::new() + .title("Couldn't find a file to send, please contact your administrator") + .footer(footer) + .color(colour::Color::RED)], + vec![], + )); + } + }; + let file_path = format!("{}/{}", path, filename); + let file = match File::open(file_path.clone()).await { + Ok(file) => file, + Err(e) => { + error!( + folder = file_path, + error = e.to_string(), + "Couldn't open file" + ); + return Ok(AnswerResult::Embed( + vec![CreateEmbed::new() + .title("Couldn't find file, please contact your administrator") + .footer(footer) + .color(colour::Color::RED)], + vec![], + )); + } + }; + let attachment = match CreateAttachment::file(&file, filename).await { + Ok(attachment) => attachment, + Err(e) => { + error!( + folder = file_path, + error = e.to_string(), + "Couldn't create attachment" + ); + return Ok(AnswerResult::Embed( + vec![CreateEmbed::new() + .title("Couldn't create attachment, please contact your administrator") + .footer(footer) + .color(colour::Color::RED)], + vec![], + )); + } + }; + if filename.ends_with(".mp3") || filename.ends_with(".mp4") { + return Ok(AnswerResult::Animated(vec![attachment])); + } + let embed = CreateEmbed::new() + .title(keyword) + .footer(footer) + .image(format!("attachment://{}", filename)); + Ok(AnswerResult::Embed(vec![embed], vec![attachment])) +} diff --git a/src/domain/meme/mod.rs b/src/domain/meme/mod.rs index 17afe5f..5f77f8c 100644 --- a/src/domain/meme/mod.rs +++ b/src/domain/meme/mod.rs @@ -1 +1,2 @@ -pub mod change_auto_meme; \ No newline at end of file +pub mod change_auto_meme; +pub mod answer; \ No newline at end of file diff --git a/src/tracing/init.rs b/src/tracing/init.rs index eb0bfc3..b88b2d3 100644 --- a/src/tracing/init.rs +++ b/src/tracing/init.rs @@ -1,5 +1,6 @@ -use std::{vec,fs::File,sync::Arc}; +use std::{fs::File, sync::Arc, vec}; +use super::tracing_kind::{Tracing, TracingKind}; use opentelemetry::KeyValue; use opentelemetry_otlp::WithExportConfig; use opentelemetry_sdk::{runtime, trace, Resource}; @@ -8,12 +9,11 @@ use tracing::level_filters::LevelFilter; use tracing::subscriber; use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer}; use tracing_opentelemetry::OpenTelemetryLayer; -use tracing_subscriber::{filter, fmt, Layer, Registry}; -use super::tracing_kind::{Tracing, TracingKind}; use tracing_subscriber::fmt::time::UtcTime; use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt; +use tracing_subscriber::{fmt, EnvFilter, Layer, Registry}; -pub fn init_tracing(tracing_config: Vec,name: String){ +pub fn init_tracing(tracing_config: Vec, name: String) { let mut layers = vec![]; for config in tracing_config { match config.kind { @@ -24,7 +24,8 @@ pub fn init_tracing(tracing_config: Vec,name: String){ .append(true) .open("trace.log") .expect("Failed to create trace.log"); - let formating_layer = BunyanFormattingLayer::new(name.clone(), Arc::new(file)).boxed(); + let formating_layer = + BunyanFormattingLayer::new(name.clone(), Arc::new(file)).boxed(); layers.push(JsonStorageLayer.boxed()); layers.push(formating_layer); } @@ -32,42 +33,55 @@ pub fn init_tracing(tracing_config: Vec,name: String){ let time_format = format_description::parse("[hour]:[minute]:[second]") .expect("format string should be valid!"); let timer = UtcTime::new(time_format); - + let env_filter = EnvFilter::builder() + .with_default_directive(LevelFilter::from(config.level).into()) + .from_env() + .unwrap() + .add_directive("serenity=error".parse().unwrap()); let terminal_out = fmt::layer() .with_thread_names(true) .with_timer(timer) .with_target(false) - .with_filter(LevelFilter::from(config.level)).boxed(); + .with_filter(env_filter) + .boxed(); layers.push(terminal_out); } TracingKind::Otel => { - let endpoint = match config.additional.get("endpoint"){ + let endpoint = match config.additional.get("endpoint") { Some(endpoint) => endpoint.to_string(), - None => "http://localhost:4317".to_string() + None => "http://localhost:4317".to_string(), }; - let pod_name = std::env::var("POD_NAME").unwrap_or_else(|_| "not_a_pod".to_string()); + let pod_name = + std::env::var("POD_NAME").unwrap_or_else(|_| "not_a_pod".to_string()); let telemetry = opentelemetry_otlp::new_pipeline() .tracing() - .with_exporter(opentelemetry_otlp::new_exporter().tonic().with_endpoint(endpoint)) + .with_exporter( + opentelemetry_otlp::new_exporter() + .tonic() + .with_endpoint(endpoint), + ) .with_trace_config(trace::config().with_resource(Resource::new(vec![ KeyValue::new("service.name", name.clone()), KeyValue::new("service.pod", pod_name.clone()), ]))) .install_batch(runtime::Tokio) .expect("Failed to install opentelemetry"); - let tele_layer = OpenTelemetryLayer::new(telemetry).with_filter(filter::LevelFilter::from(config.level)); + let env_filter = EnvFilter::builder() + .with_default_directive(LevelFilter::from(config.level).into()) + .from_env() + .unwrap() + .add_directive("serenity=error".parse().unwrap()); + let tele_layer = OpenTelemetryLayer::new(telemetry).with_filter(env_filter); layers.push(tele_layer.boxed()); - println!("Otel Tracing not implemented yet"); } } } subscriber::set_global_default(Registry::default().with(layers)) - .expect("setting default subscriber failed"); + .expect("setting default subscriber failed"); } - -pub fn stop_tracing(tracing_config: Vec,_name: String){ - if tracing_config.iter().any(|x| x.kind == TracingKind::Otel){ +pub fn stop_tracing(tracing_config: Vec, _name: String) { + if tracing_config.iter().any(|x| x.kind == TracingKind::Otel) { opentelemetry::global::shutdown_tracer_provider(); } -} \ No newline at end of file +}