From 8fb0ab88355b31083fe14d729b411d4bb8f98118 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 6 Jul 2022 22:01:40 +0200 Subject: [PATCH] feat: `message-responders` & `reload` command --- Cargo.lock | 27 ++++++++ Cargo.toml | 1 + configuration.schema.json | 1 - src/configuration.rs | 22 +++--- src/main.rs | 138 +++++++++++++++++++++++++------------- 5 files changed, 129 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 68a8818..adc3d20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + [[package]] name = "async-trait" version = "0.1.56" @@ -606,6 +615,23 @@ dependencies = [ "getrandom", ] +[[package]] +name = "regex" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + [[package]] name = "reqwest" version = "0.11.11" @@ -653,6 +679,7 @@ version = "0.1.0" dependencies = [ "chrono", "log", + "regex", "serde", "serde_json", "serenity", diff --git a/Cargo.toml b/Cargo.toml index 8c65cd1..608ddb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tokio = { version = "1.0", features = ["rt-multi-thread"] } log = "0.4" +regex = "1.0" chrono = "0.4" [dependencies.serenity] diff --git a/configuration.schema.json b/configuration.schema.json index c0ecdf0..89540f1 100644 --- a/configuration.schema.json +++ b/configuration.schema.json @@ -33,7 +33,6 @@ "type": "array", "items": { "type": "object", - "required": ["channels", "message"], "properties": { "channels": { "$ref": "#/$defs/channels", diff --git a/src/configuration.rs b/src/configuration.rs index 9674f91..1dae8a7 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -10,16 +10,16 @@ pub struct BotConfiguration { pub discord_authorization_token: String, pub administrators: Administrators, #[serde(rename = "thread-introductions")] - pub thread_introductions: Option>, + pub thread_introductions: Vec, #[serde(rename = "message-responders")] - pub message_responders: Option>, + pub message_responders: Vec, } #[derive(Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Administrators { pub roles: Vec, - pub users: Option>, + pub users: Vec, } #[derive(Serialize, Deserialize)] @@ -32,26 +32,26 @@ pub struct Introduction { #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MessageResponder { - pub includes: Option, - pub excludes: Option, - pub condition: Option, + pub includes: Includes, + pub excludes: Excludes, + pub condition: Condition, pub message: String, } #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Includes { - pub channels: Option>, + pub channels: Vec, #[serde(rename = "match")] - pub match_field: Option>, + pub match_field: Vec, } #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Excludes { - pub roles: Option>, + pub roles: Vec, #[serde(rename = "match")] - pub match_field: Option>, + pub match_field: Vec, } #[derive(Serialize, Deserialize)] @@ -64,7 +64,7 @@ pub struct Condition { #[serde(rename_all = "camelCase")] pub struct User { #[serde(rename = "server-age")] - pub server_age: Option, + pub server_age: i64, } impl BotConfiguration { diff --git a/src/main.rs b/src/main.rs index 8ee6bc9..e667614 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,15 @@ use std::sync::Arc; +use chrono::{DateTime, Duration, NaiveDateTime, Utc}; use configuration::BotConfiguration; -use log::{error, info, trace, LevelFilter}; +use log::{error, info, trace, warn, 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::interaction::{Interaction, InteractionResponseType}; -use serenity::model::Timestamp; +use serenity::model::prelude::interaction::{Interaction, InteractionResponseType, MessageFlags}; use serenity::prelude::{GatewayIntents, RwLock, TypeMapKey}; use serenity::{async_trait, Client}; mod configuration; @@ -33,37 +34,68 @@ async fn get_configuration_lock(ctx: &Context) -> Arc> .clone() } +fn contains_match(strings: &Vec, text: &String) -> bool { + strings.iter().any(|regex| Regex::new(regex).unwrap().is_match(&text)) +} + #[async_trait] 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" => { - trace!("{:?} reloading configuration.", command.user); + let member = command.member.as_ref().unwrap(); + let user_id = member.user.id.0; + + let administrators = &configuration.administrators; - let configuration_lock = get_configuration_lock(&ctx).await; + let mut permission_granted = false; - let mut configuration = configuration_lock.write().await; + // 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 new_config = - BotConfiguration::load().expect("Could not load configuration."); + let new_config = + BotConfiguration::load().expect("Could not load configuration."); - configuration.administrators = new_config.administrators; - configuration.message_responders = new_config.message_responders; - configuration.thread_introductions = new_config.thread_introductions; + configuration.administrators = new_config.administrators; + configuration.message_responders = new_config.message_responders; + configuration.thread_introductions = new_config.thread_introductions; - "Successfully reload configuration.".to_string() + "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(), }; + // send the response if let Err(why) = command .create_interaction_response(&ctx.http, |response| { response .kind(InteractionResponseType::ChannelMessageWithSource) - .interaction_response_data(|message| message.content(content)) + .interaction_response_data(|message| { + message.content(content).flags(MessageFlags::EPHEMERAL) + }) }) .await { @@ -73,32 +105,48 @@ impl EventHandler for Handler { } async fn message(&self, ctx: Context, msg: Message) { - trace!("Received message: {}", msg.content); + if msg.guild_id.is_none() || msg.author.bot { + return; + } - let configuration_lock = get_configuration_lock(&ctx).await; - let configuration = configuration_lock.read().await; + trace!("Received message: {}", msg.content); - if let Some(message_responders) = &configuration.message_responders { - if let Some(responder) = message_responders.iter().find(|responder| { - responder.includes.iter().any(|include| { - include.channels.iter().any(|channel| todo!("Implement inclusion check")) - }) && responder.excludes.iter().all(|exclude| todo!("Implement exclusion check")) - }) { - if let Some(condition) = &responder.condition { - let join_date = ctx - .http - .get_member(msg.guild_id.unwrap().0, msg.author.id.0) - .await - .unwrap() - .joined_at - .unwrap(); - - let member_age = Timestamp::now().unix_timestamp() - join_date.unix_timestamp(); - - if let Some(age) = condition.user.server_age { - todo!("Implement age check") - } + if let Some(response) = + 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 + responder.includes.channels.iter().any(|&channel_id| channel_id == msg.channel_id.0) + // check if the message was sent by a user that is not excluded from the responder + && !responder.excludes.roles.iter().any(|&role_id| role_id == msg.author.id.0) + // check if the message does not match any of the excludes + && !contains_match(&responder.excludes.match_field, &msg.content) + // check if the message matches any of the includes + && contains_match(&responder.includes.match_field, &msg.content) + }, + ) { + let min_age = response.condition.user.server_age; + + if min_age != 0 { + let joined_at = ctx + .http + .get_member(msg.guild_id.unwrap().0, msg.author.id.0) + .await + .unwrap() + .joined_at + .unwrap() + .unix_timestamp(); + + let must_joined_at = + DateTime::::from_utc(NaiveDateTime::from_timestamp(joined_at, 0), Utc); + let but_joined_at = Utc::now() - Duration::days(min_age); + + if must_joined_at <= but_joined_at { + return; } + + msg.reply_ping(&ctx.http, &response.message) + .await + .expect("Could not reply to message author."); } } } @@ -108,17 +156,11 @@ impl EventHandler for Handler { let configuration_lock = get_configuration_lock(&ctx).await; let configuration = configuration_lock.read().await; - - if let Some(introducers) = &configuration.thread_introductions { - if let Some(introducer) = introducers.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 { - error!("Error sending message: {:?}", why); - } + 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 { + error!("Error sending message: {:?}", why); } } } @@ -137,7 +179,7 @@ impl EventHandler for Handler { #[tokio::main] async fn main() { log::set_logger(&LOGGER) - .map(|()| log::set_max_level(LevelFilter::Info)) + .map(|()| log::set_max_level(LevelFilter::Warn)) .expect("Could not set logger."); let configuration = BotConfiguration::load().expect("Failed to load configuration");