1use std::time::Duration;
6
7use serde::de::{Deserializer, Error};
8use serde::ser::Serializer;
9use serde::{Deserialize, Serialize};
10
11use crate::model::id::*;
12
13#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
18#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
19#[non_exhaustive]
20pub struct Rule {
21 pub id: RuleId,
23 pub guild_id: GuildId,
25 pub name: String,
27 pub creator_id: UserId,
29 pub event_type: EventType,
31 #[serde(flatten)]
33 pub trigger: Trigger,
34 pub actions: Vec<Action>,
36 pub enabled: bool,
38 pub exempt_roles: Vec<RoleId>,
42 pub exempt_channels: Vec<ChannelId>,
46}
47
48#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
52#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
53#[serde(from = "u8", into = "u8")]
54#[non_exhaustive]
55pub enum EventType {
56 MessageSend,
57 Unknown(u8),
58}
59
60impl From<u8> for EventType {
61 fn from(value: u8) -> Self {
62 match value {
63 1 => Self::MessageSend,
64 _ => Self::Unknown(value),
65 }
66 }
67}
68
69impl From<EventType> for u8 {
70 fn from(value: EventType) -> Self {
71 match value {
72 EventType::MessageSend => 1,
73 EventType::Unknown(unknown) => unknown,
74 }
75 }
76}
77
78#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
84#[derive(Clone, Debug, PartialEq, Eq)]
85#[non_exhaustive]
86pub enum Trigger {
87 Keyword {
88 strings: Vec<String>,
95 regex_patterns: Vec<String>,
97 allow_list: Vec<String>,
99 },
100 Spam,
101 KeywordPreset {
102 presets: Vec<KeywordPresetType>,
104 allow_list: Vec<String>,
106 },
107 MentionSpam {
108 mention_total_limit: u8,
110 },
111 Unknown(u8),
112}
113
114#[derive(Deserialize, Serialize)]
116#[serde(rename = "Trigger")]
117struct InterimTrigger {
118 #[serde(rename = "trigger_type")]
119 kind: TriggerType,
120 #[serde(rename = "trigger_metadata")]
121 metadata: TriggerMetadata,
122}
123
124impl<'de> Deserialize<'de> for Trigger {
125 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
126 let trigger = InterimTrigger::deserialize(deserializer)?;
127 let trigger = match trigger.kind {
128 TriggerType::Keyword => Self::Keyword {
129 strings: trigger
130 .metadata
131 .keyword_filter
132 .ok_or_else(|| Error::missing_field("keyword_filter"))?,
133 regex_patterns: trigger
134 .metadata
135 .regex_patterns
136 .ok_or_else(|| Error::missing_field("regex_patterns"))?,
137 allow_list: trigger
138 .metadata
139 .allow_list
140 .ok_or_else(|| Error::missing_field("allow_list"))?,
141 },
142 TriggerType::Spam => Self::Spam,
143 TriggerType::KeywordPreset => Self::KeywordPreset {
144 presets: trigger.metadata.presets.ok_or_else(|| Error::missing_field("presets"))?,
145 allow_list: trigger
146 .metadata
147 .allow_list
148 .ok_or_else(|| Error::missing_field("allow_list"))?,
149 },
150 TriggerType::MentionSpam => Self::MentionSpam {
151 mention_total_limit: trigger
152 .metadata
153 .mention_total_limit
154 .ok_or_else(|| Error::missing_field("mention_total_limit"))?,
155 },
156 TriggerType::Unknown(unknown) => Self::Unknown(unknown),
157 };
158 Ok(trigger)
159 }
160}
161
162impl Serialize for Trigger {
163 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
164 let mut trigger = InterimTrigger {
165 kind: self.kind(),
166 metadata: TriggerMetadata {
167 keyword_filter: None,
168 regex_patterns: None,
169 presets: None,
170 allow_list: None,
171 mention_total_limit: None,
172 },
173 };
174 match self {
175 Self::Keyword {
176 strings,
177 regex_patterns,
178 allow_list,
179 } => {
180 trigger.metadata.keyword_filter = Some(strings.clone());
181 trigger.metadata.regex_patterns = Some(regex_patterns.clone());
182 trigger.metadata.allow_list = Some(allow_list.clone());
183 },
184 Self::KeywordPreset {
185 presets,
186 allow_list,
187 } => {
188 trigger.metadata.presets = Some(presets.clone());
189 trigger.metadata.allow_list = Some(allow_list.clone());
190 },
191 Self::MentionSpam {
192 mention_total_limit,
193 } => trigger.metadata.mention_total_limit = Some(*mention_total_limit),
194 Self::Spam | Self::Unknown(_) => {},
195 }
196 trigger.serialize(serializer)
197 }
198}
199
200impl Trigger {
201 #[must_use]
202 pub fn kind(&self) -> TriggerType {
203 match self {
204 Self::Keyword {
205 ..
206 } => TriggerType::Keyword,
207 Self::Spam => TriggerType::Spam,
208 Self::KeywordPreset {
209 ..
210 } => TriggerType::KeywordPreset,
211 Self::MentionSpam {
212 ..
213 } => TriggerType::MentionSpam,
214 Self::Unknown(unknown) => TriggerType::Unknown(*unknown),
215 }
216 }
217}
218
219#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
223#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
224#[serde(from = "u8", into = "u8")]
225#[non_exhaustive]
226pub enum TriggerType {
227 Keyword,
228 Spam,
229 KeywordPreset,
230 MentionSpam,
231 Unknown(u8),
232}
233
234impl From<u8> for TriggerType {
235 fn from(value: u8) -> Self {
236 match value {
237 1 => Self::Keyword,
238 3 => Self::Spam,
239 4 => Self::KeywordPreset,
240 5 => Self::MentionSpam,
241 _ => Self::Unknown(value),
242 }
243 }
244}
245
246impl From<TriggerType> for u8 {
247 fn from(value: TriggerType) -> Self {
248 match value {
249 TriggerType::Keyword => 1,
250 TriggerType::Spam => 3,
251 TriggerType::KeywordPreset => 4,
252 TriggerType::MentionSpam => 5,
253 TriggerType::Unknown(unknown) => unknown,
254 }
255 }
256}
257
258#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
267#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
268#[non_exhaustive]
269pub struct TriggerMetadata {
270 #[serde(skip_serializing_if = "Option::is_none")]
271 pub keyword_filter: Option<Vec<String>>,
272 #[serde(skip_serializing_if = "Option::is_none")]
273 pub regex_patterns: Option<Vec<String>>,
274 #[serde(skip_serializing_if = "Option::is_none")]
275 pub presets: Option<Vec<KeywordPresetType>>,
276 #[serde(skip_serializing_if = "Option::is_none")]
277 pub allow_list: Option<Vec<String>>,
278 #[serde(skip_serializing_if = "Option::is_none")]
279 pub mention_total_limit: Option<u8>,
280}
281
282#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
286#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
287#[serde(from = "u8", into = "u8")]
288#[non_exhaustive]
289pub enum KeywordPresetType {
290 Profanity,
292 SexualContent,
294 Slurs,
296 Unknown(u8),
297}
298
299impl From<u8> for KeywordPresetType {
300 fn from(value: u8) -> Self {
301 match value {
302 1 => Self::Profanity,
303 2 => Self::SexualContent,
304 3 => Self::Slurs,
305 _ => Self::Unknown(value),
306 }
307 }
308}
309
310impl From<KeywordPresetType> for u8 {
311 fn from(value: KeywordPresetType) -> Self {
312 match value {
313 KeywordPresetType::Profanity => 1,
314 KeywordPresetType::SexualContent => 2,
315 KeywordPresetType::Slurs => 3,
316 KeywordPresetType::Unknown(unknown) => unknown,
317 }
318 }
319}
320
321#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
325#[derive(Clone, Debug, PartialEq, Eq)]
326#[non_exhaustive]
327pub enum Action {
328 BlockMessage {
330 custom_message: Option<String>,
334 },
335 Alert(ChannelId),
337 Timeout(Duration),
347 Unknown(u8),
348}
349
350#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
355#[derive(Clone, Debug, Deserialize, Serialize)]
356#[non_exhaustive]
357pub struct ActionExecution {
358 pub guild_id: GuildId,
360 pub action: Action,
362 pub rule_id: RuleId,
364 #[serde(rename = "rule_trigger_type")]
366 pub trigger_type: TriggerType,
367 pub user_id: UserId,
369 pub channel_id: Option<ChannelId>,
371 pub message_id: Option<MessageId>,
375 pub alert_system_message_id: Option<MessageId>,
379 pub content: String,
385 pub matched_keyword: Option<String>,
387 pub matched_content: Option<String>,
393}
394
395#[derive(Default, Deserialize, Serialize)]
397struct RawActionMetadata {
398 #[serde(skip_serializing_if = "Option::is_none")]
399 channel_id: Option<ChannelId>,
400 #[serde(skip_serializing_if = "Option::is_none")]
401 duration_seconds: Option<u64>,
402 #[serde(skip_serializing_if = "Option::is_none")]
403 custom_message: Option<String>,
404}
405
406#[derive(Deserialize, Serialize)]
408struct RawAction {
409 #[serde(rename = "type")]
410 kind: ActionType,
411 #[serde(skip_serializing_if = "Option::is_none")]
412 metadata: Option<RawActionMetadata>,
413}
414
415impl<'de> Deserialize<'de> for Action {
420 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
421 let action = RawAction::deserialize(deserializer)?;
422 Ok(match action.kind {
423 ActionType::BlockMessage => Action::BlockMessage {
424 custom_message: action.metadata.and_then(|m| m.custom_message),
425 },
426 ActionType::Alert => Action::Alert(
427 action
428 .metadata
429 .ok_or_else(|| Error::missing_field("metadata"))?
430 .channel_id
431 .ok_or_else(|| Error::missing_field("channel_id"))?,
432 ),
433 ActionType::Timeout => Action::Timeout(Duration::from_secs(
434 action
435 .metadata
436 .ok_or_else(|| Error::missing_field("metadata"))?
437 .duration_seconds
438 .ok_or_else(|| Error::missing_field("duration_seconds"))?,
439 )),
440 ActionType::Unknown(unknown) => Action::Unknown(unknown),
441 })
442 }
443}
444
445impl Serialize for Action {
446 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
447 let action = match self.clone() {
448 Action::BlockMessage {
449 custom_message,
450 } => RawAction {
451 kind: ActionType::BlockMessage,
452 metadata: Some(RawActionMetadata {
453 custom_message,
454 ..Default::default()
455 }),
456 },
457 Action::Alert(channel_id) => RawAction {
458 kind: ActionType::Alert,
459 metadata: Some(RawActionMetadata {
460 channel_id: Some(channel_id),
461 ..Default::default()
462 }),
463 },
464 Action::Timeout(duration) => RawAction {
465 kind: ActionType::Timeout,
466 metadata: Some(RawActionMetadata {
467 duration_seconds: Some(duration.as_secs()),
468 ..Default::default()
469 }),
470 },
471 Action::Unknown(n) => RawAction {
472 kind: ActionType::Unknown(n),
473 metadata: None,
474 },
475 };
476 action.serialize(serializer)
477 }
478}
479
480impl Action {
481 #[must_use]
482 pub fn kind(&self) -> ActionType {
483 match self {
484 Self::BlockMessage {
485 ..
486 } => ActionType::BlockMessage,
487 Self::Alert(_) => ActionType::Alert,
488 Self::Timeout(_) => ActionType::Timeout,
489 Self::Unknown(unknown) => ActionType::Unknown(*unknown),
490 }
491 }
492}
493
494enum_number! {
495 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
499 #[serde(from = "u8", into = "u8")]
500 #[non_exhaustive]
501 pub enum ActionType {
502 BlockMessage = 1,
503 Alert = 2,
504 Timeout = 3,
505 _ => Unknown(u8),
506 }
507}
508
509#[cfg(test)]
510mod tests {
511
512 use super::*;
513 use crate::json::{assert_json, json};
514
515 #[test]
516 fn rule_trigger_serde() {
517 #[derive(Debug, PartialEq, Deserialize, Serialize)]
518 struct Rule {
519 #[serde(flatten)]
520 trigger: Trigger,
521 }
522
523 assert_json(
524 &Rule {
525 trigger: Trigger::Keyword {
526 strings: vec![String::from("foo"), String::from("bar")],
527 regex_patterns: vec![String::from("d[i1]ck")],
528 allow_list: vec![String::from("duck")],
529 },
530 },
531 json!({"trigger_type": 1, "trigger_metadata": {"keyword_filter": ["foo", "bar"], "regex_patterns": ["d[i1]ck"], "allow_list": ["duck"]}}),
532 );
533
534 assert_json(
535 &Rule {
536 trigger: Trigger::Spam,
537 },
538 json!({"trigger_type": 3, "trigger_metadata": {}}),
539 );
540
541 assert_json(
542 &Rule {
543 trigger: Trigger::KeywordPreset {
544 presets: vec![
545 KeywordPresetType::Profanity,
546 KeywordPresetType::SexualContent,
547 KeywordPresetType::Slurs,
548 ],
549 allow_list: vec![String::from("boob")],
550 },
551 },
552 json!({"trigger_type": 4, "trigger_metadata": {"presets": [1,2,3], "allow_list": ["boob"]}}),
553 );
554
555 assert_json(
556 &Rule {
557 trigger: Trigger::MentionSpam {
558 mention_total_limit: 7,
559 },
560 },
561 json!({"trigger_type": 5, "trigger_metadata": {"mention_total_limit": 7}}),
562 );
563
564 assert_json(
565 &Rule {
566 trigger: Trigger::Unknown(123),
567 },
568 json!({"trigger_type": 123, "trigger_metadata": {}}),
569 );
570 }
571
572 #[test]
573 fn action_serde() {
574 assert_json(
575 &Action::BlockMessage {
576 custom_message: None,
577 },
578 json!({"type": 1, "metadata": {}}),
579 );
580
581 assert_json(
582 &Action::Alert(ChannelId::new(123)),
583 json!({"type": 2, "metadata": {"channel_id": "123"}}),
584 );
585
586 assert_json(
587 &Action::Timeout(Duration::from_secs(1024)),
588 json!({"type": 3, "metadata": {"duration_seconds": 1024}}),
589 );
590
591 assert_json(&Action::Unknown(123), json!({"type": 123}));
592 }
593}