diff --git a/configuration.schema.json b/configuration.schema.json index 89540f1..2c2f8de 100644 --- a/configuration.schema.json +++ b/configuration.schema.json @@ -38,9 +38,9 @@ "$ref": "#/$defs/channels", "description": "A list of channel ids. The bot will only introduce in threads under these channels." }, - "message": { - "$ref": "#/$defs/message", - "description": "The message to send when the thread has been created." + "response": { + "$ref": "#/$defs/response", + "description": "The response to send when the thread has been created." } } }, @@ -91,9 +91,9 @@ }, "description": "The conditions to respond to the message." }, - "message": { - "$ref": "#/$defs/message", - "description": "The message to send when the message is responded to." + "response": { + "$ref": "#/$defs/response", + "description": "The response to send when the message is responded to." } }, "description": "The conditions to respond to a message." @@ -128,8 +128,110 @@ "uniqueItems": true, "minItems": 1 }, - "message": { - "type": "string" + "embed": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "The title of the embed." + }, + "description": { + "type": "string", + "description": "The description of the embed." + }, + "color": { + "type": "integer", + "description": "The color of the embed." + }, + "fields": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the field." + }, + "value": { + "type": "string", + "description": "The value of the field." + }, + "inline": { + "type": "boolean", + "description": "Whether the field is inline." + } + }, + "description": "The field to add to the embed." + }, + "description": "The fields to add to the embed." + }, + "image": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "The url of the image." + } + }, + "description": "The image to add to the embed." + }, + "thumbnail": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "The url of the thumbnail." + } + }, + "description": "The thumbnail to add to the embed." + }, + "author": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the author." + }, + "url": { + "type": "string", + "description": "The url of the author." + }, + "icon_url": { + "type": "string", + "description": "The url of the author's icon." + } + }, + "description": "The author to add to the embed." + }, + "footer": { + "type": "object", + "properties": { + "text": { + "type": "string", + "description": "The text of the footer." + }, + "icon_url": { + "type": "string", + "description": "The url of the footer's icon." + } + }, + "description": "The footer to add to the embed." + } + } + }, + "response": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "The message. Can be empty if the embed is not empty" + }, + "embed": { + "$ref": "#/$defs/embed", + "description": "The embed to send." + } + }, + } } } } diff --git a/src/configuration.rs b/src/configuration.rs index 1dae8a7..69870c4 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -26,7 +26,7 @@ pub struct Administrators { #[serde(rename_all = "camelCase")] pub struct Introduction { pub channels: Vec, - pub message: String, + pub response: Response, } #[derive(Serialize, Deserialize)] @@ -35,7 +35,64 @@ pub struct MessageResponder { pub includes: Includes, pub excludes: Excludes, pub condition: Condition, - pub message: String, + pub response: Response, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Response { + pub message: Option, + pub embed: Option, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Embed { + pub title: String, + pub description: String, + pub color: i32, + pub fields: Vec, + pub footer: Footer, + pub image: Image, + pub thumbnail: Thumbnail, + pub author: Author, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Field { + pub name: String, + pub value: String, + pub inline: bool, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Footer { + pub text: String, + #[serde(rename = "icon_url")] + pub icon_url: String, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Image { + pub url: String, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Thumbnail { + pub url: String, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Author { + pub name: String, + #[serde(rename = "icon_url")] + pub icon_url: String, + pub url: String, } #[derive(Serialize, Deserialize)] diff --git a/src/main.rs b/src/main.rs index ebe25e1..7fa13d4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,13 +2,13 @@ use std::sync::Arc; use chrono::{DateTime, Duration, NaiveDateTime, Utc}; use configuration::BotConfiguration; -use log::{error, info, trace, warn, LevelFilter}; +use log::{error, info, trace, LevelFilter}; use logger::logging::SimpleLogger; use regex::Regex; use serenity::client::{Context, EventHandler}; -use serenity::model::application::command::Command; use serenity::model::channel::{GuildChannel, Message}; use serenity::model::gateway::Ready; +use serenity::model::prelude::command::Command; use serenity::model::prelude::interaction::{Interaction, InteractionResponseType, MessageFlags}; use serenity::prelude::{GatewayIntents, RwLock, TypeMapKey}; use serenity::{async_trait, Client}; @@ -47,34 +47,34 @@ impl EventHandler for Handler { async fn interaction_create(&self, ctx: Context, interaction: Interaction) { trace!("Created an interaction: {:?}", interaction); - let configuration_lock = get_configuration_lock(&ctx).await; - let mut configuration = configuration_lock.write().await; - if let Interaction::ApplicationCommand(command) = interaction { - let content = match command.data.name.as_str() { - "reload" => { - let member = command.member.as_ref().unwrap(); - let user_id = member.user.id.0; - - let administrators = &configuration.administrators; - - let mut permission_granted = false; - - // check if the user is an administrator - if administrators.users.iter().any(|&id| user_id == id) { - permission_granted = true - } - // check if the user has an administrating role - if !permission_granted - && administrators.roles.iter().any(|role_id| { - member.roles.iter().any(|member_role| member_role == role_id) - }) { - permission_granted = true - } - - // if permission is granted, reload the configuration - if permission_granted { - trace!("{:?} reloading configuration.", command.user); + let configuration_lock = get_configuration_lock(&ctx).await; + let mut configuration = configuration_lock.write().await; + + let administrators = &configuration.administrators; + let member = command.member.as_ref().unwrap(); + let user_id = member.user.id.0; + let mut stop_command = false; + let mut permission_granted = false; + + // check if the user is an administrator + if administrators.users.iter().any(|&id| user_id == id) { + permission_granted = true + } + // check if the user has an administrating role + if !permission_granted + && administrators + .roles + .iter() + .any(|role_id| member.roles.iter().any(|member_role| member_role == role_id)) + { + permission_granted = true + } + + let content = if permission_granted { + match command.data.name.as_str() { + "reload" => { + trace!("{:?} reloaded the configuration.", command.user); let new_config = load_configuration(); @@ -83,12 +83,16 @@ impl EventHandler for Handler { configuration.thread_introductions = new_config.thread_introductions; "Successfully reloaded configuration.".to_string() - } else { - // else return an error message - "You do not have permission to use this command.".to_string() - } - }, - _ => "Unknown command.".to_string(), + }, + "stop" => { + trace!("{:?} stopped the bot.", command.user); + stop_command = true; + "Stopped the bot.".to_string() + }, + _ => "Unknown command.".to_string(), + } + } else { + "You do not have permission to use this command.".to_string() }; // send the response @@ -104,6 +108,10 @@ impl EventHandler for Handler { { error!("Cannot respond to slash command: {}", why); } + + if stop_command { + std::process::exit(0); + } } } @@ -113,7 +121,7 @@ impl EventHandler for Handler { return; } - if let Some(response) = + if let Some(responder) = get_configuration_lock(&ctx).await.read().await.message_responders.iter().find( |&responder| { // check if the message was sent in a channel that is included in the responder @@ -126,7 +134,7 @@ impl EventHandler for Handler { && contains_match(&responder.includes.match_field, &msg.content) }, ) { - let min_age = response.condition.user.server_age; + let min_age = responder.condition.user.server_age; if min_age != 0 { let joined_at = ctx @@ -146,7 +154,30 @@ impl EventHandler for Handler { return; } - msg.reply_ping(&ctx.http, &response.message) + msg.channel_id + .send_message(&ctx.http, |m| { + m.reference_message(&msg); + match &responder.response.embed { + Some(embed) => m.embed(|e| { + e.title(&embed.title) + .description(&embed.description) + .color(embed.color) + .fields(embed.fields.iter().map(|field| { + (field.name.clone(), field.value.clone(), field.inline) + })) + .footer(|f| { + f.text(&embed.footer.text); + f.icon_url(&embed.footer.icon_url) + }) + .thumbnail(&embed.thumbnail.url) + .image(&embed.image.url) + .author(|a| { + a.name(&embed.author.name).icon_url(&embed.author.icon_url) + }) + }), + None => m.content(responder.response.message.as_ref().unwrap()), + } + }) .await .expect("Could not reply to message author."); } @@ -162,7 +193,9 @@ impl EventHandler for Handler { if let Some(introducer) = &configuration.thread_introductions.iter().find(|introducer| { introducer.channels.iter().any(|channel_id| *channel_id == thread.parent_id.unwrap().0) }) { - if let Err(why) = thread.say(&ctx.http, &introducer.message).await { + if let Err(why) = + thread.say(&ctx.http, &introducer.response.message.as_ref().unwrap()).await + { error!("Error sending message: {:?}", why); } } @@ -171,11 +204,15 @@ impl EventHandler for Handler { async fn ready(&self, ctx: Context, ready: Ready) { info!("Connected as {}", ready.user.name); - Command::create_global_application_command(&ctx.http, |command| { - command.name("reload").description("Reloads the configuration.") - }) - .await - .expect("Could not create command."); + for (cmd, description) in + [("repload", "Reloads the configuration."), ("stop", "Stop the Discord bot.")] + { + Command::create_global_application_command(&ctx.http, |command| { + command.name(cmd).description(description) + }) + .await + .expect("Could not create command."); + } } }