use std::time::Duration;
use serde::de::{Deserializer, Error};
use serde::ser::Serializer;
use serde::{Deserialize, Serialize};
use crate::model::id::*;
#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[non_exhaustive]
pub struct Rule {
pub id: RuleId,
pub guild_id: GuildId,
pub name: String,
pub creator_id: UserId,
pub event_type: EventType,
#[serde(flatten)]
pub trigger: Trigger,
pub actions: Vec<Action>,
pub enabled: bool,
pub exempt_roles: Vec<RoleId>,
pub exempt_channels: Vec<ChannelId>,
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
#[serde(from = "u8", into = "u8")]
#[non_exhaustive]
pub enum EventType {
MessageSend,
Unknown(u8),
}
impl From<u8> for EventType {
fn from(value: u8) -> Self {
match value {
1 => Self::MessageSend,
_ => Self::Unknown(value),
}
}
}
impl From<EventType> for u8 {
fn from(value: EventType) -> Self {
match value {
EventType::MessageSend => 1,
EventType::Unknown(unknown) => unknown,
}
}
}
#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Trigger {
Keyword {
strings: Vec<String>,
regex_patterns: Vec<String>,
allow_list: Vec<String>,
},
Spam,
KeywordPreset {
presets: Vec<KeywordPresetType>,
allow_list: Vec<String>,
},
MentionSpam {
mention_total_limit: u8,
},
Unknown(u8),
}
#[derive(Deserialize, Serialize)]
#[serde(rename = "Trigger")]
struct InterimTrigger {
#[serde(rename = "trigger_type")]
kind: TriggerType,
#[serde(rename = "trigger_metadata")]
metadata: TriggerMetadata,
}
impl<'de> Deserialize<'de> for Trigger {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let trigger = InterimTrigger::deserialize(deserializer)?;
let trigger = match trigger.kind {
TriggerType::Keyword => Self::Keyword {
strings: trigger
.metadata
.keyword_filter
.ok_or_else(|| Error::missing_field("keyword_filter"))?,
regex_patterns: trigger
.metadata
.regex_patterns
.ok_or_else(|| Error::missing_field("regex_patterns"))?,
allow_list: trigger
.metadata
.allow_list
.ok_or_else(|| Error::missing_field("allow_list"))?,
},
TriggerType::Spam => Self::Spam,
TriggerType::KeywordPreset => Self::KeywordPreset {
presets: trigger.metadata.presets.ok_or_else(|| Error::missing_field("presets"))?,
allow_list: trigger
.metadata
.allow_list
.ok_or_else(|| Error::missing_field("allow_list"))?,
},
TriggerType::MentionSpam => Self::MentionSpam {
mention_total_limit: trigger
.metadata
.mention_total_limit
.ok_or_else(|| Error::missing_field("mention_total_limit"))?,
},
TriggerType::Unknown(unknown) => Self::Unknown(unknown),
};
Ok(trigger)
}
}
impl Serialize for Trigger {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut trigger = InterimTrigger {
kind: self.kind(),
metadata: TriggerMetadata {
keyword_filter: None,
regex_patterns: None,
presets: None,
allow_list: None,
mention_total_limit: None,
},
};
match self {
Self::Keyword {
strings,
regex_patterns,
allow_list,
} => {
trigger.metadata.keyword_filter = Some(strings.clone());
trigger.metadata.regex_patterns = Some(regex_patterns.clone());
trigger.metadata.allow_list = Some(allow_list.clone());
},
Self::KeywordPreset {
presets,
allow_list,
} => {
trigger.metadata.presets = Some(presets.clone());
trigger.metadata.allow_list = Some(allow_list.clone());
},
Self::MentionSpam {
mention_total_limit,
} => trigger.metadata.mention_total_limit = Some(*mention_total_limit),
Self::Spam | Self::Unknown(_) => {},
}
trigger.serialize(serializer)
}
}
impl Trigger {
#[must_use]
pub fn kind(&self) -> TriggerType {
match self {
Self::Keyword {
..
} => TriggerType::Keyword,
Self::Spam => TriggerType::Spam,
Self::KeywordPreset {
..
} => TriggerType::KeywordPreset,
Self::MentionSpam {
..
} => TriggerType::MentionSpam,
Self::Unknown(unknown) => TriggerType::Unknown(*unknown),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
#[serde(from = "u8", into = "u8")]
#[non_exhaustive]
pub enum TriggerType {
Keyword,
Spam,
KeywordPreset,
MentionSpam,
Unknown(u8),
}
impl From<u8> for TriggerType {
fn from(value: u8) -> Self {
match value {
1 => Self::Keyword,
3 => Self::Spam,
4 => Self::KeywordPreset,
5 => Self::MentionSpam,
_ => Self::Unknown(value),
}
}
}
impl From<TriggerType> for u8 {
fn from(value: TriggerType) -> Self {
match value {
TriggerType::Keyword => 1,
TriggerType::Spam => 3,
TriggerType::KeywordPreset => 4,
TriggerType::MentionSpam => 5,
TriggerType::Unknown(unknown) => unknown,
}
}
}
#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[non_exhaustive]
pub struct TriggerMetadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub keyword_filter: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub regex_patterns: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub presets: Option<Vec<KeywordPresetType>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_list: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mention_total_limit: Option<u8>,
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
#[serde(from = "u8", into = "u8")]
#[non_exhaustive]
pub enum KeywordPresetType {
Profanity,
SexualContent,
Slurs,
Unknown(u8),
}
impl From<u8> for KeywordPresetType {
fn from(value: u8) -> Self {
match value {
1 => Self::Profanity,
2 => Self::SexualContent,
3 => Self::Slurs,
_ => Self::Unknown(value),
}
}
}
impl From<KeywordPresetType> for u8 {
fn from(value: KeywordPresetType) -> Self {
match value {
KeywordPresetType::Profanity => 1,
KeywordPresetType::SexualContent => 2,
KeywordPresetType::Slurs => 3,
KeywordPresetType::Unknown(unknown) => unknown,
}
}
}
#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Action {
BlockMessage {
custom_message: Option<String>,
},
Alert(ChannelId),
Timeout(Duration),
Unknown(u8),
}
#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct ActionExecution {
pub guild_id: GuildId,
pub action: Action,
pub rule_id: RuleId,
#[serde(rename = "rule_trigger_type")]
pub trigger_type: TriggerType,
pub user_id: UserId,
pub channel_id: Option<ChannelId>,
pub message_id: Option<MessageId>,
pub alert_system_message_id: Option<MessageId>,
pub content: String,
pub matched_keyword: Option<String>,
pub matched_content: Option<String>,
}
#[derive(Default, Deserialize, Serialize)]
struct RawActionMetadata {
#[serde(skip_serializing_if = "Option::is_none")]
channel_id: Option<ChannelId>,
#[serde(skip_serializing_if = "Option::is_none")]
duration_seconds: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
custom_message: Option<String>,
}
#[derive(Deserialize, Serialize)]
struct RawAction {
#[serde(rename = "type")]
kind: ActionType,
#[serde(skip_serializing_if = "Option::is_none")]
metadata: Option<RawActionMetadata>,
}
impl<'de> Deserialize<'de> for Action {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let action = RawAction::deserialize(deserializer)?;
Ok(match action.kind {
ActionType::BlockMessage => Action::BlockMessage {
custom_message: action.metadata.and_then(|m| m.custom_message),
},
ActionType::Alert => Action::Alert(
action
.metadata
.ok_or_else(|| Error::missing_field("metadata"))?
.channel_id
.ok_or_else(|| Error::missing_field("channel_id"))?,
),
ActionType::Timeout => Action::Timeout(Duration::from_secs(
action
.metadata
.ok_or_else(|| Error::missing_field("metadata"))?
.duration_seconds
.ok_or_else(|| Error::missing_field("duration_seconds"))?,
)),
ActionType::Unknown(unknown) => Action::Unknown(unknown),
})
}
}
impl Serialize for Action {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let action = match self.clone() {
Action::BlockMessage {
custom_message,
} => RawAction {
kind: ActionType::BlockMessage,
metadata: Some(RawActionMetadata {
custom_message,
..Default::default()
}),
},
Action::Alert(channel_id) => RawAction {
kind: ActionType::Alert,
metadata: Some(RawActionMetadata {
channel_id: Some(channel_id),
..Default::default()
}),
},
Action::Timeout(duration) => RawAction {
kind: ActionType::Timeout,
metadata: Some(RawActionMetadata {
duration_seconds: Some(duration.as_secs()),
..Default::default()
}),
},
Action::Unknown(n) => RawAction {
kind: ActionType::Unknown(n),
metadata: None,
},
};
action.serialize(serializer)
}
}
impl Action {
#[must_use]
pub fn kind(&self) -> ActionType {
match self {
Self::BlockMessage {
..
} => ActionType::BlockMessage,
Self::Alert(_) => ActionType::Alert,
Self::Timeout(_) => ActionType::Timeout,
Self::Unknown(unknown) => ActionType::Unknown(*unknown),
}
}
}
enum_number! {
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
#[serde(from = "u8", into = "u8")]
#[non_exhaustive]
pub enum ActionType {
BlockMessage = 1,
Alert = 2,
Timeout = 3,
_ => Unknown(u8),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::json::{assert_json, json};
#[test]
fn rule_trigger_serde() {
#[derive(Debug, PartialEq, Deserialize, Serialize)]
struct Rule {
#[serde(flatten)]
trigger: Trigger,
}
assert_json(
&Rule {
trigger: Trigger::Keyword {
strings: vec![String::from("foo"), String::from("bar")],
regex_patterns: vec![String::from("d[i1]ck")],
allow_list: vec![String::from("duck")],
},
},
json!({"trigger_type": 1, "trigger_metadata": {"keyword_filter": ["foo", "bar"], "regex_patterns": ["d[i1]ck"], "allow_list": ["duck"]}}),
);
assert_json(
&Rule {
trigger: Trigger::Spam,
},
json!({"trigger_type": 3, "trigger_metadata": {}}),
);
assert_json(
&Rule {
trigger: Trigger::KeywordPreset {
presets: vec![
KeywordPresetType::Profanity,
KeywordPresetType::SexualContent,
KeywordPresetType::Slurs,
],
allow_list: vec![String::from("boob")],
},
},
json!({"trigger_type": 4, "trigger_metadata": {"presets": [1,2,3], "allow_list": ["boob"]}}),
);
assert_json(
&Rule {
trigger: Trigger::MentionSpam {
mention_total_limit: 7,
},
},
json!({"trigger_type": 5, "trigger_metadata": {"mention_total_limit": 7}}),
);
assert_json(
&Rule {
trigger: Trigger::Unknown(123),
},
json!({"trigger_type": 123, "trigger_metadata": {}}),
);
}
#[test]
fn action_serde() {
assert_json(
&Action::BlockMessage {
custom_message: None,
},
json!({"type": 1, "metadata": {}}),
);
assert_json(
&Action::Alert(ChannelId::new(123)),
json!({"type": 2, "metadata": {"channel_id": "123"}}),
);
assert_json(
&Action::Timeout(Duration::from_secs(1024)),
json!({"type": 3, "metadata": {"duration_seconds": 1024}}),
);
assert_json(&Action::Unknown(123), json!({"type": 123}));
}
}