twilight_http/request/guild/auto_moderation/
create_auto_moderation_rule.rs

1use crate::{
2    client::Client,
3    error::Error as HttpError,
4    request::{self, AuditLogReason, Request, TryIntoRequest},
5    response::ResponseFuture,
6    routing::Route,
7};
8use serde::Serialize;
9use twilight_model::{
10    guild::auto_moderation::{
11        AutoModerationActionType, AutoModerationEventType, AutoModerationKeywordPresetType,
12        AutoModerationRule, AutoModerationTriggerType,
13    },
14    id::{
15        marker::{ChannelMarker, GuildMarker, RoleMarker},
16        Id,
17    },
18};
19use twilight_validate::request::{
20    audit_reason as validate_audit_reason,
21    auto_moderation_action_metadata_duration_seconds as validate_auto_moderation_action_metadata_duration_seconds,
22    auto_moderation_block_action_custom_message_limit as validate_auto_moderation_block_action_custom_message_limit,
23    auto_moderation_exempt_channels as validate_auto_moderation_exempt_channels,
24    auto_moderation_exempt_roles as validate_auto_moderation_exempt_roles,
25    auto_moderation_metadata_keyword_allow_list as validate_auto_moderation_metadata_keyword_allow_list,
26    auto_moderation_metadata_keyword_filter as validate_auto_moderation_metadata_keyword_filter,
27    auto_moderation_metadata_mention_total_limit as validate_auto_moderation_metadata_mention_total_limit,
28    auto_moderation_metadata_regex_patterns as validate_auto_moderation_metadata_regex_patterns,
29    ValidationError,
30};
31
32#[derive(Serialize)]
33struct CreateAutoModerationRuleFieldsAction {
34    /// Type of action.
35    #[serde(rename = "type")]
36    pub kind: AutoModerationActionType,
37    /// Additional metadata needed during execution for this specific action
38    /// type.
39    pub metadata: CreateAutoModerationRuleFieldsActionMetadata,
40}
41
42#[derive(Default, Serialize)]
43struct CreateAutoModerationRuleFieldsActionMetadata {
44    /// Channel to which user content should be logged.
45    pub channel_id: Option<Id<ChannelMarker>>,
46    /// Additional explanation that will be shown to members whenever their message is blocked.
47    ///
48    /// Maximum value length is 150 characters.
49    pub custom_message: Option<String>,
50    /// Timeout duration in seconds.
51    ///
52    /// Maximum value is 2419200 seconds, or 4 weeks.
53    pub duration_seconds: Option<u32>,
54}
55
56#[derive(Serialize)]
57struct CreateAutoModerationRuleFieldsTriggerMetadata<'a> {
58    #[serde(skip_serializing_if = "Option::is_none")]
59    allow_list: Option<&'a [&'a str]>,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    keyword_filter: Option<&'a [&'a str]>,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    presets: Option<&'a [AutoModerationKeywordPresetType]>,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    mention_total_limit: Option<u8>,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    regex_patterns: Option<&'a [&'a str]>,
68}
69
70#[derive(Serialize)]
71struct CreateAutoModerationRuleFields<'a> {
72    actions: Option<Vec<CreateAutoModerationRuleFieldsAction>>,
73    enabled: Option<bool>,
74    event_type: AutoModerationEventType,
75    exempt_channels: Option<&'a [Id<ChannelMarker>]>,
76    exempt_roles: Option<&'a [Id<RoleMarker>]>,
77    name: &'a str,
78    trigger_metadata: Option<CreateAutoModerationRuleFieldsTriggerMetadata<'a>>,
79    trigger_type: Option<AutoModerationTriggerType>,
80}
81
82/// Create an auto moderation rule within a guild.
83///
84/// Requires the [`MANAGE_GUILD`] permission.
85///
86/// # Examples
87///
88/// Create a rule that deletes messages that contain the word "darn":
89///
90/// ```no_run
91/// # #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> {
92/// use twilight_http::Client;
93/// use twilight_model::{guild::auto_moderation::AutoModerationEventType, id::Id};
94///
95/// let client = Client::new("my token".to_owned());
96///
97/// let guild_id = Id::new(1);
98/// client
99///     .create_auto_moderation_rule(guild_id, "no darns", AutoModerationEventType::MessageSend)
100///     .action_block_message()
101///     .enabled(true)
102///     .with_keyword(&["darn"], &["d(?:4|a)rn"], &["darn it"])
103///     .await?;
104/// # Ok(()) }
105/// ```
106///
107/// [`MANAGE_GUILD`]: twilight_model::guild::Permissions::MANAGE_GUILD
108#[must_use = "requests must be configured and executed"]
109pub struct CreateAutoModerationRule<'a> {
110    fields: Result<CreateAutoModerationRuleFields<'a>, ValidationError>,
111    guild_id: Id<GuildMarker>,
112    http: &'a Client,
113    reason: Result<Option<&'a str>, ValidationError>,
114}
115
116impl<'a> CreateAutoModerationRule<'a> {
117    pub(crate) const fn new(
118        http: &'a Client,
119        guild_id: Id<GuildMarker>,
120        name: &'a str,
121        event_type: AutoModerationEventType,
122    ) -> Self {
123        Self {
124            fields: Ok(CreateAutoModerationRuleFields {
125                actions: None,
126                enabled: None,
127                event_type,
128                exempt_channels: None,
129                exempt_roles: None,
130                name,
131                trigger_metadata: None,
132                trigger_type: None,
133            }),
134            guild_id,
135            http,
136            reason: Ok(None),
137        }
138    }
139
140    /// Append an action of type [`BlockMessage`].
141    ///
142    /// [`BlockMessage`]: AutoModerationActionType::BlockMessage
143    pub fn action_block_message(mut self) -> Self {
144        self.fields = self.fields.map(|mut fields| {
145            fields.actions.get_or_insert_with(Vec::new).push(
146                CreateAutoModerationRuleFieldsAction {
147                    kind: AutoModerationActionType::BlockMessage,
148                    metadata: CreateAutoModerationRuleFieldsActionMetadata::default(),
149                },
150            );
151
152            fields
153        });
154
155        self
156    }
157
158    /// Append an action of type [`BlockMessage`] with an explanation for blocking messages.
159    ///
160    /// # Errors
161    ///
162    /// Returns a [`ValidationErrorType::AutoModerationBlockActionCustomMessageLimit`] if the custom message length
163    /// is invalid.
164    ///
165    /// [`ValidationErrorType::AutoModerationBlockActionCustomMessageLimit`]: twilight_validate::request::ValidationErrorType::AutoModerationBlockActionCustomMessageLimit
166    /// [`BlockMessage`]: AutoModerationActionType::BlockMessage
167    pub fn action_block_message_with_explanation(mut self, custom_message: &'a str) -> Self {
168        self.fields = self.fields.and_then(|mut fields| {
169            validate_auto_moderation_block_action_custom_message_limit(custom_message)?;
170            fields.actions.get_or_insert_with(Vec::new).push(
171                CreateAutoModerationRuleFieldsAction {
172                    kind: AutoModerationActionType::BlockMessage,
173                    metadata: CreateAutoModerationRuleFieldsActionMetadata {
174                        custom_message: Some(String::from(custom_message)),
175                        ..Default::default()
176                    },
177                },
178            );
179
180            Ok(fields)
181        });
182
183        self
184    }
185
186    /// Append an action of type [`SendAlertMessage`].
187    ///
188    /// [`SendAlertMessage`]: AutoModerationActionType::SendAlertMessage
189    pub fn action_send_alert_message(mut self, channel_id: Id<ChannelMarker>) -> Self {
190        self.fields = self.fields.map(|mut fields| {
191            fields.actions.get_or_insert_with(Vec::new).push(
192                CreateAutoModerationRuleFieldsAction {
193                    kind: AutoModerationActionType::SendAlertMessage,
194                    metadata: CreateAutoModerationRuleFieldsActionMetadata {
195                        channel_id: Some(channel_id),
196                        ..Default::default()
197                    },
198                },
199            );
200
201            fields
202        });
203
204        self
205    }
206
207    /// Append an action of type [`Timeout`].
208    ///
209    /// # Errors
210    ///
211    /// Returns [`ValidationErrorType::AutoModerationActionMetadataDurationSeconds`] if the duration
212    /// is invalid.
213    ///
214    /// [`Timeout`]: AutoModerationActionType::Timeout
215    /// [`ValidationErrorType::AutoModerationActionMetadataDurationSeconds`]: twilight_validate::request::ValidationErrorType::AutoModerationActionMetadataDurationSeconds
216    pub fn action_timeout(mut self, duration_seconds: u32) -> Self {
217        self.fields = self.fields.and_then(|mut fields| {
218            validate_auto_moderation_action_metadata_duration_seconds(duration_seconds)?;
219            fields.actions.get_or_insert_with(Vec::new).push(
220                CreateAutoModerationRuleFieldsAction {
221                    kind: AutoModerationActionType::Timeout,
222                    metadata: CreateAutoModerationRuleFieldsActionMetadata {
223                        duration_seconds: Some(duration_seconds),
224                        ..Default::default()
225                    },
226                },
227            );
228
229            Ok(fields)
230        });
231
232        self
233    }
234
235    /// Set whether the rule is enabled.
236    pub fn enabled(mut self, enabled: bool) -> Self {
237        self.fields = self.fields.map(|mut fields| {
238            fields.enabled = Some(enabled);
239
240            fields
241        });
242
243        self
244    }
245
246    /// Set the channels where the rule does not apply.
247    /// See [Discord Docs/Trigger Metadata].
248    ///
249    /// # Errors
250    ///
251    /// Returns [`ValidationErrorType::AutoModerationExemptChannels`] if the `exempt_roles` field is invalid.
252    ///
253    /// [Discord Docs/Trigger Metadata]: https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object-trigger-metadata
254    /// [`ValidationErrorType::AutoModerationExemptChannels`]: twilight_validate::request::ValidationErrorType::AutoModerationExemptChannels
255    pub fn exempt_channels(mut self, exempt_channels: &'a [Id<ChannelMarker>]) -> Self {
256        self.fields = self.fields.and_then(|mut fields| {
257            validate_auto_moderation_exempt_channels(exempt_channels)?;
258            fields.exempt_channels = Some(exempt_channels);
259
260            Ok(fields)
261        });
262
263        self
264    }
265
266    /// Set the roles to which the rule does not apply.
267    /// See [Discord Docs/Trigger Metadata].
268    ///
269    /// # Errors
270    ///
271    /// Returns [`ValidationErrorType::AutoModerationExemptRoles`] if the `exempt_roles` field is invalid.
272    ///
273    /// [Discord Docs/Trigger Metadata]: https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object-trigger-metadata
274    /// [`ValidationErrorType::AutoModerationExemptRoles`]: twilight_validate::request::ValidationErrorType::AutoModerationExemptRoles
275    pub fn exempt_roles(mut self, exempt_roles: &'a [Id<RoleMarker>]) -> Self {
276        self.fields = self.fields.and_then(|mut fields| {
277            validate_auto_moderation_exempt_roles(exempt_roles)?;
278            fields.exempt_roles = Some(exempt_roles);
279
280            Ok(fields)
281        });
282
283        self
284    }
285
286    /// Create the request with the trigger type [`Keyword`], then execute it.
287    ///
288    /// Rules of this type require the `keyword_filter`, `regex_patterns` and
289    /// `allow_list` fields specified, and this method ensures this.
290    /// See [Discord Docs/Keyword Matching Strategies] and
291    /// [Discord Docs/Trigger Metadata] for more information.
292    ///
293    /// Only rust-flavored regex is currently supported by Discord.
294    ///
295    /// # Errors
296    ///
297    /// Returns [`ValidationErrorType::AutoModerationMetadataKeywordFilter`] if the `keyword_filter`
298    /// field is invalid.
299    ///
300    /// Returns [`ValidationErrorType::AutoModerationMetadataKeywordFilterItem`] if a `keyword_filter`
301    /// item is invalid.
302    ///
303    /// Returns [`ValidationErrorType::AutoModerationMetadataAllowList`] if the `allow_list` field is
304    /// invalid.
305    ///
306    /// Returns [`ValidationErrorType::AutoModerationMetadataAllowListItem`] if an `allow_list` item
307    /// is invalid.
308    ///
309    /// Returns [`ValidationErrorType::AutoModerationMetadataRegexPatterns`] if the `regex_patterns`
310    /// field is invalid.
311    ///
312    /// Returns [`ValidationErrorType::AutoModerationMetadataRegexPatternsItem`] if a `regex_patterns`
313    /// item is invalid.
314    ///
315    /// [`Keyword`]: AutoModerationTriggerType::Keyword
316    /// [Discord Docs/Keyword Matching Strategies]: https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object-keyword-matching-strategies
317    /// [Discord Docs/Trigger Metadata]: https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object-trigger-metadata
318    /// [`ValidationErrorType::AutoModerationMetadataKeywordFilter`]: twilight_validate::request::ValidationErrorType::AutoModerationMetadataKeywordFilter
319    /// [`ValidationErrorType::AutoModerationMetadataKeywordFilterItem`]: twilight_validate::request::ValidationErrorType::AutoModerationMetadataKeywordFilterItem
320    /// [`ValidationErrorType::AutoModerationMetadataAllowList`]: twilight_validate::request::ValidationErrorType::AutoModerationMetadataAllowList
321    /// [`ValidationErrorType::AutoModerationMetadataAllowListItem`]: twilight_validate::request::ValidationErrorType::AutoModerationMetadataAllowListItem
322    /// [`ValidationErrorType::AutoModerationMetadataRegexPatterns`]: twilight_validate::request::ValidationErrorType::AutoModerationMetadataRegexPatterns
323    /// [`ValidationErrorType::AutoModerationMetadataRegexPatternsItem`]: twilight_validate::request::ValidationErrorType::AutoModerationMetadataRegexPatternsItem
324    pub fn with_keyword(
325        mut self,
326        keyword_filter: &'a [&'a str],
327        regex_patterns: &'a [&'a str],
328        allow_list: &'a [&'a str],
329    ) -> ResponseFuture<AutoModerationRule> {
330        self.fields = self.fields.and_then(|mut fields| {
331            validate_auto_moderation_metadata_keyword_allow_list(allow_list)?;
332            validate_auto_moderation_metadata_keyword_filter(keyword_filter)?;
333            validate_auto_moderation_metadata_regex_patterns(regex_patterns)?;
334            fields.trigger_metadata = Some(CreateAutoModerationRuleFieldsTriggerMetadata {
335                allow_list: Some(allow_list),
336                keyword_filter: Some(keyword_filter),
337                presets: None,
338                mention_total_limit: None,
339                regex_patterns: Some(regex_patterns),
340            });
341
342            fields.trigger_type = Some(AutoModerationTriggerType::Keyword);
343
344            Ok(fields)
345        });
346
347        self.exec()
348    }
349
350    /// Create the request with the trigger type [`Spam`], then execute it.
351    ///
352    /// [`Spam`]: AutoModerationTriggerType::Spam
353    pub fn with_spam(mut self) -> ResponseFuture<AutoModerationRule> {
354        self.fields = self.fields.map(|mut fields| {
355            fields.trigger_type = Some(AutoModerationTriggerType::Spam);
356
357            fields
358        });
359
360        self.exec()
361    }
362
363    /// Create the request with the trigger type [`KeywordPreset`], then execute
364    /// it.
365    ///
366    /// Rules of this type require the `presets` and `allow_list` fields
367    /// specified, and this method ensures this. See [Discord Docs/TriggerMetadata].
368    ///
369    /// # Errors
370    ///
371    /// Returns [`ValidationErrorType::AutoModerationMetadataPresetAllowList`] if the `allow_list` is
372    /// invalid.
373    ///
374    /// Returns [`ValidationErrorType::AutoModerationMetadataPresetAllowListItem`] if a `allow_list`
375    /// item is invalid.
376    ///
377    /// [`KeywordPreset`]: AutoModerationTriggerType::KeywordPreset
378    /// [Discord Docs/Trigger Metadata]: https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object-trigger-metadata
379    /// [`ValidationErrorType::AutoModerationMetadataPresetAllowList`]: twilight_validate::request::ValidationErrorType::AutoModerationMetadataPresetAllowList
380    /// [`ValidationErrorType::AutoModerationMetadataPresetAllowListItem`]: twilight_validate::request::ValidationErrorType::AutoModerationMetadataPresetAllowListItem
381    pub fn with_keyword_preset(
382        mut self,
383        presets: &'a [AutoModerationKeywordPresetType],
384        allow_list: &'a [&'a str],
385    ) -> ResponseFuture<AutoModerationRule> {
386        self.fields = self.fields.and_then(|mut fields| {
387            validate_auto_moderation_metadata_keyword_allow_list(allow_list)?;
388            fields.trigger_metadata = Some(CreateAutoModerationRuleFieldsTriggerMetadata {
389                allow_list: Some(allow_list),
390                keyword_filter: None,
391                presets: Some(presets),
392                mention_total_limit: None,
393                regex_patterns: None,
394            });
395
396            fields.trigger_type = Some(AutoModerationTriggerType::KeywordPreset);
397
398            Ok(fields)
399        });
400
401        self.exec()
402    }
403
404    /// Create the request with the trigger type [`MentionSpam`], then execute
405    /// it.
406    ///
407    /// Rules of this type requires the `mention_total_limit` field specified,
408    /// and this method ensures this. See [Discord Docs/Trigger Metadata].
409    ///
410    /// # Errors
411    ///
412    /// Returns a [`ValidationErrorType::AutoModerationMetadataMentionTotalLimit`] if `mention_total_limit`
413    /// is invalid.
414    ///
415    /// [`MentionSpam`]: AutoModerationTriggerType::MentionSpam
416    /// [Discord Docs/Trigger Metadata]: https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object-trigger-metadata
417    /// [`ValidationErrorType::AutoModerationMetadataMentionTotalLimit`]: twilight_validate::request::ValidationErrorType::AutoModerationMetadataMentionTotalLimit
418    pub fn with_mention_spam(
419        mut self,
420        mention_total_limit: u8,
421    ) -> ResponseFuture<AutoModerationRule> {
422        self.fields = self.fields.and_then(|mut fields| {
423            validate_auto_moderation_metadata_mention_total_limit(mention_total_limit)?;
424            fields.trigger_metadata = Some(CreateAutoModerationRuleFieldsTriggerMetadata {
425                allow_list: None,
426                keyword_filter: None,
427                presets: None,
428                mention_total_limit: Some(mention_total_limit),
429                regex_patterns: None,
430            });
431            fields.trigger_type = Some(AutoModerationTriggerType::MentionSpam);
432
433            Ok(fields)
434        });
435
436        self.exec()
437    }
438
439    /// Execute the request, returning a future resolving to a [`Response`].
440    ///
441    /// [`Response`]: crate::response::Response
442    fn exec(self) -> ResponseFuture<AutoModerationRule> {
443        let http = self.http;
444
445        match self.try_into_request() {
446            Ok(request) => http.request(request),
447            Err(source) => ResponseFuture::error(source),
448        }
449    }
450}
451
452impl<'a> AuditLogReason<'a> for CreateAutoModerationRule<'a> {
453    fn reason(mut self, reason: &'a str) -> Self {
454        self.reason = validate_audit_reason(reason).and(Ok(Some(reason)));
455
456        self
457    }
458}
459
460impl TryIntoRequest for CreateAutoModerationRule<'_> {
461    fn try_into_request(self) -> Result<Request, HttpError> {
462        let fields = self.fields.map_err(HttpError::validation)?;
463        let mut request = Request::builder(&Route::CreateAutoModerationRule {
464            guild_id: self.guild_id.get(),
465        })
466        .json(&fields);
467
468        if let Some(reason) = self.reason.map_err(HttpError::validation)? {
469            request = request.headers(request::audit_header(reason)?);
470        }
471
472        request.build()
473    }
474}