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