Skip to content
This repository has been archived by the owner on Aug 7, 2024. It is now read-only.

Commit

Permalink
feat: message-responders & reload command
Browse files Browse the repository at this point in the history
  • Loading branch information
oSumAtrIX committed Jul 6, 2022
1 parent 216df9d commit 8fb0ab8
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 60 deletions.
27 changes: 27 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
1 change: 0 additions & 1 deletion configuration.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
"type": "array",
"items": {
"type": "object",
"required": ["channels", "message"],
"properties": {
"channels": {
"$ref": "#/$defs/channels",
Expand Down
22 changes: 11 additions & 11 deletions src/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ pub struct BotConfiguration {
pub discord_authorization_token: String,
pub administrators: Administrators,
#[serde(rename = "thread-introductions")]
pub thread_introductions: Option<Vec<Introduction>>,
pub thread_introductions: Vec<Introduction>,
#[serde(rename = "message-responders")]
pub message_responders: Option<Vec<MessageResponder>>,
pub message_responders: Vec<MessageResponder>,
}

#[derive(Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Administrators {
pub roles: Vec<u64>,
pub users: Option<Vec<u64>>,
pub users: Vec<u64>,
}

#[derive(Serialize, Deserialize)]
Expand All @@ -32,26 +32,26 @@ pub struct Introduction {
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MessageResponder {
pub includes: Option<Includes>,
pub excludes: Option<Excludes>,
pub condition: Option<Condition>,
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<Vec<u64>>,
pub channels: Vec<u64>,
#[serde(rename = "match")]
pub match_field: Option<Vec<String>>,
pub match_field: Vec<String>,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Excludes {
pub roles: Option<Vec<u64>>,
pub roles: Vec<u64>,
#[serde(rename = "match")]
pub match_field: Option<Vec<String>>,
pub match_field: Vec<String>,
}

#[derive(Serialize, Deserialize)]
Expand All @@ -64,7 +64,7 @@ pub struct Condition {
#[serde(rename_all = "camelCase")]
pub struct User {
#[serde(rename = "server-age")]
pub server_age: Option<i16>,
pub server_age: i64,
}

impl BotConfiguration {
Expand Down
138 changes: 90 additions & 48 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -33,37 +34,68 @@ async fn get_configuration_lock(ctx: &Context) -> Arc<RwLock<BotConfiguration>>
.clone()
}

fn contains_match(strings: &Vec<String>, 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
{
Expand All @@ -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::<Utc>::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.");
}
}
}
Expand All @@ -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);
}
}
}
Expand All @@ -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");
Expand Down

0 comments on commit 8fb0ab8

Please sign in to comment.