use serde::{Deserialize, Serialize};
use crate::config::schema::{ALLOW_EVERY_USER, ChatConfig};
use crate::session::commands::is_addressed_to_bot;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RawAttachment {
pub id: String,
pub name: String,
pub url: String,
pub size: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_type: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RawMessage {
pub id: String,
pub author_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub author_name: Option<String>,
#[serde(default)]
pub author_is_bot: bool,
pub channel_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_channel_id: Option<String>,
#[serde(default)]
pub content: String,
#[serde(default)]
pub attachments: Vec<RawAttachment>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RawDeletion {
pub id: String,
pub channel_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_channel_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum InboundDecision {
Ignore {
reason: &'static str,
},
Start,
Thread { thread_id: String },
}
#[derive(Debug, Clone, PartialEq)]
pub enum DeletionDecision {
Ignore { reason: &'static str },
Withdraw {
message_id: String,
thread_id: Option<String>,
},
}
pub fn mentions_bot(content: &str, bot_id: &str) -> bool {
let both = [format!("<@{bot_id}>"), format!("<@!{bot_id}>")];
both.iter()
.any(|mention| content.contains(mention.as_str()))
}
pub fn without_bot_mention(content: &str, bot_id: &str) -> String {
let stripped = content
.replace(&format!("<@!{bot_id}>"), " ")
.replace(&format!("<@{bot_id}>"), " ");
stripped.split_whitespace().collect::<Vec<_>>().join(" ")
}
pub fn is_blocked(config: &ChatConfig, user_id: &str) -> bool {
config
.blocked_user_ids
.iter()
.any(|blocked| blocked == user_id)
}
pub fn is_permitted(config: &ChatConfig, user_id: &str) -> bool {
if is_blocked(config, user_id) {
return false;
}
config
.allowed_user_ids
.iter()
.any(|allowed| allowed == ALLOW_EVERY_USER || allowed == user_id)
}
pub fn classify_deletion(deletion: &RawDeletion, config: &ChatConfig) -> DeletionDecision {
let in_served_channel = deletion.channel_id == config.channel_id;
let in_served_thread =
deletion.parent_channel_id.as_deref() == Some(config.channel_id.as_str());
if !in_served_channel && !in_served_thread {
return DeletionDecision::Ignore {
reason: "outside the served channel",
};
}
DeletionDecision::Withdraw {
message_id: deletion.id.clone(),
thread_id: if in_served_thread {
Some(deletion.channel_id.clone())
} else {
None
},
}
}
pub fn classify(
message: &RawMessage,
config: &ChatConfig,
bot_id: Option<&str>,
) -> InboundDecision {
let in_served_channel = message.channel_id == config.channel_id;
let in_served_thread = message.parent_channel_id.as_deref() == Some(config.channel_id.as_str());
if message.author_is_bot {
if let Some(bot_id) = bot_id
&& message.author_id == bot_id
{
return InboundDecision::Ignore {
reason: "its own message",
};
}
if !in_served_thread {
return InboundDecision::Ignore {
reason: "authored by a bot",
};
}
}
if !in_served_channel && !in_served_thread {
return InboundDecision::Ignore {
reason: "outside the served channel",
};
}
if is_blocked(config, &message.author_id) {
return InboundDecision::Ignore {
reason: "the author is blocked",
};
}
if !is_permitted(config, &message.author_id) {
return InboundDecision::Ignore {
reason: "the author is not permitted",
};
}
if message.content.trim().is_empty() && message.attachments.is_empty() {
return InboundDecision::Ignore {
reason: "nothing was said and nothing was attached",
};
}
if in_served_thread {
return InboundDecision::Thread {
thread_id: message.channel_id.clone(),
};
}
if config.start_on_mention && !is_addressed_to_bot(&message.content) {
let Some(bot_id) = bot_id else {
return InboundDecision::Ignore {
reason: "the bot does not know its own name yet",
};
};
if !mentions_bot(&message.content, bot_id) {
return InboundDecision::Ignore {
reason: "the channel message did not mention the bot",
};
}
}
InboundDecision::Start
}
#[cfg(test)]
mod tests;