use std::borrow::Cow;
use std::fmt;
use std::time::Duration;
use serde::de::{Deserializer, Error, IgnoredAny, MapAccess};
use serde::ser::{SerializeStruct, Serializer};
use serde::{Deserialize, Serialize};
use serde_value::{DeserializerError, Value};
use crate::model::id::{ChannelId, GuildId, MessageId, RoleId, RuleId, UserId};
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
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)]
#[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,
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum Trigger {
Keyword(Vec<String>),
HarmfulLink,
Spam,
KeywordPreset(Vec<KeywordPresetType>),
Unknown(u8),
}
#[derive(Deserialize, Serialize)]
#[serde(rename = "Trigger")]
struct InterimTrigger<'a> {
#[serde(rename = "trigger_type")]
kind: TriggerType,
#[serde(rename = "trigger_metadata")]
metadata: InterimTriggerMetadata<'a>,
}
#[derive(Deserialize, Serialize)]
#[serde(rename = "TriggerMetadata")]
struct InterimTriggerMetadata<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
keyword_filter: Option<Cow<'a, [String]>>,
#[serde(skip_serializing_if = "Option::is_none")]
presets: Option<Cow<'a, [KeywordPresetType]>>,
}
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 => {
let keywords = trigger
.metadata
.keyword_filter
.ok_or_else(|| Error::missing_field("keyword_filter"))?;
Self::Keyword(keywords.into_owned())
},
TriggerType::HarmfulLink => Self::HarmfulLink,
TriggerType::Spam => Self::Spam,
TriggerType::KeywordPreset => {
let presets =
trigger.metadata.presets.ok_or_else(|| Error::missing_field("presets"))?;
Self::KeywordPreset(presets.into_owned())
},
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: InterimTriggerMetadata {
keyword_filter: None,
presets: None,
},
};
match self {
Self::Keyword(keywords) => trigger.metadata.keyword_filter = Some(keywords.into()),
Self::KeywordPreset(presets) => trigger.metadata.presets = Some(presets.into()),
_ => {},
}
trigger.serialize(serializer)
}
}
impl Trigger {
#[must_use]
pub fn kind(&self) -> TriggerType {
match self {
Self::Keyword(_) => TriggerType::Keyword,
Self::HarmfulLink => TriggerType::HarmfulLink,
Self::Spam => TriggerType::Spam,
Self::KeywordPreset(_) => TriggerType::KeywordPreset,
Self::Unknown(unknown) => TriggerType::Unknown(*unknown),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
#[serde(from = "u8", into = "u8")]
#[non_exhaustive]
pub enum TriggerType {
Keyword,
HarmfulLink,
Spam,
KeywordPreset,
Unknown(u8),
}
impl From<u8> for TriggerType {
fn from(value: u8) -> Self {
match value {
1 => Self::Keyword,
2 => Self::HarmfulLink,
3 => Self::Spam,
4 => Self::KeywordPreset,
_ => Self::Unknown(value),
}
}
}
impl From<TriggerType> for u8 {
fn from(value: TriggerType) -> Self {
match value {
TriggerType::Keyword => 1,
TriggerType::HarmfulLink => 2,
TriggerType::Spam => 3,
TriggerType::KeywordPreset => 4,
TriggerType::Unknown(unknown) => unknown,
}
}
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct TriggerMetadata {
keyword_filter: Option<Vec<String>>,
presets: Option<Vec<KeywordPresetType>>,
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
#[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,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Action {
BlockMessage,
Alert(ChannelId),
Timeout(Duration),
Unknown(u8),
}
#[derive(Clone, Debug, Deserialize, Serialize)]
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(Deserialize, Serialize)]
#[serde(rename = "ActionMetadata")]
struct Alert {
channel_id: ChannelId,
}
#[derive(Deserialize, Serialize)]
#[serde(rename = "ActionMetadata")]
struct Timeout {
#[serde(rename = "duration_seconds")]
duration: u64,
}
impl<'de> Deserialize<'de> for Action {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "snake_case")]
enum Field {
Type,
Metadata,
Unknown(String),
}
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = Action;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("automod rule action")
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
let mut kind = None;
let mut metadata = None;
while let Some(key) = map.next_key()? {
match key {
Field::Type => {
if kind.is_some() {
return Err(Error::duplicate_field("type"));
}
kind = Some(map.next_value()?);
},
Field::Metadata => {
if metadata.is_some() {
return Err(Error::duplicate_field("metadata"));
}
metadata = Some(map.next_value::<Value>()?);
},
Field::Unknown(_) => {
map.next_value::<IgnoredAny>()?;
},
}
}
let kind = kind.ok_or_else(|| Error::missing_field("type"))?;
match kind {
ActionType::BlockMessage => Ok(Action::BlockMessage),
ActionType::Alert => {
let alert: Alert = metadata
.ok_or_else(|| Error::missing_field("metadata"))?
.deserialize_into()
.map_err(DeserializerError::into_error)?;
Ok(Action::Alert(alert.channel_id))
},
ActionType::Timeout => {
let timeout: Timeout = metadata
.ok_or_else(|| Error::missing_field("metadata"))?
.deserialize_into()
.map_err(DeserializerError::into_error)?;
Ok(Action::Timeout(Duration::from_secs(timeout.duration)))
},
ActionType::Unknown(unknown) => Ok(Action::Unknown(unknown)),
}
}
}
deserializer.deserialize_any(Visitor)
}
}
impl Serialize for Action {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let has_metadata = matches!(self, Self::Alert(_) | Self::Timeout(_));
let len = 1 + usize::from(has_metadata);
let mut s = serializer.serialize_struct("Action", len)?;
s.serialize_field("type", &self.kind())?;
match *self {
Self::Alert(channel_id) => {
s.serialize_field("metadata", &Alert {
channel_id,
})?;
},
Self::Timeout(duration) => {
s.serialize_field("metadata", &Timeout {
duration: duration.as_secs(),
})?;
},
_ => {},
}
s.end()
}
}
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),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
#[serde(from = "u8", into = "u8")]
#[non_exhaustive]
pub enum ActionType {
BlockMessage,
Alert,
Timeout,
Unknown(u8),
}
impl From<u8> for ActionType {
fn from(value: u8) -> Self {
match value {
1 => Self::BlockMessage,
2 => Self::Alert,
3 => Self::Timeout,
unknown => Self::Unknown(unknown),
}
}
}
impl From<ActionType> for u8 {
fn from(value: ActionType) -> Self {
match value {
ActionType::BlockMessage => 1,
ActionType::Alert => 2,
ActionType::Timeout => 3,
ActionType::Unknown(unknown) => unknown,
}
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use serde_test::Token;
use super::*;
#[test]
fn rule_trigger_serde() {
#[derive(Debug, PartialEq, Deserialize, Serialize)]
struct Rule {
#[serde(flatten)]
trigger: Trigger,
}
let value = Rule {
trigger: Trigger::Keyword(vec![String::from("foo"), String::from("bar")]),
};
serde_test::assert_tokens(&value, &[
Token::Map {
len: None,
},
Token::Str("trigger_type"),
Token::U8(1),
Token::Str("trigger_metadata"),
Token::Struct {
name: "TriggerMetadata",
len: 1,
},
Token::Str("keyword_filter"),
Token::Some,
Token::Seq {
len: Some(2),
},
Token::Str("foo"),
Token::Str("bar"),
Token::SeqEnd,
Token::StructEnd,
Token::MapEnd,
]);
let value = Rule {
trigger: Trigger::HarmfulLink,
};
serde_test::assert_tokens(&value, &[
Token::Map {
len: None,
},
Token::Str("trigger_type"),
Token::U8(2),
Token::Str("trigger_metadata"),
Token::Struct {
name: "TriggerMetadata",
len: 0,
},
Token::StructEnd,
Token::MapEnd,
]);
let value = Rule {
trigger: Trigger::Spam,
};
serde_test::assert_tokens(&value, &[
Token::Map {
len: None,
},
Token::Str("trigger_type"),
Token::U8(3),
Token::Str("trigger_metadata"),
Token::Struct {
name: "TriggerMetadata",
len: 0,
},
Token::StructEnd,
Token::MapEnd,
]);
let value = Rule {
trigger: Trigger::KeywordPreset(vec![
KeywordPresetType::Profanity,
KeywordPresetType::SexualContent,
KeywordPresetType::Slurs,
]),
};
serde_test::assert_tokens(&value, &[
Token::Map {
len: None,
},
Token::Str("trigger_type"),
Token::U8(4),
Token::Str("trigger_metadata"),
Token::Struct {
name: "TriggerMetadata",
len: 1,
},
Token::Str("presets"),
Token::Some,
Token::Seq {
len: Some(3),
},
Token::U8(KeywordPresetType::Profanity.into()),
Token::U8(KeywordPresetType::SexualContent.into()),
Token::U8(KeywordPresetType::Slurs.into()),
Token::SeqEnd,
Token::StructEnd,
Token::MapEnd,
]);
let value = Rule {
trigger: Trigger::Unknown(123),
};
serde_test::assert_tokens(&value, &[
Token::Map {
len: None,
},
Token::Str("trigger_type"),
Token::U8(123),
Token::Str("trigger_metadata"),
Token::Struct {
name: "TriggerMetadata",
len: 0,
},
Token::StructEnd,
Token::MapEnd,
]);
}
#[test]
fn action_serde() {
let value = Action::BlockMessage;
serde_test::assert_tokens(&value, &[
Token::Struct {
name: "Action",
len: 1,
},
Token::Str("type"),
Token::U8(1),
Token::StructEnd,
]);
let value = Action::Alert(ChannelId(123));
serde_test::assert_tokens(&value, &[
Token::Struct {
name: "Action",
len: 2,
},
Token::Str("type"),
Token::U8(2),
Token::Str("metadata"),
Token::Struct {
name: "ActionMetadata",
len: 1,
},
Token::Str("channel_id"),
Token::NewtypeStruct {
name: "ChannelId",
},
Token::Str("123"),
Token::StructEnd,
Token::StructEnd,
]);
let value = Action::Timeout(Duration::from_secs(1024));
serde_test::assert_tokens(&value, &[
Token::Struct {
name: "Action",
len: 2,
},
Token::Str("type"),
Token::U8(3),
Token::Str("metadata"),
Token::Struct {
name: "ActionMetadata",
len: 1,
},
Token::Str("duration_seconds"),
Token::U64(1024),
Token::StructEnd,
Token::StructEnd,
]);
let value = Action::Unknown(123);
serde_test::assert_tokens(&value, &[
Token::Struct {
name: "Action",
len: 1,
},
Token::Str("type"),
Token::U8(123),
Token::StructEnd,
]);
}
}