From d00871a8c7db25de196f2aba41f4cf860bb76853 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Mon, 2 May 2022 20:40:06 -0700 Subject: [PATCH 01/51] Add auto moderation --- discord/enums.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/discord/enums.py b/discord/enums.py index 4d9805e02e..8794765a4a 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -232,6 +232,7 @@ class MessageType(Enum): thread_starter_message = 21 guild_invite_reminder = 22 context_menu_command = 23 + auto_moderation_action = 24 class VoiceRegion(Enum): @@ -380,6 +381,10 @@ class AuditLogAction(Enum): thread_create = 110 thread_update = 111 thread_delete = 112 + auto_moderation_rule_create = 140 + auto_moderation_rule_update = 141 + auto_moderation_rule_delete = 142 + @property def category(self) -> Optional[AuditLogActionCategory]: @@ -431,6 +436,9 @@ def category(self) -> Optional[AuditLogActionCategory]: AuditLogAction.thread_create: AuditLogActionCategory.create, AuditLogAction.thread_update: AuditLogActionCategory.update, AuditLogAction.thread_delete: AuditLogActionCategory.delete, + AuditLogAction.auto_moderation_rule_create: AuditLogActionCategory.create, + AuditLogAction.auto_moderation_rule_update: AuditLogActionCategory.update, + AuditLogAction.auto_moderation_rule_delete: AuditLogActionCategory.delete, } return lookup[self] @@ -467,6 +475,8 @@ def target_type(self) -> Optional[str]: return "scheduled_event" elif v < 113: return "thread" + elif v < 143: + return "auto_moderation_rule" class UserFlags(Enum): From 0bb1e1b9463275cb5f13a8ac235a1b23e4bb4ad3 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Mon, 2 May 2022 20:40:48 -0700 Subject: [PATCH 02/51] fix MessageType type --- discord/types/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/types/message.py b/discord/types/message.py index 9aa0d3ef28..62dd9ec5c9 100644 --- a/discord/types/message.py +++ b/discord/types/message.py @@ -112,7 +112,7 @@ class _MessageOptional(TypedDict, total=False): components: List[Component] -MessageType = Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 18, 19, 20, 21] +MessageType = Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 18, 19, 20, 21, 22, 23, 24] class Message(_MessageOptional): From de958ea6538344c376429bf5b0d6b3d95ec405ba Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Tue, 3 May 2022 13:52:03 -0700 Subject: [PATCH 03/51] Add embed type --- discord/types/embed.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/types/embed.py b/discord/types/embed.py index 6dccd1906e..8e64c946f5 100644 --- a/discord/types/embed.py +++ b/discord/types/embed.py @@ -77,7 +77,7 @@ class EmbedAuthor(TypedDict, total=False): proxy_icon_url: str -EmbedType = Literal["rich", "image", "video", "gifv", "article", "link"] +EmbedType = Literal["rich", "image", "video", "gifv", "article", "link", "auto_moderation_message"] class Embed(TypedDict, total=False): From 8b6b4f557b87864f2fa72a1fce7bf20aca48e625 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Tue, 3 May 2022 13:53:30 -0700 Subject: [PATCH 04/51] Add AUTO_MODERATION guild feature --- discord/types/guild.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/types/guild.py b/discord/types/guild.py index 7ecaaa96b1..73b684a5c7 100644 --- a/discord/types/guild.py +++ b/discord/types/guild.py @@ -114,6 +114,7 @@ class _GuildOptional(TypedDict, total=False): "VERIFIED", "VIP_REGIONS", "WELCOME_SCREEN_ENABLED", + "AUTO_MODERATION", ] From 7fc0fda4af75cb3f27de6439086a70406e4c0756 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Tue, 3 May 2022 13:54:17 -0700 Subject: [PATCH 05/51] Document AUTO_MODERATION feature --- discord/guild.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/guild.py b/discord/guild.py index 8f20658210..76ab27fe9e 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -234,6 +234,7 @@ class Guild(Hashable): - ``VERIFIED``: Guild is a verified server. - ``VIP_REGIONS``: Guild has VIP voice regions. - ``WELCOME_SCREEN_ENABLED``: Guild has enabled the welcome screen. + - ``AUTO_MODERATION``: Guild has enabled the auto moderation system. premium_tier: :class:`int` The premium tier for this guild. Corresponds to "Nitro Server" in the official UI. From 14af3cb14a527de8de9f1b4bddaac0c3c4d570d0 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Fri, 6 May 2022 08:14:03 -0700 Subject: [PATCH 06/51] Update enums.py --- discord/enums.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/discord/enums.py b/discord/enums.py index 8794765a4a..1760473d4f 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -384,6 +384,7 @@ class AuditLogAction(Enum): auto_moderation_rule_create = 140 auto_moderation_rule_update = 141 auto_moderation_rule_delete = 142 + auto_moderation_block_message = 143 @property @@ -439,6 +440,7 @@ def category(self) -> Optional[AuditLogActionCategory]: AuditLogAction.auto_moderation_rule_create: AuditLogActionCategory.create, AuditLogAction.auto_moderation_rule_update: AuditLogActionCategory.update, AuditLogAction.auto_moderation_rule_delete: AuditLogActionCategory.delete, + AuditLogAction.auto_moderation_block_message: None, } return lookup[self] @@ -475,7 +477,7 @@ def target_type(self) -> Optional[str]: return "scheduled_event" elif v < 113: return "thread" - elif v < 143: + elif v < 144: return "auto_moderation_rule" From 6992a26f7d8e8d6ab9488f8c82070baf47ffc1ff Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Fri, 13 May 2022 13:52:15 -0700 Subject: [PATCH 07/51] Add more automod enums --- discord/enums.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/discord/enums.py b/discord/enums.py index 40371e1118..8b751f66ca 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -775,6 +775,19 @@ class ScheduledEventLocationType(Enum): voice = 2 external = 3 + +class AutoModRuleTriggerType(Enum): + keyword_filter = 1 + + +class AutoModEventType(Enum): + message_send = 1 + + +class AutoModRuleAction(Enum): + block_message = 1 + send_an_alert = 2 + T = TypeVar("T") From d31ed4bd58e1e73edf77fa186c03ac86401013bb Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Fri, 13 May 2022 13:59:33 -0700 Subject: [PATCH 08/51] Add AutoMod types --- discord/types/automod.py | 43 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 discord/types/automod.py diff --git a/discord/types/automod.py b/discord/types/automod.py new file mode 100644 index 0000000000..337b40c663 --- /dev/null +++ b/discord/types/automod.py @@ -0,0 +1,43 @@ +""" +The MIT License (MIT) +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development +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 + +from typing import TypedDict, Literal, List, Dict + +from .snowflake import Snowflake + +AutoModRuleTriggerType = Literal[1] + +AutoModEventType = Literal[1] + +AutoModRuleAction = Literal[1, 2] + +class AutoModRule(TypedDict): + name: str + trigger_type: AutoModRuleTriggerType + event_type: AutoModEventType + actions: List[AutoModRuleAction] + trigger_metadata: Dict # TODO: unclear what this is meant to be + enabled: bool + exempt_roles: Snowflake + exempt_channels: Snowflake + position: int From 05a00cb8613daf65abfa2b1fc5638d4c1ef3cae0 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Thu, 19 May 2022 14:38:31 -0700 Subject: [PATCH 09/51] Update automod typing --- discord/types/automod.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/discord/types/automod.py b/discord/types/automod.py index 337b40c663..892bf5802e 100644 --- a/discord/types/automod.py +++ b/discord/types/automod.py @@ -25,19 +25,26 @@ from .snowflake import Snowflake -AutoModRuleTriggerType = Literal[1] +AutoModRuleTriggerType = Literal[1, 2, 3, 4] AutoModEventType = Literal[1] -AutoModRuleAction = Literal[1, 2] +AutoModActionType = Literal[1, 2] + +class AutoModAction(TypedDict): + type: AutoModActionType + metadata: Dict # TODO + class AutoModRule(TypedDict): + id: Snowflake + guild_id: Snowflake name: str - trigger_type: AutoModRuleTriggerType + creator_id: Snowflake event_type: AutoModEventType + trigger_type: AutoModRuleTriggerType actions: List[AutoModRuleAction] trigger_metadata: Dict # TODO: unclear what this is meant to be enabled: bool - exempt_roles: Snowflake - exempt_channels: Snowflake - position: int + exempt_roles: List[Snowflake] + exempt_channels: List[Snowflake] From 4bfc67e98e03a9b62250f73e3b53d79b2c9517c2 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Fri, 20 May 2022 13:53:41 -0700 Subject: [PATCH 10/51] Update audit_log.py --- discord/types/audit_log.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/discord/types/audit_log.py b/discord/types/audit_log.py index 273b6e1bb0..6b2d4c19ae 100644 --- a/discord/types/audit_log.py +++ b/discord/types/audit_log.py @@ -41,6 +41,7 @@ from .threads import Thread from .user import User from .webhook import Webhook +from .automod import AutoModRule AuditLogEvent = Literal[ 1, @@ -90,6 +91,11 @@ 110, 111, 112, + 121, + 140, + 141, + 142, + 143, ] @@ -283,3 +289,4 @@ class AuditLog(TypedDict): integrations: List[PartialIntegration] threads: List[Thread] scheduled_events: List[ScheduledEvent] + auto_moderation_rules: List[AutoModRule] From 22fb84a1760050fdafb70b47975432d8028e1bd9 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Fri, 20 May 2022 13:56:49 -0700 Subject: [PATCH 11/51] Update enums.py --- discord/enums.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index 8b751f66ca..da13d91a8d 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -777,7 +777,10 @@ class ScheduledEventLocationType(Enum): class AutoModRuleTriggerType(Enum): - keyword_filter = 1 + keyword = 1 + harmful_links = 2 + spam = 3 + default_keyword_list = 4 class AutoModEventType(Enum): @@ -786,7 +789,7 @@ class AutoModEventType(Enum): class AutoModRuleAction(Enum): block_message = 1 - send_an_alert = 2 + log_to_channel = 2 T = TypeVar("T") From da1dbf23460d1ce71e9e3cd1005fba2f8d34de2c Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Fri, 20 May 2022 14:03:21 -0700 Subject: [PATCH 12/51] Update http.py --- discord/http.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/discord/http.py b/discord/http.py index 08def4cf1e..b88e51b4b5 100644 --- a/discord/http.py +++ b/discord/http.py @@ -71,6 +71,7 @@ from .types import ( appinfo, audit_log, + automod, channel, components, embed, @@ -2317,6 +2318,45 @@ def get_guild_command_permissions( ) return self.request(r) + # Guild Automod Rules + + def get_auto_moderation_rules( + self, + guild_id: Snowflake, + ) -> Response[List[automod.AutoModRule]]: + r = Route( + "GET", + "/guilds/{guild_id}/auto-moderation/rules", + guild_id=guild_id, + ) + return self.request(r) + + def get_auto_moderation_rule( + self, + guild_id: Snowflake, + rule_id: Snowflake, + ) -> Response[automod.AutoModRule]: + r = Route( + "GET", + "/guilds/{guild_id}/auto-moderation/rules/{rule_id}", + guild_id=guild_id, + rule_id=rule_id, + ) + return self.request(r) + + def create_auto_moderation_rule( + self, + guild_id: Snowflake, + payload, # TODO: Typehint + ) -> Response[automod.AutoModRule]: + r = Route( + "POST", + "/guilds/{guild_id}/auto-moderation/rules", + guild_id=guild_id, + rule_id=rule_id, + ) + return self.request(r) + # Interaction responses def _edit_webhook_helper( From cf7e82503deac17ce0624fbf385322e689c10116 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Fri, 20 May 2022 14:06:17 -0700 Subject: [PATCH 13/51] Update http.py --- discord/http.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/discord/http.py b/discord/http.py index b88e51b4b5..1da0ac30a2 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2353,6 +2353,32 @@ def create_auto_moderation_rule( "POST", "/guilds/{guild_id}/auto-moderation/rules", guild_id=guild_id, + ) + return self.request(r, json=payload) + + def modify_auto_moderation_rule( + self, + guild_id: Snowflake, + rule_id: Snowflake, + payload, # TODO: Typehint + ) -> Response[automod.AutoModRule]: + r = Route( + "POST", + "/guilds/{guild_id}/auto-moderation/rules/{rule_id}", + guild_id=guild_id, + rule_id=rule_id, + ) + return self.request(r, json=payload) + + def delete_auto_moderation_rule( + self, + guild_id: Snowflake, + rule_id: Snowflake, + ) -> Response[None]: + r = Route( + "DELETE", + "/guilds/{guild_id}/auto-moderation/rules/{rule_id}", + guild_id=guild_id, rule_id=rule_id, ) return self.request(r) From c2c4e070e57c0e22a933a89470535a2cda3fc66b Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Fri, 20 May 2022 14:10:34 -0700 Subject: [PATCH 14/51] Update flags.py --- discord/flags.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/discord/flags.py b/discord/flags.py index c17790e647..9973630b5d 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -970,6 +970,33 @@ def scheduled_events(self): - :meth:`Guild.get_scheduled_event` """ return 1 << 16 + + @flag_value + def auto_moderation_configuration(self): + """:class:`bool`: Whether guild auto moderation configuration events are enabled. + + This corresponds to the following events: + + - :func:`on_auto_moderation_rule_create` + - :func:`on_auto_moderation_rule_update` + - :func:`on_auto_moderation_rule_delete` + + This also corresponds to the following attributes and classes in terms of cache: + + - :class:`AutoModerationRule` + - :meth:`Guild.auto_moderation_rules` + """ + return 1 << 20 + + @flag_value + def auto_moderation_execution(self): + """:class:`bool`: Whether guild auto moderation execution events are enabled. + + This corresponds to the following events: + + - :func:`on_auto_moderation_action_execution` + """ + return 1 << 21 @fill_with_flags() From 0335be3cd4c8f1cd8e8a72b1e665fe71b9ea85fa Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Fri, 20 May 2022 14:15:50 -0700 Subject: [PATCH 15/51] Update state.py --- discord/state.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/discord/state.py b/discord/state.py index c1571d9255..95814577c9 100644 --- a/discord/state.py +++ b/discord/state.py @@ -616,6 +616,22 @@ def parse_resumed(self, data) -> None: def parse_application_command_permissions_update(self, data) -> None: # unsure what the implementation would be like pass + + def parse_auto_moderation_rule_create(data) -> None: + # construct object, dispatch event, cache data + pass + + def parse_auto_moderation_rule_update(data) -> None: + # construct object, dispatch event, cache data + pass + + def parse_auto_moderation_rule_delete(data) -> None: + # construct object, dispatch event, cache data + pass + + def parse_auto_moderation_action_execution(data) -> None: + # construct object, dispatch event, cache data + pass def parse_message_create(self, data) -> None: channel, _ = self._get_guild_channel(data) From d54f8443fe57d0a5578a80c3e2c6bd76f72718ca Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Fri, 20 May 2022 14:19:19 -0700 Subject: [PATCH 16/51] Update http.py --- discord/http.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/http.py b/discord/http.py index 1da0ac30a2..7889842ae8 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2347,7 +2347,7 @@ def get_auto_moderation_rule( def create_auto_moderation_rule( self, guild_id: Snowflake, - payload, # TODO: Typehint + payload: automod.CreateAutoModRule, ) -> Response[automod.AutoModRule]: r = Route( "POST", @@ -2360,7 +2360,7 @@ def modify_auto_moderation_rule( self, guild_id: Snowflake, rule_id: Snowflake, - payload, # TODO: Typehint + payload: automod.ModifyAutoModRule, ) -> Response[automod.AutoModRule]: r = Route( "POST", From 4a82eacc370618f12bf9a6b0ad574f44a442fb0a Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Fri, 20 May 2022 14:19:47 -0700 Subject: [PATCH 17/51] Update http.py --- discord/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/http.py b/discord/http.py index 7889842ae8..a21606473a 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2360,7 +2360,7 @@ def modify_auto_moderation_rule( self, guild_id: Snowflake, rule_id: Snowflake, - payload: automod.ModifyAutoModRule, + payload: automod.EditAutoModRule, ) -> Response[automod.AutoModRule]: r = Route( "POST", From c34621026351c78072ee3c3d5ae25d4060384361 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Fri, 20 May 2022 14:23:53 -0700 Subject: [PATCH 18/51] Update automod.py --- discord/types/automod.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/discord/types/automod.py b/discord/types/automod.py index 892bf5802e..7b8d94b7e5 100644 --- a/discord/types/automod.py +++ b/discord/types/automod.py @@ -48,3 +48,27 @@ class AutoModRule(TypedDict): enabled: bool exempt_roles: List[Snowflake] exempt_channels: List[Snowflake] + + +class _CreateAutoModRuleOptional(TypedDict, total=False): + enabled: bool + exempt_roles: List[Snowflake] + exempt_channels: List[Snowflake] + + +class CreateAutoModRule(_CreateAutoModRuleOptional): + name: str + event_type: AutoModEventType + trigger_type: AutoModRuleTriggerType + trigger_metadata: Dict # TODO: unclear what this is meant to be + actions: List[AutoModRuleAction] + + +class EditAutoModRule(TypedDict, total=False): + name: str + event_type: AutoModEventType + trigger_metadata: Dict # TODO: unclear what this is meant to be + actions: List[AutoModRuleAction] + enabled: bool + exempt_roles: List[Snowflake] + exempt_channels: List[Snowflake] From 8bda4175348119873aad688e2cbdf9703466bd96 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Fri, 20 May 2022 14:41:34 -0700 Subject: [PATCH 19/51] Create automod.py --- discord/automod.py | 109 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 discord/automod.py diff --git a/discord/automod.py b/discord/automod.py new file mode 100644 index 0000000000..bf6707a9c3 --- /dev/null +++ b/discord/automod.py @@ -0,0 +1,109 @@ +""" +The MIT License (MIT) + +Copyright (c) 2021-present Pycord Development + +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, Union + +from . import utils +from .enums import ( + AutoModRuleTriggerType, + AutoModEventType, + AutoModRuleAction, + try_enum, +) +from .errors import ValidationError +from .mixins import Hashable +from .object import Object + +__all__ = ( + "AutoModRule", +) + +if TYPE_CHECKING: + from .abc import Snowflake + from .guild import Guild + from .member import Member + from .state import ConnectionState + from .types.automod import AutoModRule as AutoModRulePayload + +MISSING = utils.MISSING + + +class AutoModRule(Hashable): + """Represents a guild's auto moderation rule. + + .. versionadded:: 2.0 + + Attributes + ---------- + value: Union[:class:`str`, :class:`StageChannel`, :class:`VoiceChannel`, :class:`Object`] + The actual location of the scheduled event. + type: :class:`ScheduledEventLocationType` + The type of location. + """ + + __slots__ = ( + "_state", + "id", + "guild", + "guild_id", + "name", + "creator", + "creator_id", + "event_type", + "trigger_type", + "enabled", + "exempt_role_ids", + "exempt_channel_ids", + ) + + def __init__( + self, + *, + state: ConnectionState, + guild: Guild, + creator: Member, + data: AutoModRulePayload, + ): + self._state: ConnectionState = state + self.id: Snowflake = int(data["id"]) + self.guild: Optional[Guild] = guild + self.guild_id: Snowflake = int(data["guild_id"]) + self.name: str = data["name"] + self.creator: Optional[Member] = creator + self.creator_id: Snowflake = int(data["creator_id"]) + self.event_type: AutoModEventType = try_enum(AutoModEventType, data["event_type"]) + self.trigger_type: AutoModRuleTriggerType = try_enum(AutoModRuleTriggerType, data["trigger_type"]) + # TODO: trigger_metadata, actions+object + self.enabled: bool = data["enabled"] + self.exempt_role_ids: List[Snowflake] = data["exempt_roles"] + self.exempt_channel_ids: List[Snowflake] = data["exempt_channels"] + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return self.name From 996a8341f858bc19ad27a38d1eb4347571bdf1f4 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Fri, 20 May 2022 14:41:53 -0700 Subject: [PATCH 20/51] Update automod.py I would never copy/paste --- discord/automod.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/discord/automod.py b/discord/automod.py index bf6707a9c3..19a08aa05b 100644 --- a/discord/automod.py +++ b/discord/automod.py @@ -56,13 +56,6 @@ class AutoModRule(Hashable): """Represents a guild's auto moderation rule. .. versionadded:: 2.0 - - Attributes - ---------- - value: Union[:class:`str`, :class:`StageChannel`, :class:`VoiceChannel`, :class:`Object`] - The actual location of the scheduled event. - type: :class:`ScheduledEventLocationType` - The type of location. """ __slots__ = ( From 1f676ce939008500a953f041d9065c2989ea3471 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Mon, 23 May 2022 16:09:03 +0000 Subject: [PATCH 21/51] Actions and some renaming --- discord/automod.py | 33 ++++++++++++++++++++++++++++----- discord/enums.py | 4 ++-- discord/types/automod.py | 12 ++++++------ 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/discord/automod.py b/discord/automod.py index 19a08aa05b..e371f2037c 100644 --- a/discord/automod.py +++ b/discord/automod.py @@ -25,13 +25,13 @@ from __future__ import annotations import datetime -from typing import TYPE_CHECKING, Any, Dict, Optional, Union +from typing import TYPE_CHECKING, Optional, List from . import utils from .enums import ( - AutoModRuleTriggerType, + AutoModTriggerType, AutoModEventType, - AutoModRuleAction, + AutoModActionType, try_enum, ) from .errors import ValidationError @@ -48,10 +48,31 @@ from .member import Member from .state import ConnectionState from .types.automod import AutoModRule as AutoModRulePayload + from .types.automod import AutoModAction as AutoModActionPayload MISSING = utils.MISSING +class AutoModAction: + """Represents an action for a guild's auto moderation rule. + + .. versionadded:: 2.0 + """ + + __slots__ = ( + "type", + "metadata", + ) + + def __init__(self, data: AutoModActionPayload): + self.type: AutoModActionType = try_enum(AutoModActionType, data["type"]) + # TODO: Metadata + + def __repr__(self) -> str: + return f"" + + + class AutoModRule(Hashable): """Represents a guild's auto moderation rule. @@ -68,6 +89,7 @@ class AutoModRule(Hashable): "creator_id", "event_type", "trigger_type", + "actions", "enabled", "exempt_role_ids", "exempt_channel_ids", @@ -89,8 +111,9 @@ def __init__( self.creator: Optional[Member] = creator self.creator_id: Snowflake = int(data["creator_id"]) self.event_type: AutoModEventType = try_enum(AutoModEventType, data["event_type"]) - self.trigger_type: AutoModRuleTriggerType = try_enum(AutoModRuleTriggerType, data["trigger_type"]) - # TODO: trigger_metadata, actions+object + self.trigger_type: AutoModTriggerType = try_enum(AutoModTriggerType, data["trigger_type"]) + # TODO: trigger_metadata + self.actions: List[AutoModAction] = [AutoModAction(d) for d in data["actions"]] self.enabled: bool = data["enabled"] self.exempt_role_ids: List[Snowflake] = data["exempt_roles"] self.exempt_channel_ids: List[Snowflake] = data["exempt_channels"] diff --git a/discord/enums.py b/discord/enums.py index da13d91a8d..e5d6fdb595 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -776,7 +776,7 @@ class ScheduledEventLocationType(Enum): external = 3 -class AutoModRuleTriggerType(Enum): +class AutoModTriggerType(Enum): keyword = 1 harmful_links = 2 spam = 3 @@ -787,7 +787,7 @@ class AutoModEventType(Enum): message_send = 1 -class AutoModRuleAction(Enum): +class AutoModActionType(Enum): block_message = 1 log_to_channel = 2 diff --git a/discord/types/automod.py b/discord/types/automod.py index 7b8d94b7e5..650ea483f9 100644 --- a/discord/types/automod.py +++ b/discord/types/automod.py @@ -25,7 +25,7 @@ from .snowflake import Snowflake -AutoModRuleTriggerType = Literal[1, 2, 3, 4] +AutoModTriggerType = Literal[1, 2, 3, 4] AutoModEventType = Literal[1] @@ -42,8 +42,8 @@ class AutoModRule(TypedDict): name: str creator_id: Snowflake event_type: AutoModEventType - trigger_type: AutoModRuleTriggerType - actions: List[AutoModRuleAction] + trigger_type: AutoModTriggerType + actions: List[AutoModAction] trigger_metadata: Dict # TODO: unclear what this is meant to be enabled: bool exempt_roles: List[Snowflake] @@ -59,16 +59,16 @@ class _CreateAutoModRuleOptional(TypedDict, total=False): class CreateAutoModRule(_CreateAutoModRuleOptional): name: str event_type: AutoModEventType - trigger_type: AutoModRuleTriggerType + trigger_type: AutoModTriggerType trigger_metadata: Dict # TODO: unclear what this is meant to be - actions: List[AutoModRuleAction] + actions: List[AutoModAction] class EditAutoModRule(TypedDict, total=False): name: str event_type: AutoModEventType trigger_metadata: Dict # TODO: unclear what this is meant to be - actions: List[AutoModRuleAction] + actions: List[AutoModAction] enabled: bool exempt_roles: List[Snowflake] exempt_channels: List[Snowflake] From b38fd765a79518ceaea6be0f3e88d0d9f9b98106 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Mon, 23 May 2022 21:42:48 +0000 Subject: [PATCH 22/51] more things --- discord/automod.py | 179 ++++++++++++++++++++++++++++++++++++++++++--- discord/guild.py | 76 ++++++++++++++++++- discord/http.py | 2 +- 3 files changed, 243 insertions(+), 14 deletions(-) diff --git a/discord/automod.py b/discord/automod.py index e371f2037c..7159f31710 100644 --- a/discord/automod.py +++ b/discord/automod.py @@ -25,7 +25,7 @@ from __future__ import annotations import datetime -from typing import TYPE_CHECKING, Optional, List +from typing import TYPE_CHECKING, Dict, Optional, List, Union from . import utils from .enums import ( @@ -46,6 +46,8 @@ from .abc import Snowflake from .guild import Guild from .member import Member + from .role import Role + from .channel import ForumChannel, TextChannel, VoiceChannel from .state import ConnectionState from .types.automod import AutoModRule as AutoModRulePayload from .types.automod import AutoModAction as AutoModActionPayload @@ -64,10 +66,20 @@ class AutoModAction: "metadata", ) - def __init__(self, data: AutoModActionPayload): - self.type: AutoModActionType = try_enum(AutoModActionType, data["type"]) + def __init__(self, action_type: AutoModActionType, metadata: Dict): + self.type: AutoModActionType = action_type # TODO: Metadata + def to_dict(self) -> Dict: + return { + "type": self.type.value, + "metadata": self.metadata, + } + + @classmethod + def from_dict(cls, data: Dict): + return cls(try_enum(AutoModActionType, data["type"]), data["metadata"]) + def __repr__(self) -> str: return f"" @@ -77,6 +89,47 @@ class AutoModRule(Hashable): """Represents a guild's auto moderation rule. .. versionadded:: 2.0 + + .. container:: operations + + .. describe:: x == y + + Checks if two rules are equal. + + .. describe:: x != y + + Checks if two rules are not equal. + + .. describe:: hash(x) + + Returns the rule's hash. + + .. describe:: str(x) + + Returns the rule's name. + + Attributes + ---------- + id: :class:`int` + The rule's ID. + guild: :class:`Guild` + The guild this rule belongs to. + name: :class:`str` + The rule's name. + creator_id: :class:`int` + The ID of the user who created this rule. + event_type: :class:`AutoModEventType` + Indicates in what context the rule is checked. + trigger_type: :class:`AutoModTriggerType` + Indicates what type of information is checked to determine whether the rule is triggered. + actions: List[:class:`AutoModAction`] + The actions to perform when the rule is triggered. + enabled: :class:`bool` + Whether this rule is enabled. + exempt_role_ids: List[:class:`int`] + The IDs of the roles that are exempt from this rule. + exempt_channel_ids: List[:class:`int`] + The IDs of the channels that are exempt from this rule. """ __slots__ = ( @@ -85,7 +138,6 @@ class AutoModRule(Hashable): "guild", "guild_id", "name", - "creator", "creator_id", "event_type", "trigger_type", @@ -100,26 +152,129 @@ def __init__( *, state: ConnectionState, guild: Guild, - creator: Member, data: AutoModRulePayload, ): self._state: ConnectionState = state - self.id: Snowflake = int(data["id"]) + self.id: int = int(data["id"]) self.guild: Optional[Guild] = guild - self.guild_id: Snowflake = int(data["guild_id"]) + self.guild_id: int = int(data["guild_id"]) self.name: str = data["name"] - self.creator: Optional[Member] = creator - self.creator_id: Snowflake = int(data["creator_id"]) + self.creator_id: int = int(data["creator_id"]) self.event_type: AutoModEventType = try_enum(AutoModEventType, data["event_type"]) self.trigger_type: AutoModTriggerType = try_enum(AutoModTriggerType, data["trigger_type"]) # TODO: trigger_metadata - self.actions: List[AutoModAction] = [AutoModAction(d) for d in data["actions"]] + self.actions: List[AutoModAction] = [AutoModAction.from_dict(d) for d in data["actions"]] self.enabled: bool = data["enabled"] - self.exempt_role_ids: List[Snowflake] = data["exempt_roles"] - self.exempt_channel_ids: List[Snowflake] = data["exempt_channels"] + self.exempt_role_ids: List[int] = [int(r) for r in data["exempt_roles"]] + self.exempt_channel_ids: List[int] = [int(c) for c in data["exempt_channels"]] def __repr__(self) -> str: return f"" def __str__(self) -> str: return self.name + + @property + def creator(self) -> Optional[Member]: + """Optional[:class:`Member`]: The member who created this rule.""" + if self.guild is None: + return None + return self.guild.get_member(self.creator_id) + + @property + def exempt_roles(self) -> List[Union[Role, Object]]: + """List[Union[:class:`Role`, :class:`Object`]]: The roles that are exempt + from this rule. + + If a role is not found in the guild's cache, + then it will be returned as an :class:`Object`. + """ + if self.guild is None: + return [] + return [self.guild.get_role(role_id) or Object(role_id) for role_id in self.exempt_role_ids] + + @property + def exempt_channels(self) -> List[Union[Union[TextChannel, ForumChannel, VoiceChannel], Object]]: + """List[Union[Union[TextChannel, ForumChannel, VoiceChannel], Object]]: The channels + that are exempt from this rule. + + If a channel is not found in the guild's cache, + then it will be returned as an :class:`Object`. + """ + if self.guild is None: + return [] + return [self.guild.get_channel(channel_id) or Object(channel_id) for channel_id in self.exempt_channel_ids] + + async def delete(self) -> None: + """|coro| + + Deletes this rule. + """ + await self._state.http.delete_auto_moderation_rule(self.guild_id, self.id) + + async def edit( + self, + *, + name: str = MISSING, + event_type: AutoModEventType = MISSING, + # TODO: trigger metadata + actions: List[AutoModAction] = MISSING, + enabled: bool = MISSING, + exempt_roles: List[Snowflake] = MISSING, + exempt_channels: List[Snowflake] = MISSING, + ) -> Optional[AutoModRule]: + """|coro| + + Edits the rule. + + Parameters + ----------- + name: :class:`str` + The rule's new name. + event_type: :class:`AutoModEventType` + The new context in which the rule is checked. + actions: List[:class:`AutoModAction`] + The new actions to perform when the rule is triggered. + enabled: :class:`bool` + Whether this rule is enabled. + exempt_roles: List[:class:`Snowflake`] + The roles that will be exempt from this rule. + exempt_channels: List[:class:`Snowflake`] + The channels that will be exempt from this rule. + + Raises + ------- + Forbidden + You do not have the proper permissions to the action requested. + HTTPException + The operation failed. + + Returns + -------- + Optional[:class:`.AutoModRule`] + The newly updated rule, if applicable. This is only returned + when fields are updated. + """ + http = self._state.http + guild_id = self.guild.id + payload = {} + if name is not MISSING: + payload["name"] = name + if event_type is not MISSING: + payload["event_type"] = event_type.value + # TODO: trigger metadata + if actions is not MISSING: + payload["actions"] = [a.to_dict() for a in actions] + if enabled is not MISSING: + payload["enabled"] = enabled + if exempt_roles is not MISSING: + payload["exempt_roles"] = [r.id for r in exempt_roles] + if exempt_channels is not MISSING: + payload["exempt_channels"] = [c.id for c in exempt_channels] + + if payload: + data = await http.edit_auto_moderation_rule(guild_id, self.id, payload) + return AutoModRule(state=self._state, guild=self.guild, data=data) + + + diff --git a/discord/guild.py b/discord/guild.py index 315ba98ce5..d7ff080d2d 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -43,7 +43,9 @@ overload, ) + from . import abc, utils +from .automod import AutoModAction, AutoModRule from .asset import Asset from .channel import * from .channel import _guild_channel_factory, _threaded_guild_channel_factory @@ -51,6 +53,8 @@ from .emoji import Emoji from .enums import ( AuditLogAction, + AutoModEventType, + AutoModTriggerType, ChannelType, ContentFilter, NotificationLevel, @@ -534,7 +538,7 @@ def _from_data(self, guild: GuildPayload) -> None: for obj in guild.get("voice_states", []): self._update_voice_state(obj, int(obj["channel_id"])) - + # TODO: refactor/remove? def _sync(self, data: GuildPayload) -> None: try: @@ -3515,3 +3519,73 @@ async def create_scheduled_event( def scheduled_events(self) -> List[ScheduledEvent]: """List[:class:`.ScheduledEvent`]: A list of scheduled events in this guild.""" return list(self._scheduled_events.values()) + + async def fetch_auto_moderation_rules(self) -> List[AutoModRule]: + """|coro| + + Retrieves a list of auto moderation rules for this guild. + + Raises + ------- + HTTPException + Getting the auto moderation rules failed. + Forbidden + You do not have the required permissions to get the auto moderation rules. + + Returns + -------- + List[:class:`AutoModRule`] + The auto moderation rules for this guild. + """ + data = await self._state.http.get_auto_moderation_rules(self.id) + result = [] + for rule in data: + result.append(AutoModRule(state=self._state, guild=self, data=rule)) + return result + + async def fetch_auto_moderation_rule(self, id: int) -> AutoModRule: + """|coro| + + Retrieves a :class:`AutoModRule` from rule ID. + + Raises + ------- + HTTPException + Getting the auto moderation rule failed. + Forbidden + You do not have the required permissions to get the auto moderation rule. + + Returns + -------- + :class:`AutoModRule` + The requested auto moderation rule. + """ + data = await self._state.http.get_auto_moderation_rule(self.id, id) + return AutoModRule(state=self._state, guild=self, data=data) + + async def create_auto_moderation_rule( + self, + *, + name: str, + event_type: AutoModEventType, + trigger_type: AutoModTriggerType, + # TODO: trigger metadata + actions: List[AutoModAction], + enabled: bool = False, + exempt_roles: List[Snowflake] = None, + exempt_channels: List[Snowflake] = None, + ) -> AutoModRule: + # TODO: docstring + payload = { + "name": name, + "event_type": event_type.value, + "trigger_type": trigger_type.value, + "actions": [a.to_dict() for a in actions], + "enabled": enabled, + } + if exempt_roles: + payload["exempt_roles"] = [r.id for r in exempt_roles] + if exempt_channels: + payload["exempt_channels"] = [c.id for c in exempt_channels] + data = await self._state.http.create_auto_moderation_rule(self.id, payload) + return AutoModRule(state=self._state, guild=self, data=data) diff --git a/discord/http.py b/discord/http.py index a21606473a..0e7c71aef9 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2356,7 +2356,7 @@ def create_auto_moderation_rule( ) return self.request(r, json=payload) - def modify_auto_moderation_rule( + def edit_auto_moderation_rule( self, guild_id: Snowflake, rule_id: Snowflake, From 69c01439fe87bf9cbac34984c75a242dd68587ee Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Tue, 24 May 2022 15:55:18 +0000 Subject: [PATCH 23/51] add gateway events --- discord/automod.py | 24 +++++++++++++----------- discord/raw_models.py | 24 ++++++++++++++++++++++++ discord/state.py | 31 +++++++++++++++++++------------ discord/types/raw_models.py | 6 ++++++ 4 files changed, 62 insertions(+), 23 deletions(-) diff --git a/discord/automod.py b/discord/automod.py index 7159f31710..7700d2aeb3 100644 --- a/discord/automod.py +++ b/discord/automod.py @@ -25,6 +25,7 @@ from __future__ import annotations import datetime +from functools import cached_property from typing import TYPE_CHECKING, Dict, Optional, List, Union from . import utils @@ -68,7 +69,7 @@ class AutoModAction: def __init__(self, action_type: AutoModActionType, metadata: Dict): self.type: AutoModActionType = action_type - # TODO: Metadata + self.metadata = metadata # TODO: Metadata def to_dict(self) -> Dict: return { @@ -112,8 +113,6 @@ class AutoModRule(Hashable): ---------- id: :class:`int` The rule's ID. - guild: :class:`Guild` - The guild this rule belongs to. name: :class:`str` The rule's name. creator_id: :class:`int` @@ -135,7 +134,6 @@ class AutoModRule(Hashable): __slots__ = ( "_state", "id", - "guild", "guild_id", "name", "creator_id", @@ -151,12 +149,10 @@ def __init__( self, *, state: ConnectionState, - guild: Guild, data: AutoModRulePayload, ): self._state: ConnectionState = state self.id: int = int(data["id"]) - self.guild: Optional[Guild] = guild self.guild_id: int = int(data["guild_id"]) self.name: str = data["name"] self.creator_id: int = int(data["creator_id"]) @@ -174,14 +170,19 @@ def __repr__(self) -> str: def __str__(self) -> str: return self.name - @property + @cached_property + def guild(self) -> Optional[Guild]: + """Optional[:class:`Guild`]: The guild this rule belongs to.""" + return self._state._get_guild(self.guild_id) + + @cached_property def creator(self) -> Optional[Member]: """Optional[:class:`Member`]: The member who created this rule.""" if self.guild is None: return None return self.guild.get_member(self.creator_id) - @property + @cached_property def exempt_roles(self) -> List[Union[Role, Object]]: """List[Union[:class:`Role`, :class:`Object`]]: The roles that are exempt from this rule. @@ -193,7 +194,7 @@ def exempt_roles(self) -> List[Union[Role, Object]]: return [] return [self.guild.get_role(role_id) or Object(role_id) for role_id in self.exempt_role_ids] - @property + @cached_property def exempt_channels(self) -> List[Union[Union[TextChannel, ForumChannel, VoiceChannel], Object]]: """List[Union[Union[TextChannel, ForumChannel, VoiceChannel], Object]]: The channels that are exempt from this rule. @@ -225,7 +226,7 @@ async def edit( ) -> Optional[AutoModRule]: """|coro| - Edits the rule. + Edits this rule. Parameters ----------- @@ -267,6 +268,7 @@ async def edit( payload["actions"] = [a.to_dict() for a in actions] if enabled is not MISSING: payload["enabled"] = enabled + # Maybe consider enforcing limits on the number of exempt roles/channels? if exempt_roles is not MISSING: payload["exempt_roles"] = [r.id for r in exempt_roles] if exempt_channels is not MISSING: @@ -274,7 +276,7 @@ async def edit( if payload: data = await http.edit_auto_moderation_rule(guild_id, self.id, payload) - return AutoModRule(state=self._state, guild=self.guild, data=data) + return AutoModRule(state=self._state, data=data) diff --git a/discord/raw_models.py b/discord/raw_models.py index b86ff27d00..99743eca67 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -28,6 +28,8 @@ import datetime from typing import TYPE_CHECKING, List, Optional, Set +from .automod import AutoModAction + from .enums import ChannelType, try_enum if TYPE_CHECKING: @@ -47,6 +49,7 @@ ScheduledEventSubscription, ThreadDeleteEvent, TypingEvent, + AutoModActionExecutionEvent, ) @@ -61,6 +64,7 @@ "RawThreadDeleteEvent", "RawTypingEvent", "RawScheduledEventSubscription", + "RawAutoModActionExecutionEvent", ) @@ -386,3 +390,23 @@ def __init__(self, data: ScheduledEventSubscription, event_type: str): self.user_id: int = int(data["user_id"]) self.guild: Guild = None self.event_type: str = event_type + + +class RawAutoModActionExecutionEvent(_RawReprMixin): + """Represents the payload for a :func:`raw_auto_moderation_action_execution` + + .. versionadded:: 2.0 + + Attributes + ----------- + action: :class:`AutoModAction` + The action that was executed. + guild_id: :class:`int` + The guild ID that the action was executed in. + """ + + __slots__ = ("action", "guild_id") + + def __init__(self, data: AutoModActionExecutionEvent) -> None: + self.action: AutoModAction = AutoModAction.from_dict(data["action"]) + self.guild_id: int = int(data["guild_id"]) diff --git a/discord/state.py b/discord/state.py index 95814577c9..b26960b5fb 100644 --- a/discord/state.py +++ b/discord/state.py @@ -47,8 +47,10 @@ Union, ) + from . import utils from .activity import BaseActivity +from .automod import AutoModAction, AutoModRule from .channel import * from .channel import _channel_factory from .emoji import Emoji @@ -617,21 +619,26 @@ def parse_application_command_permissions_update(self, data) -> None: # unsure what the implementation would be like pass - def parse_auto_moderation_rule_create(data) -> None: - # construct object, dispatch event, cache data - pass + def parse_auto_moderation_rule_create(self, data) -> None: + rule = AutoModRule(state=self, data=data) + self.dispatch("auto_moderation_rule_create", rule) - def parse_auto_moderation_rule_update(data) -> None: - # construct object, dispatch event, cache data - pass + def parse_auto_moderation_rule_update(self, data) -> None: + # somehow get a 'before' object? + rule = AutoModRule(state=self, data=data) + self.dispatch("auto_moderation_rule_update", rule) - def parse_auto_moderation_rule_delete(data) -> None: - # construct object, dispatch event, cache data - pass + def parse_auto_moderation_rule_delete(self, data) -> None: + rule = AutoModRule(state=self, data=data) + self.dispatch("auto_moderation_rule_delete", rule) - def parse_auto_moderation_action_execution(data) -> None: - # construct object, dispatch event, cache data - pass + def parse_auto_moderation_action_execution(self, data) -> None: + raw = RawAutoModActionExecutionEvent(data) + self.dispatch("raw_auto_moderation_action_execution", raw) + guild = self._get_guild(int(data["guild_id"])) + if guild is not None: + action = AutoModAction.from_dict(data["action"]) + self.dispatch("auto_moderation_action_execution", guild, action) def parse_message_create(self, data) -> None: channel, _ = self._get_guild_channel(data) diff --git a/discord/types/raw_models.py b/discord/types/raw_models.py index 268ef1dba3..9799f80d04 100644 --- a/discord/types/raw_models.py +++ b/discord/types/raw_models.py @@ -28,6 +28,7 @@ from .emoji import PartialEmoji from .member import Member from .snowflake import Snowflake +from .types.automod import AutoModAction class _MessageEventOptional(TypedDict, total=False): @@ -111,3 +112,8 @@ class ScheduledEventSubscription(TypedDict, total=False): event_id: Snowflake user_id: Snowflake guild_id: Snowflake + + +class AutoModActionExecutionEvent(TypedDict): + action: AutoModAction + guild_id: Snowflake From 458e4003fb2327b393da734d59200904b2cddb22 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Tue, 24 May 2022 16:02:51 +0000 Subject: [PATCH 24/51] update docs --- discord/flags.py | 5 ----- discord/state.py | 1 + docs/api.rst | 58 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/discord/flags.py b/discord/flags.py index 9973630b5d..ad3808bca6 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -980,11 +980,6 @@ def auto_moderation_configuration(self): - :func:`on_auto_moderation_rule_create` - :func:`on_auto_moderation_rule_update` - :func:`on_auto_moderation_rule_delete` - - This also corresponds to the following attributes and classes in terms of cache: - - - :class:`AutoModerationRule` - - :meth:`Guild.auto_moderation_rules` """ return 1 << 20 diff --git a/discord/state.py b/discord/state.py index b26960b5fb..408c83269f 100644 --- a/discord/state.py +++ b/discord/state.py @@ -633,6 +633,7 @@ def parse_auto_moderation_rule_delete(self, data) -> None: self.dispatch("auto_moderation_rule_delete", rule) def parse_auto_moderation_action_execution(self, data) -> None: + # should there be a raw event for this? raw = RawAutoModActionExecutionEvent(data) self.dispatch("raw_auto_moderation_action_execution", raw) guild = self._get_guild(int(data["guild_id"])) diff --git a/docs/api.rst b/docs/api.rst index 7c6648a8b3..4404eeae46 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1399,6 +1399,64 @@ to handle it, which defaults to print a traceback and ignoring the exception. :param payload: The raw event payload data. :type payload: :class:`RawScheduledEventSubscription` +.. function:: on_auto_moderation_rule_create(rule) + + Called when an auto moderation rule is created. + + The bot must have :attr:`~Permissions.manage_guild` to receive this, and + :attr:`Intents.auto_moderation_configuration` must be enabled. + + :param rule: The newly created rule. + :type rule: :class:`AutoModRule` + +.. function:: on_auto_moderation_rule_update(rule) + + Called when an auto moderation rule is updated. + + The bot must have :attr:`~Permissions.manage_guild` to receive this, and + :attr:`Intents.auto_moderation_configuration` must be enabled. + + :param rule: The updated rule. + :type rule: :class:`AutoModRule` + +.. function:: on_auto_moderation_rule_delete(rule) + + Called when an auto moderation rule is deleted. + + The bot must have :attr:`~Permissions.manage_guild` to receive this, and + :attr:`Intents.auto_moderation_configuration` must be enabled. + + :param rule: The deleted rule. + :type rule: :class:`AutoModRule` + +.. function:: on_auto_moderation_action_execution(guild, action) + + Called when an auto moderation action is executed. + If the guild is not found in the internal cache, then this event will not be + called. Consider using :func:`on_raw_auto_moderation_action_execution` instead. + + The bot must have :attr:`~Permissions.manage_guild` to receive this, and + :attr:`Intents.auto_moderation_configuration` must be enabled. + + :param guild: The guild the action was executed in. + :type guild: :class:`Guild` + :param action: The action that was executed. + :type action: :class:`AutoModAction` + +.. function:: on_raw_auto_moderation_action_execution(payload) + + Called when an auto moderation action is executed. + + Unlike :meth:`on_auto_moderation_action_execution`, this event will be + called regardless of the state of the internal cache. + + The bot must have :attr:`~Permissions.manage_guild` to receive this, and + :attr:`Intents.auto_moderation_configuration` must be enabled. + + :param payload: The raw event payload data. + :type payload: :class:`RawAutoModActionExecution` + + .. _discord-api-utils: Utility Functions From 2efe7fe5205b7f9b9230cce1dcd84dfb6580eed1 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Wed, 25 May 2022 14:09:49 -0700 Subject: [PATCH 25/51] Update raw_models.py --- discord/types/raw_models.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/discord/types/raw_models.py b/discord/types/raw_models.py index 9799f80d04..d1b3a4bb52 100644 --- a/discord/types/raw_models.py +++ b/discord/types/raw_models.py @@ -28,7 +28,7 @@ from .emoji import PartialEmoji from .member import Member from .snowflake import Snowflake -from .types.automod import AutoModAction +from .types.automod import AutoModAction, AutoModTriggerType class _MessageEventOptional(TypedDict, total=False): @@ -115,5 +115,13 @@ class ScheduledEventSubscription(TypedDict, total=False): class AutoModActionExecutionEvent(TypedDict): - action: AutoModAction guild_id: Snowflake + action: AutoModAction + rule_id: Snowflake + rule_trigger_type: AutoModTriggerType + channel_id: Optional[Snowflake] + message_id: Optional[Snowflake] + alert_system_message_id: Optional[Snowflake] + content: str + matched_keyword: Optional[str] + matched_content: Optional[str] From 913d3917eaf5582152ea5506da6d55c8de95c5e9 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Wed, 25 May 2022 14:11:21 -0700 Subject: [PATCH 26/51] Update enums.py --- discord/enums.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index e5d6fdb595..006d0a5934 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -778,9 +778,9 @@ class ScheduledEventLocationType(Enum): class AutoModTriggerType(Enum): keyword = 1 - harmful_links = 2 + harmful_link = 2 spam = 3 - default_keyword_list = 4 + keyword_preset = 4 class AutoModEventType(Enum): From 4450d3c468f9ac8ab72433aefdac929f8b198c2d Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Wed, 25 May 2022 14:15:36 -0700 Subject: [PATCH 27/51] Update automod.py --- discord/types/automod.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/discord/types/automod.py b/discord/types/automod.py index 650ea483f9..ae48387840 100644 --- a/discord/types/automod.py +++ b/discord/types/automod.py @@ -31,9 +31,21 @@ AutoModActionType = Literal[1, 2] +AutoModWordsetType = Literal["PROFANITY", "SEXUAL_CONTENT", "SLURS"] + + +class AutoModTriggerMetadata(TypedDict, total=False): + keyword_filter: List[str] + keyword_lists: List[AutoModWordsetType] + + +class AutoModActionMetadata(TypedDict, total=False): + channel_id: Snowflake + + class AutoModAction(TypedDict): type: AutoModActionType - metadata: Dict # TODO + metadata: AutoModActionMetadata class AutoModRule(TypedDict): @@ -44,7 +56,7 @@ class AutoModRule(TypedDict): event_type: AutoModEventType trigger_type: AutoModTriggerType actions: List[AutoModAction] - trigger_metadata: Dict # TODO: unclear what this is meant to be + trigger_metadata: AutoModTriggerMetadata enabled: bool exempt_roles: List[Snowflake] exempt_channels: List[Snowflake] @@ -60,14 +72,14 @@ class CreateAutoModRule(_CreateAutoModRuleOptional): name: str event_type: AutoModEventType trigger_type: AutoModTriggerType - trigger_metadata: Dict # TODO: unclear what this is meant to be + trigger_metadata: AutoModTriggerMetadata actions: List[AutoModAction] class EditAutoModRule(TypedDict, total=False): name: str event_type: AutoModEventType - trigger_metadata: Dict # TODO: unclear what this is meant to be + trigger_metadata: AutoModTriggerMetadata actions: List[AutoModAction] enabled: bool exempt_roles: List[Snowflake] From da2d30b11b1c36098490e64a2ae85968b6253f47 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Wed, 25 May 2022 21:42:36 +0000 Subject: [PATCH 28/51] more stuff --- discord/automod.py | 142 +++++++++++++++++++++++++++++++++++++++++---- discord/enums.py | 8 ++- discord/guild.py | 6 +- 3 files changed, 141 insertions(+), 15 deletions(-) diff --git a/discord/automod.py b/discord/automod.py index 7700d2aeb3..c8225e8e45 100644 --- a/discord/automod.py +++ b/discord/automod.py @@ -24,7 +24,6 @@ from __future__ import annotations -import datetime from functools import cached_property from typing import TYPE_CHECKING, Dict, Optional, List, Union @@ -33,9 +32,9 @@ AutoModTriggerType, AutoModEventType, AutoModActionType, + AutoModWordsetType, try_enum, ) -from .errors import ValidationError from .mixins import Hashable from .object import Object @@ -50,16 +49,75 @@ from .role import Role from .channel import ForumChannel, TextChannel, VoiceChannel from .state import ConnectionState - from .types.automod import AutoModRule as AutoModRulePayload - from .types.automod import AutoModAction as AutoModActionPayload + from .types.automod import ( + AutoModRule as AutoModRulePayload, + AutoModAction as AutoModActionPayload, + AutoModTriggerMetadata as AutoModTriggerMetadataPayload, + AutoModActionMetadata as AutoModActionMetadataPayload, + ) MISSING = utils.MISSING +class AutoModActionMetadata: + """Represents an action's metadata. + + Depending on the action's type, different attributes will be used. + + .. versionadded:: 2.0 + + Attributes + ----------- + channel_id: :class:`int` + The ID of the channel to send the message to. Only for actions of type :attr:`AutoModActionType.send_alert_message`. + """ + # maybe add a table of action types and attributes? + + __slots__ = ( + "channel_id", + ) + + def __init__(self, channel_id: int = MISSING): + self.channel_id = channel_id + + def to_dict(self) -> Dict: + data = {} + if self.channel_id is not MISSING: + data["channel_id"] = self.channel_id + return data + + @classmethod + def from_dict(cls, data: AutoModActionMetadataPayload): + kwargs = {} + if data.get("channel_id") is not None: + kwargs["channel_id"] = int(data["channel_id"]) + return cls(**kwargs) + + def __repr__(self) -> str: + repr_attrs = ( + "channel_id", + ) + inner = [] + for attr in repr_attrs: + value = getattr(self, attr) + if value is not MISSING: + inner.append(f"{attr}={value}") + inner = " ".join(inner) + return f"" + + + class AutoModAction: """Represents an action for a guild's auto moderation rule. .. versionadded:: 2.0 + + Attributes + ----------- + type: :class:`AutoModActionType` + The action's type. + metadata: :class:`AutoModActionMetadata` + The action's metadata. """ __slots__ = ( @@ -67,9 +125,9 @@ class AutoModAction: "metadata", ) - def __init__(self, action_type: AutoModActionType, metadata: Dict): + def __init__(self, action_type: AutoModActionType, metadata: AutoModActionMetadata): self.type: AutoModActionType = action_type - self.metadata = metadata # TODO: Metadata + self.metadata: AutoModActionMetadata = metadata def to_dict(self) -> Dict: return { @@ -78,13 +136,69 @@ def to_dict(self) -> Dict: } @classmethod - def from_dict(cls, data: Dict): - return cls(try_enum(AutoModActionType, data["type"]), data["metadata"]) + def from_dict(cls, data: AutoModActionPayload): + return cls(try_enum(AutoModActionType, data["type"]), AutoModActionMetadata.from_dict(data["metadata"])) def __repr__(self) -> str: return f"" +class AutoModTriggerMetadata: + """Represents a rule's trigger metadata. + + Depending on the trigger type, different attributes will be used. + + .. versionadded:: 2.0 + + Attributes + ----------- + keyword_filter: List[:class:`str`] + A list of substrings to filter. Only for triggers of type :attr:`AutoModTriggerType.keyword`. + keyword_lists: List[:class:`AutoModWordsetType`] + A list of preset wordsets to filter. Only for triggers of type :attr:`AutoModTriggerType.keyword_preset`. + """ + # maybe add a table of action types and attributes? + # wording for keyword_lists could change + + __slots__ = ( + "keyword_filter", + "keyword_lists", + ) + + def __init__(self, keyword_filter: List[str] = MISSING, keyword_lists: List[AutoModWordsetType] = MISSING): + self.keyword_filter = keyword_filter + self.keyword_lists = keyword_lists + + def to_dict(self) -> Dict: + data = {} + if self.keyword_filter is not MISSING: + data["keyword_filter"] = self.keyword_filter + if self.keyword_lists is not MISSING: + data["keyword_lists"] = [wordset.value for wordset in self.keyword_lists] + return data + + @classmethod + def from_dict(cls, data: AutoModActionMetadataPayload): + kwargs = {} + if data.get("keyword_filter") is not None: + kwargs["keyword_filter"] = data["keyword_filter"] + if data.get("keyword_lists") is not None: + kwargs["keyword_lists"] = [try_enum(AutoModWordsetType, wordset) for wordset in data["keyword_lists"]] + return cls(**kwargs) + + def __repr__(self) -> str: + repr_attrs = ( + "keyword_filter", + "keyword_lists", + ) + inner = [] + for attr in repr_attrs: + value = getattr(self, attr) + if value is not MISSING: + inner.append(f"{attr}={value}") + inner = " ".join(inner) + return f"" + class AutoModRule(Hashable): """Represents a guild's auto moderation rule. @@ -121,6 +235,8 @@ class AutoModRule(Hashable): Indicates in what context the rule is checked. trigger_type: :class:`AutoModTriggerType` Indicates what type of information is checked to determine whether the rule is triggered. + trigger_metadata: :class:`AutoModTriggerMetadata` + The rule's trigger metadata. actions: List[:class:`AutoModAction`] The actions to perform when the rule is triggered. enabled: :class:`bool` @@ -139,6 +255,7 @@ class AutoModRule(Hashable): "creator_id", "event_type", "trigger_type", + "trigger_metadata", "actions", "enabled", "exempt_role_ids", @@ -158,7 +275,7 @@ def __init__( self.creator_id: int = int(data["creator_id"]) self.event_type: AutoModEventType = try_enum(AutoModEventType, data["event_type"]) self.trigger_type: AutoModTriggerType = try_enum(AutoModTriggerType, data["trigger_type"]) - # TODO: trigger_metadata + self.trigger_metadata: AutoModTriggerMetadata = AutoModTriggerMetadata.from_dict(data["trigger_metadata"]) self.actions: List[AutoModAction] = [AutoModAction.from_dict(d) for d in data["actions"]] self.enabled: bool = data["enabled"] self.exempt_role_ids: List[int] = [int(r) for r in data["exempt_roles"]] @@ -218,7 +335,7 @@ async def edit( *, name: str = MISSING, event_type: AutoModEventType = MISSING, - # TODO: trigger metadata + trigger_metadata: AutoModTriggerMetadata = MISSING, actions: List[AutoModAction] = MISSING, enabled: bool = MISSING, exempt_roles: List[Snowflake] = MISSING, @@ -234,6 +351,8 @@ async def edit( The rule's new name. event_type: :class:`AutoModEventType` The new context in which the rule is checked. + trigger_metadata: :class:`AutoModTriggerMetadata` + The new trigger metadata. actions: List[:class:`AutoModAction`] The new actions to perform when the rule is triggered. enabled: :class:`bool` @@ -263,7 +382,8 @@ async def edit( payload["name"] = name if event_type is not MISSING: payload["event_type"] = event_type.value - # TODO: trigger metadata + if trigger_metadata is not MISSING: + payload["trigger_metadata"] = trigger_metadata.to_dict() if actions is not MISSING: payload["actions"] = [a.to_dict() for a in actions] if enabled is not MISSING: diff --git a/discord/enums.py b/discord/enums.py index 006d0a5934..6486d5e8ef 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -789,7 +789,13 @@ class AutoModEventType(Enum): class AutoModActionType(Enum): block_message = 1 - log_to_channel = 2 + send_alert_message = 2 + + +class AutoModWordsetType(Enum): + profanity = "PROFANITY" + sexual_content = "SEXUAL_CONTENT" + slurs = "SLURS" T = TypeVar("T") diff --git a/discord/guild.py b/discord/guild.py index d7ff080d2d..9e1e64860f 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -3540,7 +3540,7 @@ async def fetch_auto_moderation_rules(self) -> List[AutoModRule]: data = await self._state.http.get_auto_moderation_rules(self.id) result = [] for rule in data: - result.append(AutoModRule(state=self._state, guild=self, data=rule)) + result.append(AutoModRule(state=self._state, data=rule)) return result async def fetch_auto_moderation_rule(self, id: int) -> AutoModRule: @@ -3561,7 +3561,7 @@ async def fetch_auto_moderation_rule(self, id: int) -> AutoModRule: The requested auto moderation rule. """ data = await self._state.http.get_auto_moderation_rule(self.id, id) - return AutoModRule(state=self._state, guild=self, data=data) + return AutoModRule(state=self._state, data=data) async def create_auto_moderation_rule( self, @@ -3569,7 +3569,7 @@ async def create_auto_moderation_rule( name: str, event_type: AutoModEventType, trigger_type: AutoModTriggerType, - # TODO: trigger metadata + trigger_metadata: AutoModTriggerMetadata, # TODO actions: List[AutoModAction], enabled: bool = False, exempt_roles: List[Snowflake] = None, From ec46e6a0bd0ddbdcef2838a022bfc26d2ed3dd1e Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Thu, 26 May 2022 16:06:17 +0000 Subject: [PATCH 29/51] Add missing import --- discord/guild.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 9e1e64860f..784618be84 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -45,7 +45,7 @@ from . import abc, utils -from .automod import AutoModAction, AutoModRule +from .automod import AutoModAction, AutoModRule, AutoModTriggerMetadata from .asset import Asset from .channel import * from .channel import _guild_channel_factory, _threaded_guild_channel_factory @@ -3580,6 +3580,7 @@ async def create_auto_moderation_rule( "name": name, "event_type": event_type.value, "trigger_type": trigger_type.value, + "trigger_metadata": trigger_metadata.to_dict(), "actions": [a.to_dict() for a in actions], "enabled": enabled, } @@ -3588,4 +3589,4 @@ async def create_auto_moderation_rule( if exempt_channels: payload["exempt_channels"] = [c.id for c in exempt_channels] data = await self._state.http.create_auto_moderation_rule(self.id, payload) - return AutoModRule(state=self._state, guild=self, data=data) + return AutoModRule(state=self._state, data=data) From 5a1a89e9125982285907f27244ab04aae00583d0 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Thu, 26 May 2022 16:14:46 +0000 Subject: [PATCH 30/51] update docs --- discord/guild.py | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 784618be84..bc8c5410a4 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -3569,13 +3569,46 @@ async def create_auto_moderation_rule( name: str, event_type: AutoModEventType, trigger_type: AutoModTriggerType, - trigger_metadata: AutoModTriggerMetadata, # TODO + trigger_metadata: AutoModTriggerMetadata, actions: List[AutoModAction], enabled: bool = False, exempt_roles: List[Snowflake] = None, exempt_channels: List[Snowflake] = None, ) -> AutoModRule: - # TODO: docstring + """ + Creates an auto moderation rule. + + Parameters + ----------- + name: :class:`str` + The name of the auto moderation rule. + event_type: :class:`AutoModEventType` + The type of event that triggers the rule. + trigger_type: :class:`AutoModTriggerType` + The rule's trigger type. + trigger_metadata: :class:`AutoModTriggerMetadata` + The rule's trigger metadata. + actions: :class:`List[AutoModAction]` + The actions to take when the rule is triggered. + enabled: :class:`bool` + Whether the rule is enabled. + exempt_roles: :class:`List[Snowflake]` + A list of roles that are exempt from the rule. + exempt_channels: :class:`List[Snowflake]` + A list of channels that are exempt from the rule. + + Raises + ------- + HTTPException + Creating the auto moderation rule failed. + Forbidden + You do not have the required permissions to create an auto moderation rule. + + Returns + -------- + :class:`AutoModRule` + The new auto moderation rule. + """ payload = { "name": name, "event_type": event_type.value, From 3fa5f391276b006ad29a3f9352c455c0327492f5 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Thu, 26 May 2022 17:11:13 +0000 Subject: [PATCH 31/51] Document permission requirements --- discord/automod.py | 9 ++++++++- discord/guild.py | 6 +++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/discord/automod.py b/discord/automod.py index c8225e8e45..349917b7cb 100644 --- a/discord/automod.py +++ b/discord/automod.py @@ -327,6 +327,13 @@ async def delete(self) -> None: """|coro| Deletes this rule. + + Raises + ------- + Forbidden + You do not have the Manage Guild permission. + HTTPException + The operation failed. """ await self._state.http.delete_auto_moderation_rule(self.guild_id, self.id) @@ -365,7 +372,7 @@ async def edit( Raises ------- Forbidden - You do not have the proper permissions to the action requested. + You do not have the Manage Guild permission. HTTPException The operation failed. diff --git a/discord/guild.py b/discord/guild.py index bc8c5410a4..6a013ad097 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -3530,7 +3530,7 @@ async def fetch_auto_moderation_rules(self) -> List[AutoModRule]: HTTPException Getting the auto moderation rules failed. Forbidden - You do not have the required permissions to get the auto moderation rules. + You do not have the Manage Guild permission. Returns -------- @@ -3553,7 +3553,7 @@ async def fetch_auto_moderation_rule(self, id: int) -> AutoModRule: HTTPException Getting the auto moderation rule failed. Forbidden - You do not have the required permissions to get the auto moderation rule. + You do not have the Manage Guild permission. Returns -------- @@ -3602,7 +3602,7 @@ async def create_auto_moderation_rule( HTTPException Creating the auto moderation rule failed. Forbidden - You do not have the required permissions to create an auto moderation rule. + You do not have the Manage Guild permission. Returns -------- From 01a13c0721869351716c524b259950718b1d2a81 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Thu, 2 Jun 2022 03:28:43 +0000 Subject: [PATCH 32/51] add support for automod timeouts --- discord/automod.py | 20 ++++++++++++++++---- discord/enums.py | 1 + discord/types/automod.py | 7 +++++-- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/discord/automod.py b/discord/automod.py index 349917b7cb..a18ef30f0c 100644 --- a/discord/automod.py +++ b/discord/automod.py @@ -25,6 +25,7 @@ from __future__ import annotations from functools import cached_property +from datetime import timedelta from typing import TYPE_CHECKING, Dict, Optional, List, Union from . import utils @@ -70,32 +71,42 @@ class AutoModActionMetadata: ----------- channel_id: :class:`int` The ID of the channel to send the message to. Only for actions of type :attr:`AutoModActionType.send_alert_message`. + timeout_duration: :class:`datetime.timedelta` + How long the member that triggered the action should be timed out for. Only for actions of type :attr:`AutoModActionType.timeout`. """ # maybe add a table of action types and attributes? __slots__ = ( "channel_id", + "timeout_duration", ) - def __init__(self, channel_id: int = MISSING): - self.channel_id = channel_id + def __init__(self, channel_id: int = MISSING, timeout_duration: timedelta = MISSING): + self.channel_id: int = channel_id + self.timeout_duration: timedelta = timeout_duration def to_dict(self) -> Dict: data = {} if self.channel_id is not MISSING: data["channel_id"] = self.channel_id + if self.timeout_duration is not MISSING: + data["duration_seconds"] = self.timeout_duration.total_seconds() return data @classmethod def from_dict(cls, data: AutoModActionMetadataPayload): kwargs = {} - if data.get("channel_id") is not None: - kwargs["channel_id"] = int(data["channel_id"]) + if (channel_id := data.get("channel_id")) is not None: + kwargs["channel_id"] = int(channel_id) + if (duration_seconds := data.get("duration_seconds")) is not None: + # might need an explicit int cast + kwargs["timeout_duration"] = timedelta(seconds=duration_seconds) return cls(**kwargs) def __repr__(self) -> str: repr_attrs = ( "channel_id", + "timeout_duration", ) inner = [] for attr in repr_attrs: @@ -119,6 +130,7 @@ class AutoModAction: metadata: :class:`AutoModActionMetadata` The action's metadata. """ + # note that AutoModActionType.timeout is only valid for trigger type 1? __slots__ = ( "type", diff --git a/discord/enums.py b/discord/enums.py index 6486d5e8ef..32f0102425 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -790,6 +790,7 @@ class AutoModEventType(Enum): class AutoModActionType(Enum): block_message = 1 send_alert_message = 2 + timeout = 3 class AutoModWordsetType(Enum): diff --git a/discord/types/automod.py b/discord/types/automod.py index ae48387840..c027228ff1 100644 --- a/discord/types/automod.py +++ b/discord/types/automod.py @@ -1,7 +1,8 @@ """ The MIT License (MIT) -Copyright (c) 2015-2021 Rapptz + Copyright (c) 2021-present Pycord Development + 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 @@ -29,7 +30,8 @@ AutoModEventType = Literal[1] -AutoModActionType = Literal[1, 2] +AutoModActionType = Literal[1, 2, 3] + AutoModWordsetType = Literal["PROFANITY", "SEXUAL_CONTENT", "SLURS"] @@ -41,6 +43,7 @@ class AutoModTriggerMetadata(TypedDict, total=False): class AutoModActionMetadata(TypedDict, total=False): channel_id: Snowflake + duration_seconds: int # documented as snowflake for some reason class AutoModAction(TypedDict): From f1a41d799e20f5a309447334b87579e961688ed1 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Thu, 2 Jun 2022 09:44:41 -0700 Subject: [PATCH 33/51] Update raw_models.py --- discord/types/raw_models.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/discord/types/raw_models.py b/discord/types/raw_models.py index d1b3a4bb52..bc55dc17f0 100644 --- a/discord/types/raw_models.py +++ b/discord/types/raw_models.py @@ -114,14 +114,16 @@ class ScheduledEventSubscription(TypedDict, total=False): guild_id: Snowflake -class AutoModActionExecutionEvent(TypedDict): +class _AutoModActionExecutionEventOptional(TypedDict, total=False): + channel_id: Snowflake + message_id: Snowflake + alert_system_message_id: Snowflake + matched_keyword: str + matched_content: str + +class AutoModActionExecutionEvent(_AutoModActionExecutionEventOptional): guild_id: Snowflake action: AutoModAction rule_id: Snowflake rule_trigger_type: AutoModTriggerType - channel_id: Optional[Snowflake] - message_id: Optional[Snowflake] - alert_system_message_id: Optional[Snowflake] content: str - matched_keyword: Optional[str] - matched_content: Optional[str] From 5eeab40e1e6e4eab909ae8fc0e2674631339f169 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Wed, 8 Jun 2022 21:10:09 +0000 Subject: [PATCH 34/51] keyword_lists to presets --- discord/automod.py | 24 ++++++++++++------------ discord/enums.py | 8 ++++---- discord/types/automod.py | 5 ++--- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/discord/automod.py b/discord/automod.py index a18ef30f0c..73ea60df09 100644 --- a/discord/automod.py +++ b/discord/automod.py @@ -33,7 +33,7 @@ AutoModTriggerType, AutoModEventType, AutoModActionType, - AutoModWordsetType, + AutoModKeywordPresetType, try_enum, ) from .mixins import Hashable @@ -166,27 +166,27 @@ class AutoModTriggerMetadata: ----------- keyword_filter: List[:class:`str`] A list of substrings to filter. Only for triggers of type :attr:`AutoModTriggerType.keyword`. - keyword_lists: List[:class:`AutoModWordsetType`] - A list of preset wordsets to filter. Only for triggers of type :attr:`AutoModTriggerType.keyword_preset`. + presets: List[:class:`AutoModKeywordPresetType`] + A list of keyword presets to filter. Only for triggers of type :attr:`AutoModTriggerType.keyword_preset`. """ # maybe add a table of action types and attributes? - # wording for keyword_lists could change + # wording for presets could change __slots__ = ( "keyword_filter", - "keyword_lists", + "presets", ) - def __init__(self, keyword_filter: List[str] = MISSING, keyword_lists: List[AutoModWordsetType] = MISSING): + def __init__(self, keyword_filter: List[str] = MISSING, presets: List[AutoModKeywordPresetType] = MISSING): self.keyword_filter = keyword_filter - self.keyword_lists = keyword_lists + self.presets = presets def to_dict(self) -> Dict: data = {} if self.keyword_filter is not MISSING: data["keyword_filter"] = self.keyword_filter - if self.keyword_lists is not MISSING: - data["keyword_lists"] = [wordset.value for wordset in self.keyword_lists] + if self.presets is not MISSING: + data["presets"] = [wordset.value for wordset in self.presets] return data @classmethod @@ -194,14 +194,14 @@ def from_dict(cls, data: AutoModActionMetadataPayload): kwargs = {} if data.get("keyword_filter") is not None: kwargs["keyword_filter"] = data["keyword_filter"] - if data.get("keyword_lists") is not None: - kwargs["keyword_lists"] = [try_enum(AutoModWordsetType, wordset) for wordset in data["keyword_lists"]] + if data.get("presets") is not None: + kwargs["presets"] = [try_enum(AutoModKeywordPresetType, wordset) for wordset in data["presets"]] return cls(**kwargs) def __repr__(self) -> str: repr_attrs = ( "keyword_filter", - "keyword_lists", + "presets", ) inner = [] for attr in repr_attrs: diff --git a/discord/enums.py b/discord/enums.py index 32f0102425..a2d365e9cc 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -793,10 +793,10 @@ class AutoModActionType(Enum): timeout = 3 -class AutoModWordsetType(Enum): - profanity = "PROFANITY" - sexual_content = "SEXUAL_CONTENT" - slurs = "SLURS" +class AutoModKeywordPresetType(Enum): + profanity = 1 + sexual_content = 2 + slurs = 3 T = TypeVar("T") diff --git a/discord/types/automod.py b/discord/types/automod.py index c027228ff1..600950d001 100644 --- a/discord/types/automod.py +++ b/discord/types/automod.py @@ -32,13 +32,12 @@ AutoModActionType = Literal[1, 2, 3] - -AutoModWordsetType = Literal["PROFANITY", "SEXUAL_CONTENT", "SLURS"] +AutoModKeywordPresetType = Literal[1, 2, 3] class AutoModTriggerMetadata(TypedDict, total=False): keyword_filter: List[str] - keyword_lists: List[AutoModWordsetType] + presets: List[AutoModKeywordPresetType] class AutoModActionMetadata(TypedDict, total=False): From 423fac9d9f91113812b83bbe6291699f66ce3678 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Wed, 8 Jun 2022 21:42:09 +0000 Subject: [PATCH 35/51] rework auto_moderation_action_execution event --- discord/raw_models.py | 100 +++++++++++++++++++++++++++++++++--- discord/state.py | 9 +--- discord/types/raw_models.py | 3 +- docs/api.rst | 22 ++------ 4 files changed, 100 insertions(+), 34 deletions(-) diff --git a/discord/raw_models.py b/discord/raw_models.py index 99743eca67..6751504746 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -33,6 +33,8 @@ from .enums import ChannelType, try_enum if TYPE_CHECKING: + from .state import ConnectionState + from .abc import MessageableChannel from .guild import Guild from .member import Member from .message import Message @@ -49,7 +51,7 @@ ScheduledEventSubscription, ThreadDeleteEvent, TypingEvent, - AutoModActionExecutionEvent, + AutoModActionExecutionEvent as AutoModActionExecution, ) @@ -64,7 +66,7 @@ "RawThreadDeleteEvent", "RawTypingEvent", "RawScheduledEventSubscription", - "RawAutoModActionExecutionEvent", + "AutoModActionExecutionEvent", ) @@ -392,8 +394,8 @@ def __init__(self, data: ScheduledEventSubscription, event_type: str): self.event_type: str = event_type -class RawAutoModActionExecutionEvent(_RawReprMixin): - """Represents the payload for a :func:`raw_auto_moderation_action_execution` +class AutoModActionExecutionEvent: + """Represents the payload for a :func:`auto_moderation_action_execution` .. versionadded:: 2.0 @@ -401,12 +403,96 @@ class RawAutoModActionExecutionEvent(_RawReprMixin): ----------- action: :class:`AutoModAction` The action that was executed. + rule_id: :class:`int` + The ID of the rule that the action belongs to. guild_id: :class:`int` - The guild ID that the action was executed in. + The ID of the guild that the action was executed in. + guild: Optional[:class:`Guild`] + The guild that the action was executed in, if cached. + user_id: :class:`int` + The ID of the user that triggered the action. + member: Optional[:class:`Member`] + The member that triggered the action, if cached. + channel_id: Optional[:class:`int`] + The ID of the channel in which the member's content was posted. + channel: Optional[Union[:class:`TextChannel`, :class:`Thread`, :class:`VoiceChannel`]] + The channel in which the member's content was posted, if cached. + message_id: Optional[:class:`int`] + The ID of the message that triggered the action. This is only available if the + message was not blocked. + message: Optional[:class:`Message`] + The message that triggered the action, if cached. + alert_system_message_id: Optional[:class:`int`] + The ID of the system auto moderation message that was posted as a result + of the action. + alert_system_message: Optional[:class:`Message`] + The system auto moderation message that was posted as a result of the action, + if cached. + content: :class:`str` + The content of the message that triggered the action. + matched_keyword: :class:`str` + The word or phrase configured that was matched in the content. + matched_content: :class:`str` + The substring in the content that was matched. """ - __slots__ = ("action", "guild_id") + __slots__ = ( + "action", + "rule_id", + "guild_id", + "guild", + "user_id", + "member", + "content", + "matched_keyword", + "matched_content", + "channel_id", + "channel", + "message_id", + "message", + "alert_system_message_id", + "alert_system_message", + ) - def __init__(self, data: AutoModActionExecutionEvent) -> None: + def __init__(self, state: ConnectionState, data: AutoModActionExecution) -> None: self.action: AutoModAction = AutoModAction.from_dict(data["action"]) + self.rule_id: int = int(data["rule_id"]) self.guild_id: int = int(data["guild_id"]) + self.guild: Optional[Guild] = state._get_guild(self.guild_id) + self.user_id: int = int(data["user_id"]) + if self.guild: + self.member: Optional[Member] = self.guild.get_member(self.user_id) + else: + self.member: Optional[Member] = None + self.content: str = data["content"] + self.matched_keyword: str = data["matched_keyword"] + self.matched_content: str = data["matched_content"] + + try: + # I don't see why this would be optional, but it's documented as such + # so we should treat it that way + self.channel_id: Optional[int] = int(data["channel_id"]) + self.channel: Optional[MessageableChannel] = self.guild.get_channel_or_thread(self.channel_id) + except KeyError: + self.channel_id: Optional[int] = None + self.channel: Optional[MessageableChannel] = None + try: + self.message_id: Optional[int] = int(data["message_id"]) + self.message: Optional[Message] = state._get_message(self.message_id) + except KeyError: + self.message_id: Optional[int] = None + self.message: Optional[Message] = None + try: + self.alert_system_message_id: Optional[int] = int(data["alert_system_message_id"]) + self.alert_system_message: Optional[Message] = state._get_message(self.alert_system_message_id) + except KeyError: + self.alert_system_message_id: Optional[int] = None + self.alert_system_message: Optional[Message] = None + + def __repr__(self) -> str: + return ( + f"" + ) + diff --git a/discord/state.py b/discord/state.py index 408c83269f..45a8b9b26b 100644 --- a/discord/state.py +++ b/discord/state.py @@ -633,13 +633,8 @@ def parse_auto_moderation_rule_delete(self, data) -> None: self.dispatch("auto_moderation_rule_delete", rule) def parse_auto_moderation_action_execution(self, data) -> None: - # should there be a raw event for this? - raw = RawAutoModActionExecutionEvent(data) - self.dispatch("raw_auto_moderation_action_execution", raw) - guild = self._get_guild(int(data["guild_id"])) - if guild is not None: - action = AutoModAction.from_dict(data["action"]) - self.dispatch("auto_moderation_action_execution", guild, action) + event = AutoModActionExecutionEvent(self, data) + self.dispatch("auto_moderation_action_execution", event) def parse_message_create(self, data) -> None: channel, _ = self._get_guild_channel(data) diff --git a/discord/types/raw_models.py b/discord/types/raw_models.py index bc55dc17f0..ac1069f1ff 100644 --- a/discord/types/raw_models.py +++ b/discord/types/raw_models.py @@ -28,7 +28,7 @@ from .emoji import PartialEmoji from .member import Member from .snowflake import Snowflake -from .types.automod import AutoModAction, AutoModTriggerType +from .automod import AutoModAction, AutoModTriggerType class _MessageEventOptional(TypedDict, total=False): @@ -122,6 +122,7 @@ class _AutoModActionExecutionEventOptional(TypedDict, total=False): matched_content: str class AutoModActionExecutionEvent(_AutoModActionExecutionEventOptional): + user_id: Snowflake guild_id: Snowflake action: AutoModAction rule_id: Snowflake diff --git a/docs/api.rst b/docs/api.rst index 4404eeae46..b58b678b02 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1432,29 +1432,13 @@ to handle it, which defaults to print a traceback and ignoring the exception. .. function:: on_auto_moderation_action_execution(guild, action) Called when an auto moderation action is executed. - If the guild is not found in the internal cache, then this event will not be - called. Consider using :func:`on_raw_auto_moderation_action_execution` instead. The bot must have :attr:`~Permissions.manage_guild` to receive this, and - :attr:`Intents.auto_moderation_configuration` must be enabled. - - :param guild: The guild the action was executed in. - :type guild: :class:`Guild` - :param action: The action that was executed. - :type action: :class:`AutoModAction` - -.. function:: on_raw_auto_moderation_action_execution(payload) - - Called when an auto moderation action is executed. + :attr:`Intents.auto_moderation_execution` must be enabled. - Unlike :meth:`on_auto_moderation_action_execution`, this event will be - called regardless of the state of the internal cache. + :param payload: The event's data. + :type payload: :class:`AutoModActionExecutionEvent` - The bot must have :attr:`~Permissions.manage_guild` to receive this, and - :attr:`Intents.auto_moderation_configuration` must be enabled. - - :param payload: The raw event payload data. - :type payload: :class:`RawAutoModActionExecution` .. _discord-api-utils: From 6737cfce9c7b53293aee3661541e52ad3b5d0112 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Wed, 15 Jun 2022 21:34:31 -0700 Subject: [PATCH 36/51] Remove comment --- discord/types/automod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/types/automod.py b/discord/types/automod.py index 600950d001..0f5c34a804 100644 --- a/discord/types/automod.py +++ b/discord/types/automod.py @@ -42,7 +42,7 @@ class AutoModTriggerMetadata(TypedDict, total=False): class AutoModActionMetadata(TypedDict, total=False): channel_id: Snowflake - duration_seconds: int # documented as snowflake for some reason + duration_seconds: int class AutoModAction(TypedDict): From 3778054f24cce7d8311ff4e97245ae7ec0358f2b Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Mon, 20 Jun 2022 08:44:18 -0700 Subject: [PATCH 37/51] Formatting fixes Co-authored-by: baronkobama --- discord/automod.py | 53 +++++++++++++++++++++++++------------ discord/guild.py | 5 ++-- discord/raw_models.py | 3 ++- discord/state.py | 1 - discord/types/automod.py | 4 +-- discord/types/raw_models.py | 3 ++- docs/api.rst | 2 -- 7 files changed, 45 insertions(+), 26 deletions(-) diff --git a/discord/automod.py b/discord/automod.py index 73ea60df09..3988276287 100644 --- a/discord/automod.py +++ b/discord/automod.py @@ -24,16 +24,16 @@ from __future__ import annotations -from functools import cached_property from datetime import timedelta -from typing import TYPE_CHECKING, Dict, Optional, List, Union +from functools import cached_property +from typing import TYPE_CHECKING, Dict, List, Optional, Union from . import utils from .enums import ( - AutoModTriggerType, - AutoModEventType, AutoModActionType, + AutoModEventType, AutoModKeywordPresetType, + AutoModTriggerType, try_enum, ) from .mixins import Hashable @@ -45,16 +45,16 @@ if TYPE_CHECKING: from .abc import Snowflake + from .channel import ForumChannel, TextChannel, VoiceChannel from .guild import Guild from .member import Member from .role import Role - from .channel import ForumChannel, TextChannel, VoiceChannel from .state import ConnectionState from .types.automod import ( - AutoModRule as AutoModRulePayload, AutoModAction as AutoModActionPayload, - AutoModTriggerMetadata as AutoModTriggerMetadataPayload, AutoModActionMetadata as AutoModActionMetadataPayload, + AutoModRule as AutoModRulePayload, + AutoModTriggerMetadata as AutoModTriggerMetadataPayload, ) MISSING = utils.MISSING @@ -87,20 +87,26 @@ def __init__(self, channel_id: int = MISSING, timeout_duration: timedelta = MISS def to_dict(self) -> Dict: data = {} + if self.channel_id is not MISSING: data["channel_id"] = self.channel_id + if self.timeout_duration is not MISSING: data["duration_seconds"] = self.timeout_duration.total_seconds() + return data @classmethod def from_dict(cls, data: AutoModActionMetadataPayload): kwargs = {} + if (channel_id := data.get("channel_id")) is not None: kwargs["channel_id"] = int(channel_id) + if (duration_seconds := data.get("duration_seconds")) is not None: # might need an explicit int cast kwargs["timeout_duration"] = timedelta(seconds=duration_seconds) + return cls(**kwargs) def __repr__(self) -> str: @@ -109,11 +115,12 @@ def __repr__(self) -> str: "timeout_duration", ) inner = [] + for attr in repr_attrs: - value = getattr(self, attr) - if value is not MISSING: + if (value := getattr(self, attr)) is not MISSING: inner.append(f"{attr}={value}") inner = " ".join(inner) + return f"" @@ -183,19 +190,25 @@ def __init__(self, keyword_filter: List[str] = MISSING, presets: List[AutoModKey def to_dict(self) -> Dict: data = {} + if self.keyword_filter is not MISSING: data["keyword_filter"] = self.keyword_filter + if self.presets is not MISSING: data["presets"] = [wordset.value for wordset in self.presets] + return data @classmethod def from_dict(cls, data: AutoModActionMetadataPayload): kwargs = {} - if data.get("keyword_filter") is not None: - kwargs["keyword_filter"] = data["keyword_filter"] - if data.get("presets") is not None: - kwargs["presets"] = [try_enum(AutoModKeywordPresetType, wordset) for wordset in data["presets"]] + + if (keyword_filter := data.get("keyword_filter")) is not None: + kwargs["keyword_filter"] = keyword_filter + + if (presets := data.get("presets")) is not None: + kwargs["presets"] = [try_enum(AutoModKeywordPresetType, wordset) for wordset in presets] + return cls(**kwargs) def __repr__(self) -> str: @@ -204,11 +217,12 @@ def __repr__(self) -> str: "presets", ) inner = [] + for attr in repr_attrs: - value = getattr(self, attr) - if value is not MISSING: + if (value := getattr(self, attr)) is not MISSING: inner.append(f"{attr}={value}") inner = " ".join(inner) + return f"" @@ -397,19 +411,26 @@ async def edit( http = self._state.http guild_id = self.guild.id payload = {} + if name is not MISSING: payload["name"] = name + if event_type is not MISSING: payload["event_type"] = event_type.value + if trigger_metadata is not MISSING: payload["trigger_metadata"] = trigger_metadata.to_dict() + if actions is not MISSING: payload["actions"] = [a.to_dict() for a in actions] + if enabled is not MISSING: payload["enabled"] = enabled + # Maybe consider enforcing limits on the number of exempt roles/channels? if exempt_roles is not MISSING: payload["exempt_roles"] = [r.id for r in exempt_roles] + if exempt_channels is not MISSING: payload["exempt_channels"] = [c.id for c in exempt_channels] @@ -417,5 +438,3 @@ async def edit( data = await http.edit_auto_moderation_rule(guild_id, self.id, payload) return AutoModRule(state=self._state, data=data) - - diff --git a/discord/guild.py b/discord/guild.py index 86176b624f..185e0ec8cd 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -43,7 +43,6 @@ overload, ) - from . import abc, utils from .automod import AutoModAction, AutoModRule, AutoModTriggerMetadata from .asset import Asset @@ -555,7 +554,6 @@ def _from_data(self, guild: GuildPayload) -> None: for obj in guild.get("voice_states", []): self._update_voice_state(obj, int(obj["channel_id"])) - # TODO: refactor/remove? def _sync(self, data: GuildPayload) -> None: try: @@ -3634,9 +3632,12 @@ async def create_auto_moderation_rule( "actions": [a.to_dict() for a in actions], "enabled": enabled, } + if exempt_roles: payload["exempt_roles"] = [r.id for r in exempt_roles] + if exempt_channels: payload["exempt_channels"] = [c.id for c in exempt_channels] + data = await self._state.http.create_auto_moderation_rule(self.id, payload) return AutoModRule(state=self._state, data=data) diff --git a/discord/raw_models.py b/discord/raw_models.py index 6751504746..f930144f97 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -29,7 +29,6 @@ from typing import TYPE_CHECKING, List, Optional, Set from .automod import AutoModAction - from .enums import ChannelType, try_enum if TYPE_CHECKING: @@ -476,12 +475,14 @@ def __init__(self, state: ConnectionState, data: AutoModActionExecution) -> None except KeyError: self.channel_id: Optional[int] = None self.channel: Optional[MessageableChannel] = None + try: self.message_id: Optional[int] = int(data["message_id"]) self.message: Optional[Message] = state._get_message(self.message_id) except KeyError: self.message_id: Optional[int] = None self.message: Optional[Message] = None + try: self.alert_system_message_id: Optional[int] = int(data["alert_system_message_id"]) self.alert_system_message: Optional[Message] = state._get_message(self.alert_system_message_id) diff --git a/discord/state.py b/discord/state.py index 45a8b9b26b..f7f1483a9c 100644 --- a/discord/state.py +++ b/discord/state.py @@ -47,7 +47,6 @@ Union, ) - from . import utils from .activity import BaseActivity from .automod import AutoModAction, AutoModRule diff --git a/discord/types/automod.py b/discord/types/automod.py index 0f5c34a804..ebb6b8278d 100644 --- a/discord/types/automod.py +++ b/discord/types/automod.py @@ -22,7 +22,7 @@ from __future__ import annotations -from typing import TypedDict, Literal, List, Dict +from typing import Dict, List, Literal, TypedDict from .snowflake import Snowflake @@ -57,8 +57,8 @@ class AutoModRule(TypedDict): creator_id: Snowflake event_type: AutoModEventType trigger_type: AutoModTriggerType - actions: List[AutoModAction] trigger_metadata: AutoModTriggerMetadata + actions: List[AutoModAction] enabled: bool exempt_roles: List[Snowflake] exempt_channels: List[Snowflake] diff --git a/discord/types/raw_models.py b/discord/types/raw_models.py index ac1069f1ff..e73fc18f2b 100644 --- a/discord/types/raw_models.py +++ b/discord/types/raw_models.py @@ -121,10 +121,11 @@ class _AutoModActionExecutionEventOptional(TypedDict, total=False): matched_keyword: str matched_content: str + class AutoModActionExecutionEvent(_AutoModActionExecutionEventOptional): - user_id: Snowflake guild_id: Snowflake action: AutoModAction rule_id: Snowflake rule_trigger_type: AutoModTriggerType + user_id: Snowflake content: str diff --git a/docs/api.rst b/docs/api.rst index b58b678b02..f54b2212a9 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1439,8 +1439,6 @@ to handle it, which defaults to print a traceback and ignoring the exception. :param payload: The event's data. :type payload: :class:`AutoModActionExecutionEvent` - - .. _discord-api-utils: Utility Functions From e73ad6acbc983bf6f8a103500b3f71814882cbd8 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Mon, 20 Jun 2022 08:45:44 -0700 Subject: [PATCH 38/51] Fix incorrect HTTP method Co-authored-by: baronkobama --- discord/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/http.py b/discord/http.py index 9d65953227..ce280e9858 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2361,7 +2361,7 @@ def edit_auto_moderation_rule( payload: automod.EditAutoModRule, ) -> Response[automod.AutoModRule]: r = Route( - "POST", + "PATCH", "/guilds/{guild_id}/auto-moderation/rules/{rule_id}", guild_id=guild_id, rule_id=rule_id, From 5a1cd59bca0c5be291cf2a4e397c6ba08461e20b Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Mon, 20 Jun 2022 08:48:22 -0700 Subject: [PATCH 39/51] Update discord/guild.py Co-authored-by: baronkobama --- discord/guild.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 9be1c6d063..d51f1228dc 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -3542,10 +3542,7 @@ async def fetch_auto_moderation_rules(self) -> List[AutoModRule]: The auto moderation rules for this guild. """ data = await self._state.http.get_auto_moderation_rules(self.id) - result = [] - for rule in data: - result.append(AutoModRule(state=self._state, data=rule)) - return result + return [AutoModRule(state=self._state, data=rule) for rule in data] async def fetch_auto_moderation_rule(self, id: int) -> AutoModRule: """|coro| From 1983ba7e95cf864275c64dc8b0abc8879aabad18 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Mon, 20 Jun 2022 08:49:38 -0700 Subject: [PATCH 40/51] Remove unnecessary variable --- discord/automod.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/discord/automod.py b/discord/automod.py index 3988276287..0070609bd8 100644 --- a/discord/automod.py +++ b/discord/automod.py @@ -409,7 +409,6 @@ async def edit( when fields are updated. """ http = self._state.http - guild_id = self.guild.id payload = {} if name is not MISSING: @@ -435,6 +434,6 @@ async def edit( payload["exempt_channels"] = [c.id for c in exempt_channels] if payload: - data = await http.edit_auto_moderation_rule(guild_id, self.id, payload) + data = await http.edit_auto_moderation_rule(self.guild_id, self.id, payload) return AutoModRule(state=self._state, data=data) From ac61789a25519fb8cd05236fe796f56a1cfe7c19 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Mon, 20 Jun 2022 08:51:20 -0700 Subject: [PATCH 41/51] Alphabetize --- discord/guild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/guild.py b/discord/guild.py index d51f1228dc..6300469798 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -206,6 +206,7 @@ class Guild(Hashable): - ``ANIMATED_BANNER``: Guild can upload an animated banner. - ``ANIMATED_ICON``: Guild can upload an animated icon. + - ``AUTO_MODERATION``: Guild has enabled the auto moderation system. - ``BANNER``: Guild can upload and use a banner. (i.e. :attr:`.banner`) - ``CHANNEL_BANNER``: Guild can upload and use a channel banners. - ``COMMERCE``: Guild can sell things using store channels, which have now been removed. @@ -241,7 +242,6 @@ class Guild(Hashable): - ``VERIFIED``: Guild is a verified server. - ``VIP_REGIONS``: Guild has VIP voice regions. - ``WELCOME_SCREEN_ENABLED``: Guild has enabled the welcome screen. - - ``AUTO_MODERATION``: Guild has enabled the auto moderation system. premium_tier: :class:`int` The premium tier for this guild. Corresponds to "Nitro Server" in the official UI. From 7e018354d324c82bd41c8f05730522581db19778 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Mon, 20 Jun 2022 08:53:03 -0700 Subject: [PATCH 42/51] Alphabetizing --- discord/raw_models.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/discord/raw_models.py b/discord/raw_models.py index f930144f97..9b8fef4415 100644 --- a/discord/raw_models.py +++ b/discord/raw_models.py @@ -32,14 +32,15 @@ from .enums import ChannelType, try_enum if TYPE_CHECKING: - from .state import ConnectionState from .abc import MessageableChannel from .guild import Guild from .member import Member from .message import Message from .partial_emoji import PartialEmoji + from .state import ConnectionState from .threads import Thread from .types.raw_models import ( + AutoModActionExecutionEvent as AutoModActionExecution, BulkMessageDeleteEvent, IntegrationDeleteEvent, MessageDeleteEvent, @@ -50,7 +51,6 @@ ScheduledEventSubscription, ThreadDeleteEvent, TypingEvent, - AutoModActionExecutionEvent as AutoModActionExecution, ) @@ -459,13 +459,14 @@ def __init__(self, state: ConnectionState, data: AutoModActionExecution) -> None self.guild_id: int = int(data["guild_id"]) self.guild: Optional[Guild] = state._get_guild(self.guild_id) self.user_id: int = int(data["user_id"]) + self.content: str = data["content"] + self.matched_keyword: str = data["matched_keyword"] + self.matched_content: str = data["matched_content"] + if self.guild: self.member: Optional[Member] = self.guild.get_member(self.user_id) else: self.member: Optional[Member] = None - self.content: str = data["content"] - self.matched_keyword: str = data["matched_keyword"] - self.matched_content: str = data["matched_content"] try: # I don't see why this would be optional, but it's documented as such From cea2872c32d0dc1a1ced8bd205fa047f09647ae7 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Mon, 20 Jun 2022 08:55:13 -0700 Subject: [PATCH 43/51] Return Object in exempt_* properties --- discord/automod.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/discord/automod.py b/discord/automod.py index 0070609bd8..d49e518537 100644 --- a/discord/automod.py +++ b/discord/automod.py @@ -334,19 +334,19 @@ def exempt_roles(self) -> List[Union[Role, Object]]: then it will be returned as an :class:`Object`. """ if self.guild is None: - return [] + return [Object(role_id) for role_id in self.exempt_role_ids] return [self.guild.get_role(role_id) or Object(role_id) for role_id in self.exempt_role_ids] @cached_property def exempt_channels(self) -> List[Union[Union[TextChannel, ForumChannel, VoiceChannel], Object]]: - """List[Union[Union[TextChannel, ForumChannel, VoiceChannel], Object]]: The channels + """List[Union[Union[:class:`TextChannel`, :class:`ForumChannel`, :class:`VoiceChannel`], :class:`Object`]]: The channels that are exempt from this rule. If a channel is not found in the guild's cache, then it will be returned as an :class:`Object`. """ if self.guild is None: - return [] + return [Object(channel_id) for channel_id in self.exempt_channel_ids] return [self.guild.get_channel(channel_id) or Object(channel_id) for channel_id in self.exempt_channel_ids] async def delete(self) -> None: From f6d7db05ebb42b8e378564599f90a0f566308ee2 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Mon, 20 Jun 2022 09:01:01 -0700 Subject: [PATCH 44/51] Alphabetizing --- discord/types/audit_log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/types/audit_log.py b/discord/types/audit_log.py index ed3c1955d4..1057e80a2f 100644 --- a/discord/types/audit_log.py +++ b/discord/types/audit_log.py @@ -27,6 +27,7 @@ from typing import List, Literal, Optional, TypedDict, Union +from .automod import AutoModRule from .channel import ChannelType, PermissionOverwrite, VideoQualityMode from .guild import ( DefaultMessageNotificationLevel, @@ -41,7 +42,6 @@ from .threads import Thread from .user import User from .webhook import Webhook -from .automod import AutoModRule AuditLogEvent = Literal[ 1, From 70764c06e96fc27ceb038c70f4239b28a3d508f8 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Mon, 20 Jun 2022 09:02:05 -0700 Subject: [PATCH 45/51] Update guild.py --- discord/types/guild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/types/guild.py b/discord/types/guild.py index 91b514b69d..8435e9c45e 100644 --- a/discord/types/guild.py +++ b/discord/types/guild.py @@ -82,6 +82,7 @@ class _GuildOptional(TypedDict, total=False): GuildFeature = Literal[ "ANIMATED_BANNER", "ANIMATED_ICON", + "AUTO_MODERATION", "BANNER", "COMMERCE", "COMMUNITY", @@ -114,7 +115,6 @@ class _GuildOptional(TypedDict, total=False): "VERIFIED", "VIP_REGIONS", "WELCOME_SCREEN_ENABLED", - "AUTO_MODERATION", ] From 7125ce596b7f27d2a20d18d20df0f8726af2db34 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Mon, 20 Jun 2022 09:02:31 -0700 Subject: [PATCH 46/51] Alphabetizing --- discord/types/raw_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/types/raw_models.py b/discord/types/raw_models.py index e73fc18f2b..94dd780113 100644 --- a/discord/types/raw_models.py +++ b/discord/types/raw_models.py @@ -25,10 +25,10 @@ from typing import List, TypedDict +from .automod import AutoModAction, AutoModTriggerType from .emoji import PartialEmoji from .member import Member from .snowflake import Snowflake -from .automod import AutoModAction, AutoModTriggerType class _MessageEventOptional(TypedDict, total=False): From 6879c6d6dcdf7941181736ec9ccf1ab8c5009389 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Mon, 20 Jun 2022 09:04:41 -0700 Subject: [PATCH 47/51] Support for reasons --- discord/http.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/discord/http.py b/discord/http.py index ce280e9858..4fce824082 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2346,19 +2346,21 @@ def create_auto_moderation_rule( self, guild_id: Snowflake, payload: automod.CreateAutoModRule, + reason: Optional[str] = None, ) -> Response[automod.AutoModRule]: r = Route( "POST", "/guilds/{guild_id}/auto-moderation/rules", guild_id=guild_id, ) - return self.request(r, json=payload) + return self.request(r, json=payload, reason=reason) def edit_auto_moderation_rule( self, guild_id: Snowflake, rule_id: Snowflake, payload: automod.EditAutoModRule, + reason: Optional[str] = None, ) -> Response[automod.AutoModRule]: r = Route( "PATCH", @@ -2366,12 +2368,13 @@ def edit_auto_moderation_rule( guild_id=guild_id, rule_id=rule_id, ) - return self.request(r, json=payload) + return self.request(r, json=payload, reason=reason) def delete_auto_moderation_rule( self, guild_id: Snowflake, rule_id: Snowflake, + reason: Optional[str] = None, ) -> Response[None]: r = Route( "DELETE", @@ -2379,7 +2382,7 @@ def delete_auto_moderation_rule( guild_id=guild_id, rule_id=rule_id, ) - return self.request(r) + return self.request(r, reason=reason) # Interaction responses From 0d8df7df92f170fa4b63eacb4b3c5fad453fe960 Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Mon, 20 Jun 2022 09:05:41 -0700 Subject: [PATCH 48/51] Support for reasons --- discord/automod.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/discord/automod.py b/discord/automod.py index d49e518537..c4b3446967 100644 --- a/discord/automod.py +++ b/discord/automod.py @@ -349,7 +349,7 @@ def exempt_channels(self) -> List[Union[Union[TextChannel, ForumChannel, VoiceCh return [Object(channel_id) for channel_id in self.exempt_channel_ids] return [self.guild.get_channel(channel_id) or Object(channel_id) for channel_id in self.exempt_channel_ids] - async def delete(self) -> None: + async def delete(self, reason: Optional[str] = None) -> None: """|coro| Deletes this rule. @@ -361,7 +361,7 @@ async def delete(self) -> None: HTTPException The operation failed. """ - await self._state.http.delete_auto_moderation_rule(self.guild_id, self.id) + await self._state.http.delete_auto_moderation_rule(self.guild_id, self.id, reason=reason) async def edit( self, @@ -373,6 +373,7 @@ async def edit( enabled: bool = MISSING, exempt_roles: List[Snowflake] = MISSING, exempt_channels: List[Snowflake] = MISSING, + reason: Optional[str] = None, ) -> Optional[AutoModRule]: """|coro| @@ -434,6 +435,6 @@ async def edit( payload["exempt_channels"] = [c.id for c in exempt_channels] if payload: - data = await http.edit_auto_moderation_rule(self.guild_id, self.id, payload) + data = await http.edit_auto_moderation_rule(self.guild_id, self.id, payload, reason=reason) return AutoModRule(state=self._state, data=data) From 9edab821771c076b988dd8e15fa0f667a1f7b7ce Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Mon, 20 Jun 2022 09:07:43 -0700 Subject: [PATCH 49/51] Support for reasons and fix docstrings --- discord/guild.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 6300469798..95296cf526 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -3575,6 +3575,7 @@ async def create_auto_moderation_rule( enabled: bool = False, exempt_roles: List[Snowflake] = None, exempt_channels: List[Snowflake] = None, + reason: Optional[str] = None, ) -> AutoModRule: """ Creates an auto moderation rule. @@ -3589,14 +3590,16 @@ async def create_auto_moderation_rule( The rule's trigger type. trigger_metadata: :class:`AutoModTriggerMetadata` The rule's trigger metadata. - actions: :class:`List[AutoModAction]` + actions: List[:class:`AutoModAction`] The actions to take when the rule is triggered. enabled: :class:`bool` Whether the rule is enabled. - exempt_roles: :class:`List[Snowflake]` + exempt_roles: List[:class:`Snowflake`] A list of roles that are exempt from the rule. - exempt_channels: :class:`List[Snowflake]` + exempt_channels: List[:class:`Snowflake`] A list of channels that are exempt from the rule. + reason: Optional[:class:`str`] + The reason for creating the rule. Shows up in the audit log. Raises ------- @@ -3626,4 +3629,4 @@ async def create_auto_moderation_rule( payload["exempt_channels"] = [c.id for c in exempt_channels] data = await self._state.http.create_auto_moderation_rule(self.id, payload) - return AutoModRule(state=self._state, data=data) + return AutoModRule(state=self._state, data=data, reason=reason) From 37982369630a4301522c0654a840b365e1098f1f Mon Sep 17 00:00:00 2001 From: plun1331 <49261529+plun1331@users.noreply.github.com> Date: Mon, 20 Jun 2022 09:08:32 -0700 Subject: [PATCH 50/51] Update docstrings --- discord/automod.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/discord/automod.py b/discord/automod.py index c4b3446967..435d391818 100644 --- a/discord/automod.py +++ b/discord/automod.py @@ -354,6 +354,11 @@ async def delete(self, reason: Optional[str] = None) -> None: Deletes this rule. + Parameters + ----------- + reason: Optional[:class:`str`] + The reason for deleting this rule. Shows up in the audit log. + Raises ------- Forbidden @@ -395,6 +400,8 @@ async def edit( The roles that will be exempt from this rule. exempt_channels: List[:class:`Snowflake`] The channels that will be exempt from this rule. + reason: Optional[:class:`str`] + The reason for editing this rule. Shows up in the audit log. Raises ------- From ed75d01de31eb6ccfd1e3fd42d123152716e1225 Mon Sep 17 00:00:00 2001 From: Lala Sabathil Date: Sat, 2 Jul 2022 13:27:47 +0200 Subject: [PATCH 51/51] Update discord/automod.py Co-authored-by: Middledot <78228142+Middledot@users.noreply.github.com> --- discord/automod.py | 1 - 1 file changed, 1 deletion(-) diff --git a/discord/automod.py b/discord/automod.py index 435d391818..4eeee9a46e 100644 --- a/discord/automod.py +++ b/discord/automod.py @@ -444,4 +444,3 @@ async def edit( if payload: data = await http.edit_auto_moderation_rule(self.guild_id, self.id, payload, reason=reason) return AutoModRule(state=self._state, data=data) -