diff --git a/discord/__init__.py b/discord/__init__.py index 9872f72ce..092e2277f 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -25,6 +25,7 @@ from .affinity import * from .application import * from .asset import * from .audit_logs import * +from .automod import * from .billing import * from .calls import * from .channel import * diff --git a/discord/audit_logs.py b/discord/audit_logs.py index 4cbc727ca..1d43b6843 100644 --- a/discord/audit_logs.py +++ b/discord/audit_logs.py @@ -258,6 +258,7 @@ class AuditLogChanges: 'entity_type': (None, _enum_transformer(enums.EntityType)), 'preferred_locale': (None, _enum_transformer(enums.Locale)), 'image_hash': ('cover_image', _transform_cover_image), + 'trigger_type': (None, _enum_transformer(enums.AutoModRuleTriggerType)), } # fmt: on @@ -370,6 +371,12 @@ class _AuditLogProxyMessageBulkDelete(_AuditLogProxy): count: int +class _AuditLogProxyAutoModAction(_AuditLogProxy): + automod_rule_name: str + automod_rule_trigger_type: str + channel: Union[abc.GuildChannel, Thread] + + class AuditLogEntry(Hashable): r"""Represents an Audit Log entry. @@ -435,6 +442,7 @@ class AuditLogEntry(Hashable): _AuditLogProxyPinAction, _AuditLogProxyStageInstanceAction, _AuditLogProxyMessageBulkDelete, + _AuditLogProxyAutoModAction, Member, User, None, Role, Object ] = None @@ -466,6 +474,16 @@ class AuditLogEntry(Hashable): channel=self.guild.get_channel_or_thread(channel_id) or Object(id=channel_id), message_id=int(extra['message_id']), ) + elif self.action is enums.AuditLogAction.automod_block_message: + channel_id = int(extra['channel_id']) + self.extra = _AuditLogProxyAutoModAction( + automod_rule_name=extra['auto_moderation_rule_name'], + automod_rule_trigger_type=enums.try_enum( + enums.AutoModRuleTriggerType, extra['auto_moderation_rule_trigger_type'] + ), + channel=self.guild.get_channel_or_thread(channel_id) or Object(id=channel_id), + ) + elif self.action.name.startswith('overwrite_'): # the overwrite_ actions have a dict with some information instance_id = int(extra['id']) @@ -595,3 +613,6 @@ class AuditLogEntry(Hashable): def _convert_target_guild_scheduled_event(self, target_id: int) -> Union[ScheduledEvent, Object]: return self.guild.get_scheduled_event(target_id) or Object(id=target_id) + + def _convert_target_auto_moderation(self, target_id: int) -> Union[Member, Object]: + return self.guild.get_member(target_id) or Object(target_id) diff --git a/discord/automod.py b/discord/automod.py new file mode 100644 index 000000000..f46d916fc --- /dev/null +++ b/discord/automod.py @@ -0,0 +1,487 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations +import datetime + +from typing import TYPE_CHECKING, Any, Dict, Optional, List, Sequence, Set, Union, Sequence + + +from .enums import AutoModRuleTriggerType, AutoModRuleActionType, AutoModRuleEventType, try_enum +from .flags import AutoModPresets +from . import utils +from .utils import MISSING, cached_slot_property + +if TYPE_CHECKING: + from typing_extensions import Self + from .abc import Snowflake, GuildChannel + from .threads import Thread + from .guild import Guild + from .member import Member + from .state import ConnectionState + from .types.automod import ( + AutoModerationRule as AutoModerationRulePayload, + AutoModerationTriggerMetadata as AutoModerationTriggerMetadataPayload, + AutoModerationAction as AutoModerationActionPayload, + AutoModerationActionExecution as AutoModerationActionExecutionPayload, + ) + from .role import Role + +__all__ = ( + 'AutoModRuleAction', + 'AutoModTrigger', + 'AutoModRule', + 'AutoModAction', +) + + +class AutoModRuleAction: + """Represents an auto moderation's rule action. + + .. versionadded:: 2.0 + + Attributes + ----------- + type: :class:`AutoModRuleActionType` + The type of action to take. + channel_id: Optional[:class:`int`] + The ID of the channel to send the alert message to, if any. + duration: Optional[:class:`datetime.timedelta`] + The duration of the timeout to apply, if any. + Has a maximum of 28 days. + """ + + __slots__ = ('type', 'channel_id', 'duration') + + def __init__(self, *, channel_id: Optional[int] = None, duration: Optional[datetime.timedelta] = None) -> None: + self.channel_id: Optional[int] = channel_id + self.duration: Optional[datetime.timedelta] = duration + if channel_id and duration: + raise ValueError('Please provide only one of ``channel`` or ``duration``') + + if channel_id: + self.type = AutoModRuleActionType.send_alert_message + elif duration: + self.type = AutoModRuleActionType.timeout + else: + self.type = AutoModRuleActionType.block_message + + def __repr__(self) -> str: + return f'' + + @classmethod + def from_data(cls, data: AutoModerationActionPayload) -> Self: + type_ = try_enum(AutoModRuleActionType, data['type']) + if data['type'] == AutoModRuleActionType.timeout.value: + duration_seconds = data['metadata']['duration_seconds'] + return cls(duration=datetime.timedelta(seconds=duration_seconds)) + elif data['type'] == AutoModRuleActionType.send_alert_message.value: + channel_id = int(data['metadata']['channel_id']) + return cls(channel_id=channel_id) + return cls() + + def to_dict(self) -> Dict[str, Any]: + ret = {'type': self.type.value, 'metadata': {}} + if self.type is AutoModRuleActionType.timeout: + ret['metadata'] = {'duration_seconds': int(self.duration.total_seconds())} # type: ignore # duration cannot be None here + elif self.type is AutoModRuleActionType.send_alert_message: + ret['metadata'] = {'channel_id': str(self.channel_id)} + return ret + + +class AutoModTrigger: + """Represents a trigger for an auto moderation rule. + + .. versionadded:: 2.0 + + Attributes + ----------- + type: :class:`AutoModRuleTriggerType` + The type of trigger. + keyword_filter: Optional[List[:class:`str`]] + The list of strings that will trigger the keyword filter. + presets: Optional[:class:`AutoModPresets`] + The presets used with the preset keyword filter. + """ + + __slots__ = ('type', 'keyword_filter', 'presets') + + def __init__( + self, + *, + keyword_filter: Optional[List[str]] = None, + presets: Optional[AutoModPresets] = None, + ) -> None: + self.keyword_filter: Optional[List[str]] = keyword_filter + self.presets: Optional[AutoModPresets] = presets + if keyword_filter and presets: + raise ValueError('Please pass only one of keyword_filter or presets.') + + if self.keyword_filter is not None: + self.type = AutoModRuleTriggerType.keyword + else: + self.type = AutoModRuleTriggerType.keyword_preset + + @classmethod + def from_data(cls, type: int, data: Optional[AutoModerationTriggerMetadataPayload]) -> Self: + type_ = try_enum(AutoModRuleTriggerType, type) + if type_ is AutoModRuleTriggerType.keyword: + return cls(keyword_filter=data['keyword_filter']) # type: ignore # unable to typeguard due to outer payload + else: + return cls(presets=AutoModPresets._from_value(data['presets'])) # type: ignore # unable to typeguard due to outer payload + + def to_metadata_dict(self) -> Dict[str, Any]: + if self.keyword_filter is not None: + return {'keyword_filter': self.keyword_filter} + elif self.presets is not None: + return {'presets': self.presets.to_array()} + + return {} + + +class AutoModRule: + """Represents an auto moderation rule. + + .. versionadded:: 2.0 + + Attributes + ----------- + id: :class:`int` + The ID of the rule. + guild: :class:`Guild` + The guild the rule is for. + name: :class:`str` + The name of the rule. + creator_id: :class:`int` + The ID of the user that created the rule. + trigger: :class:`AutoModTrigger` + The rule's trigger. + enabled: :class:`bool` + Whether the rule is enabled. + exempt_role_ids: Set[:class:`int`] + The IDs of the roles that are exempt from the rule. + exempt_channel_ids: Set[:class:`int`] + The IDs of the channels that are exempt from the rule. + """ + + __slots__ = ( + '_state', + '_cs_exempt_roles', + '_cs_exempt_channels', + '_cs_actions', + 'id', + 'guild', + 'name', + 'creator_id', + 'event_type', + 'trigger', + 'enabled', + 'exempt_role_ids', + 'exempt_channel_ids', + '_actions', + ) + + def __init__(self, *, data: AutoModerationRulePayload, guild: Guild, state: ConnectionState) -> None: + self._state: ConnectionState = state + self.guild: Guild = guild + self.id: int = int(data['id']) + self.name: str = data['name'] + self.creator_id = int(data['creator_id']) + self.event_type: AutoModRuleEventType = try_enum(AutoModRuleEventType, data['event_type']) + self.trigger: AutoModTrigger = AutoModTrigger.from_data(data['trigger_type'], data=data.get('trigger_metadata')) + self.enabled: bool = data['enabled'] + self.exempt_role_ids: Set[int] = {int(role_id) for role_id in data['exempt_roles']} + self.exempt_channel_ids: Set[int] = {int(channel_id) for channel_id in data['exempt_channels']} + self._actions: List[AutoModerationActionPayload] = data['actions'] + + def __repr__(self) -> str: + return f'' + + def to_dict(self) -> AutoModerationRulePayload: + ret: AutoModerationRulePayload = { + 'id': str(self.id), + 'guild_id': str(self.guild.id), + 'name': self.name, + 'creator_id': str(self.creator_id), + 'event_type': self.event_type.value, + 'trigger_type': self.trigger.type.value, + 'trigger_metadata': self.trigger.to_metadata_dict(), + 'actions': [action.to_dict() for action in self.actions], + 'enabled': self.enabled, + 'exempt_roles': [str(role_id) for role_id in self.exempt_role_ids], + 'exempt_channels': [str(channel_id) for channel_id in self.exempt_channel_ids], + } # type: ignore # trigger types break the flow here. + + return ret + + @property + def creator(self) -> Optional[Member]: + """Optional[:class:`Member`]: The member that created this rule.""" + return self.guild.get_member(self.creator_id) + + @cached_slot_property('_cs_exempt_roles') + def exempt_roles(self) -> List[Role]: + """List[:class:`Role`]: The roles that are exempt from this rule.""" + result = [] + get_role = self.guild.get_role + for role_id in self.exempt_role_ids: + role = get_role(role_id) + if role is not None: + result.append(role) + + return utils._unique(result) + + @cached_slot_property('_cs_exempt_channels') + def exempt_channels(self) -> List[Union[GuildChannel, Thread]]: + """List[Union[:class:`abc.GuildChannel`, :class:`Thread`]]: The channels that are exempt from this rule.""" + it = filter(None, map(self.guild._resolve_channel, self.exempt_channel_ids)) + return utils._unique(it) + + @cached_slot_property('_cs_actions') + def actions(self) -> List[AutoModRuleAction]: + """List[:class:`AutoModRuleAction`]: The actions that are taken when this rule is triggered.""" + return [AutoModRuleAction.from_data(action) for action in self._actions] + + def is_exempt(self, obj: Snowflake, /) -> bool: + """Check if an object is exempt from the automod rule. + + Parameters + ----------- + obj: :class:`abc.Snowflake` + The role, channel, or thread to check. + + Returns + -------- + :class:`bool` + Whether the object is exempt from the automod rule. + """ + return obj.id in self.exempt_channel_ids or obj.id in self.exempt_role_ids + + async def edit( + self, + *, + name: str = MISSING, + event_type: AutoModRuleEventType = MISSING, + actions: List[AutoModRuleAction] = MISSING, + trigger: AutoModTrigger = MISSING, + enabled: bool = MISSING, + exempt_roles: Sequence[Snowflake] = MISSING, + exempt_channels: Sequence[Snowflake] = MISSING, + reason: str = MISSING, + ) -> Self: + """|coro| + + Edits this auto moderation rule. + + You must have :attr:`Permissions.manage_guild` to edit rules. + + Parameters + ----------- + name: :class:`str` + The new name to change to. + event_type: :class:`AutoModRuleEventType` + The new event type to change to. + actions: List[:class:`AutoModRuleAction`] + The new rule actions to update. + trigger: :class:`AutoModTrigger` + The new trigger to update. + You can only change the trigger metadata, not the type. + enabled: :class:`bool` + Whether the rule should be enabled or not. + exempt_roles: Sequence[:class:`abc.Snowflake`] + The new roles to exempt from the rule. + exempt_channels: Sequence[:class:`abc.Snowflake`] + The new channels to exempt from the rule. + reason: :class:`str` + The reason for updating this rule. Shows up on the audit log. + + Raises + ------- + Forbidden + You do not have permission to edit this rule. + HTTPException + Editing the rule failed. + + Returns + -------- + :class:`AutoModRule` + The updated auto moderation rule. + """ + payload = {} + if actions is not MISSING: + payload['actions'] = [action.to_dict() for action in actions] + + if name is not MISSING: + payload['name'] = name + + if event_type is not MISSING: + payload['event_type'] = event_type + + if trigger is not MISSING: + payload['trigger_metadata'] = trigger.to_metadata_dict() + + if enabled is not MISSING: + payload['enabled'] = enabled + + if exempt_roles is not MISSING: + payload['exempt_roles'] = exempt_roles + + if exempt_channels is not MISSING: + payload['exempt_channels'] = exempt_channels + + data = await self._state.http.edit_auto_moderation_rule( + self.guild.id, + self.id, + reason=reason, + **payload, + ) + + return AutoModRule(data=data, guild=self.guild, state=self._state) + + async def delete(self, *, reason: str = MISSING) -> None: + """|coro| + + Deletes the auto moderation rule. + + You must have :attr:`Permissions.manage_guild` to delete rules. + + Parameters + ----------- + reason: :class:`str` + The reason for deleting this rule. Shows up on the audit log. + + Raises + ------- + Forbidden + You do not have permissions to delete the rule. + HTTPException + Deleting the rule failed. + """ + await self._state.http.delete_auto_moderation_rule(self.guild.id, self.id, reason=reason) + + +class AutoModAction: + """Represents an action that was taken as the result of a moderation rule. + + .. versionadded:: 2.0 + + Attributes + ----------- + action: :class:`AutoModRuleAction` + The action that was taken. + message_id: Optional[:class:`int`] + The message ID that triggered the action. + rule_id: :class:`int` + The ID of the rule that was triggered. + rule_trigger_type: :class:`AutoModRuleTriggerType` + The trigger type of the rule that was triggered. + guild_id: :class:`int` + The ID of the guild where the rule was triggered. + user_id: :class:`int` + The ID of the user that triggered the rule. + channel_id: :class:`int` + The ID of the channel where the rule was triggered. + alert_system_message_id: Optional[:class:`int`] + The ID of the system message that was sent to the predefined alert channel. + content: :class:`str` + The content of the message that triggered the rule. + Requires the :attr:`Intents.message_content` or it will always return an empty string. + matched_keyword: Optional[:class:`str`] + The matched keyword from the triggering message. + matched_content: Optional[:class:`str`] + The matched content from the triggering message. + Requires the :attr:`Intents.message_content` or it will always return an empty string. + """ + + __slots__ = ( + '_state', + 'action', + 'rule_id', + 'rule_trigger_type', + 'guild_id', + 'user_id', + 'channel_id', + 'message_id', + 'alert_system_message_id', + 'content', + 'matched_keyword', + 'matched_content', + ) + + def __init__(self, *, data: AutoModerationActionExecutionPayload, state: ConnectionState) -> None: + self._state: ConnectionState = state + self.message_id: Optional[int] = utils._get_as_snowflake(data, 'message_id') + self.action: AutoModRuleAction = AutoModRuleAction.from_data(data['action']) + self.rule_id: int = int(data['rule_id']) + self.rule_trigger_type: AutoModRuleTriggerType = try_enum(AutoModRuleTriggerType, data['rule_trigger_type']) + self.guild_id: int = int(data['guild_id']) + self.channel_id: Optional[int] = utils._get_as_snowflake(data, 'channel_id') + self.user_id: int = int(data['user_id']) + self.alert_system_message_id: Optional[int] = utils._get_as_snowflake(data, 'alert_system_message_id') + self.content: str = data['content'] + self.matched_keyword: Optional[str] = data['matched_keyword'] + self.matched_content: Optional[str] = data['matched_content'] + + def __repr__(self) -> str: + return f'' + + @property + def guild(self) -> Guild: + """:class:`Guild`: The guild this action was taken in.""" + return self._state._get_or_create_unavailable_guild(self.guild_id) + + @property + def channel(self) -> Optional[Union[GuildChannel, Thread]]: + """Optional[Union[:class:`abc.GuildChannel`, :class:`Thread`]]: The channel this action was taken in.""" + if self.channel_id: + return self.guild.get_channel(self.channel_id) + return None + + @property + def member(self) -> Optional[Member]: + """Optional[:class:`Member`]: The member this action was taken against /who triggered this rule.""" + return self.guild.get_member(self.user_id) + + async def fetch_rule(self) -> AutoModRule: + """|coro| + + Fetch the rule whose action was taken. + + You must have the :attr:`Permissions.manage_guild` permission to use this. + + Raises + ------- + Forbidden + You do not have permissions to view the rule. + HTTPException + Fetching the rule failed. + + Returns + -------- + :class:`AutoModRule` + The rule that was executed. + """ + + data = await self._state.http.get_auto_moderation_rule(self.guild.id, self.rule_id) + return AutoModRule(data=data, guild=self.guild, state=self._state) diff --git a/discord/enums.py b/discord/enums.py index b6511ead8..85f154e9c 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -108,6 +108,9 @@ __all__ = ( 'ContentRatingAgency', 'Distributor', 'EntitlementType', + 'AutoModRuleTriggerType', + 'AutoModRuleEventType', + 'AutoModRuleActionType', ) if TYPE_CHECKING: @@ -279,6 +282,7 @@ class MessageType(Enum): thread_starter_message = 21 guild_invite_reminder = 22 context_menu_command = 23 + auto_moderation_action = 24 class SpeakingState(Enum): @@ -454,106 +458,116 @@ class AuditLogActionCategory(Enum): class AuditLogAction(Enum): # fmt: off - guild_update = 1 - channel_create = 10 - channel_update = 11 - channel_delete = 12 - overwrite_create = 13 - overwrite_update = 14 - overwrite_delete = 15 - kick = 20 - member_prune = 21 - ban = 22 - unban = 23 - member_update = 24 - member_role_update = 25 - member_move = 26 - member_disconnect = 27 - bot_add = 28 - role_create = 30 - role_update = 31 - role_delete = 32 - invite_create = 40 - invite_update = 41 - invite_delete = 42 - webhook_create = 50 - webhook_update = 51 - webhook_delete = 52 - emoji_create = 60 - emoji_update = 61 - emoji_delete = 62 - message_delete = 72 - message_bulk_delete = 73 - message_pin = 74 - message_unpin = 75 - integration_create = 80 - integration_update = 81 - integration_delete = 82 - stage_instance_create = 83 - stage_instance_update = 84 - stage_instance_delete = 85 - sticker_create = 90 - sticker_update = 91 - sticker_delete = 92 - scheduled_event_create = 100 - scheduled_event_update = 101 - scheduled_event_delete = 102 - thread_create = 110 - thread_update = 111 - thread_delete = 112 + guild_update = 1 + channel_create = 10 + channel_update = 11 + channel_delete = 12 + overwrite_create = 13 + overwrite_update = 14 + overwrite_delete = 15 + kick = 20 + member_prune = 21 + ban = 22 + unban = 23 + member_update = 24 + member_role_update = 25 + member_move = 26 + member_disconnect = 27 + bot_add = 28 + role_create = 30 + role_update = 31 + role_delete = 32 + invite_create = 40 + invite_update = 41 + invite_delete = 42 + webhook_create = 50 + webhook_update = 51 + webhook_delete = 52 + emoji_create = 60 + emoji_update = 61 + emoji_delete = 62 + message_delete = 72 + message_bulk_delete = 73 + message_pin = 74 + message_unpin = 75 + integration_create = 80 + integration_update = 81 + integration_delete = 82 + stage_instance_create = 83 + stage_instance_update = 84 + stage_instance_delete = 85 + sticker_create = 90 + sticker_update = 91 + sticker_delete = 92 + scheduled_event_create = 100 + scheduled_event_update = 101 + scheduled_event_delete = 102 + thread_create = 110 + thread_update = 111 + thread_delete = 112 + app_command_permission_update = 121 + automod_rule_create = 140 + automod_rule_update = 141 + automod_rule_delete = 142 + automod_block_message = 143 # fmt: on @property def category(self) -> Optional[AuditLogActionCategory]: # fmt: off lookup: Dict[AuditLogAction, Optional[AuditLogActionCategory]] = { - AuditLogAction.guild_update: AuditLogActionCategory.update, - AuditLogAction.channel_create: AuditLogActionCategory.create, - AuditLogAction.channel_update: AuditLogActionCategory.update, - AuditLogAction.channel_delete: AuditLogActionCategory.delete, - AuditLogAction.overwrite_create: AuditLogActionCategory.create, - AuditLogAction.overwrite_update: AuditLogActionCategory.update, - AuditLogAction.overwrite_delete: AuditLogActionCategory.delete, - AuditLogAction.kick: None, - AuditLogAction.member_prune: None, - AuditLogAction.ban: None, - AuditLogAction.unban: None, - AuditLogAction.member_update: AuditLogActionCategory.update, - AuditLogAction.member_role_update: AuditLogActionCategory.update, - AuditLogAction.member_move: None, - AuditLogAction.member_disconnect: None, - AuditLogAction.bot_add: None, - AuditLogAction.role_create: AuditLogActionCategory.create, - AuditLogAction.role_update: AuditLogActionCategory.update, - AuditLogAction.role_delete: AuditLogActionCategory.delete, - AuditLogAction.invite_create: AuditLogActionCategory.create, - AuditLogAction.invite_update: AuditLogActionCategory.update, - AuditLogAction.invite_delete: AuditLogActionCategory.delete, - AuditLogAction.webhook_create: AuditLogActionCategory.create, - AuditLogAction.webhook_update: AuditLogActionCategory.update, - AuditLogAction.webhook_delete: AuditLogActionCategory.delete, - AuditLogAction.emoji_create: AuditLogActionCategory.create, - AuditLogAction.emoji_update: AuditLogActionCategory.update, - AuditLogAction.emoji_delete: AuditLogActionCategory.delete, - AuditLogAction.message_delete: AuditLogActionCategory.delete, - AuditLogAction.message_bulk_delete: AuditLogActionCategory.delete, - AuditLogAction.message_pin: None, - AuditLogAction.message_unpin: None, - AuditLogAction.integration_create: AuditLogActionCategory.create, - AuditLogAction.integration_update: AuditLogActionCategory.update, - AuditLogAction.integration_delete: AuditLogActionCategory.delete, - AuditLogAction.stage_instance_create: AuditLogActionCategory.create, - AuditLogAction.stage_instance_update: AuditLogActionCategory.update, - AuditLogAction.stage_instance_delete: AuditLogActionCategory.delete, - AuditLogAction.sticker_create: AuditLogActionCategory.create, - AuditLogAction.sticker_update: AuditLogActionCategory.update, - AuditLogAction.sticker_delete: AuditLogActionCategory.delete, - AuditLogAction.scheduled_event_create: AuditLogActionCategory.create, - AuditLogAction.scheduled_event_update: AuditLogActionCategory.update, - AuditLogAction.scheduled_event_delete: AuditLogActionCategory.delete, - AuditLogAction.thread_create: AuditLogActionCategory.create, - AuditLogAction.thread_update: AuditLogActionCategory.update, - AuditLogAction.thread_delete: AuditLogActionCategory.delete, + AuditLogAction.guild_update: AuditLogActionCategory.update, + AuditLogAction.channel_create: AuditLogActionCategory.create, + AuditLogAction.channel_update: AuditLogActionCategory.update, + AuditLogAction.channel_delete: AuditLogActionCategory.delete, + AuditLogAction.overwrite_create: AuditLogActionCategory.create, + AuditLogAction.overwrite_update: AuditLogActionCategory.update, + AuditLogAction.overwrite_delete: AuditLogActionCategory.delete, + AuditLogAction.kick: None, + AuditLogAction.member_prune: None, + AuditLogAction.ban: None, + AuditLogAction.unban: None, + AuditLogAction.member_update: AuditLogActionCategory.update, + AuditLogAction.member_role_update: AuditLogActionCategory.update, + AuditLogAction.member_move: None, + AuditLogAction.member_disconnect: None, + AuditLogAction.bot_add: None, + AuditLogAction.role_create: AuditLogActionCategory.create, + AuditLogAction.role_update: AuditLogActionCategory.update, + AuditLogAction.role_delete: AuditLogActionCategory.delete, + AuditLogAction.invite_create: AuditLogActionCategory.create, + AuditLogAction.invite_update: AuditLogActionCategory.update, + AuditLogAction.invite_delete: AuditLogActionCategory.delete, + AuditLogAction.webhook_create: AuditLogActionCategory.create, + AuditLogAction.webhook_update: AuditLogActionCategory.update, + AuditLogAction.webhook_delete: AuditLogActionCategory.delete, + AuditLogAction.emoji_create: AuditLogActionCategory.create, + AuditLogAction.emoji_update: AuditLogActionCategory.update, + AuditLogAction.emoji_delete: AuditLogActionCategory.delete, + AuditLogAction.message_delete: AuditLogActionCategory.delete, + AuditLogAction.message_bulk_delete: AuditLogActionCategory.delete, + AuditLogAction.message_pin: None, + AuditLogAction.message_unpin: None, + AuditLogAction.integration_create: AuditLogActionCategory.create, + AuditLogAction.integration_update: AuditLogActionCategory.update, + AuditLogAction.integration_delete: AuditLogActionCategory.delete, + AuditLogAction.stage_instance_create: AuditLogActionCategory.create, + AuditLogAction.stage_instance_update: AuditLogActionCategory.update, + AuditLogAction.stage_instance_delete: AuditLogActionCategory.delete, + AuditLogAction.sticker_create: AuditLogActionCategory.create, + AuditLogAction.sticker_update: AuditLogActionCategory.update, + AuditLogAction.sticker_delete: AuditLogActionCategory.delete, + AuditLogAction.scheduled_event_create: AuditLogActionCategory.create, + AuditLogAction.scheduled_event_update: AuditLogActionCategory.update, + AuditLogAction.scheduled_event_delete: AuditLogActionCategory.delete, + AuditLogAction.thread_create: AuditLogActionCategory.create, + AuditLogAction.thread_delete: AuditLogActionCategory.delete, + AuditLogAction.thread_update: AuditLogActionCategory.update, + AuditLogAction.app_command_permission_update: AuditLogActionCategory.update, + AuditLogAction.automod_rule_create: AuditLogActionCategory.create, + AuditLogAction.automod_rule_update: AuditLogActionCategory.update, + AuditLogAction.automod_rule_delete: AuditLogActionCategory.delete, + AuditLogAction.automod_block_message: None, } # fmt: on return lookup[self] @@ -591,6 +605,10 @@ class AuditLogAction(Enum): return 'guild_scheduled_event' elif v < 113: return 'thread' + elif v < 122: + return 'integration_or_app_command' + elif v < 144: + return 'auto_moderation' class UserFlags(Enum): @@ -1430,6 +1448,23 @@ class EntitlementType(Enum): return self.value +class AutoModRuleTriggerType(Enum): + keyword = 1 + harmful_link = 2 + spam = 3 + keyword_preset = 4 + + +class AutoModRuleEventType(Enum): + message_send = 1 + + +class AutoModRuleActionType(Enum): + block_message = 1 + send_alert_message = 2 + timeout = 3 + + def create_unknown_value(cls: Type[E], val: Any) -> E: value_cls = cls._enum_value_cls_ # type: ignore # This is narrowed below name = f'unknown_{val}' diff --git a/discord/flags.py b/discord/flags.py index 738209e29..302bf3843 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -24,6 +24,7 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations +from functools import reduce from typing import TYPE_CHECKING, Any, Callable, ClassVar, Dict, Iterator, List, Optional, Tuple, Type, TypeVar, overload from .enums import UserFlags @@ -54,6 +55,7 @@ __all__ = ( 'FriendDiscoveryFlags', 'HubProgressFlags', 'OnboardingProgressFlags', + 'AutoModPresets', ) BF = TypeVar('BF', bound='BaseFlags') @@ -189,6 +191,17 @@ class BaseFlags: raise TypeError(f'Value to set for {self.__class__.__name__} must be a bool.') +class ArrayFlags(BaseFlags): + @classmethod + def _from_value(cls: Type[Self], value: List[int]) -> Self: + self = cls.__new__(cls) + self.value = reduce(lambda a, b: a | (1 << b - 1), value) + return self + + def to_array(self) -> List[int]: + return [i + 1 for i in range(self.value.bit_length()) if self.value & (1 << i)] + + @fill_with_flags() class Capabilities(BaseFlags): """Wraps up the Discord gateway capabilities. @@ -2148,3 +2161,78 @@ class OnboardingProgressFlags(BaseFlags): def notice_cleared(self): """:class:`bool`: Returns ``True`` if the user has cleared the onboarding notice.""" return 1 << 1 + + +@fill_with_flags() +class AutoModPresets(ArrayFlags): + r"""Wraps up the Discord :class:`AutoModRule` presets. + + .. container:: operations + .. describe:: x == y + + Checks if two AutoModPresets flags are equal. + .. describe:: x != y + + Checks if two AutoModPresets flags are not equal. + .. describe:: x | y, x |= y + + Returns a AutoModPresets instance with all enabled flags from + both x and y. + .. describe:: x & y, x &= y + + Returns a AutoModPresets instance with only flags enabled on + both x and y. + .. describe:: x ^ y, x ^= y + + Returns a AutoModPresets instance with only flags enabled on + only one of x or y, not on both. + .. describe:: ~x + + Returns a AutoModPresets instance with all flags inverted from x. + .. describe:: hash(x) + + Return the flag's hash. + .. describe:: iter(x) + + Returns an iterator of ``(name, value)`` pairs. This allows it + to be, for example, constructed as a dict or a list of pairs. + + .. versionadded:: 2.0 + + Attributes + ----------- + value: :class:`int` + The raw value. You should query flags via the properties + rather than using this raw value. + """ + + @classmethod + def all(cls: Type[Self]) -> Self: + """A factory method that creates a :class:`AutoModPresets` with everything enabled.""" + bits = max(cls.VALID_FLAGS.values()).bit_length() + value = (1 << bits) - 1 + self = cls.__new__(cls) + self.value = value + return self + + @classmethod + def none(cls: Type[Self]) -> Self: + """A factory method that creates a :class:`AutoModPresets` with everything disabled.""" + self = cls.__new__(cls) + self.value = self.DEFAULT_VALUE + return self + + @flag_value + def profanity(self): + """:class:`bool`: Whether to use the preset profanity filter.""" + return 1 << 0 + + @flag_value + def sexual_content(self): + """:class:`bool`: Whether to use the preset sexual content filter.""" + return 1 << 1 + + @flag_value + def slurs(self): + """:class:`bool`: Whether to use the preset slurs filter.""" + return 1 << 2 diff --git a/discord/guild.py b/discord/guild.py index ee2fc599a..5dac2b899 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -71,6 +71,7 @@ from .enums import ( NSFWLevel, MFALevel, Locale, + AutoModRuleEventType, ) from .mixins import Hashable from .user import User @@ -92,16 +93,7 @@ from .welcome_screen import * from .application import PartialApplication from .guild_premium import PremiumGuildSubscription from .entitlements import Entitlement - - -__all__ = ( - 'Guild', - 'BanEntry', -) - -MISSING = utils.MISSING - -_log = logging.getLogger(__name__) +from .automod import AutoModRule, AutoModTrigger, AutoModRuleAction if TYPE_CHECKING: from .abc import Snowflake, SnowflakeTime @@ -139,6 +131,13 @@ if TYPE_CHECKING: GuildChannel = Union[VocalGuildChannel, ForumChannel, TextChannel, CategoryChannel] ByCategoryItem = Tuple[Optional[CategoryChannel], List[GuildChannel]] +MISSING = utils.MISSING + +__all__ = ( + 'Guild', + 'BanEntry', +) + class CommandCounts(NamedTuple): chat_input: int @@ -4131,3 +4130,121 @@ class Guild(Hashable): to your :class:`Client`. """ await self._state.request_guild(self.id, **kwargs) + + async def automod_rules(self) -> List[AutoModRule]: + """|coro| + + Fetches all automod rules from the guild. + + You must have the :attr:`Permissions.manage_guild` to use this. + + .. versionadded:: 2.0 + + Raises + ------- + Forbidden + You do not have permission to view the automod rule. + NotFound + There are no automod rules within this guild. + + Returns + -------- + List[:class:`AutoModRule`] + The automod rules that were fetched. + """ + data = await self._state.http.get_auto_moderation_rules(self.id) + return [AutoModRule(data=d, guild=self, state=self._state) for d in data] + + async def fetch_automod_rule(self, automod_rule_id: int, /) -> AutoModRule: + """|coro| + + Fetches an active automod rule from the guild. + + You must have the :attr:`Permissions.manage_guild` to use this. + + .. versionadded:: 2.0 + + Parameters + ----------- + automod_rule_id: :class:`int` + The ID of the automod rule to fetch. + + Raises + ------- + Forbidden + You do not have permission to view the automod rule. + + Returns + -------- + :class:`AutoModRule` + The automod rule that was fetched. + """ + data = await self._state.http.get_auto_moderation_rule(self.id, automod_rule_id) + return AutoModRule(data=data, guild=self, state=self._state) + + async def create_automod_rule( + self, + *, + name: str, + event_type: AutoModRuleEventType, + trigger: AutoModTrigger, + actions: List[AutoModRuleAction], + enabled: bool = MISSING, + exempt_roles: Sequence[Snowflake] = MISSING, + exempt_channels: Sequence[Snowflake] = MISSING, + reason: str = MISSING, + ) -> AutoModRule: + """|coro| + + Create an automod rule. + + You must have the :attr:`Permissions.manage_guild` permission to use this. + + .. versionadded:: 2.0 + + Parameters + ----------- + name: :class:`str` + The name of the automod rule. + event_type: :class:`AutoModRuleEventType` + The type of event that the automod rule will trigger on. + trigger: :class:`AutoModTrigger` + The trigger that will trigger the automod rule. + actions: List[:class:`AutoModRuleAction`] + The actions that will be taken when the automod rule is triggered. + enabled: :class:`bool` + Whether the automod rule is enabled. + Discord will default to ``False``. + exempt_roles: Sequence[:class:`abc.Snowflake`] + A list of roles that will be exempt from the automod rule. + exempt_channels: Sequence[:class:`abc.Snowflake`] + A list of channels that will be exempt from the automod rule. + reason: :class:`str` + The reason for creating this automod rule. Shows up on the audit log. + + Raises + ------- + Forbidden + You do not have permissions to create an automod rule. + HTTPException + Creating the automod rule failed. + + Returns + -------- + :class:`AutoModRule` + The automod rule that was created. + """ + data = await self._state.http.create_auto_moderation_rule( + self.id, + name=name, + event_type=event_type.value, + trigger_type=trigger.type.value, + trigger_metadata=trigger.to_metadata_dict() or None, + actions=[a.to_dict() for a in actions], + enabled=enabled, + exempt_roles=[str(r.id) for r in exempt_roles] if exempt_roles else None, + exempt_channel=[str(c.id) for c in exempt_channels] if exempt_channels else None, + reason=reason, + ) + + return AutoModRule(data=data, guild=self, state=self._state) diff --git a/discord/http.py b/discord/http.py index 5540eb18c..e4df840f1 100644 --- a/discord/http.py +++ b/discord/http.py @@ -87,6 +87,7 @@ if TYPE_CHECKING: from .types import ( application, audit_log, + automod, billing, channel, emoji, @@ -3681,6 +3682,63 @@ class HTTPClient: def unenroll_active_developer(self) -> Response[None]: return self.request(Route('DELETE', '/developers/active-program'), super_properties_to_track=True) + def get_auto_moderation_rules(self, guild_id: Snowflake) -> Response[List[automod.AutoModerationRule]]: + return self.request(Route('GET', '/guilds/{guild_id}/auto-moderation/rules', guild_id=guild_id)) + + def get_auto_moderation_rule(self, guild_id: Snowflake, rule_id: Snowflake) -> Response[automod.AutoModerationRule]: + return self.request( + Route('GET', '/guilds/{guild_id}/auto-moderation/rules/{rule_id}', guild_id=guild_id, rule_id=rule_id) + ) + + def create_auto_moderation_rule( + self, guild_id: Snowflake, *, reason: Optional[str], **payload: Any + ) -> Response[automod.AutoModerationRule]: + valid_keys = ( + 'name', + 'event_type', + 'trigger_type', + 'trigger_metadata', + 'actions', + 'enabled', + 'exempt_roles', + 'exempt_channels', + ) + + payload = {k: v for k, v in payload.items() if k in valid_keys and v is not None} + + return self.request( + Route('POST', '/guilds/{guild_id}/auto-moderation/rules', guild_id=guild_id), json=payload, reason=reason + ) + + def edit_auto_moderation_rule( + self, guild_id: Snowflake, rule_id: Snowflake, *, reason: Optional[str], **payload: Any + ) -> Response[automod.AutoModerationRule]: + valid_keys = ( + 'name', + 'event_type', + 'trigger_metadata', + 'actions', + 'enabled', + 'exempt_roles', + 'exempt_channels', + ) + + payload = {k: v for k, v in payload.items() if k in valid_keys and v is not None} + + return self.request( + Route('PATCH', '/guilds/{guild_id}/auto-moderation/rules/{rule_id}', guild_id=guild_id, rule_id=rule_id), + json=payload, + reason=reason, + ) + + def delete_auto_moderation_rule( + self, guild_id: Snowflake, rule_id: Snowflake, *, reason: Optional[str] + ) -> Response[None]: + return self.request( + Route('DELETE', '/guilds/{guild_id}/auto-moderation/rules/{rule_id}', guild_id=guild_id, rule_id=rule_id), + reason=reason, + ) + # Misc async def get_gateway(self, *, encoding: str = 'json', zlib: bool = True) -> str: diff --git a/discord/state.py b/discord/state.py index e70de9178..00541fc61 100644 --- a/discord/state.py +++ b/discord/state.py @@ -92,6 +92,7 @@ from .payments import Payment from .entitlements import Entitlement, Gift from .guild_premium import PremiumGuildSubscriptionSlot from .library import LibraryApplication +from .automod import AutoModRule, AutoModAction if TYPE_CHECKING: from typing_extensions import Self @@ -106,6 +107,7 @@ if TYPE_CHECKING: from .gateway import DiscordWebSocket from .calls import Call + from .types.automod import AutoModerationRule, AutoModerationActionExecution from .types.snowflake import Snowflake from .types.activity import Activity as ActivityPayload from .types.application import Achievement as AchievementPayload, IntegrationApplication as IntegrationApplicationPayload @@ -1973,6 +1975,42 @@ class ConnectionState: guild.stickers = tuple(map(lambda d: self.store_sticker(guild, d), data['stickers'])) self.dispatch('guild_stickers_update', guild, before_stickers, guild.stickers) + def parse_auto_moderation_rule_create(self, data: AutoModerationRule) -> None: + guild = self._get_guild(int(data['guild_id'])) + if guild is None: + _log.debug('AUTO_MODERATION_RULE_CREATE referencing an unknown guild ID: %s. Discarding.', data['guild_id']) + return + + rule = AutoModRule(data=data, guild=guild, state=self) + self.dispatch('automod_rule_create', rule) + + def parse_auto_moderation_rule_update(self, data: AutoModerationRule) -> None: + guild = self._get_guild(int(data['guild_id'])) + if guild is None: + _log.debug('AUTO_MODERATION_RULE_UPDATE referencing an unknown guild ID: %s. Discarding.', data['guild_id']) + return + + rule = AutoModRule(data=data, guild=guild, state=self) + self.dispatch('automod_rule_update', rule) + + def parse_auto_moderation_rule_delete(self, data: AutoModerationRule) -> None: + guild = self._get_guild(int(data['guild_id'])) + if guild is None: + _log.debug('AUTO_MODERATION_RULE_DELETE referencing an unknown guild ID: %s. Discarding.', data['guild_id']) + return + + rule = AutoModRule(data=data, guild=guild, state=self) + self.dispatch('automod_rule_delete', rule) + + def parse_auto_moderation_action_execution(self, data: AutoModerationActionExecution) -> None: + guild = self._get_guild(int(data['guild_id'])) + if guild is None: + _log.debug('AUTO_MODERATION_ACTION_EXECUTION referencing an unknown guild ID: %s. Discarding.', data['guild_id']) + return + + execution = AutoModAction(data=data, state=self) + self.dispatch('automod_action', execution) + def _get_create_guild(self, data: gw.GuildCreateEvent): guild = self._get_guild(int(data['id'])) # Discord being Discord sometimes sends a GUILD_CREATE after an OPCode 14 is sent (a la bots) diff --git a/discord/types/audit_log.py b/discord/types/audit_log.py index c2997596b..0737528ce 100644 --- a/discord/types/audit_log.py +++ b/discord/types/audit_log.py @@ -272,6 +272,10 @@ class AuditEntryInfo(TypedDict): id: Snowflake type: Literal['0', '1'] role_name: str + application_id: Snowflake + guild_id: Snowflake + auto_moderation_rule_name: str + auto_moderation_rule_trigger_type: str class AuditLogEntry(TypedDict): diff --git a/discord/types/automod.py b/discord/types/automod.py new file mode 100644 index 000000000..0a1150fa9 --- /dev/null +++ b/discord/types/automod.py @@ -0,0 +1,119 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-present Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from typing import Literal, TypedDict, List, Union, Optional +from typing_extensions import NotRequired + +from .snowflake import Snowflake + +AutoModerationRuleTriggerType = Literal[1, 2, 3, 4] +AutoModerationActionTriggerType = Literal[1, 2, 3] +AutoModerationRuleEventType = Literal[1] +AutoModerationTriggerPresets = Literal[1, 2, 3] + + +class Empty(TypedDict): + ... + + +class _AutoModerationActionMetadataAlert(TypedDict): + channel_id: Snowflake + + +class _AutoModerationActionMetadataTimeout(TypedDict): + duration_seconds: int + + +class _AutoModerationActionBlockMessage(TypedDict): + type: Literal[1] + metadata: NotRequired[Empty] + + +class _AutoModerationActionAlert(TypedDict): + type: Literal[2] + metadata: _AutoModerationActionMetadataAlert + + +class _AutoModerationActionTimeout(TypedDict): + type: Literal[3] + metadata: _AutoModerationActionMetadataTimeout + + +AutoModerationAction = Union[_AutoModerationActionBlockMessage, _AutoModerationActionAlert, _AutoModerationActionTimeout] + + +class _AutoModerationTriggerMetadataKeyword(TypedDict): + keyword_filter: List[str] + + +class _AutoModerationTriggerMetadataKeywordPreset(TypedDict): + presets: List[AutoModerationTriggerPresets] + + +AutoModerationTriggerMetadata = Union[ + _AutoModerationTriggerMetadataKeyword, _AutoModerationTriggerMetadataKeywordPreset, Empty +] + + +class _BaseAutoModerationRule(TypedDict): + id: Snowflake + guild_id: Snowflake + name: str + creator_id: Snowflake + event_type: AutoModerationRuleEventType + actions: List[AutoModerationAction] + enabled: bool + exempt_roles: List[Snowflake] + exempt_channels: List[Snowflake] + + +class _AutoModerationRuleKeyword(_BaseAutoModerationRule): + trigger_type: Literal[1] + trigger_metadata: _AutoModerationTriggerMetadataKeyword + + +class _AutoModerationRuleKeywordPreset(_BaseAutoModerationRule): + trigger_type: Literal[4] + trigger_metadata: _AutoModerationTriggerMetadataKeywordPreset + + +class _AutoModerationRuleOther(_BaseAutoModerationRule): + trigger_type: Literal[2, 3] + + +AutoModerationRule = Union[_AutoModerationRuleKeyword, _AutoModerationRuleKeywordPreset, _AutoModerationRuleOther] + + +class AutoModerationActionExecution(TypedDict): + guild_id: Snowflake + action: AutoModerationAction + rule_id: Snowflake + rule_trigger_type: AutoModerationRuleTriggerType + user_id: Snowflake + channel_id: NotRequired[Snowflake] + message_id: NotRequired[Snowflake] + alert_system_message_id: NotRequired[Snowflake] + content: str + matched_keyword: Optional[str] + matched_content: Optional[str] diff --git a/discord/types/gateway.py b/discord/types/gateway.py index 77e880b7e..d8a73f8f5 100644 --- a/discord/types/gateway.py +++ b/discord/types/gateway.py @@ -26,6 +26,7 @@ from typing import List, Literal, Optional, TypedDict, Union from typing_extensions import NotRequired, Required from .activity import Activity, ClientStatus, PartialPresenceUpdate, StatusType +from .automod import AutoModerationAction, AutoModerationRuleTriggerType from .voice import GuildVoiceState from .integration import BaseIntegration, IntegrationApplication from .role import Role @@ -463,3 +464,17 @@ class ProtoSettingsEvent(TypedDict): class RecentMentionDeleteEvent(TypedDict): message_id: Snowflake + + +class AutoModerationActionExecution(TypedDict): + guild_id: Snowflake + action: AutoModerationAction + rule_id: Snowflake + rule_trigger_type: AutoModerationRuleTriggerType + user_id: Snowflake + channel_id: NotRequired[Snowflake] + message_id: NotRequired[Snowflake] + alert_system_message_id: NotRequired[Snowflake] + content: str + matched_keyword: Optional[str] + matched_content: Optional[str] diff --git a/docs/api.rst b/docs/api.rst index 360aaf07b..5fefe2c42 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -158,6 +158,53 @@ to handle it, which defaults to logging the traceback and ignoring the exception All the events must be a |coroutine_link|_. If they aren't, then you might get unexpected errors. In order to turn a function into a coroutine they must be defined with ``async def``. +AutoMod +~~~~~~~ + +.. function:: on_automod_rule_create(rule) + + Called when a :class:`AutoModRule` is created. + + This requires :attr:`Intents.auto_moderation_configuration` to be enabled. + + .. versionadded:: 2.0 + + :param rule: The rule that was created. + :type rule: :class:`AutoModRule` + +.. function:: on_automod_rule_update(rule) + + Called when a :class:`AutoModRule` is updated. + + This requires :attr:`Intents.auto_moderation_configuration` to be enabled. + + .. versionadded:: 2.0 + + :param rule: The rule that was updated. + :type rule: :class:`AutoModRule` + +.. function:: on_automod_rule_delete(rule) + + Called when a :class:`AutoModRule` is deleted. + + This requires :attr:`Intents.auto_moderation_configuration` to be enabled. + + .. versionadded:: 2.0 + + :param rule: The rule that was deleted. + :type rule: :class:`AutoModRule` + +.. function:: on_automod_action(execution) + + Called when a :class:`AutoModAction` is created/performed. + + This requires :attr:`Intents.auto_moderation_execution` to be enabled. + + .. versionadded:: 2.0 + + :param execution: The rule execution that was performed. + :type execution: :class:`AutoModAction` + Channels ~~~~~~~~~ @@ -2701,6 +2748,84 @@ of :class:`enum.Enum`. .. versionadded:: 2.0 + .. attribute:: automod_rule_create + + An automod rule was created. + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + a :class:`Object` with the ID of the automod rule that was created. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.name` + - :attr:`~AuditLogDiff.enabled` + - :attr:`~AuditLogDiff.event_type` + - :attr:`~AuditLogDiff.trigger_type` + - :attr:`~AuditLogDiff.trigger_metadata` + - :attr:`~AuditLogDiff.actions` + - :attr:`~AuditLogDiff.exempt_roles` + - :attr:`~AuditLogDiff.exempt_channels` + + .. versionadded:: 2.0 + + .. attribute:: automod_role_update + + An automod rule was updated. + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + a :class:`Object` with the ID of the automod rule that was updated. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.name` + - :attr:`~AuditLogDiff.enabled` + - :attr:`~AuditLogDiff.event_type` + - :attr:`~AuditLogDiff.trigger_type` + - :attr:`~AuditLogDiff.trigger_metadata` + - :attr:`~AuditLogDiff.actions` + - :attr:`~AuditLogDiff.exempt_roles` + - :attr:`~AuditLogDiff.exempt_channels` + + .. versionadded:: 2.0 + + .. attribute:: automod_rule_delete + + An automod rule was deleted. + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + a :class:`Object` with the ID of the automod rule that was deleted. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.name` + - :attr:`~AuditLogDiff.enabled` + - :attr:`~AuditLogDiff.event_type` + - :attr:`~AuditLogDiff.trigger_type` + - :attr:`~AuditLogDiff.trigger_metadata` + - :attr:`~AuditLogDiff.actions` + - :attr:`~AuditLogDiff.exempt_roles` + - :attr:`~AuditLogDiff.exempt_channels` + + .. versionadded:: 2.0 + + .. attribute:: automod_block_message + + An automod rule blocked a message from being sent. + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + a :class:`Member` with the ID of the person who triggered the automod rule. + + When this is the action, the type of :attr:`~AuditLogEntry.extra` is + set to an unspecified proxy object with 3 attributes: + + - ``automod_rule_name``: The name of the automod rule that was triggered. + - ``automod_rule_trigger``: A :class:`AutoModRuleTriggerType` representation of the rule type that was triggered. + - ``channel``: The channel in which the automod rule was triggered. + + When this is the action, :attr:`AuditLogEntry.changes` is empty. + + .. versionadded:: 2.0 + .. class:: AuditLogActionCategory Represents the category that the :class:`AuditLogAction` belongs to. @@ -5126,6 +5251,56 @@ of :class:`enum.Enum`. An attachment parameter. +.. class:: AutoModRuleTriggerType + + Represents the trigger type of an auto mod rule. + + .. versionadded:: 2.0 + + .. attribute:: keyword + + The rule will trigger when a keyword is mentioned. + + .. attribute:: harmful_link + + The rule will trigger when a harmful link is posted. + + .. attribute:: spam + + The rule will trigger when a spam message is posted. + + .. attribute:: keyword_preset + + The rule will trigger when something triggers based on the set keyword preset types. + +.. class:: AutoModRuleEventType + + Represents the event type of an auto mod rule. + + .. versionadded:: 2.0 + + .. attribute:: message_send + + The rule will trigger when a message is sent. + +.. class:: AutoModRuleActionType + + Represents the action type of an auto mod rule. + + .. versionadded:: 2.0 + + .. attribute:: block_message + + The rule will block a message from being sent. + + .. attribute:: send_alert_message + + The rule will send an alert message to a predefined channel. + + .. attribute:: timeout + + The rule will timeout a user. + .. _discord-api-audit-logs: Audit Log Data @@ -5713,6 +5888,54 @@ AuditLogDiff :type: :class:`Asset` + .. attribute:: app_command_permissions + + The permissions of the app command. + + :type: :class:`~discord.app_commands.AppCommandPermissions` + + .. attribute:: enabled + + Whether the automod rule is active or not. + + :type: :class:`bool` + + .. attribute:: event_type + + The event type for triggering the automod rule. + + :type: :class:`str` + + .. attribute:: trigger_type + + The trigger type for the automod rule. + + :type: :class:`AutoModRuleTriggerType` + + .. attribute:: trigger_metadata + + The trigger metadata for the automod rule. + + :type: Dict[:class:`str`, Any] + + .. attribute:: actions + + The actions to take when an automod rule is triggered. + + :type: List[Dict[:class:`str`, Any]] + + .. attribute:: exempt_roles + + The list of roles that are exempt from the automod rule. + + :type: List[:class:`str`] + + .. attribute:: exempt_channels + + The list of channels that are exempt from the automod rule. + + :type: List[:class:`str`] + .. this is currently missing the following keys: reason and application_id I'm not sure how to about porting these @@ -6385,6 +6608,30 @@ Integration .. autoclass:: StreamIntegration() :members: +AutoMod +~~~~~~~ + +.. autoclass:: AutoModRule() + :members: + +.. autoclass:: AutoModAction() + :members: + +.. attributetable:: AutoModPresets + +.. autoclass:: AutoModPresets + :members: + +.. attributetable:: AutoModRuleAction + +.. autoclass:: AutoModRuleAction + :members: + +.. attributetable:: AutoModTrigger + +.. autoclass:: AutoModTrigger + :members: + Member ~~~~~~